diff --git a/.drone.jsonnet b/.drone.jsonnet index 4a98a62f64..64e175cee2 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -77,10 +77,7 @@ local clean_up_old_test_sims_on_commit_trigger = { { name: 'Build and Run Tests', commands: [ - 'echo "Explicitly running unit tests on `App_Store_Release` configuration to ensure optimisation behaviour is consistent"', - 'echo "If tests fail inconsistently from local builds this is likely the difference"', - 'echo ""', - 'NSUnbufferedIO=YES set -o pipefail && xcodebuild test -project Session.xcodeproj -scheme Session -derivedDataPath ./build/derivedData -resultBundlePath ./build/artifacts/testResults.xcresult -parallelizeTargets -configuration "App_Store_Release" -destination "platform=iOS Simulator,id=$(<./build/artifacts/sim_uuid)" -parallel-testing-enabled NO -test-timeouts-enabled YES -maximum-test-execution-time-allowance 10 -collect-test-diagnostics never ENABLE_TESTABILITY=YES 2>&1 | xcbeautify --is-ci', + './Scripts/build_ci.sh test -resultBundlePath ./build/artifacts/testResults.xcresult -destination "platform=iOS Simulator,id=$(<./build/artifacts/sim_uuid)" -parallel-testing-enabled NO -test-timeouts-enabled YES -maximum-test-execution-time-allowance 10 -collect-test-diagnostics never ENABLE_TESTABILITY=YES', ], depends_on: [ 'Reset SPM Cache if Needed', @@ -89,19 +86,37 @@ local clean_up_old_test_sims_on_commit_trigger = { ], }, { - name: 'Unit Test Summary', + name: 'Stop Simulator Keep-Alive', commands: [ + 'echo "Signaling simulator keep-alive to stop and clean up..."', sim_delete_cmd, - 'xcresultparser --output-format cli --failed-tests-only ./build/artifacts/testResults.xcresult' ], - depends_on: ['Build and Run Tests'] + depends_on: ['Build and Run Tests'], + when: { + status: ['success', 'failure'], + }, + }, + { + name: 'Log Failed Test Summary', + commands: [ + 'echo "--- FAILED TESTS ---"', + 'xcresultparser --output-format cli --failed-tests-only ./build/artifacts/testResults.xcresult', + 'exit 1' // Always fail if this runs to make it more obvious in the UI + ], + depends_on: ['Build and Run Tests'], + when: { + status: ['failure'], // Only run this on failure + }, }, { - name: 'Convert xcresult to xml', + name: 'Generate Code Coverage Report', commands: [ 'xcresultparser --output-format cobertura ./build/artifacts/testResults.xcresult > ./build/artifacts/coverage.xml', ], depends_on: ['Build and Run Tests'], + when: { + status: ['success'], + }, }, ], }, @@ -135,8 +150,7 @@ local clean_up_old_test_sims_on_commit_trigger = { { name: 'Build', commands: [ - 'mkdir build', - 'NSUnbufferedIO=YES set -o pipefail && xcodebuild archive -project Session.xcodeproj -scheme Session -derivedDataPath ./build/derivedData -parallelizeTargets -configuration "App_Store_Release" -sdk iphonesimulator -archivePath ./build/Session_sim.xcarchive -destination "generic/platform=iOS Simulator" | xcbeautify --is-ci', + './Scripts/build_ci.sh archive -sdk iphonesimulator -archivePath ./build/Session_sim.xcarchive -destination "generic/platform=iOS Simulator"', ], depends_on: [ 'Reset SPM Cache if Needed', diff --git a/Scripts/EmojiGenerator.swift b/Scripts/EmojiGenerator.swift index 519ce65220..1c2102c81f 100755 --- a/Scripts/EmojiGenerator.swift +++ b/Scripts/EmojiGenerator.swift @@ -53,7 +53,7 @@ enum RemoteModel { case flags = "Flags" case components = "Component" - var localizedKey: String = { + var localizedKey: String { switch self { case .smileys: return "Smileys" @@ -77,8 +77,8 @@ enum RemoteModel { return "Flags" case .components: return "Component" - } - }() + } + } } static func fetchEmojiData() throws -> Data { @@ -569,8 +569,9 @@ extension EmojiGenerator { fileHandle.indent { let stringKey = "emojiCategory\(category.localizedKey)" let stringComment = "The name for the emoji category '\(category.rawValue)'" - - fileHandle.writeLine("return NSLocalizedString(\"\(stringKey)\", comment: \"\(stringComment)\")") + + fileHandle.writeLine("// \(stringComment)") + fileHandle.writeLine("return \"\(stringKey)\".localized()") } } fileHandle.writeLine("}") diff --git a/Scripts/build_ci.sh b/Scripts/build_ci.sh new file mode 100755 index 0000000000..9ff7932d3d --- /dev/null +++ b/Scripts/build_ci.sh @@ -0,0 +1,93 @@ +#!/bin/bash + +IFS=$' \t\n' + +set -euo pipefail + +if [ $# -lt 1 ]; then + echo "Error: Missing mode. Usage: $0 [test|archive] [unique_xcodebuild_args...]" + exit 1 +fi + +MODE="$1" +shift + +COMMON_ARGS=( + -project Session.xcodeproj + -scheme Session + -derivedDataPath ./build/derivedData + -parallelizeTargets + -configuration "App_Store_Release" +) + +UNIQUE_ARGS=("$@") +XCODEBUILD_RAW_LOG=$(mktemp) + +trap 'rm -f "$XCODEBUILD_RAW_LOG"' EXIT + +if [[ "$MODE" == "test" ]]; then + + echo "--- Running Build and Unit Tests (App_Store_Release) ---" + + xcodebuild_exit_code=0 + + # We wrap the pipeline in parentheses to capture the exit code of xcodebuild + # which is at PIPESTATUS[0]. We do not use tee to a file here, as the complexity + # of reading back the UUID is not necessary if we pass it via args. + ( + NSUnbufferedIO=YES xcodebuild test \ + "${COMMON_ARGS[@]}" \ + "${UNIQUE_ARGS[@]}" 2>&1 | tee "$XCODEBUILD_RAW_LOG" | xcbeautify --is-ci + ) || xcodebuild_exit_code=${PIPESTATUS[0]} + + echo "" + echo "--- xcodebuild finished with exit code: $xcodebuild_exit_code ---" + + if [ "$xcodebuild_exit_code" -eq 0 ]; then + echo "✅ All tests passed and build succeeded!" + exit 0 + fi + + echo "" + echo "🔴 Build failed" + echo "----------------------------------------------------" + echo "Checking for test failures in xcresult bundle..." + + xcresultparser --output-format cli --no-test-result --coverage ./build/artifacts/testResults.xcresult + parser_output=$(xcresultparser --output-format cli --no-test-result ./build/artifacts/testResults.xcresult) + + build_errors_count=$(echo "$parser_output" | grep "Number of errors" | awk '{print $NF}') + failed_tests_count=$(echo "$parser_output" | grep "Number of failed tests" | awk '{print $NF}') + + if [ "${build_errors_count:-0}" -gt 0 ] || [ "${failed_tests_count:-0}" -gt 0 ]; then + echo "" + echo "🔴 Found $build_errors_count build error(s) and $failed_tests_count failed test(s) in the xcresult bundle." + exit 1 + else + echo "No test failures found in results. Failure was likely a build error." + echo "" + + echo "--- Summary of Potential Build Errors ---" + grep -E --color=always '(:[0-9]+:[0-9]+: error:)|(ld: error:)|(error: linker command failed)|(PhaseScriptExecution)|(rsync error:)' "$XCODEBUILD_RAW_LOG" || true + echo "" + echo "--- End of Raw Log ---" + tail -n 20 "$XCODEBUILD_RAW_LOG" + echo "-------------------------" + exit "$xcodebuild_exit_code" + fi + + echo "----------------------------------------------------" + exit "$xcodebuild_exit_code" + +elif [[ "$MODE" == "archive" ]]; then + + echo "--- Running Simulator Archive Build (App_Store_Release) ---" + + NSUnbufferedIO=YES xcodebuild archive \ + "${COMMON_ARGS[@]}" \ + "${UNIQUE_ARGS[@]}" 2>&1 | xcbeautify --is-ci + +else + echo "Error: Invalid mode '$MODE' specified. Use 'test' or 'archive'." + exit 1 +fi diff --git a/Scripts/build_libSession_util.sh b/Scripts/build_libSession_util.sh index 2f9cbbba6e..df2d64b325 100755 --- a/Scripts/build_libSession_util.sh +++ b/Scripts/build_libSession_util.sh @@ -37,7 +37,7 @@ fi if [ "${COMPILE_LIB_SESSION}" != "YES" ]; then echo "Restoring original headers to Xcode Indexer cache from backup..." rm -rf "${INDEX_DIR}/include" - rsync -rt --exclude='.DS_Store' "${PRE_BUILT_FRAMEWORK_DIR}/${TARGET_ARCH_DIR}/Headers/" "${INDEX_DIR}/include" + rsync -rt --exclude='.DS_Store' "${PRE_BUILT_FRAMEWORK_DIR}/${FRAMEWORK_DIR}/${TARGET_ARCH_DIR}/Headers/" "${INDEX_DIR}/include" echo "Using pre-packaged SessionUtil" exit 0 diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index c7a238e98e..918924bf05 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -98,7 +98,7 @@ 7B4EF25A2934743000CB351D /* SessionTableViewTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4EF2592934743000CB351D /* SessionTableViewTitleView.swift */; }; 7B50D64D28AC7CF80086CCEC /* silence.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 7B50D64C28AC7CF80086CCEC /* silence.aiff */; }; 7B5233C42900E90F00F8F375 /* SessionLabelCarouselView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B5233C32900E90F00F8F375 /* SessionLabelCarouselView.swift */; }; - 7B5233C6290636D700F8F375 /* _018_DisappearingMessagesConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B5233C5290636D700F8F375 /* _018_DisappearingMessagesConfiguration.swift */; }; + 7B5233C6290636D700F8F375 /* _032_DisappearingMessagesConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B5233C5290636D700F8F375 /* _032_DisappearingMessagesConfiguration.swift */; }; 7B5802992AAEF1B50050EEB1 /* OpenGroupInvitationView_SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B5802982AAEF1B50050EEB1 /* OpenGroupInvitationView_SwiftUI.swift */; }; 7B7037432834B81F000DCF35 /* ReactionContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7037422834B81F000DCF35 /* ReactionContainerView.swift */; }; 7B7037452834BCC0000DCF35 /* ReactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7037442834BCC0000DCF35 /* ReactionView.swift */; }; @@ -106,9 +106,7 @@ 7B7CB18E270D066F0079FF93 /* IncomingCallBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB18D270D066F0079FF93 /* IncomingCallBanner.swift */; }; 7B7CB190270FB2150079FF93 /* MiniCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB18F270FB2150079FF93 /* MiniCallView.swift */; }; 7B7CB192271508AD0079FF93 /* CallRingTonePlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB191271508AD0079FF93 /* CallRingTonePlayer.swift */; }; - 7B81682328A4C1210069F315 /* UpdateTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682228A4C1210069F315 /* UpdateTypes.swift */; }; - 7B81682828B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682728B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift */; }; - 7B81682A28B6F1420069F315 /* ReactionResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682928B6F1420069F315 /* ReactionResponse.swift */; }; + 7B81682828B310D50069F315 /* _015_HomeQueryOptimisationIndexes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682728B310D50069F315 /* _015_HomeQueryOptimisationIndexes.swift */; }; 7B81682C28B72F480069F315 /* PendingChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682B28B72F480069F315 /* PendingChange.swift */; }; 7B81FB5A2AB01B17002FB267 /* LoadingIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81FB582AB01AA8002FB267 /* LoadingIndicatorView.swift */; }; 7B8D5FC428332600008324D9 /* VisibleMessage+Reaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8D5FC328332600008324D9 /* VisibleMessage+Reaction.swift */; }; @@ -129,7 +127,7 @@ 7BA68909272A27BE00EFC32F /* SessionCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA68908272A27BE00EFC32F /* SessionCall.swift */; }; 7BA6890D27325CCC00EFC32F /* SessionCallManager+CXCallController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA6890C27325CCC00EFC32F /* SessionCallManager+CXCallController.swift */; }; 7BA6890F27325CE300EFC32F /* SessionCallManager+CXProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA6890E27325CE300EFC32F /* SessionCallManager+CXProvider.swift */; }; - 7BAA7B6628D2DE4700AE1489 /* _009_OpenGroupPermission.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAA7B6528D2DE4700AE1489 /* _009_OpenGroupPermission.swift */; }; + 7BAA7B6628D2DE4700AE1489 /* _018_OpenGroupPermission.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAA7B6528D2DE4700AE1489 /* _018_OpenGroupPermission.swift */; }; 7BAADFCC27B0EF23007BCF92 /* CallVideoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAADFCB27B0EF23007BCF92 /* CallVideoView.swift */; }; 7BAADFCE27B215FE007BCF92 /* UIView+Draggable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAADFCD27B215FE007BCF92 /* UIView+Draggable.swift */; }; 7BAF54CF27ACCEEC003D12F8 /* GlobalSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAF54CC27ACCEEC003D12F8 /* GlobalSearchViewController.swift */; }; @@ -173,23 +171,22 @@ 942256A12C23F90700C0FDBF /* CustomTopTabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 942256A02C23F90700C0FDBF /* CustomTopTabBar.swift */; }; 9422EE2B2B8C3A97004C740D /* String+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9422EE2A2B8C3A97004C740D /* String+Utilities.swift */; }; 942ADDD42D9F9613006E0BB0 /* NewTagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 942ADDD32D9F960C006E0BB0 /* NewTagView.swift */; }; + 942BA9BF2E4ABBA1007C4595 /* _045_LastProfileUpdateTimestamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 942BA9BE2E4ABB9F007C4595 /* _045_LastProfileUpdateTimestamp.swift */; }; 943C6D822B75E061004ACE64 /* Message+DisappearingMessages.swift in Sources */ = {isa = PBXBuildFile; fileRef = 943C6D812B75E061004ACE64 /* Message+DisappearingMessages.swift */; }; - 945D9C582D6FDBE7003C4C0C /* _005_AddJobUniqueHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 945D9C572D6FDBE7003C4C0C /* _005_AddJobUniqueHash.swift */; }; 946F5A732D5DA3AC00A5ADCE /* Punycode in Frameworks */ = {isa = PBXBuildFile; productRef = 946F5A722D5DA3AC00A5ADCE /* Punycode */; }; 9473386E2BDF5F3E00B9E169 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 9473386D2BDF5F3E00B9E169 /* InfoPlist.xcstrings */; }; 9479981C2DD44ADC008F5CD5 /* ThreadNotificationSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9479981B2DD44AC5008F5CD5 /* ThreadNotificationSettingsViewModel.swift */; }; - 947D7FD42D509FC900E8E413 /* SessionNetworkAPI+Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FD12D509FC900E8E413 /* SessionNetworkAPI+Models.swift */; }; 947D7FD62D509FC900E8E413 /* SessionNetworkAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FCF2D509FC900E8E413 /* SessionNetworkAPI.swift */; }; - 947D7FD72D509FC900E8E413 /* SessionNetworkAPI+Network.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FD22D509FC900E8E413 /* SessionNetworkAPI+Network.swift */; }; - 947D7FD82D509FC900E8E413 /* SessionNetworkAPI+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FD02D509FC900E8E413 /* SessionNetworkAPI+Database.swift */; }; + 947D7FD72D509FC900E8E413 /* HTTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FD22D509FC900E8E413 /* HTTPClient.swift */; }; + 947D7FD82D509FC900E8E413 /* KeyValueStore+SessionNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FD02D509FC900E8E413 /* KeyValueStore+SessionNetwork.swift */; }; 947D7FDE2D5180F200E8E413 /* SessionNetworkScreen+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FDB2D5180F200E8E413 /* SessionNetworkScreen+ViewModel.swift */; }; - 947D7FE32D5181F400E8E413 /* _006_RenameTableSettingToKeyValueStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FE22D5181F400E8E413 /* _006_RenameTableSettingToKeyValueStore.swift */; }; 947D7FE72D51837200E8E413 /* ArrowCapsule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FE42D51837200E8E413 /* ArrowCapsule.swift */; }; 947D7FE82D51837200E8E413 /* PopoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FE52D51837200E8E413 /* PopoverView.swift */; }; 947D7FE92D51837200E8E413 /* Text+CopyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FE62D51837200E8E413 /* Text+CopyButton.swift */; }; 9499E6032DDD9BF900091434 /* ExpandableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9499E6022DDD9BEE00091434 /* ExpandableLabel.swift */; }; 9499E68B2DF92F4E00091434 /* ThreadNotificationSettingsViewModelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9499E68A2DF92F3B00091434 /* ThreadNotificationSettingsViewModelSpec.swift */; }; - 94A6B9DB2DD6BF7C00DB4B44 /* Constants+URLs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94A6B9DA2DD6BF6E00DB4B44 /* Constants+URLs.swift */; }; + 949D91222E822D5A0074F595 /* String+SessionProBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 949D91212E822D520074F595 /* String+SessionProBadge.swift */; }; + 94A6B9DB2DD6BF7C00DB4B44 /* Constants+Apple.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94A6B9DA2DD6BF6E00DB4B44 /* Constants+Apple.swift */; }; 94AAB14B2E1E198200A6FA18 /* Modal+SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94AAB14A2E1E197800A6FA18 /* Modal+SwiftUI.swift */; }; 94AAB14D2E1F39B500A6FA18 /* ProCTAModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94AAB14C2E1F39AB00A6FA18 /* ProCTAModal.swift */; }; 94AAB14F2E1F6CC100A6FA18 /* SessionProBadge+SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94AAB14E2E1F6CB300A6FA18 /* SessionProBadge+SwiftUI.swift */; }; @@ -197,21 +194,23 @@ 94AAB1532E1F8AE200A6FA18 /* ShineButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94AAB1522E1F8AD900A6FA18 /* ShineButton.swift */; }; 94AAB1572E24BD3700A6FA18 /* PinnedConversationsCTA.webp in Resources */ = {isa = PBXBuildFile; fileRef = 94AAB1562E24BD3700A6FA18 /* PinnedConversationsCTA.webp */; }; 94AAB1582E24BD3700A6FA18 /* PinnedConversationsCTA.webp in Resources */ = {isa = PBXBuildFile; fileRef = 94AAB1562E24BD3700A6FA18 /* PinnedConversationsCTA.webp */; }; + 94AAB15E2E24C97400A6FA18 /* AnimatedProfileCTA.webp in Resources */ = {isa = PBXBuildFile; fileRef = 94AAB15B2E24C97400A6FA18 /* AnimatedProfileCTA.webp */; }; + 94AAB1602E24C97400A6FA18 /* AnimatedProfileCTA.webp in Resources */ = {isa = PBXBuildFile; fileRef = 94AAB15B2E24C97400A6FA18 /* AnimatedProfileCTA.webp */; }; 94B3DC172AF8592200C88531 /* QuoteView_SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94B3DC162AF8592200C88531 /* QuoteView_SwiftUI.swift */; }; - 94B6BAFA2E38454F00E718BB /* SessionProState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94B6BAF92E38454F00E718BB /* SessionProState.swift */; }; + 94B6BAF62E30A88800E718BB /* SessionProState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94B6BAF52E30A88800E718BB /* SessionProState.swift */; }; + 94B6BB062E431B6300E718BB /* AnimatedProfileCTAAnimationCropped.webp in Resources */ = {isa = PBXBuildFile; fileRef = 94B6BB052E431B6300E718BB /* AnimatedProfileCTAAnimationCropped.webp */; }; + 94B6BB072E431B6300E718BB /* AnimatedProfileCTAAnimationCropped.webp in Resources */ = {isa = PBXBuildFile; fileRef = 94B6BB052E431B6300E718BB /* AnimatedProfileCTAAnimationCropped.webp */; }; 94C58AC92D2E037200609195 /* Permissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94C58AC82D2E036E00609195 /* Permissions.swift */; }; 94CD95BB2E08D9E00097754D /* SessionProBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94CD95BA2E08D9D40097754D /* SessionProBadge.swift */; }; 94CD95BD2E09083C0097754D /* LibSession+Pro.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94CD95BC2E0908340097754D /* LibSession+Pro.swift */; }; - 94CD95C12E0CBF430097754D /* _029_AddProMessageFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94CD95C02E0CBF1C0097754D /* _029_AddProMessageFlag.swift */; }; + 94CD95C12E0CBF430097754D /* _044_AddProMessageFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94CD95C02E0CBF1C0097754D /* _044_AddProMessageFlag.swift */; }; 94CD962D2E1B85920097754D /* InputViewButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94CD962B2E1B85920097754D /* InputViewButton.swift */; }; 94CD962E2E1B85920097754D /* InputTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94CD962A2E1B85920097754D /* InputTextView.swift */; }; 94CD96302E1B88430097754D /* CGRect+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94CD962F2E1B88430097754D /* CGRect+Utilities.swift */; }; 94CD96322E1B88C20097754D /* ExpandingAttachmentsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94CD96312E1B88C20097754D /* ExpandingAttachmentsButton.swift */; }; 94CD96402E1BABE90097754D /* GenericCTA.webp in Resources */ = {isa = PBXBuildFile; fileRef = 94CD963C2E1BABE90097754D /* GenericCTA.webp */; }; 94CD96412E1BABE90097754D /* HigherCharLimitCTA.webp in Resources */ = {isa = PBXBuildFile; fileRef = 94CD963E2E1BABE90097754D /* HigherCharLimitCTA.webp */; }; - 94CD96422E1BABE90097754D /* GenericCTAAnimation.webp in Resources */ = {isa = PBXBuildFile; fileRef = 94CD963D2E1BABE90097754D /* GenericCTAAnimation.webp */; }; 94CD96432E1BAC0F0097754D /* GenericCTA.webp in Resources */ = {isa = PBXBuildFile; fileRef = 94CD963C2E1BABE90097754D /* GenericCTA.webp */; }; - 94CD96442E1BAC0F0097754D /* GenericCTAAnimation.webp in Resources */ = {isa = PBXBuildFile; fileRef = 94CD963D2E1BABE90097754D /* GenericCTAAnimation.webp */; }; 94CD96452E1BAC0F0097754D /* HigherCharLimitCTA.webp in Resources */ = {isa = PBXBuildFile; fileRef = 94CD963E2E1BABE90097754D /* HigherCharLimitCTA.webp */; }; 94E9BC0D2C7BFBDA006984EA /* Localization+Style.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94E9BC0C2C7BFBDA006984EA /* Localization+Style.swift */; }; A11CD70D17FA230600A2D1B1 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A11CD70C17FA230600A2D1B1 /* QuartzCore.framework */; }; @@ -251,7 +250,6 @@ B8856D72256F1421001CE70E /* OWSWindowManager.h in Headers */ = {isa = PBXBuildFile; fileRef = C38EF2FB255B6DBD007E1867 /* OWSWindowManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; B8856DE6256F15F2001CE70E /* String+SSK.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB3F255A580C00E217F9 /* String+SSK.swift */; }; B8856E09256F1676001CE70E /* UIDevice+featureSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF237255B6D65007E1867 /* UIDevice+featureSupport.swift */; }; - B886B4A72398B23E00211ABE /* (null) in Sources */ = {isa = PBXBuildFile; }; B886B4A92398BA1500211ABE /* QRCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B886B4A82398BA1500211ABE /* QRCode.swift */; }; B88FA7F2260C3EB10049422F /* OpenGroupSuggestionGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88FA7F1260C3EB10049422F /* OpenGroupSuggestionGrid.swift */; }; B893063F2383961A005EAA8E /* ScanQRCodeWrapperVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B893063E2383961A005EAA8E /* ScanQRCodeWrapperVC.swift */; }; @@ -268,7 +266,7 @@ B8D0A25925E367AC00C1835E /* Notification+MessageReceiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D0A25825E367AC00C1835E /* Notification+MessageReceiver.swift */; }; B8D0A26925E4A2C200C1835E /* Onboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D0A26825E4A2C200C1835E /* Onboarding.swift */; }; B8D64FBB25BA78310029CFC0 /* SessionMessagingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */; }; - B8D64FBD25BA78310029CFC0 /* SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; }; + B8D64FBD25BA78310029CFC0 /* SessionNetworkingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionNetworkingKit.framework */; }; B8D64FBE25BA78310029CFC0 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; }; B8D64FC725BA78520029CFC0 /* SessionMessagingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */; }; B8D64FCB25BA78A90029CFC0 /* SignalUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C33FD9AB255A548A00E217F9 /* SignalUtilitiesKit.framework */; }; @@ -286,7 +284,7 @@ C300A5F22554B09800555489 /* MessageSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = C300A5F12554B09800555489 /* MessageSender.swift */; }; C300A60D2554B31900555489 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5CE2553860700C340D1 /* Logging.swift */; }; C302093E25DCBF08001F572D /* MentionSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C302093D25DCBF07001F572D /* MentionSelectionView.swift */; }; - C32824D325C9F9790062D0A7 /* SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; }; + C32824D325C9F9790062D0A7 /* SessionNetworkingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionNetworkingKit.framework */; }; C328250F25CA06020062D0A7 /* VoiceMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C328250E25CA06020062D0A7 /* VoiceMessageView.swift */; }; C328251F25CA3A900062D0A7 /* QuoteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C328251E25CA3A900062D0A7 /* QuoteView.swift */; }; C328253025CA55370062D0A7 /* ContextMenuWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C328252F25CA55360062D0A7 /* ContextMenuWindow.swift */; }; @@ -314,13 +312,12 @@ C331FFFE2558FF3B00070591 /* FullConversationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BB82AA238F669C00BA5194 /* FullConversationCell.swift */; }; C33FD9B3255A548A00E217F9 /* SignalUtilitiesKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C33FD9AB255A548A00E217F9 /* SignalUtilitiesKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; C33FD9C2255A54EF00E217F9 /* SessionMessagingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */; }; - C33FD9C4255A54EF00E217F9 /* SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; }; + C33FD9C4255A54EF00E217F9 /* SessionNetworkingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionNetworkingKit.framework */; }; C33FD9C5255A54EF00E217F9 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; }; C33FDC58255A582000E217F9 /* ReverseDispatchQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA9E255A57FF00E217F9 /* ReverseDispatchQueue.swift */; }; C33FDD8D255A582000E217F9 /* OWSSignalAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBD3255A581800E217F9 /* OWSSignalAddress.swift */; }; C3402FE52559036600EA6424 /* SessionUIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C331FF1B2558F9D300070591 /* SessionUIKit.framework */; }; C34C8F7423A7830B00D82669 /* SpaceMono-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = C34C8F7323A7830A00D82669 /* SpaceMono-Bold.ttf */; }; - C352A2FF25574B6300338F3E /* (null) in Sources */ = {isa = PBXBuildFile; }; C3548F0824456AB6009433A8 /* UIView+Wrapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3548F0724456AB6009433A8 /* UIView+Wrapping.swift */; }; C354E75A23FE2A7600CE22E3 /* BaseVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C354E75923FE2A7600CE22E3 /* BaseVC.swift */; }; C35D0DB525AE5F1200B6BF49 /* UIEdgeInsets.swift in Sources */ = {isa = PBXBuildFile; fileRef = C35D0DB425AE5F1200B6BF49 /* UIEdgeInsets.swift */; }; @@ -329,7 +326,7 @@ C374EEF425DB31D40073A857 /* VoiceMessageRecordingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C374EEF325DB31D40073A857 /* VoiceMessageRecordingView.swift */; }; C379DCF4256735770002D4EB /* VisibleMessage+Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = C379DCF3256735770002D4EB /* VisibleMessage+Attachment.swift */; }; C37F5414255BAFA7002AEA92 /* SignalUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C33FD9AB255A548A00E217F9 /* SignalUtilitiesKit.framework */; }; - C37F54DC255BB84A002AEA92 /* SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; }; + C37F54DC255BB84A002AEA92 /* SessionNetworkingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionNetworkingKit.framework */; }; C38EF00C255B61CC007E1867 /* SignalUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C33FD9AB255A548A00E217F9 /* SignalUtilitiesKit.framework */; }; C38EF24D255B6D67007E1867 /* UIView+OWS.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF240255B6D67007E1867 /* UIView+OWS.swift */; }; C38EF24E255B6D67007E1867 /* Collection+OWS.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF241255B6D67007E1867 /* Collection+OWS.swift */; }; @@ -373,16 +370,14 @@ C3ADC66126426688005F1414 /* ShareNavController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3ADC66026426688005F1414 /* ShareNavController.swift */; }; C3BBE0AA2554D4DE0050F1E3 /* Dictionary+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D52553860A00C340D1 /* Dictionary+Utilities.swift */; }; C3BBE0C72554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3BBE0C62554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift */; }; - C3C2A5A3255385C100C340D1 /* SessionSnodeKit.h in Headers */ = {isa = PBXBuildFile; fileRef = C3C2A5A1255385C100C340D1 /* SessionSnodeKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; - C3C2A5A7255385C100C340D1 /* SessionSnodeKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - C3C2A5C2255385EE00C340D1 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5B9255385ED00C340D1 /* Configuration.swift */; }; - C3C2A5E02553860B00C340D1 /* Threading+SSK.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D42553860A00C340D1 /* Threading+SSK.swift */; }; + C3C2A5A3255385C100C340D1 /* SessionNetworkingKit.h in Headers */ = {isa = PBXBuildFile; fileRef = C3C2A5A1255385C100C340D1 /* SessionNetworkingKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C3C2A5A7255385C100C340D1 /* SessionNetworkingKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionNetworkingKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; C3C2A5E42553860B00C340D1 /* Data+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D82553860B00C340D1 /* Data+Utilities.swift */; }; C3C2A681255388CC00C340D1 /* SessionUtilitiesKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; C3C2A6C62553896A00C340D1 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; }; C3C2A6F425539DE700C340D1 /* SessionMessagingKit.h in Headers */ = {isa = PBXBuildFile; fileRef = C3C2A6F225539DE700C340D1 /* SessionMessagingKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; C3C2A6F825539DE700C340D1 /* SessionMessagingKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - C3C2A70B25539E1E00C340D1 /* SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; }; + C3C2A70B25539E1E00C340D1 /* SessionNetworkingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionNetworkingKit.framework */; }; C3C2A74425539EB700C340D1 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A74325539EB700C340D1 /* Message.swift */; }; C3C2A74D2553A39700C340D1 /* VisibleMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A74C2553A39700C340D1 /* VisibleMessage.swift */; }; C3C2A75F2553A3C500C340D1 /* VisibleMessage+LinkPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A75E2553A3C500C340D1 /* VisibleMessage+LinkPreview.swift */; }; @@ -423,15 +418,13 @@ FD01504B2CA243CB005B08A1 /* Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0150472CA243CB005B08A1 /* Mock.swift */; }; FD01504C2CA243CB005B08A1 /* Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0150472CA243CB005B08A1 /* Mock.swift */; }; FD01504E2CA243E7005B08A1 /* TypeConversionUtilitiesSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD01504D2CA243E7005B08A1 /* TypeConversionUtilitiesSpec.swift */; }; - FD0150502CA24468005B08A1 /* SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; platformFilter = ios; }; + FD0150502CA24468005B08A1 /* SessionNetworkingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionNetworkingKit.framework */; platformFilter = ios; }; FD0150522CA2446D005B08A1 /* Quick in Frameworks */ = {isa = PBXBuildFile; productRef = FD0150512CA2446D005B08A1 /* Quick */; }; FD0150542CA24471005B08A1 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = FD0150532CA24471005B08A1 /* Nimble */; }; FD0150582CA27DF3005B08A1 /* ScrollableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0150572CA27DEE005B08A1 /* ScrollableLabel.swift */; }; - FD02CC122C367762009AB976 /* Request+PushNotificationAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD02CC112C367761009AB976 /* Request+PushNotificationAPI.swift */; }; - FD02CC142C3677E6009AB976 /* Request+OpenGroupAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD02CC132C3677E6009AB976 /* Request+OpenGroupAPI.swift */; }; FD02CC162C3681EF009AB976 /* RevokeSubaccountResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD02CC152C3681EF009AB976 /* RevokeSubaccountResponse.swift */; }; FD05593D2DFA3A2800DC48CE /* VoipPayloadKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD05593C2DFA3A2200DC48CE /* VoipPayloadKey.swift */; }; - FD05594E2E012D2700DC48CE /* _028_RenameAttachments.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD05594D2E012D1A00DC48CE /* _028_RenameAttachments.swift */; }; + FD05594E2E012D2700DC48CE /* _043_RenameAttachments.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD05594D2E012D1A00DC48CE /* _043_RenameAttachments.swift */; }; FD0559562E026E1B00DC48CE /* ObservingDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0559542E026CC900DC48CE /* ObservingDatabase.swift */; }; FD0606C32BCE13ED00C3816E /* MessageRequestFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0606C22BCE13ED00C3816E /* MessageRequestFooterView.swift */; }; FD078E5427E197CA000769AF /* OpenGroupManagerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2909D27D85751005DAE71 /* OpenGroupManagerSpec.swift */; }; @@ -453,7 +446,7 @@ FD09799927FFC1A300936362 /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09799827FFC1A300936362 /* Attachment.swift */; }; FD09799B27FFC82D00936362 /* Quote.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09799A27FFC82D00936362 /* Quote.swift */; }; FD09B7E328865FDA00ED0B66 /* HighlightMentionBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09B7E228865FDA00ED0B66 /* HighlightMentionBackgroundView.swift */; }; - FD09B7E5288670BB00ED0B66 /* _008_EmojiReacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09B7E4288670BB00ED0B66 /* _008_EmojiReacts.swift */; }; + FD09B7E5288670BB00ED0B66 /* _017_EmojiReacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09B7E4288670BB00ED0B66 /* _017_EmojiReacts.swift */; }; FD09B7E7288670FD00ED0B66 /* Reaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09B7E6288670FD00ED0B66 /* Reaction.swift */; }; FD09C5E628260FF9000CE219 /* MediaGalleryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09C5E528260FF9000CE219 /* MediaGalleryViewModel.swift */; }; FD09C5E828264937000CE219 /* MediaDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09C5E728264937000CE219 /* MediaDetailViewController.swift */; }; @@ -466,7 +459,6 @@ FD10AF122AF85D11007709E5 /* Feature+ServiceNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD10AF112AF85D11007709E5 /* Feature+ServiceNetwork.swift */; }; FD11E22D2CA4D12C001BAF58 /* DifferenceKit in Frameworks */ = {isa = PBXBuildFile; productRef = FD2286782C38D4FF00BC06F7 /* DifferenceKit */; }; FD11E22E2CA4D12C001BAF58 /* WebRTC in Frameworks */ = {isa = PBXBuildFile; productRef = FDEF57292C3CF50B00131302 /* WebRTC */; }; - FD12A83D2AD63BCC00EEBA0D /* EditableState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD12A83C2AD63BCC00EEBA0D /* EditableState.swift */; }; FD12A83F2AD63BDF00EEBA0D /* Navigatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD12A83E2AD63BDF00EEBA0D /* Navigatable.swift */; }; FD12A8412AD63BEA00EEBA0D /* NavigatableState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD12A8402AD63BEA00EEBA0D /* NavigatableState.swift */; }; FD12A8432AD63BF600EEBA0D /* ObservableTableSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD12A8422AD63BF600EEBA0D /* ObservableTableSource.swift */; }; @@ -476,18 +468,12 @@ FD16AB5B2A1DD7CA0083D849 /* PlaceholderIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2A3255B6D93007E1867 /* PlaceholderIcon.swift */; }; FD16AB5F2A1DD98F0083D849 /* ProfilePictureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2A4255B6D93007E1867 /* ProfilePictureView.swift */; }; FD16AB612A1DD9B60083D849 /* ProfilePictureView+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD16AB602A1DD9B60083D849 /* ProfilePictureView+Convenience.swift */; }; - FD17D79927F40AB800122BE0 /* _003_YDBToGRDBMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79827F40AB800122BE0 /* _003_YDBToGRDBMigration.swift */; }; - FD17D7A027F40CC800122BE0 /* _001_InitialSetupMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79F27F40CC800122BE0 /* _001_InitialSetupMigration.swift */; }; + FD17D79927F40AB800122BE0 /* _009_SMK_YDBToGRDBMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79827F40AB800122BE0 /* _009_SMK_YDBToGRDBMigration.swift */; }; FD17D7A127F40D2500122BE0 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD28A4F527EAD44C00FF65E7 /* Storage.swift */; }; - FD17D7A227F40F0500122BE0 /* _001_InitialSetupMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79527F3E04600122BE0 /* _001_InitialSetupMigration.swift */; }; - FD17D7A427F40F8100122BE0 /* _003_YDBToGRDBMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7A327F40F8100122BE0 /* _003_YDBToGRDBMigration.swift */; }; + FD17D7A227F40F0500122BE0 /* _006_SMK_InitialSetupMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79527F3E04600122BE0 /* _006_SMK_InitialSetupMigration.swift */; }; FD17D7AE27F41C4300122BE0 /* SnodeReceivedMessageInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7AD27F41C4300122BE0 /* SnodeReceivedMessageInfo.swift */; }; FD17D7B827F51ECA00122BE0 /* Migration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7B727F51ECA00122BE0 /* Migration.swift */; }; - FD17D7BA27F51F2100122BE0 /* TargetMigrations.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7B927F51F2100122BE0 /* TargetMigrations.swift */; }; - FD17D7C727F5207C00122BE0 /* DatabaseMigrator+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7C627F5207C00122BE0 /* DatabaseMigrator+Utilities.swift */; }; - FD17D7CA27F546D900122BE0 /* _001_InitialSetupMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7C927F546D900122BE0 /* _001_InitialSetupMigration.swift */; }; FD17D7E527F6A09900122BE0 /* Identity.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7E427F6A09900122BE0 /* Identity.swift */; }; - FD17D7E727F6A16700122BE0 /* _003_YDBToGRDBMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7E627F6A16700122BE0 /* _003_YDBToGRDBMigration.swift */; }; FD19363C2ACA3134004BCF0F /* ResponseInfo+SnodeAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD19363B2ACA3134004BCF0F /* ResponseInfo+SnodeAPI.swift */; }; FD19363F2ACA66DE004BCF0F /* DatabaseSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD19363E2ACA66DE004BCF0F /* DatabaseSpec.swift */; }; FD1A553E2E14BE11003761E4 /* PagedData.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1A553D2E14BE0E003761E4 /* PagedData.swift */; }; @@ -495,7 +481,7 @@ FD1A55432E179AED003761E4 /* ObservableKeyEvent+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1A55422E179AE6003761E4 /* ObservableKeyEvent+Utilities.swift */; }; FD1A94FB2900D1C2000D73D3 /* PersistableRecord+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1A94FA2900D1C2000D73D3 /* PersistableRecord+Utilities.swift */; }; FD1C98E4282E3C5B00B76F9E /* UINavigationBar+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1C98E3282E3C5B00B76F9E /* UINavigationBar+Utilities.swift */; }; - FD1D732E2A86114600E3F410 /* _015_BlockCommunityMessageRequests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1D732D2A86114600E3F410 /* _015_BlockCommunityMessageRequests.swift */; }; + FD1D732E2A86114600E3F410 /* _029_BlockCommunityMessageRequests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1D732D2A86114600E3F410 /* _029_BlockCommunityMessageRequests.swift */; }; FD22726B2C32911C004D8A6C /* SendReadReceiptsJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272532C32911A004D8A6C /* SendReadReceiptsJob.swift */; }; FD22726C2C32911C004D8A6C /* GroupLeavingJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272542C32911A004D8A6C /* GroupLeavingJob.swift */; }; FD22726D2C32911C004D8A6C /* CheckForAppUpdatesJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272552C32911A004D8A6C /* CheckForAppUpdatesJob.swift */; }; @@ -545,11 +531,8 @@ FD2272D42C34ECE1004D8A6C /* BencodeEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272D32C34ECE1004D8A6C /* BencodeEncoder.swift */; }; FD2272D62C34ED6A004D8A6C /* RetryWithDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83DCDC2A739D350065FFAE /* RetryWithDependencies.swift */; }; FD2272D82C34EDE7004D8A6C /* SnodeAPIEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272D72C34EDE6004D8A6C /* SnodeAPIEndpoint.swift */; }; - FD2272D92C34EED6004D8A6C /* _001_ThemePreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37E9F828A5F14A003AE748 /* _001_ThemePreferences.swift */; }; FD2272DD2C34EFFA004D8A6C /* AppSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272DC2C34EFFA004D8A6C /* AppSetup.swift */; }; - FD2272DE2C34F11F004D8A6C /* _001_ThemePreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37E9F828A5F14A003AE748 /* _001_ThemePreferences.swift */; }; FD2272E02C3502BE004D8A6C /* Setting+Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272DF2C3502BE004D8A6C /* Setting+Theme.swift */; }; - FD2272E42C35134B004D8A6C /* Data+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272E32C35134B004D8A6C /* Data+Utilities.swift */; }; FD2272E62C351378004D8A6C /* SUIKImageFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272E52C351378004D8A6C /* SUIKImageFormat.swift */; }; FD2272EA2C351CA7004D8A6C /* Threading.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272E92C351CA7004D8A6C /* Threading.swift */; }; FD2272EC2C352155004D8A6C /* Feature.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272EB2C352155004D8A6C /* Feature.swift */; }; @@ -593,7 +576,6 @@ FD245C5D2850660F00B966DD /* OWSAudioPlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2F7255B6DBC007E1867 /* OWSAudioPlayer.m */; }; FD245C5F2850662200B966DD /* OWSWindowManager.m in Sources */ = {isa = PBXBuildFile; fileRef = C38EF306255B6DBE007E1867 /* OWSWindowManager.m */; }; FD245C632850664600B966DD /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD245C612850664300B966DD /* Configuration.swift */; }; - FD245C662850665900B966DD /* OpenGroupAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88FA7B726045D100049422F /* OpenGroupAPI.swift */; }; FD245C682850666300B966DD /* Message+Destination.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352A30825574D8400338F3E /* Message+Destination.swift */; }; FD245C692850666800B966DD /* ExpirationTimerUpdate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C300A5E62554B07300555489 /* ExpirationTimerUpdate.swift */; }; FD245C6B2850667400B966DD /* VisibleMessage+Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C300A5B12554AF9800555489 /* VisibleMessage+Profile.swift */; }; @@ -607,7 +589,7 @@ FD2DD5902C6DD13C0073D9BE /* DifferenceKit in Frameworks */ = {isa = PBXBuildFile; productRef = FD2DD58F2C6DD13C0073D9BE /* DifferenceKit */; }; FD336F602CAA28CF00C0B51B /* CommonSMKMockExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F562CAA28CF00C0B51B /* CommonSMKMockExtensions.swift */; }; FD336F612CAA28CF00C0B51B /* MockNotificationsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F5C2CAA28CF00C0B51B /* MockNotificationsManager.swift */; }; - FD336F622CAA28CF00C0B51B /* CustomArgSummaryDescribable+SMK.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F572CAA28CF00C0B51B /* CustomArgSummaryDescribable+SMK.swift */; }; + FD336F622CAA28CF00C0B51B /* CustomArgSummaryDescribable+SessionMessagingKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F572CAA28CF00C0B51B /* CustomArgSummaryDescribable+SessionMessagingKit.swift */; }; FD336F632CAA28CF00C0B51B /* MockOGMCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F5D2CAA28CF00C0B51B /* MockOGMCache.swift */; }; FD336F642CAA28CF00C0B51B /* MockCommunityPollerCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F582CAA28CF00C0B51B /* MockCommunityPollerCache.swift */; }; FD336F652CAA28CF00C0B51B /* MockDisplayPictureCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F592CAA28CF00C0B51B /* MockDisplayPictureCache.swift */; }; @@ -624,8 +606,8 @@ FD336F742CABB97800C0B51B /* PreparedRequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0150302CA24310005B08A1 /* PreparedRequestSpec.swift */; }; FD336F752CABB97800C0B51B /* RequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0150312CA24310005B08A1 /* RequestSpec.swift */; }; FD336F762CABB97800C0B51B /* BencodeResponseSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD383722AFDD6D7001367F2 /* BencodeResponseSpec.swift */; }; - FD3559462CC1FF200088F2A9 /* _020_AddMissingWhisperFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3559452CC1FF140088F2A9 /* _020_AddMissingWhisperFlag.swift */; }; - FD368A6829DE8F9C000DBF1E /* _012_AddFTSIfNeeded.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD368A6729DE8F9B000DBF1E /* _012_AddFTSIfNeeded.swift */; }; + FD3559462CC1FF200088F2A9 /* _034_AddMissingWhisperFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3559452CC1FF140088F2A9 /* _034_AddMissingWhisperFlag.swift */; }; + FD368A6829DE8F9C000DBF1E /* _026_AddFTSIfNeeded.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD368A6729DE8F9B000DBF1E /* _026_AddFTSIfNeeded.swift */; }; FD368A6A29DE9E30000DBF1E /* UIContextualAction+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD368A6929DE9E30000DBF1E /* UIContextualAction+Utilities.swift */; }; FD3765DF2AD8F03100DC1489 /* MockSnodeAPICache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3765DE2AD8F03100DC1489 /* MockSnodeAPICache.swift */; }; FD3765E02AD8F05100DC1489 /* MockSnodeAPICache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3765DE2AD8F03100DC1489 /* MockSnodeAPICache.swift */; }; @@ -634,8 +616,6 @@ FD3765E72ADE1AA300DC1489 /* UnrevokeSubaccountRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3765E62ADE1AA300DC1489 /* UnrevokeSubaccountRequest.swift */; }; FD3765E92ADE1AAE00DC1489 /* UnrevokeSubaccountResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3765E82ADE1AAE00DC1489 /* UnrevokeSubaccountResponse.swift */; }; FD3765EA2ADE37B400DC1489 /* Authentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD47E0AA2AA68EEA00A55E41 /* Authentication.swift */; }; - FD3765F42ADE5A0800DC1489 /* AuthenticatedRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3765F32ADE5A0800DC1489 /* AuthenticatedRequest.swift */; }; - FD3765F62ADE5BA500DC1489 /* ServiceInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3765F52ADE5BA500DC1489 /* ServiceInfo.swift */; }; FD37E9C328A1C6F3003AE748 /* ThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37E9C228A1C6F3003AE748 /* ThemeManager.swift */; }; FD37E9C628A1D4EC003AE748 /* Theme+ClassicDark.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37E9C528A1D4EC003AE748 /* Theme+ClassicDark.swift */; }; FD37E9C828A1D73F003AE748 /* Theme+Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37E9C728A1D73F003AE748 /* Theme+Colors.swift */; }; @@ -653,19 +633,20 @@ FD37EA0128A60473003AE748 /* UIKit+Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0028A60473003AE748 /* UIKit+Theme.swift */; }; FD37EA0328A9FDCC003AE748 /* HelpViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0228A9FDCC003AE748 /* HelpViewModel.swift */; }; FD37EA0528AA00C1003AE748 /* NotificationSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0428AA00C1003AE748 /* NotificationSettingsViewModel.swift */; }; - FD37EA0D28AB2A45003AE748 /* _005_FixDeletedMessageReadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0C28AB2A45003AE748 /* _005_FixDeletedMessageReadState.swift */; }; - FD37EA0F28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0E28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift */; }; + FD37EA0D28AB2A45003AE748 /* _013_FixDeletedMessageReadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0C28AB2A45003AE748 /* _013_FixDeletedMessageReadState.swift */; }; + FD37EA0F28AB3330003AE748 /* _014_FixHiddenModAdminSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0E28AB3330003AE748 /* _014_FixHiddenModAdminSupport.swift */; }; FD37EA1528AB42CB003AE748 /* IdentitySpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA1428AB42CB003AE748 /* IdentitySpec.swift */; }; FD37EA1728AC5605003AE748 /* NotificationContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA1628AC5605003AE748 /* NotificationContentViewModel.swift */; }; FD37EA1928AC5CCA003AE748 /* NotificationSoundViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA1828AC5CCA003AE748 /* NotificationSoundViewModel.swift */; }; FD39352C28F382920084DADA /* VersionFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD39352B28F382920084DADA /* VersionFooterView.swift */; }; - FD39353628F7C3390084DADA /* _004_FlagMessageHashAsDeletedOrInvalid.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD39353528F7C3390084DADA /* _004_FlagMessageHashAsDeletedOrInvalid.swift */; }; + FD3937082E4AD3FE00571F17 /* NoopDependency.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3937072E4AD3F800571F17 /* NoopDependency.swift */; }; + FD39370C2E4D7BCA00571F17 /* DocumentPickerHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD39370B2E4D7BBE00571F17 /* DocumentPickerHandler.swift */; }; FD3AABE928306BBD00E5099A /* ThreadPickerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3AABE828306BBD00E5099A /* ThreadPickerViewModel.swift */; }; FD3C906727E416AF00CD579F /* BlindedIdLookupSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906627E416AF00CD579F /* BlindedIdLookupSpec.swift */; }; FD3E0C84283B5835002A425C /* SessionThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3E0C83283B5835002A425C /* SessionThreadViewModel.swift */; }; FD3F2EE72DE6CC4100FD6849 /* NotificationsManagerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3F2EE62DE6CC3B00FD6849 /* NotificationsManagerSpec.swift */; }; FD3F2EF22DF273D900FD6849 /* ThemedAttributedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3F2EF12DF273D100FD6849 /* ThemedAttributedString.swift */; }; - FD3FAB592ADF906300DC5421 /* Profile+CurrentUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3FAB582ADF906300DC5421 /* Profile+CurrentUser.swift */; }; + FD3FAB592ADF906300DC5421 /* Profile+Updating.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3FAB582ADF906300DC5421 /* Profile+Updating.swift */; }; FD3FAB5F2AE9BC2200DC5421 /* EditGroupViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3FAB5E2AE9BC2200DC5421 /* EditGroupViewModel.swift */; }; FD3FAB612AEA194E00DC5421 /* UserListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3FAB602AEA194E00DC5421 /* UserListViewModel.swift */; }; FD3FAB632AEB9A1500DC5421 /* ToastController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3FAB622AEB9A1500DC5421 /* ToastController.swift */; }; @@ -676,13 +657,12 @@ FD428B192B4B576F006D0888 /* AppContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD428B182B4B576F006D0888 /* AppContext.swift */; }; FD428B1B2B4B6098006D0888 /* Notifications+Lifecycle.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD428B1A2B4B6098006D0888 /* Notifications+Lifecycle.swift */; }; FD428B1F2B4B758B006D0888 /* AppReadiness.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD428B1E2B4B758B006D0888 /* AppReadiness.swift */; }; - FD428B232B4B9969006D0888 /* _017_RebuildFTSIfNeeded_2_4_5.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD428B222B4B9969006D0888 /* _017_RebuildFTSIfNeeded_2_4_5.swift */; }; + FD428B232B4B9969006D0888 /* _031_RebuildFTSIfNeeded_2_4_5.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD428B222B4B9969006D0888 /* _031_RebuildFTSIfNeeded_2_4_5.swift */; }; FD42ECCE2E287CD4002D03EA /* ThemeColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD42ECCD2E287CD1002D03EA /* ThemeColor.swift */; }; FD42ECD02E289261002D03EA /* ThemeLinearGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD42ECCF2E289257002D03EA /* ThemeLinearGradient.swift */; }; FD42ECD22E3071DE002D03EA /* ThemeText.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD42ECD12E3071DC002D03EA /* ThemeText.swift */; }; FD42ECD42E32FF2E002D03EA /* StringUtilitiesSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD42ECD32E32FF2A002D03EA /* StringUtilitiesSpec.swift */; }; FD42ECD62E3308B5002D03EA /* ObservableKey+SessionUtilitiesKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD42ECD52E3308AC002D03EA /* ObservableKey+SessionUtilitiesKit.swift */; }; - FD42F9A8285064B800A0C77D /* PushNotificationAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBDE255A581900E217F9 /* PushNotificationAPI.swift */; }; FD432434299C6985008A0213 /* PendingReadReceipt.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD432433299C6985008A0213 /* PendingReadReceipt.swift */; }; FD47E0B12AA6A05800A55E41 /* Authentication+SessionMessagingKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD47E0B02AA6A05800A55E41 /* Authentication+SessionMessagingKit.swift */; }; FD47E0B52AA6D7AA00A55E41 /* Request+SnodeAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD47E0B42AA6D7AA00A55E41 /* Request+SnodeAPI.swift */; }; @@ -694,7 +674,7 @@ FD481A972CAE0AE000ECC4CF /* MockAppContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD481A932CAE0ADD00ECC4CF /* MockAppContext.swift */; }; FD481A992CB4CAAA00ECC4CF /* MockLibSessionCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F5B2CAA28CF00C0B51B /* MockLibSessionCache.swift */; }; FD481A9A2CB4CAE500ECC4CF /* CommonSMKMockExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F562CAA28CF00C0B51B /* CommonSMKMockExtensions.swift */; }; - FD481A9B2CB4CAF100ECC4CF /* CustomArgSummaryDescribable+SMK.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F572CAA28CF00C0B51B /* CustomArgSummaryDescribable+SMK.swift */; }; + FD481A9B2CB4CAF100ECC4CF /* CustomArgSummaryDescribable+SessionMessagingKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F572CAA28CF00C0B51B /* CustomArgSummaryDescribable+SessionMessagingKit.swift */; }; FD481A9C2CB4D58300ECC4CF /* MockSnodeAPICache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3765DE2AD8F03100DC1489 /* MockSnodeAPICache.swift */; }; FD481AA32CB889AE00ECC4CF /* RetrieveDefaultOpenGroupRoomsJobSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD481AA22CB889A400ECC4CF /* RetrieveDefaultOpenGroupRoomsJobSpec.swift */; }; FD49E2462B05C1D500FFBBB5 /* MockKeychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD49E2452B05C1D500FFBBB5 /* MockKeychain.swift */; }; @@ -703,9 +683,8 @@ FD49E2492B05C1D500FFBBB5 /* MockKeychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD49E2452B05C1D500FFBBB5 /* MockKeychain.swift */; }; FD4B200E283492210034334B /* InsetLockableTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD4B200D283492210034334B /* InsetLockableTableView.swift */; }; FD4BB22B2D63F20700D0DC3D /* MigrationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD4BB22A2D63F20600D0DC3D /* MigrationHelper.swift */; }; - FD4BB22C2D63FA8600D0DC3D /* (null) in Sources */ = {isa = PBXBuildFile; }; FD4C4E9C2B02E2A300C72199 /* DisplayPictureError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD4C4E9B2B02E2A300C72199 /* DisplayPictureError.swift */; }; - FD4C53AF2CC1D62E003B10F4 /* _021_ReworkRecipientState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD4C53AE2CC1D61E003B10F4 /* _021_ReworkRecipientState.swift */; }; + FD4C53AF2CC1D62E003B10F4 /* _035_ReworkRecipientState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD4C53AE2CC1D61E003B10F4 /* _035_ReworkRecipientState.swift */; }; FD52090328B4680F006098F6 /* RadioButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52090228B4680F006098F6 /* RadioButton.swift */; }; FD52090528B4915F006098F6 /* PrivacySettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52090428B4915F006098F6 /* PrivacySettingsViewModel.swift */; }; FD52CB5A2E12166F00A4DA70 /* OnboardingSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52CB592E12166D00A4DA70 /* OnboardingSpec.swift */; }; @@ -729,7 +708,6 @@ FD5E93D12C100FD70038C25A /* FileUploadResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4387127B5BB3B00C60D73 /* FileUploadResponse.swift */; }; FD5E93D22C12B0580038C25A /* AppVersionResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4383727B3863200C60D73 /* AppVersionResponse.swift */; }; FD61FCF92D308CC9005752DE /* GroupMemberSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD61FCF82D308CC5005752DE /* GroupMemberSpec.swift */; }; - FD61FCFB2D34A5EA005752DE /* _007_SplitSnodeReceivedMessageInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD61FCFA2D34A5DE005752DE /* _007_SplitSnodeReceivedMessageInfo.swift */; }; FD65318A2AA025C500DFEEAA /* TestDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6531892AA025C500DFEEAA /* TestDependencies.swift */; }; FD65318B2AA025C500DFEEAA /* TestDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6531892AA025C500DFEEAA /* TestDependencies.swift */; }; FD65318C2AA025C500DFEEAA /* TestDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6531892AA025C500DFEEAA /* TestDependencies.swift */; }; @@ -740,7 +718,7 @@ FD6673FD2D77F54600041530 /* ScreenLockViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6673FC2D77F54400041530 /* ScreenLockViewController.swift */; }; FD6673FF2D77F9C100041530 /* ScreenLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6673FE2D77F9BE00041530 /* ScreenLock.swift */; }; FD6674002D77F9FD00041530 /* ScreenLockWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52090828B59411006098F6 /* ScreenLockWindow.swift */; }; - FD66CB2A2BF3449B00268FAB /* SessionSnodeKit.xctestplan in Resources */ = {isa = PBXBuildFile; fileRef = FD66CB272BF3449B00268FAB /* SessionSnodeKit.xctestplan */; }; + FD66CB2A2BF3449B00268FAB /* SessionNetworkingKit.xctestplan in Resources */ = {isa = PBXBuildFile; fileRef = FD66CB272BF3449B00268FAB /* SessionNetworkingKit.xctestplan */; }; FD6A38E92C2A630E00762359 /* CocoaLumberjackSwift in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A38E82C2A630E00762359 /* CocoaLumberjackSwift */; }; FD6A38EC2C2A63B500762359 /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A38EB2C2A63B500762359 /* KeychainSwift */; }; FD6A38EF2C2A641200762359 /* DifferenceKit in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A38EE2C2A641200762359 /* DifferenceKit */; }; @@ -753,17 +731,80 @@ FD6A393B2C2AD3A300762359 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A393A2C2AD3A300762359 /* Nimble */; }; FD6A393D2C2AD3AC00762359 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A393C2C2AD3AC00762359 /* Nimble */; }; FD6A39412C2AD3B600762359 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A39402C2AD3B600762359 /* Nimble */; }; - FD6A7A6D2818C61500035AC1 /* _002_SetupStandardJobs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A7A6C2818C61500035AC1 /* _002_SetupStandardJobs.swift */; }; + FD6B928C2E779DCC004463B5 /* FileServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B928B2E779DC8004463B5 /* FileServer.swift */; }; + FD6B928E2E779E99004463B5 /* FileServerEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B928D2E779E95004463B5 /* FileServerEndpoint.swift */; }; + FD6B92902E779EDD004463B5 /* FileServerAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B928F2E779EDA004463B5 /* FileServerAPI.swift */; }; + FD6B92922E779FC8004463B5 /* SessionNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B92912E779FC6004463B5 /* SessionNetwork.swift */; }; + FD6B92942E77A003004463B5 /* SessionNetworkEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B92932E779FFE004463B5 /* SessionNetworkEndpoint.swift */; }; + FD6B92972E77A047004463B5 /* Price.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B92962E77A042004463B5 /* Price.swift */; }; + FD6B92992E77A06E004463B5 /* Token.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B92982E77A06C004463B5 /* Token.swift */; }; + FD6B929B2E77A084004463B5 /* NetworkInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B929A2E77A083004463B5 /* NetworkInfo.swift */; }; + FD6B929D2E77A096004463B5 /* Info.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B929C2E77A095004463B5 /* Info.swift */; }; + FD6B92A32E77A18B004463B5 /* SnodeAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B92A22E77A189004463B5 /* SnodeAPI.swift */; }; + FD6B92AB2E77A920004463B5 /* SOGS.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B92A92E77A8F8004463B5 /* SOGS.swift */; }; + FD6B92AC2E77A993004463B5 /* SOGSEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381F27B36ADC00C60D73 /* SOGSEndpoint.swift */; }; + FD6B92AD2E77A9F1004463B5 /* SOGSError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4380827B31D4E00C60D73 /* SOGSError.swift */; }; + FD6B92AE2E77A9F7004463B5 /* SOGSAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88FA7B726045D100049422F /* SOGSAPI.swift */; }; + FD6B92AF2E77AA03004463B5 /* HTTPQueryParam+SOGS.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8487E29405994007DCAE5 /* HTTPQueryParam+SOGS.swift */; }; + FD6B92B02E77AA03004463B5 /* Request+SOGS.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD02CC132C3677E6009AB976 /* Request+SOGS.swift */; }; + FD6B92B12E77AA03004463B5 /* HTTPHeader+SOGS.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8487D29405993007DCAE5 /* HTTPHeader+SOGS.swift */; }; + FD6B92B22E77AA03004463B5 /* UpdateTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682228A4C1210069F315 /* UpdateTypes.swift */; }; + FD6B92B32E77AA03004463B5 /* Personalization.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381627B32EC700C60D73 /* Personalization.swift */; }; + FD6B92B42E77AA11004463B5 /* PinnedMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4385E27B4C4A200C60D73 /* PinnedMessage.swift */; }; + FD6B92B52E77AA11004463B5 /* SendDirectMessageResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9C827D0487A005E1583 /* SendDirectMessageResponse.swift */; }; + FD6B92B62E77AA11004463B5 /* UserUnbanRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438A527BB113A00C60D73 /* UserUnbanRequest.swift */; }; + FD6B92B72E77AA11004463B5 /* UserModeratorRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438A927BB12BB00C60D73 /* UserModeratorRequest.swift */; }; + FD6B92B82E77AA11004463B5 /* SendSOGSMessageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4387727B5C35400C60D73 /* SendSOGSMessageRequest.swift */; }; + FD6B92B92E77AA11004463B5 /* Room.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4385C27B4C18900C60D73 /* Room.swift */; }; + FD6B92BA2E77AA11004463B5 /* RoomPollInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4386427B4DE7600C60D73 /* RoomPollInfo.swift */; }; + FD6B92BB2E77AA11004463B5 /* UpdateMessageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438CA27BB7DB100C60D73 /* UpdateMessageRequest.swift */; }; + FD6B92BC2E77AA11004463B5 /* ReactionResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682928B6F1420069F315 /* ReactionResponse.swift */; }; + FD6B92BD2E77AA11004463B5 /* UserBanRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438A327BB107F00C60D73 /* UserBanRequest.swift */; }; + FD6B92BE2E77AA11004463B5 /* DirectMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438C627BB6DF000C60D73 /* DirectMessage.swift */; }; + FD6B92BF2E77AA11004463B5 /* DeleteInboxResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD20C192A0A03AC003898FB /* DeleteInboxResponse.swift */; }; + FD6B92C02E77AA11004463B5 /* SendDirectMessageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438C827BB706500C60D73 /* SendDirectMessageRequest.swift */; }; + FD6B92C12E77AA11004463B5 /* CapabilitiesResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4386627B4E10E00C60D73 /* CapabilitiesResponse.swift */; }; + FD6B92C22E77AA11004463B5 /* SOGSMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4386227B4D94E00C60D73 /* SOGSMessage.swift */; }; + FD6B92C62E77AD0F004463B5 /* Crypto+FileServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B92C52E77AD0B004463B5 /* Crypto+FileServer.swift */; }; + FD6B92C82E77AD39004463B5 /* Crypto+SOGS.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B92C72E77AD35004463B5 /* Crypto+SOGS.swift */; }; + FD6B92CD2E77B22D004463B5 /* SOGSMessageSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2909027D709CA005DAE71 /* SOGSMessageSpec.swift */; }; + FD6B92CE2E77B234004463B5 /* RoomPollInfoSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2908827D70656005DAE71 /* RoomPollInfoSpec.swift */; }; + FD6B92CF2E77B234004463B5 /* RoomSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2908627D7047F005DAE71 /* RoomSpec.swift */; }; + FD6B92D02E77B23B004463B5 /* CapabilitiesResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B92CB2E77B1E5004463B5 /* CapabilitiesResponse.swift */; }; + FD6B92D12E77B253004463B5 /* SendSOGSMessageRequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2908A27D707F3005DAE71 /* SendSOGSMessageRequestSpec.swift */; }; + FD6B92D22E77B270004463B5 /* SendDirectMessageRequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2908E27D70938005DAE71 /* SendDirectMessageRequestSpec.swift */; }; + FD6B92D32E77B270004463B5 /* UpdateMessageRequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2908C27D70905005DAE71 /* UpdateMessageRequestSpec.swift */; }; + FD6B92D42E77B2C7004463B5 /* SOGSAPISpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4389927BA002500C60D73 /* SOGSAPISpec.swift */; }; + FD6B92D62E77B55D004463B5 /* SOGSEndpointSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2909327D710B4005DAE71 /* SOGSEndpointSpec.swift */; }; + FD6B92D72E77B55D004463B5 /* SOGSErrorSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2909527D71252005DAE71 /* SOGSErrorSpec.swift */; }; + FD6B92D82E77B55D004463B5 /* PersonalizationSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2909727D7129B005DAE71 /* PersonalizationSpec.swift */; }; + FD6B92DB2E77B597004463B5 /* CryptoSOGSAPISpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B92DA2E77B592004463B5 /* CryptoSOGSAPISpec.swift */; }; + FD6B92DE2E77BDE2004463B5 /* Authentication+SOGS.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B92DC2E77BB7E004463B5 /* Authentication+SOGS.swift */; }; + FD6B92E12E77C1E1004463B5 /* PushNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B92E02E77C1DC004463B5 /* PushNotification.swift */; }; + FD6B92E22E77C21D004463B5 /* PushNotificationAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBDE255A581900E217F9 /* PushNotificationAPI.swift */; }; + FD6B92E42E77C256004463B5 /* PushNotificationAPI+SessionMessagingKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B92E32E77C250004463B5 /* PushNotificationAPI+SessionMessagingKit.swift */; }; + FD6B92E62E77C5A2004463B5 /* Service.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D482A16EC20007267C7 /* Service.swift */; }; + FD6B92E72E77C5A2004463B5 /* Request+PushNotificationAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD02CC112C367761009AB976 /* Request+PushNotificationAPI.swift */; }; + FD6B92E82E77C5B7004463B5 /* PushNotificationEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D4F2A16EE50007267C7 /* PushNotificationEndpoint.swift */; }; + FD6B92E92E77C5D1004463B5 /* SubscribeResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D4A2A16ECBA007267C7 /* SubscribeResponse.swift */; }; + FD6B92EA2E77C5D1004463B5 /* NotificationMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFBB74C2A1F3C4E00CA7350 /* NotificationMetadata.swift */; }; + FD6B92EB2E77C5D1004463B5 /* AuthenticatedRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3765F32ADE5A0800DC1489 /* AuthenticatedRequest.swift */; }; + FD6B92EF2E77C5D1004463B5 /* UnsubscribeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D552A171FE4007267C7 /* UnsubscribeRequest.swift */; }; + FD6B92F02E77C5D1004463B5 /* SubscribeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D462A16E4CA007267C7 /* SubscribeRequest.swift */; }; + FD6B92F22E77C5D1004463B5 /* UnsubscribeResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D572A17207D007267C7 /* UnsubscribeResponse.swift */; }; + FD6B92F42E77C61A004463B5 /* ServiceInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3765F52ADE5BA500DC1489 /* ServiceInfo.swift */; }; + FD6B92F72E77C6D7004463B5 /* Crypto+PushNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6B92F62E77C6D3004463B5 /* Crypto+PushNotification.swift */; }; + FD6B92F82E77C725004463B5 /* ProcessResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD82C3E2A205D0A00425F05 /* ProcessResult.swift */; }; FD6C67242CF6E72E00B350A7 /* NoopSessionCallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6C67232CF6E72900B350A7 /* NoopSessionCallManager.swift */; }; FD6D9CF92CA152B300F706A8 /* Session+SNUIKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754BB2C9B9A8E002A2623 /* Session+SNUIKit.swift */; }; FD6DA9CF2D015B440092085A /* Lucide in Frameworks */ = {isa = PBXBuildFile; productRef = FD6DA9CE2D015B440092085A /* Lucide */; }; FD6DA9D22D0160F10092085A /* Lucide in Frameworks */ = {isa = PBXBuildFile; productRef = FD6DA9D12D0160F10092085A /* Lucide */; }; - FD6DF00B2ACFE40D0084BA4C /* _005_AddSnodeReveivedMessageInfoPrimaryKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6DF00A2ACFE40D0084BA4C /* _005_AddSnodeReveivedMessageInfoPrimaryKey.swift */; }; - FD6E4C8A2A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6E4C892A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift */; }; + FD6F5B5E2E657A24009A8D01 /* CancellationAwareAsyncStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6F5B5D2E657A24009A8D01 /* CancellationAwareAsyncStream.swift */; }; + FD6F5B602E657A33009A8D01 /* StreamLifecycleManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6F5B5F2E657A32009A8D01 /* StreamLifecycleManager.swift */; }; FD705A92278D051200F16121 /* ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A91278D051200F16121 /* ReusableView.swift */; }; - FD70F25C2DC1F184003729B7 /* _026_MessageDeduplicationTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD70F25B2DC1F176003729B7 /* _026_MessageDeduplicationTable.swift */; }; + FD70F25C2DC1F184003729B7 /* _040_MessageDeduplicationTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD70F25B2DC1F176003729B7 /* _040_MessageDeduplicationTable.swift */; }; FD7115EB28C5D78E00B47552 /* ThreadSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7115EA28C5D78E00B47552 /* ThreadSettingsViewModel.swift */; }; - FD7115F228C6CB3900B47552 /* _010_AddThreadIdToFTS.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7115F128C6CB3900B47552 /* _010_AddThreadIdToFTS.swift */; }; + FD7115F228C6CB3900B47552 /* _019_AddThreadIdToFTS.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7115F128C6CB3900B47552 /* _019_AddThreadIdToFTS.swift */; }; FD7115F428C71EB200B47552 /* ThreadDisappearingMessagesSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7115F328C71EB200B47552 /* ThreadDisappearingMessagesSettingsViewModel.swift */; }; FD7115F828C8151C00B47552 /* DisposableBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7115F728C8151C00B47552 /* DisposableBarButtonItem.swift */; }; FD7115FA28C8153400B47552 /* UIBarButtonItem+Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7115F928C8153400B47552 /* UIBarButtonItem+Combine.swift */; }; @@ -803,7 +844,7 @@ FD716E722850647600C96BF4 /* Data+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EF127BF6BA200510D0C /* Data+Utilities.swift */; }; FD72BDA12BE368C800CF6CF6 /* UIWindowLevel+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD72BDA02BE368C800CF6CF6 /* UIWindowLevel+Utilities.swift */; }; FD72BDA42BE3690B00CF6CF6 /* CryptoSMKSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD72BDA32BE3690B00CF6CF6 /* CryptoSMKSpec.swift */; }; - FD72BDA72BE369DC00CF6CF6 /* CryptoOpenGroupAPISpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD72BDA62BE369DC00CF6CF6 /* CryptoOpenGroupAPISpec.swift */; }; + FD72BDA72BE369DC00CF6CF6 /* CryptoOpenGroupSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD72BDA62BE369DC00CF6CF6 /* CryptoOpenGroupSpec.swift */; }; FD7443402D07A25C00862443 /* PushRegistrationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD74433F2D07A25C00862443 /* PushRegistrationManager.swift */; }; FD7443422D07A27E00862443 /* SyncPushTokensJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7443412D07A27E00862443 /* SyncPushTokensJob.swift */; }; FD74434A2D07CA9F00862443 /* Codable+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7443492D07CA9F00862443 /* Codable+Utilities.swift */; }; @@ -817,13 +858,12 @@ FD7692F72A53A2ED000E4B70 /* SessionThreadViewModelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7692F62A53A2ED000E4B70 /* SessionThreadViewModelSpec.swift */; }; FD7728962849E7E90018502F /* String+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7728952849E7E90018502F /* String+Utilities.swift */; }; FD7728982849E8110018502F /* UITableView+ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7728972849E8110018502F /* UITableView+ReusableView.swift */; }; - FD778B6429B189FF001BAC6B /* _014_GenerateInitialUserConfigDumps.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD778B6329B189FF001BAC6B /* _014_GenerateInitialUserConfigDumps.swift */; }; + FD778B6429B189FF001BAC6B /* _028_GenerateInitialUserConfigDumps.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD778B6329B189FF001BAC6B /* _028_GenerateInitialUserConfigDumps.swift */; }; FD78E9EE2DD6D32500D55B50 /* ImageDataManager+Singleton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB11A622DD5BDDD00BEF49F /* ImageDataManager+Singleton.swift */; }; - FD78E9F02DD6D61200D55B50 /* Data+Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754CB2C9BAF37002A2623 /* Data+Image.swift */; }; FD78E9F22DDA9EA200D55B50 /* MockImageDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD78E9F12DDA9E9B00D55B50 /* MockImageDataManager.swift */; }; FD78E9F42DDABA4F00D55B50 /* AttachmentUploader.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD78E9F32DDABA4200D55B50 /* AttachmentUploader.swift */; }; FD78E9F62DDD43AD00D55B50 /* Mutation.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD78E9F52DDD43AB00D55B50 /* Mutation.swift */; }; - FD78E9FA2DDD74D200D55B50 /* _027_MoveSettingsToLibSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD78E9F72DDD742100D55B50 /* _027_MoveSettingsToLibSession.swift */; }; + FD78E9FA2DDD74D200D55B50 /* _042_MoveSettingsToLibSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD78E9F72DDD742100D55B50 /* _042_MoveSettingsToLibSession.swift */; }; FD78E9FD2DDD97F200D55B50 /* Setting.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD78E9FC2DDD97F000D55B50 /* Setting.swift */; }; FD78EA022DDEBC3200D55B50 /* DebounceTaskManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD78EA012DDEBC2C00D55B50 /* DebounceTaskManager.swift */; }; FD78EA042DDEC3C500D55B50 /* MultiTaskManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD78EA032DDEC3C000D55B50 /* MultiTaskManager.swift */; }; @@ -831,7 +871,6 @@ FD78EA0A2DDFE45E00D55B50 /* Interaction+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD78EA092DDFE45900D55B50 /* Interaction+UI.swift */; }; FD78EA0B2DDFE45E00D55B50 /* Interaction+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD78EA092DDFE45900D55B50 /* Interaction+UI.swift */; }; FD78EA0D2DDFEDE200D55B50 /* LibSession+Local.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD78EA0C2DDFEDDF00D55B50 /* LibSession+Local.swift */; }; - FD7F74572BAA9D31006DDFD8 /* _006_DropSnodeCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7F74562BAA9D31006DDFD8 /* _006_DropSnodeCache.swift */; }; FD7F745B2BAAA35E006DDFD8 /* LibSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7F745A2BAAA35E006DDFD8 /* LibSession.swift */; }; FD7F745F2BAAA3B4006DDFD8 /* TypeConversion+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7F745E2BAAA3B4006DDFD8 /* TypeConversion+Utilities.swift */; }; FD83B9B327CF200A005E1583 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; platformFilter = ios; }; @@ -839,8 +878,7 @@ FD83B9BF27CF2294005E1583 /* TestConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9BD27CF2243005E1583 /* TestConstants.swift */; }; FD83B9C027CF2294005E1583 /* TestConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9BD27CF2243005E1583 /* TestConstants.swift */; }; FD83B9C527CF3E2A005E1583 /* OpenGroupSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9C427CF3E2A005E1583 /* OpenGroupSpec.swift */; }; - FD83B9C727CF3F10005E1583 /* CapabilitiesSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9C627CF3F10005E1583 /* CapabilitiesSpec.swift */; }; - FD83B9C927D0487A005E1583 /* SendDirectMessageResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9C827D0487A005E1583 /* SendDirectMessageResponse.swift */; }; + FD83B9C727CF3F10005E1583 /* CapabilitySpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9C627CF3F10005E1583 /* CapabilitySpec.swift */; }; FD83B9D227D59495005E1583 /* MockUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9D127D59495005E1583 /* MockUserDefaults.swift */; }; FD848B8B283DC509000E298B /* PagedDatabaseObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD848B8A283DC509000E298B /* PagedDatabaseObserver.swift */; }; FD848B8D283E0B26000E298B /* MessageInputTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD848B8C283E0B26000E298B /* MessageInputTypes.swift */; }; @@ -852,7 +890,7 @@ FD860CB62D66913F00BBE29C /* ThemePreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD860CB52D66913B00BBE29C /* ThemePreviewView.swift */; }; FD860CB82D66BC9900BBE29C /* AppIconViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD860CB72D66BC9500BBE29C /* AppIconViewModel.swift */; }; FD860CBA2D66BF2A00BBE29C /* AppIconGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD860CB92D66BF2300BBE29C /* AppIconGridView.swift */; }; - FD860CBC2D6E7A9F00BBE29C /* _024_FixBustedInteractionVariant.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD860CBB2D6E7A9400BBE29C /* _024_FixBustedInteractionVariant.swift */; }; + FD860CBC2D6E7A9F00BBE29C /* _038_FixBustedInteractionVariant.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD860CBB2D6E7A9400BBE29C /* _038_FixBustedInteractionVariant.swift */; }; FD860CBE2D6E7DAA00BBE29C /* DeveloperSettingsViewModel+Testing.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD860CBD2D6E7DA000BBE29C /* DeveloperSettingsViewModel+Testing.swift */; }; FD860CC92D6ED2ED00BBE29C /* DifferenceKit in Frameworks */ = {isa = PBXBuildFile; productRef = FD860CC82D6ED2ED00BBE29C /* DifferenceKit */; }; FD86FDA32BC5020600EC251B /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = FD86FDA22BC5020600EC251B /* PrivacyInfo.xcprivacy */; }; @@ -875,13 +913,11 @@ FD8A5B252DC05B16004C689B /* Number+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8A5B242DC05B16004C689B /* Number+Utilities.swift */; }; FD8A5B292DC060E2004C689B /* Double+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8A5B282DC060DD004C689B /* Double+Utilities.swift */; }; FD8A5B302DC18D61004C689B /* GeneralCacheSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8A5B2F2DC18D5E004C689B /* GeneralCacheSpec.swift */; }; - FD8A5B322DC191B4004C689B /* _025_DropLegacyClosedGroupKeyPairTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8A5B312DC191AB004C689B /* _025_DropLegacyClosedGroupKeyPairTable.swift */; }; - FD8A5B342DC1A732004C689B /* _008_ResetUserConfigLastHashes.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8A5B332DC1A726004C689B /* _008_ResetUserConfigLastHashes.swift */; }; + FD8A5B322DC191B4004C689B /* _039_DropLegacyClosedGroupKeyPairTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8A5B312DC191AB004C689B /* _039_DropLegacyClosedGroupKeyPairTable.swift */; }; FD8ECF7B29340FFD00C0D1BB /* LibSession+SessionMessagingKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF7A29340FFD00C0D1BB /* LibSession+SessionMessagingKit.swift */; }; - FD8ECF7D2934293A00C0D1BB /* _013_SessionUtilChanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF7C2934293A00C0D1BB /* _013_SessionUtilChanges.swift */; }; + FD8ECF7D2934293A00C0D1BB /* _027_SessionUtilChanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF7C2934293A00C0D1BB /* _027_SessionUtilChanges.swift */; }; FD8ECF7F2934298100C0D1BB /* ConfigDump.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF7E2934298100C0D1BB /* ConfigDump.swift */; }; FD8FD7622C37B7BD001E38C7 /* Position.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71162B28E1451400B47552 /* Position.swift */; }; - FD9004142818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9004132818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift */; }; FD9004152818B46300ABAAF6 /* JobRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B7432804EF1B004C14C5 /* JobRunner.swift */; }; FD9004162818B46700ABAAF6 /* JobRunnerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE77F68280F9EDA002CFC5D /* JobRunnerError.swift */; }; FD96F3A529DBC3DC00401309 /* MessageSendJobSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD96F3A429DBC3DC00401309 /* MessageSendJobSpec.swift */; }; @@ -931,7 +967,7 @@ FDB3DA8D2E24881B00148F8D /* ImageLoading+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB3DA8C2E24881200148F8D /* ImageLoading+Convenience.swift */; }; FDB4BBC72838B91E00B7C95D /* LinkPreviewError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB4BBC62838B91E00B7C95D /* LinkPreviewError.swift */; }; FDB5DAC12A9443A5002C8721 /* MessageSender+Groups.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB5DAC02A9443A5002C8721 /* MessageSender+Groups.swift */; }; - FDB5DAC72A9447E7002C8721 /* _022_GroupsRebuildChanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB5DAC62A9447E7002C8721 /* _022_GroupsRebuildChanges.swift */; }; + FDB5DAC72A9447E7002C8721 /* _036_GroupsRebuildChanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB5DAC62A9447E7002C8721 /* _036_GroupsRebuildChanges.swift */; }; FDB5DAD42A9483F3002C8721 /* GroupUpdateInviteMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB5DAD32A9483F3002C8721 /* GroupUpdateInviteMessage.swift */; }; FDB5DADA2A95D839002C8721 /* GroupUpdateInfoChangeMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB5DAD92A95D839002C8721 /* GroupUpdateInfoChangeMessage.swift */; }; FDB5DADC2A95D840002C8721 /* GroupUpdateMemberChangeMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB5DADB2A95D840002C8721 /* GroupUpdateMemberChangeMessage.swift */; }; @@ -955,72 +991,51 @@ FDB7400B28EB99A70094D718 /* TimeInterval+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB7400A28EB99A70094D718 /* TimeInterval+Utilities.swift */; }; FDB7400D28EBEC240094D718 /* DateHeaderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB7400C28EBEC240094D718 /* DateHeaderCell.swift */; }; FDBA8A842D597975007C19C0 /* FailedGroupInvitesAndPromotionsJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDBA8A832D59796F007C19C0 /* FailedGroupInvitesAndPromotionsJob.swift */; }; - FDBB25E32988B13800F1508E /* _004_AddJobPriority.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDBB25E22988B13800F1508E /* _004_AddJobPriority.swift */; }; FDBB25E72988BBBE00F1508E /* UIContextualAction+Theming.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDBB25E62988BBBD00F1508E /* UIContextualAction+Theming.swift */; }; FDBEE52E2B6A18B900C143A0 /* UserDefaultsConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDBEE52D2B6A18B900C143A0 /* UserDefaultsConfig.swift */; }; FDC0F0042BFECE12002CBFB9 /* TimeUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC0F0032BFECE12002CBFB9 /* TimeUnit.swift */; }; - FDC13D472A16E4CA007267C7 /* SubscribeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D462A16E4CA007267C7 /* SubscribeRequest.swift */; }; - FDC13D492A16EC20007267C7 /* Service.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D482A16EC20007267C7 /* Service.swift */; }; - FDC13D4B2A16ECBA007267C7 /* SubscribeResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D4A2A16ECBA007267C7 /* SubscribeResponse.swift */; }; - FDC13D502A16EE50007267C7 /* PushNotificationAPIEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D4F2A16EE50007267C7 /* PushNotificationAPIEndpoint.swift */; }; - FDC13D542A16FF29007267C7 /* LegacyGroupRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D532A16FF29007267C7 /* LegacyGroupRequest.swift */; }; - FDC13D562A171FE4007267C7 /* UnsubscribeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D552A171FE4007267C7 /* UnsubscribeRequest.swift */; }; - FDC13D582A17207D007267C7 /* UnsubscribeResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D572A17207D007267C7 /* UnsubscribeResponse.swift */; }; - FDC13D5A2A1721C5007267C7 /* LegacyNotifyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D592A1721C5007267C7 /* LegacyNotifyRequest.swift */; }; FDC1BD662CFD6C4F002CDC71 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC1BD652CFD6C4E002CDC71 /* Config.swift */; }; FDC1BD682CFE6EEB002CDC71 /* DeveloperSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC1BD672CFE6EEA002CDC71 /* DeveloperSettingsViewModel.swift */; }; FDC1BD6A2CFE7B6B002CDC71 /* DirectoryArchiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC1BD692CFE7B67002CDC71 /* DirectoryArchiver.swift */; }; FDC289472C881A3800020BC2 /* MutableIdentifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC289462C881A3800020BC2 /* MutableIdentifiable.swift */; }; - FDC2908727D7047F005DAE71 /* RoomSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2908627D7047F005DAE71 /* RoomSpec.swift */; }; - FDC2908927D70656005DAE71 /* RoomPollInfoSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2908827D70656005DAE71 /* RoomPollInfoSpec.swift */; }; - FDC2908B27D707F3005DAE71 /* SendMessageRequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2908A27D707F3005DAE71 /* SendMessageRequestSpec.swift */; }; - FDC2908D27D70905005DAE71 /* UpdateMessageRequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2908C27D70905005DAE71 /* UpdateMessageRequestSpec.swift */; }; - FDC2908F27D70938005DAE71 /* SendDirectMessageRequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2908E27D70938005DAE71 /* SendDirectMessageRequestSpec.swift */; }; - FDC2909127D709CA005DAE71 /* SOGSMessageSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2909027D709CA005DAE71 /* SOGSMessageSpec.swift */; }; - FDC2909427D710B4005DAE71 /* SOGSEndpointSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2909327D710B4005DAE71 /* SOGSEndpointSpec.swift */; }; - FDC2909627D71252005DAE71 /* SOGSErrorSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2909527D71252005DAE71 /* SOGSErrorSpec.swift */; }; - FDC2909827D7129B005DAE71 /* PersonalizationSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2909727D7129B005DAE71 /* PersonalizationSpec.swift */; }; FDC290A827D9B46D005DAE71 /* NimbleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC290A727D9B46D005DAE71 /* NimbleExtensions.swift */; }; FDC290A927D9B46D005DAE71 /* NimbleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC290A727D9B46D005DAE71 /* NimbleExtensions.swift */; }; - FDC4380927B31D4E00C60D73 /* OpenGroupAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4380827B31D4E00C60D73 /* OpenGroupAPIError.swift */; }; - FDC4381727B32EC700C60D73 /* Personalization.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381627B32EC700C60D73 /* Personalization.swift */; }; - FDC4382027B36ADC00C60D73 /* SOGSEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381F27B36ADC00C60D73 /* SOGSEndpoint.swift */; }; - FDC4382F27B383AF00C60D73 /* LegacyPushServerResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4382E27B383AF00C60D73 /* LegacyPushServerResponse.swift */; }; - FDC4385D27B4C18900C60D73 /* Room.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4385C27B4C18900C60D73 /* Room.swift */; }; - FDC4385F27B4C4A200C60D73 /* PinnedMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4385E27B4C4A200C60D73 /* PinnedMessage.swift */; }; - FDC4386327B4D94E00C60D73 /* SOGSMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4386227B4D94E00C60D73 /* SOGSMessage.swift */; }; - FDC4386527B4DE7600C60D73 /* RoomPollInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4386427B4DE7600C60D73 /* RoomPollInfo.swift */; }; - FDC4386727B4E10E00C60D73 /* Capabilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4386627B4E10E00C60D73 /* Capabilities.swift */; }; FDC4386C27B4E90300C60D73 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; }; - FDC4387827B5C35400C60D73 /* SendMessageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4387727B5C35400C60D73 /* SendMessageRequest.swift */; }; FDC4389227B9FFC700C60D73 /* SessionMessagingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */; platformFilter = ios; }; - FDC4389A27BA002500C60D73 /* OpenGroupAPISpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4389927BA002500C60D73 /* OpenGroupAPISpec.swift */; }; - FDC438A427BB107F00C60D73 /* UserBanRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438A327BB107F00C60D73 /* UserBanRequest.swift */; }; - FDC438A627BB113A00C60D73 /* UserUnbanRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438A527BB113A00C60D73 /* UserUnbanRequest.swift */; }; - FDC438AA27BB12BB00C60D73 /* UserModeratorRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438A927BB12BB00C60D73 /* UserModeratorRequest.swift */; }; - FDC438C727BB6DF000C60D73 /* DirectMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438C627BB6DF000C60D73 /* DirectMessage.swift */; }; - FDC438C927BB706500C60D73 /* SendDirectMessageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438C827BB706500C60D73 /* SendDirectMessageRequest.swift */; }; - FDC438CB27BB7DB100C60D73 /* UpdateMessageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438CA27BB7DB100C60D73 /* UpdateMessageRequest.swift */; }; FDC438CD27BC641200C60D73 /* Set+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438CC27BC641200C60D73 /* Set+Utilities.swift */; }; FDC498B92AC15FE300EDD897 /* AppNotificationAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC498B82AC15FE300EDD897 /* AppNotificationAction.swift */; }; FDC6D7602862B3F600B04575 /* Dependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC6D75F2862B3F600B04575 /* Dependencies.swift */; }; - FDCD2E032A41294E00964D6A /* LegacyGroupOnlyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCD2E022A41294E00964D6A /* LegacyGroupOnlyRequest.swift */; }; FDCDB8E02811007F00352A0C /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCDB8DF2811007F00352A0C /* HomeViewModel.swift */; }; FDD20C162A09E64A003898FB /* GetExpiriesRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD20C152A09E64A003898FB /* GetExpiriesRequest.swift */; }; FDD20C182A09E7D3003898FB /* GetExpiriesResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD20C172A09E7D3003898FB /* GetExpiriesResponse.swift */; }; - FDD20C1A2A0A03AC003898FB /* DeleteInboxResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD20C192A0A03AC003898FB /* DeleteInboxResponse.swift */; }; + FDD23ADF2E457CAA0057E853 /* _016_ThemePreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37E9F828A5F14A003AE748 /* _016_ThemePreferences.swift */; }; + FDD23AE02E457CD40057E853 /* _004_SNK_InitialSetupMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79F27F40CC800122BE0 /* _004_SNK_InitialSetupMigration.swift */; }; + FDD23AE12E457CDE0057E853 /* _005_SNK_SetupStandardJobs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A7A6C2818C61500035AC1 /* _005_SNK_SetupStandardJobs.swift */; }; + FDD23AE22E457CE50057E853 /* _008_SNK_YDBToGRDBMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7A327F40F8100122BE0 /* _008_SNK_YDBToGRDBMigration.swift */; }; + FDD23AE32E457CFE0057E853 /* _010_FlagMessageHashAsDeletedOrInvalid.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD39353528F7C3390084DADA /* _010_FlagMessageHashAsDeletedOrInvalid.swift */; }; + FDD23AE42E458C810057E853 /* _021_AddSnodeReveivedMessageInfoPrimaryKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6DF00A2ACFE40D0084BA4C /* _021_AddSnodeReveivedMessageInfoPrimaryKey.swift */; }; + FDD23AE52E458C940057E853 /* _022_DropSnodeCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7F74562BAA9D31006DDFD8 /* _022_DropSnodeCache.swift */; }; + FDD23AE62E458CAA0057E853 /* _023_SplitSnodeReceivedMessageInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD61FCFA2D34A5DE005752DE /* _023_SplitSnodeReceivedMessageInfo.swift */; }; + FDD23AE72E458DBC0057E853 /* _001_SUK_InitialSetupMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7C927F546D900122BE0 /* _001_SUK_InitialSetupMigration.swift */; }; + FDD23AE82E458DD40057E853 /* _002_SUK_SetupStandardJobs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9004132818AD0B00ABAAF6 /* _002_SUK_SetupStandardJobs.swift */; }; + FDD23AE92E458E020057E853 /* _003_SUK_YDBToGRDBMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7E627F6A16700122BE0 /* _003_SUK_YDBToGRDBMigration.swift */; }; + FDD23AEA2E458EB00057E853 /* _012_AddJobPriority.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDBB25E22988B13800F1508E /* _012_AddJobPriority.swift */; }; + FDD23AEB2E458F4D0057E853 /* _020_AddJobUniqueHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 945D9C572D6FDBE7003C4C0C /* _020_AddJobUniqueHash.swift */; }; + FDD23AEC2E458F980057E853 /* _024_ResetUserConfigLastHashes.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8A5B332DC1A726004C689B /* _024_ResetUserConfigLastHashes.swift */; }; + FDD23AED2E4590A10057E853 /* _041_RenameTableSettingToKeyValueStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947D7FE22D5181F400E8E413 /* _041_RenameTableSettingToKeyValueStore.swift */; }; + FDD23AEE2E459E470057E853 /* _001_SUK_InitialSetupMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7C927F546D900122BE0 /* _001_SUK_InitialSetupMigration.swift */; }; + FDD23AEF2E459EC90057E853 /* _012_AddJobPriority.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDBB25E22988B13800F1508E /* _012_AddJobPriority.swift */; }; + FDD23AF02E459EDD0057E853 /* _020_AddJobUniqueHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 945D9C572D6FDBE7003C4C0C /* _020_AddJobUniqueHash.swift */; }; FDD2506E283711D600198BDA /* DifferenceKit+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD2506D283711D600198BDA /* DifferenceKit+Utilities.swift */; }; FDD250722837234B00198BDA /* MediaGalleryNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */; }; - FDD82C3F2A205D0A00425F05 /* ProcessResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD82C3E2A205D0A00425F05 /* ProcessResult.swift */; }; - FDDD554E2C1FCB77006CBF03 /* _019_ScheduleAppUpdateCheckJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDD554D2C1FCB77006CBF03 /* _019_ScheduleAppUpdateCheckJob.swift */; }; + FDDD554E2C1FCB77006CBF03 /* _033_ScheduleAppUpdateCheckJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDD554D2C1FCB77006CBF03 /* _033_ScheduleAppUpdateCheckJob.swift */; }; FDDF074429C3E3D000E5E8B5 /* FetchRequest+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDF074329C3E3D000E5E8B5 /* FetchRequest+Utilities.swift */; }; FDDF074A29DAB36900E5E8B5 /* JobRunnerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDF074929DAB36900E5E8B5 /* JobRunnerSpec.swift */; }; FDE125232A837E4E002DA685 /* MainAppContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE125222A837E4E002DA685 /* MainAppContext.swift */; }; FDE33BBC2D5C124900E56F42 /* DispatchTimeInterval+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE33BBB2D5C124300E56F42 /* DispatchTimeInterval+Utilities.swift */; }; - FDE33BBE2D5C3AF100E56F42 /* _023_GroupsExpiredFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE33BBD2D5C3AE800E56F42 /* _023_GroupsExpiredFlag.swift */; }; + FDE33BBE2D5C3AF100E56F42 /* _037_GroupsExpiredFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE33BBD2D5C3AE800E56F42 /* _037_GroupsExpiredFlag.swift */; }; FDE519F72AB7CDC700450C53 /* Result+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE519F62AB7CDC700450C53 /* Result+Utilities.swift */; }; FDE5218E2E03A06B00061B8E /* AttachmentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE5218D2E03A06700061B8E /* AttachmentManager.swift */; }; - FDE521902E04CCEB00061B8E /* AVURLAsset+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE5218F2E04CCE600061B8E /* AVURLAsset+Utilities.swift */; }; FDE521942E050B1100061B8E /* DismissCallbackAVPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE521932E050B0800061B8E /* DismissCallbackAVPlayerViewController.swift */; }; FDE5219A2E08DBB800061B8E /* ImageLoading+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE521992E08DBB000061B8E /* ImageLoading+Convenience.swift */; }; FDE5219C2E08E76C00061B8E /* SessionAsyncImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE5219B2E08E76600061B8E /* SessionAsyncImage.swift */; }; @@ -1029,9 +1044,15 @@ FDE521A22E0D23AB00061B8E /* ObservableKey+SessionMessagingKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE521A12E0D23A200061B8E /* ObservableKey+SessionMessagingKit.swift */; }; FDE521A62E0E6C8C00061B8E /* MockNotificationsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F5C2CAA28CF00C0B51B /* MockNotificationsManager.swift */; }; FDE6E99829F8E63A00F93C5D /* Accessibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE6E99729F8E63A00F93C5D /* Accessibility.swift */; }; + FDE71B032E77CCEE0023F5F9 /* HTTPHeader+FileServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE71B022E77CCE90023F5F9 /* HTTPHeader+FileServer.swift */; }; + FDE71B052E77E1AA0023F5F9 /* ObservableKey+SessionNetworkingKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE71B042E77E1A00023F5F9 /* ObservableKey+SessionNetworkingKit.swift */; }; + FDE71B0B2E79352D0023F5F9 /* DeveloperSettingsGroupsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE71B0A2E7935250023F5F9 /* DeveloperSettingsGroupsViewModel.swift */; }; + FDE71B0D2E793B250023F5F9 /* DeveloperSettingsProViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE71B0C2E793B1F0023F5F9 /* DeveloperSettingsProViewModel.swift */; }; + FDE71B0F2E7A195B0023F5F9 /* Session - Anonymous Messenger.storekit in Resources */ = {isa = PBXBuildFile; fileRef = FDE71B0E2E7A195B0023F5F9 /* Session - Anonymous Messenger.storekit */; }; + FDE71B5F2E7A73570023F5F9 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FDE71B5E2E7A73560023F5F9 /* StoreKit.framework */; }; FDE7549B2C940108002A2623 /* MessageViewModel+DeletionActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE7549A2C940108002A2623 /* MessageViewModel+DeletionActions.swift */; }; FDE7549D2C9961A4002A2623 /* CommunityPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE7549C2C9961A4002A2623 /* CommunityPoller.swift */; }; - FDE754A12C9A60A6002A2623 /* Crypto+OpenGroupAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754A02C9A60A6002A2623 /* Crypto+OpenGroupAPI.swift */; }; + FDE754A12C9A60A6002A2623 /* Crypto+OpenGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754A02C9A60A6002A2623 /* Crypto+OpenGroup.swift */; }; FDE754A32C9A8FD1002A2623 /* SwarmPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754A22C9A8FD1002A2623 /* SwarmPoller.swift */; }; FDE754A82C9B964D002A2623 /* MessageReceiverGroupsSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754A42C9B964D002A2623 /* MessageReceiverGroupsSpec.swift */; }; FDE754A92C9B964D002A2623 /* MessageSenderGroupsSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754A52C9B964D002A2623 /* MessageSenderGroupsSpec.swift */; }; @@ -1058,7 +1079,7 @@ FDE754DE2C9BAF8A002A2623 /* Crypto+SessionUtilitiesKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754D82C9BAF89002A2623 /* Crypto+SessionUtilitiesKit.swift */; }; FDE754DF2C9BAF8A002A2623 /* KeyPair.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754D92C9BAF89002A2623 /* KeyPair.swift */; }; FDE754E02C9BAF8A002A2623 /* Hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754DA2C9BAF8A002A2623 /* Hex.swift */; }; - FDE754E32C9BAFF4002A2623 /* Crypto+SessionSnodeKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754E12C9BAFF4002A2623 /* Crypto+SessionSnodeKit.swift */; }; + FDE754E32C9BAFF4002A2623 /* Crypto+SessionNetworkingKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754E12C9BAFF4002A2623 /* Crypto+SessionNetworkingKit.swift */; }; FDE754E52C9BB012002A2623 /* BezierPathView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754E42C9BB012002A2623 /* BezierPathView.swift */; }; FDE754E72C9BB051002A2623 /* OWSViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754E62C9BB051002A2623 /* OWSViewController.swift */; }; FDE754F02C9BB08B002A2623 /* Crypto+Attachments.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754EC2C9BB08B002A2623 /* Crypto+Attachments.swift */; }; @@ -1067,9 +1088,9 @@ FDE754F82C9BB0B0002A2623 /* UserNotificationConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754F32C9BB0AF002A2623 /* UserNotificationConfig.swift */; }; FDE754F92C9BB0B0002A2623 /* NotificationActionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754F42C9BB0AF002A2623 /* NotificationActionHandler.swift */; }; FDE754FA2C9BB0B0002A2623 /* NotificationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754F52C9BB0AF002A2623 /* NotificationPresenter.swift */; }; - FDE754FE2C9BB0D0002A2623 /* Threading+SMK.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754FD2C9BB0D0002A2623 /* Threading+SMK.swift */; }; + FDE754FE2C9BB0D0002A2623 /* Threading+SessionMessagingKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754FD2C9BB0D0002A2623 /* Threading+SessionMessagingKit.swift */; }; FDE755002C9BB0FA002A2623 /* SessionEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754FF2C9BB0FA002A2623 /* SessionEnvironment.swift */; }; - FDE755022C9BB122002A2623 /* _011_AddPendingReadReceipts.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE755012C9BB122002A2623 /* _011_AddPendingReadReceipts.swift */; }; + FDE755022C9BB122002A2623 /* _025_AddPendingReadReceipts.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE755012C9BB122002A2623 /* _025_AddPendingReadReceipts.swift */; }; FDE755052C9BB4EE002A2623 /* BencodeDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE755032C9BB4ED002A2623 /* BencodeDecoder.swift */; }; FDE755062C9BB4EE002A2623 /* Bencode.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE755042C9BB4ED002A2623 /* Bencode.swift */; }; FDE755182C9BC169002A2623 /* UIAlertAction+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE755132C9BC169002A2623 /* UIAlertAction+Utilities.swift */; }; @@ -1080,17 +1101,11 @@ FDE755202C9BC1A6002A2623 /* CacheConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE7551F2C9BC1A6002A2623 /* CacheConfig.swift */; }; FDE755222C9BC1BA002A2623 /* LibSessionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE755212C9BC1BA002A2623 /* LibSessionError.swift */; }; FDE755242C9BC1D1002A2623 /* Publisher+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE755232C9BC1D1002A2623 /* Publisher+Utilities.swift */; }; - FDEF57212C3CF03A00131302 /* (null) in Sources */ = {isa = PBXBuildFile; }; - FDEF57222C3CF03D00131302 /* (null) in Sources */ = {isa = PBXBuildFile; }; - FDEF57232C3CF04300131302 /* (null) in Sources */ = {isa = PBXBuildFile; }; - FDEF57242C3CF04700131302 /* (null) in Sources */ = {isa = PBXBuildFile; }; - FDEF57252C3CF04C00131302 /* (null) in Sources */ = {isa = PBXBuildFile; }; - FDEF57262C3CF05F00131302 /* (null) in Sources */ = {isa = PBXBuildFile; }; FDEF573E2C40F2A100131302 /* GroupUpdateMemberLeftNotificationMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDEF573D2C40F2A100131302 /* GroupUpdateMemberLeftNotificationMessage.swift */; }; FDEF57712C44D2D300131302 /* GeoLite2-Country-Blocks-IPv4 in Resources */ = {isa = PBXBuildFile; fileRef = FDEF57702C44D2D300131302 /* GeoLite2-Country-Blocks-IPv4 */; }; FDF01FAD2A9ECC4200CAF969 /* SingletonConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF01FAC2A9ECC4200CAF969 /* SingletonConfig.swift */; }; FDF0B73C27FFD3D6004C14C5 /* LinkPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B73B27FFD3D6004C14C5 /* LinkPreview.swift */; }; - FDF0B7422804EA4F004C14C5 /* _002_SetupStandardJobs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B7412804EA4F004C14C5 /* _002_SetupStandardJobs.swift */; }; + FDF0B7422804EA4F004C14C5 /* _007_SMK_SetupStandardJobs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B7412804EA4F004C14C5 /* _007_SMK_SetupStandardJobs.swift */; }; FDF0B74928060D13004C14C5 /* QuotedReplyModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B74828060D13004C14C5 /* QuotedReplyModel.swift */; }; FDF0B74B28061F7A004C14C5 /* InteractionAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B74A28061F7A004C14C5 /* InteractionAttachment.swift */; }; FDF0B7512807BA56004C14C5 /* NotificationsManagerType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B7502807BA56004C14C5 /* NotificationsManagerType.swift */; }; @@ -1104,14 +1119,11 @@ FDF2220F281B55E6000A4995 /* QueryInterfaceRequest+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF2220E281B55E6000A4995 /* QueryInterfaceRequest+Utilities.swift */; }; FDF22211281B5E0B000A4995 /* TableRecord+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF22210281B5E0B000A4995 /* TableRecord+Utilities.swift */; }; FDF2F0222DAE1AF500491E8A /* MessageReceiver+LegacyClosedGroups.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF2F0212DAE1AEF00491E8A /* MessageReceiver+LegacyClosedGroups.swift */; }; - FDF40CDE2897A1BC006A0CC4 /* _004_RemoveLegacyYDB.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF40CDD2897A1BC006A0CC4 /* _004_RemoveLegacyYDB.swift */; }; + FDF40CDE2897A1BC006A0CC4 /* _011_RemoveLegacyYDB.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF40CDD2897A1BC006A0CC4 /* _011_RemoveLegacyYDB.swift */; }; FDF71EA32B072C2800A8D6B5 /* LibSessionMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF71EA22B072C2800A8D6B5 /* LibSessionMessage.swift */; }; FDF71EA52B07363500A8D6B5 /* MessageReceiver+LibSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF71EA42B07363500A8D6B5 /* MessageReceiver+LibSession.swift */; }; - FDF8487F29405994007DCAE5 /* HTTPHeader+OpenGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8487D29405993007DCAE5 /* HTTPHeader+OpenGroup.swift */; }; - FDF8488029405994007DCAE5 /* HTTPQueryParam+OpenGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8487E29405994007DCAE5 /* HTTPQueryParam+OpenGroup.swift */; }; FDF8488929405B27007DCAE5 /* Data+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFD645A27F26D4600808CA1 /* Data+Utilities.swift */; }; FDF8489129405C13007DCAE5 /* SnodeAPINamespace.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8489029405C13007DCAE5 /* SnodeAPINamespace.swift */; }; - FDF8489429405C1B007DCAE5 /* SnodeAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8489329405C1B007DCAE5 /* SnodeAPI.swift */; }; FDF848BC29405C5A007DCAE5 /* SnodeRecursiveResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8489A29405C5A007DCAE5 /* SnodeRecursiveResponse.swift */; }; FDF848BD29405C5A007DCAE5 /* GetMessagesRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8489B29405C5A007DCAE5 /* GetMessagesRequest.swift */; }; FDF848BF29405C5A007DCAE5 /* SnodeResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8489D29405C5A007DCAE5 /* SnodeResponse.swift */; }; @@ -1147,17 +1159,18 @@ FDF848F329413DB0007DCAE5 /* ImagePickerHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848F229413DB0007DCAE5 /* ImagePickerHandler.swift */; }; FDF848F529413EEC007DCAE5 /* SessionCell+Styling.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848F429413EEC007DCAE5 /* SessionCell+Styling.swift */; }; FDF848F729414477007DCAE5 /* CurrentUserPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848F629414477007DCAE5 /* CurrentUserPoller.swift */; }; - FDFBB74D2A1F3C4E00CA7350 /* NotificationMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFBB74C2A1F3C4E00CA7350 /* NotificationMetadata.swift */; }; FDFC4D9A29F0C51500992FB6 /* String+Trimming.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D22553860900C340D1 /* String+Trimming.swift */; }; FDFD645D27F273F300808CA1 /* MockGeneralCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFD645C27F273F300808CA1 /* MockGeneralCache.swift */; }; FDFDE124282D04F20098B17F /* MediaDismissAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFDE123282D04F20098B17F /* MediaDismissAnimationController.swift */; }; FDFDE126282D05380098B17F /* MediaInteractiveDismiss.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFDE125282D05380098B17F /* MediaInteractiveDismiss.swift */; }; FDFDE128282D05530098B17F /* MediaPresentationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFDE127282D05530098B17F /* MediaPresentationContext.swift */; }; FDFDE12A282D056B0098B17F /* MediaZoomAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFDE129282D056B0098B17F /* MediaZoomAnimationController.swift */; }; - FDFE75B12ABD2D2400655640 /* _016_MakeBrokenProfileTimestampsNullable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFE75B02ABD2D2400655640 /* _016_MakeBrokenProfileTimestampsNullable.swift */; }; + FDFE75B12ABD2D2400655640 /* _030_MakeBrokenProfileTimestampsNullable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFE75B02ABD2D2400655640 /* _030_MakeBrokenProfileTimestampsNullable.swift */; }; FDFE75B42ABD46B600655640 /* MockUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9D127D59495005E1583 /* MockUserDefaults.swift */; }; FDFE75B52ABD46B700655640 /* MockUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9D127D59495005E1583 /* MockUserDefaults.swift */; }; FDFF9FDF2A787F57005E0628 /* JSONEncoder+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFF9FDE2A787F57005E0628 /* JSONEncoder+Utilities.swift */; }; + FED288F32E4C28CF00C31171 /* AppReviewPromptDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED288F22E4C28CF00C31171 /* AppReviewPromptDialog.swift */; }; + FED288F82E4C3BE100C31171 /* AppReviewPromptModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED288F72E4C3BE100C31171 /* AppReviewPromptModel.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -1359,7 +1372,7 @@ files = ( C3C2A681255388CC00C340D1 /* SessionUtilitiesKit.framework in Embed Frameworks */, C33FD9B3255A548A00E217F9 /* SignalUtilitiesKit.framework in Embed Frameworks */, - C3C2A5A7255385C100C340D1 /* SessionSnodeKit.framework in Embed Frameworks */, + C3C2A5A7255385C100C340D1 /* SessionNetworkingKit.framework in Embed Frameworks */, C331FF232558F9D300070591 /* SessionUIKit.framework in Embed Frameworks */, C3C2A6F825539DE700C340D1 /* SessionMessagingKit.framework in Embed Frameworks */, ); @@ -1461,7 +1474,7 @@ 7B4EF2592934743000CB351D /* SessionTableViewTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionTableViewTitleView.swift; sourceTree = ""; }; 7B50D64C28AC7CF80086CCEC /* silence.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = silence.aiff; sourceTree = ""; }; 7B5233C32900E90F00F8F375 /* SessionLabelCarouselView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionLabelCarouselView.swift; sourceTree = ""; }; - 7B5233C5290636D700F8F375 /* _018_DisappearingMessagesConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _018_DisappearingMessagesConfiguration.swift; sourceTree = ""; }; + 7B5233C5290636D700F8F375 /* _032_DisappearingMessagesConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _032_DisappearingMessagesConfiguration.swift; sourceTree = ""; }; 7B5802982AAEF1B50050EEB1 /* OpenGroupInvitationView_SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupInvitationView_SwiftUI.swift; sourceTree = ""; }; 7B7037422834B81F000DCF35 /* ReactionContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionContainerView.swift; sourceTree = ""; }; 7B7037442834BCC0000DCF35 /* ReactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionView.swift; sourceTree = ""; }; @@ -1470,7 +1483,7 @@ 7B7CB18F270FB2150079FF93 /* MiniCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MiniCallView.swift; sourceTree = ""; }; 7B7CB191271508AD0079FF93 /* CallRingTonePlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallRingTonePlayer.swift; sourceTree = ""; }; 7B81682228A4C1210069F315 /* UpdateTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateTypes.swift; sourceTree = ""; }; - 7B81682728B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _007_HomeQueryOptimisationIndexes.swift; sourceTree = ""; }; + 7B81682728B310D50069F315 /* _015_HomeQueryOptimisationIndexes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _015_HomeQueryOptimisationIndexes.swift; sourceTree = ""; }; 7B81682928B6F1420069F315 /* ReactionResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionResponse.swift; sourceTree = ""; }; 7B81682B28B72F480069F315 /* PendingChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PendingChange.swift; sourceTree = ""; }; 7B81FB582AB01AA8002FB267 /* LoadingIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingIndicatorView.swift; sourceTree = ""; }; @@ -1493,7 +1506,7 @@ 7BA68908272A27BE00EFC32F /* SessionCall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionCall.swift; sourceTree = ""; }; 7BA6890C27325CCC00EFC32F /* SessionCallManager+CXCallController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionCallManager+CXCallController.swift"; sourceTree = ""; }; 7BA6890E27325CE300EFC32F /* SessionCallManager+CXProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionCallManager+CXProvider.swift"; sourceTree = ""; }; - 7BAA7B6528D2DE4700AE1489 /* _009_OpenGroupPermission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _009_OpenGroupPermission.swift; sourceTree = ""; }; + 7BAA7B6528D2DE4700AE1489 /* _018_OpenGroupPermission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _018_OpenGroupPermission.swift; sourceTree = ""; }; 7BAADFCB27B0EF23007BCF92 /* CallVideoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallVideoView.swift; sourceTree = ""; }; 7BAADFCD27B215FE007BCF92 /* UIView+Draggable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Draggable.swift"; sourceTree = ""; }; 7BAF54CC27ACCEEC003D12F8 /* GlobalSearchViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlobalSearchViewController.swift; sourceTree = ""; }; @@ -1521,7 +1534,6 @@ 9409433F2C7ED62300D9D2E0 /* StartupError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartupError.swift; sourceTree = ""; }; 941375BA2D5184B60058F244 /* HTTPHeader+SessionNetwork.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HTTPHeader+SessionNetwork.swift"; sourceTree = ""; }; 941375BC2D5195F30058F244 /* KeyValueStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyValueStore.swift; sourceTree = ""; }; - 941375BE2D5196D10058F244 /* Number+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Number+Utilities.swift"; sourceTree = ""; }; 9422567D2C23F8BB00C0FDBF /* StartConversationScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StartConversationScreen.swift; sourceTree = ""; }; 9422567E2C23F8BB00C0FDBF /* NewMessageScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewMessageScreen.swift; sourceTree = ""; }; 9422567F2C23F8BB00C0FDBF /* InviteAFriendScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InviteAFriendScreen.swift; sourceTree = ""; }; @@ -1542,46 +1554,48 @@ 942256A02C23F90700C0FDBF /* CustomTopTabBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomTopTabBar.swift; sourceTree = ""; }; 9422EE2A2B8C3A97004C740D /* String+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Utilities.swift"; sourceTree = ""; }; 942ADDD32D9F960C006E0BB0 /* NewTagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTagView.swift; sourceTree = ""; }; + 942BA9BE2E4ABB9F007C4595 /* _045_LastProfileUpdateTimestamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _045_LastProfileUpdateTimestamp.swift; sourceTree = ""; }; 94367C422C6C828500814252 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; 943C6D812B75E061004ACE64 /* Message+DisappearingMessages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Message+DisappearingMessages.swift"; sourceTree = ""; }; 943C6D832B86B5F1004ACE64 /* Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Localization.swift; sourceTree = ""; }; - 945D9C572D6FDBE7003C4C0C /* _005_AddJobUniqueHash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _005_AddJobUniqueHash.swift; sourceTree = ""; }; + 945D9C572D6FDBE7003C4C0C /* _020_AddJobUniqueHash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _020_AddJobUniqueHash.swift; sourceTree = ""; }; 9471CAA72CACFB4E00090FB7 /* GenerateLicenses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerateLicenses.swift; sourceTree = ""; }; 9473386D2BDF5F3E00B9E169 /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = ""; }; 9479981B2DD44AC5008F5CD5 /* ThreadNotificationSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadNotificationSettingsViewModel.swift; sourceTree = ""; }; 947AD68F2C8968FF000B2730 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; 947D7FCF2D509FC900E8E413 /* SessionNetworkAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionNetworkAPI.swift; sourceTree = ""; }; - 947D7FD02D509FC900E8E413 /* SessionNetworkAPI+Database.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionNetworkAPI+Database.swift"; sourceTree = ""; }; - 947D7FD12D509FC900E8E413 /* SessionNetworkAPI+Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionNetworkAPI+Models.swift"; sourceTree = ""; }; - 947D7FD22D509FC900E8E413 /* SessionNetworkAPI+Network.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionNetworkAPI+Network.swift"; sourceTree = ""; }; + 947D7FD02D509FC900E8E413 /* KeyValueStore+SessionNetwork.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KeyValueStore+SessionNetwork.swift"; sourceTree = ""; }; + 947D7FD22D509FC900E8E413 /* HTTPClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPClient.swift; sourceTree = ""; }; 947D7FD92D5180F200E8E413 /* SessionNetworkScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionNetworkScreen.swift; sourceTree = ""; }; 947D7FDA2D5180F200E8E413 /* SessionNetworkScreen+Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionNetworkScreen+Models.swift"; sourceTree = ""; }; 947D7FDB2D5180F200E8E413 /* SessionNetworkScreen+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionNetworkScreen+ViewModel.swift"; sourceTree = ""; }; - 947D7FE22D5181F400E8E413 /* _006_RenameTableSettingToKeyValueStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _006_RenameTableSettingToKeyValueStore.swift; sourceTree = ""; }; + 947D7FE22D5181F400E8E413 /* _041_RenameTableSettingToKeyValueStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _041_RenameTableSettingToKeyValueStore.swift; sourceTree = ""; }; 947D7FE42D51837200E8E413 /* ArrowCapsule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrowCapsule.swift; sourceTree = ""; }; 947D7FE52D51837200E8E413 /* PopoverView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopoverView.swift; sourceTree = ""; }; 947D7FE62D51837200E8E413 /* Text+CopyButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Text+CopyButton.swift"; sourceTree = ""; }; 9499E6022DDD9BEE00091434 /* ExpandableLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandableLabel.swift; sourceTree = ""; }; 9499E68A2DF92F3B00091434 /* ThreadNotificationSettingsViewModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadNotificationSettingsViewModelSpec.swift; sourceTree = ""; }; - 94A6B9DA2DD6BF6E00DB4B44 /* Constants+URLs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Constants+URLs.swift"; sourceTree = ""; }; + 949D91212E822D520074F595 /* String+SessionProBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+SessionProBadge.swift"; sourceTree = ""; }; + 94A6B9DA2DD6BF6E00DB4B44 /* Constants+Apple.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Constants+Apple.swift"; sourceTree = ""; }; 94AAB14A2E1E197800A6FA18 /* Modal+SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Modal+SwiftUI.swift"; sourceTree = ""; }; 94AAB14C2E1F39AB00A6FA18 /* ProCTAModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProCTAModal.swift; sourceTree = ""; }; 94AAB14E2E1F6CB300A6FA18 /* SessionProBadge+SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionProBadge+SwiftUI.swift"; sourceTree = ""; }; 94AAB1502E1F752600A6FA18 /* CyclicGradientView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CyclicGradientView.swift; sourceTree = ""; }; 94AAB1522E1F8AD900A6FA18 /* ShineButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShineButton.swift; sourceTree = ""; }; 94AAB1562E24BD3700A6FA18 /* PinnedConversationsCTA.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = PinnedConversationsCTA.webp; sourceTree = ""; }; + 94AAB15B2E24C97400A6FA18 /* AnimatedProfileCTA.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = AnimatedProfileCTA.webp; sourceTree = ""; }; 94B3DC162AF8592200C88531 /* QuoteView_SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuoteView_SwiftUI.swift; sourceTree = ""; }; - 94B6BAF92E38454F00E718BB /* SessionProState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionProState.swift; sourceTree = ""; }; + 94B6BAF52E30A88800E718BB /* SessionProState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionProState.swift; sourceTree = ""; }; + 94B6BB052E431B6300E718BB /* AnimatedProfileCTAAnimationCropped.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = AnimatedProfileCTAAnimationCropped.webp; sourceTree = ""; }; 94C58AC82D2E036E00609195 /* Permissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Permissions.swift; sourceTree = ""; }; 94CD95BA2E08D9D40097754D /* SessionProBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionProBadge.swift; sourceTree = ""; }; 94CD95BC2E0908340097754D /* LibSession+Pro.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LibSession+Pro.swift"; sourceTree = ""; }; - 94CD95C02E0CBF1C0097754D /* _029_AddProMessageFlag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _029_AddProMessageFlag.swift; sourceTree = ""; }; + 94CD95C02E0CBF1C0097754D /* _044_AddProMessageFlag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _044_AddProMessageFlag.swift; sourceTree = ""; }; 94CD962A2E1B85920097754D /* InputTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputTextView.swift; sourceTree = ""; }; 94CD962B2E1B85920097754D /* InputViewButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputViewButton.swift; sourceTree = ""; }; 94CD962F2E1B88430097754D /* CGRect+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGRect+Utilities.swift"; sourceTree = ""; }; 94CD96312E1B88C20097754D /* ExpandingAttachmentsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandingAttachmentsButton.swift; sourceTree = ""; }; 94CD963C2E1BABE90097754D /* GenericCTA.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = GenericCTA.webp; sourceTree = ""; }; - 94CD963D2E1BABE90097754D /* GenericCTAAnimation.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = GenericCTAAnimation.webp; sourceTree = ""; }; 94CD963E2E1BABE90097754D /* HigherCharLimitCTA.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = HigherCharLimitCTA.webp; sourceTree = ""; }; 94E9BC0C2C7BFBDA006984EA /* Localization+Style.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Localization+Style.swift"; sourceTree = ""; }; A11CD70C17FA230600A2D1B1 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; @@ -1622,7 +1636,7 @@ B879D44A247E1D9200DB3608 /* PathStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathStatusView.swift; sourceTree = ""; }; B885D5F52334A32100EE0D8E /* UIView+Constraints.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Constraints.swift"; sourceTree = ""; }; B886B4A82398BA1500211ABE /* QRCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCode.swift; sourceTree = ""; }; - B88FA7B726045D100049422F /* OpenGroupAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupAPI.swift; sourceTree = ""; }; + B88FA7B726045D100049422F /* SOGSAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOGSAPI.swift; sourceTree = ""; }; B88FA7F1260C3EB10049422F /* OpenGroupSuggestionGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupSuggestionGrid.swift; sourceTree = ""; }; B893063E2383961A005EAA8E /* ScanQRCodeWrapperVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanQRCodeWrapperVC.swift; sourceTree = ""; }; B894D0742339EDCF00B4D94D /* NukeDataModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NukeDataModal.swift; sourceTree = ""; }; @@ -1743,13 +1757,11 @@ C3AAFFF125AE99710089E6DD /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; C3ADC66026426688005F1414 /* ShareNavController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareNavController.swift; sourceTree = ""; }; C3BBE0C62554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FixedWidthInteger+BigEndian.swift"; sourceTree = ""; }; - C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SessionSnodeKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - C3C2A5A1255385C100C340D1 /* SessionSnodeKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SessionSnodeKit.h; sourceTree = ""; }; + C3C2A59F255385C100C340D1 /* SessionNetworkingKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SessionNetworkingKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C3C2A5A1255385C100C340D1 /* SessionNetworkingKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SessionNetworkingKit.h; sourceTree = ""; }; C3C2A5A2255385C100C340D1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - C3C2A5B9255385ED00C340D1 /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; C3C2A5CE2553860700C340D1 /* Logging.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = ""; }; C3C2A5D22553860900C340D1 /* String+Trimming.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Trimming.swift"; sourceTree = ""; }; - C3C2A5D42553860A00C340D1 /* Threading+SSK.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Threading+SSK.swift"; sourceTree = ""; }; C3C2A5D52553860A00C340D1 /* Dictionary+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Dictionary+Utilities.swift"; sourceTree = ""; }; C3C2A5D82553860B00C340D1 /* Data+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Data+Utilities.swift"; sourceTree = ""; }; C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SessionUtilitiesKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1803,10 +1815,10 @@ FD01504D2CA243E7005B08A1 /* TypeConversionUtilitiesSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypeConversionUtilitiesSpec.swift; sourceTree = ""; }; FD0150572CA27DEE005B08A1 /* ScrollableLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollableLabel.swift; sourceTree = ""; }; FD02CC112C367761009AB976 /* Request+PushNotificationAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Request+PushNotificationAPI.swift"; sourceTree = ""; }; - FD02CC132C3677E6009AB976 /* Request+OpenGroupAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Request+OpenGroupAPI.swift"; sourceTree = ""; }; + FD02CC132C3677E6009AB976 /* Request+SOGS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Request+SOGS.swift"; sourceTree = ""; }; FD02CC152C3681EF009AB976 /* RevokeSubaccountResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RevokeSubaccountResponse.swift; sourceTree = ""; }; FD05593C2DFA3A2200DC48CE /* VoipPayloadKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoipPayloadKey.swift; sourceTree = ""; }; - FD05594D2E012D1A00DC48CE /* _028_RenameAttachments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _028_RenameAttachments.swift; sourceTree = ""; }; + FD05594D2E012D1A00DC48CE /* _043_RenameAttachments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _043_RenameAttachments.swift; sourceTree = ""; }; FD0559542E026CC900DC48CE /* ObservingDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservingDatabase.swift; sourceTree = ""; }; FD0606C22BCE13ED00C3816E /* MessageRequestFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestFooterView.swift; sourceTree = ""; }; FD0969F82A69FFE700C5C365 /* Mocked.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mocked.swift; sourceTree = ""; }; @@ -1825,7 +1837,7 @@ FD09799827FFC1A300936362 /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = ""; }; FD09799A27FFC82D00936362 /* Quote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Quote.swift; sourceTree = ""; }; FD09B7E228865FDA00ED0B66 /* HighlightMentionBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightMentionBackgroundView.swift; sourceTree = ""; }; - FD09B7E4288670BB00ED0B66 /* _008_EmojiReacts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _008_EmojiReacts.swift; sourceTree = ""; }; + FD09B7E4288670BB00ED0B66 /* _017_EmojiReacts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _017_EmojiReacts.swift; sourceTree = ""; }; FD09B7E6288670FD00ED0B66 /* Reaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Reaction.swift; sourceTree = ""; }; FD09C5E528260FF9000CE219 /* MediaGalleryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaGalleryViewModel.swift; sourceTree = ""; }; FD09C5E728264937000CE219 /* MediaDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaDetailViewController.swift; sourceTree = ""; }; @@ -1837,7 +1849,6 @@ FD10AF0B2AF32B9A007709E5 /* SessionListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionListViewModel.swift; sourceTree = ""; }; FD10AF112AF85D11007709E5 /* Feature+ServiceNetwork.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Feature+ServiceNetwork.swift"; sourceTree = ""; }; FD11E22F2CA4F498001BAF58 /* DestinationSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DestinationSpec.swift; sourceTree = ""; }; - FD12A83C2AD63BCC00EEBA0D /* EditableState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditableState.swift; sourceTree = ""; }; FD12A83E2AD63BDF00EEBA0D /* Navigatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Navigatable.swift; sourceTree = ""; }; FD12A8402AD63BEA00EEBA0D /* NavigatableState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigatableState.swift; sourceTree = ""; }; FD12A8422AD63BF600EEBA0D /* ObservableTableSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableTableSource.swift; sourceTree = ""; }; @@ -1845,19 +1856,17 @@ FD12A8462AD63C3400EEBA0D /* PagedObservationSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagedObservationSource.swift; sourceTree = ""; }; FD12A8482AD63C4700EEBA0D /* SessionNavItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionNavItem.swift; sourceTree = ""; }; FD16AB602A1DD9B60083D849 /* ProfilePictureView+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfilePictureView+Convenience.swift"; sourceTree = ""; }; - FD17D79527F3E04600122BE0 /* _001_InitialSetupMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _001_InitialSetupMigration.swift; sourceTree = ""; }; - FD17D79827F40AB800122BE0 /* _003_YDBToGRDBMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _003_YDBToGRDBMigration.swift; sourceTree = ""; }; - FD17D79F27F40CC800122BE0 /* _001_InitialSetupMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _001_InitialSetupMigration.swift; sourceTree = ""; }; - FD17D7A327F40F8100122BE0 /* _003_YDBToGRDBMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _003_YDBToGRDBMigration.swift; sourceTree = ""; }; + FD17D79527F3E04600122BE0 /* _006_SMK_InitialSetupMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _006_SMK_InitialSetupMigration.swift; sourceTree = ""; }; + FD17D79827F40AB800122BE0 /* _009_SMK_YDBToGRDBMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _009_SMK_YDBToGRDBMigration.swift; sourceTree = ""; }; + FD17D79F27F40CC800122BE0 /* _004_SNK_InitialSetupMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _004_SNK_InitialSetupMigration.swift; sourceTree = ""; }; + FD17D7A327F40F8100122BE0 /* _008_SNK_YDBToGRDBMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _008_SNK_YDBToGRDBMigration.swift; sourceTree = ""; }; FD17D7AD27F41C4300122BE0 /* SnodeReceivedMessageInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnodeReceivedMessageInfo.swift; sourceTree = ""; }; FD17D7AF27F4225C00122BE0 /* Set+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Set+Utilities.swift"; sourceTree = ""; }; FD17D7B727F51ECA00122BE0 /* Migration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Migration.swift; sourceTree = ""; }; - FD17D7B927F51F2100122BE0 /* TargetMigrations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TargetMigrations.swift; sourceTree = ""; }; FD17D7BE27F51F8200122BE0 /* ColumnExpressible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColumnExpressible.swift; sourceTree = ""; }; - FD17D7C627F5207C00122BE0 /* DatabaseMigrator+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DatabaseMigrator+Utilities.swift"; sourceTree = ""; }; - FD17D7C927F546D900122BE0 /* _001_InitialSetupMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _001_InitialSetupMigration.swift; sourceTree = ""; }; + FD17D7C927F546D900122BE0 /* _001_SUK_InitialSetupMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _001_SUK_InitialSetupMigration.swift; sourceTree = ""; }; FD17D7E427F6A09900122BE0 /* Identity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Identity.swift; sourceTree = ""; }; - FD17D7E627F6A16700122BE0 /* _003_YDBToGRDBMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _003_YDBToGRDBMigration.swift; sourceTree = ""; }; + FD17D7E627F6A16700122BE0 /* _003_SUK_YDBToGRDBMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _003_SUK_YDBToGRDBMigration.swift; sourceTree = ""; }; FD19363B2ACA3134004BCF0F /* ResponseInfo+SnodeAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ResponseInfo+SnodeAPI.swift"; sourceTree = ""; }; FD19363E2ACA66DE004BCF0F /* DatabaseSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseSpec.swift; sourceTree = ""; }; FD1A553D2E14BE0E003761E4 /* PagedData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagedData.swift; sourceTree = ""; }; @@ -1865,7 +1874,7 @@ FD1A55422E179AE6003761E4 /* ObservableKeyEvent+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ObservableKeyEvent+Utilities.swift"; sourceTree = ""; }; FD1A94FA2900D1C2000D73D3 /* PersistableRecord+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PersistableRecord+Utilities.swift"; sourceTree = ""; }; FD1C98E3282E3C5B00B76F9E /* UINavigationBar+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationBar+Utilities.swift"; sourceTree = ""; }; - FD1D732D2A86114600E3F410 /* _015_BlockCommunityMessageRequests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _015_BlockCommunityMessageRequests.swift; sourceTree = ""; }; + FD1D732D2A86114600E3F410 /* _029_BlockCommunityMessageRequests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _029_BlockCommunityMessageRequests.swift; sourceTree = ""; }; FD2272532C32911A004D8A6C /* SendReadReceiptsJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendReadReceiptsJob.swift; sourceTree = ""; }; FD2272542C32911A004D8A6C /* GroupLeavingJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupLeavingJob.swift; sourceTree = ""; }; FD2272552C32911A004D8A6C /* CheckForAppUpdatesJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheckForAppUpdatesJob.swift; sourceTree = ""; }; @@ -1911,7 +1920,6 @@ FD2272D72C34EDE6004D8A6C /* SnodeAPIEndpoint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnodeAPIEndpoint.swift; sourceTree = ""; }; FD2272DC2C34EFFA004D8A6C /* AppSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSetup.swift; sourceTree = ""; }; FD2272DF2C3502BE004D8A6C /* Setting+Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Setting+Theme.swift"; sourceTree = ""; }; - FD2272E32C35134B004D8A6C /* Data+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Utilities.swift"; sourceTree = ""; }; FD2272E52C351378004D8A6C /* SUIKImageFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SUIKImageFormat.swift; sourceTree = ""; }; FD2272E92C351CA7004D8A6C /* Threading.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Threading.swift; sourceTree = ""; }; FD2272EB2C352155004D8A6C /* Feature.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Feature.swift; sourceTree = ""; }; @@ -1937,7 +1945,7 @@ FD2AAAEF28ED57B500A49611 /* SynchronousStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SynchronousStorage.swift; sourceTree = ""; }; FD2B4B032949887A00AB4848 /* QueryInterfaceRequest+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "QueryInterfaceRequest+Utilities.swift"; sourceTree = ""; }; FD336F562CAA28CF00C0B51B /* CommonSMKMockExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonSMKMockExtensions.swift; sourceTree = ""; }; - FD336F572CAA28CF00C0B51B /* CustomArgSummaryDescribable+SMK.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CustomArgSummaryDescribable+SMK.swift"; sourceTree = ""; }; + FD336F572CAA28CF00C0B51B /* CustomArgSummaryDescribable+SessionMessagingKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CustomArgSummaryDescribable+SessionMessagingKit.swift"; sourceTree = ""; }; FD336F582CAA28CF00C0B51B /* MockCommunityPollerCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCommunityPollerCache.swift; sourceTree = ""; }; FD336F592CAA28CF00C0B51B /* MockDisplayPictureCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDisplayPictureCache.swift; sourceTree = ""; }; FD336F5A2CAA28CF00C0B51B /* MockGroupPollerCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockGroupPollerCache.swift; sourceTree = ""; }; @@ -1948,8 +1956,8 @@ FD336F5F2CAA28CF00C0B51B /* MockSwarmPoller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSwarmPoller.swift; sourceTree = ""; }; FD336F6B2CAA29C200C0B51B /* CommunityPollerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommunityPollerSpec.swift; sourceTree = ""; }; FD336F6E2CAA37CB00C0B51B /* MockCommunityPoller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCommunityPoller.swift; sourceTree = ""; }; - FD3559452CC1FF140088F2A9 /* _020_AddMissingWhisperFlag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _020_AddMissingWhisperFlag.swift; sourceTree = ""; }; - FD368A6729DE8F9B000DBF1E /* _012_AddFTSIfNeeded.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _012_AddFTSIfNeeded.swift; sourceTree = ""; }; + FD3559452CC1FF140088F2A9 /* _034_AddMissingWhisperFlag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _034_AddMissingWhisperFlag.swift; sourceTree = ""; }; + FD368A6729DE8F9B000DBF1E /* _026_AddFTSIfNeeded.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _026_AddFTSIfNeeded.swift; sourceTree = ""; }; FD368A6929DE9E30000DBF1E /* UIContextualAction+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIContextualAction+Utilities.swift"; sourceTree = ""; }; FD3765DE2AD8F03100DC1489 /* MockSnodeAPICache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSnodeAPICache.swift; sourceTree = ""; }; FD3765E12AD8F53B00DC1489 /* CommonSSKMockExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonSSKMockExtensions.swift; sourceTree = ""; }; @@ -1970,7 +1978,7 @@ FD37E9DA28A244E9003AE748 /* ThemeMessagePreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeMessagePreviewView.swift; sourceTree = ""; }; FD37E9DC28A384EB003AE748 /* PrimaryColorSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryColorSelectionView.swift; sourceTree = ""; }; FD37E9F528A5F106003AE748 /* Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; - FD37E9F828A5F14A003AE748 /* _001_ThemePreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _001_ThemePreferences.swift; sourceTree = ""; }; + FD37E9F828A5F14A003AE748 /* _016_ThemePreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _016_ThemePreferences.swift; sourceTree = ""; }; FD37E9FE28A5F2CD003AE748 /* Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; FD37EA0028A60473003AE748 /* UIKit+Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIKit+Theme.swift"; sourceTree = ""; }; FD37EA0228A9FDCC003AE748 /* HelpViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelpViewModel.swift; sourceTree = ""; }; @@ -1978,19 +1986,21 @@ FD37EA0628AA2CCA003AE748 /* SessionTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionTableViewController.swift; sourceTree = ""; }; FD37EA0828AA2D27003AE748 /* SessionTableViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionTableViewModel.swift; sourceTree = ""; }; FD37EA0A28AB12E2003AE748 /* SessionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionCell.swift; sourceTree = ""; }; - FD37EA0C28AB2A45003AE748 /* _005_FixDeletedMessageReadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _005_FixDeletedMessageReadState.swift; sourceTree = ""; }; - FD37EA0E28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _006_FixHiddenModAdminSupport.swift; sourceTree = ""; }; + FD37EA0C28AB2A45003AE748 /* _013_FixDeletedMessageReadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _013_FixDeletedMessageReadState.swift; sourceTree = ""; }; + FD37EA0E28AB3330003AE748 /* _014_FixHiddenModAdminSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _014_FixHiddenModAdminSupport.swift; sourceTree = ""; }; FD37EA1428AB42CB003AE748 /* IdentitySpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentitySpec.swift; sourceTree = ""; }; FD37EA1628AC5605003AE748 /* NotificationContentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationContentViewModel.swift; sourceTree = ""; }; FD37EA1828AC5CCA003AE748 /* NotificationSoundViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSoundViewModel.swift; sourceTree = ""; }; FD39352B28F382920084DADA /* VersionFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionFooterView.swift; sourceTree = ""; }; - FD39353528F7C3390084DADA /* _004_FlagMessageHashAsDeletedOrInvalid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _004_FlagMessageHashAsDeletedOrInvalid.swift; sourceTree = ""; }; + FD39353528F7C3390084DADA /* _010_FlagMessageHashAsDeletedOrInvalid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _010_FlagMessageHashAsDeletedOrInvalid.swift; sourceTree = ""; }; + FD3937072E4AD3F800571F17 /* NoopDependency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoopDependency.swift; sourceTree = ""; }; + FD39370B2E4D7BBE00571F17 /* DocumentPickerHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentPickerHandler.swift; sourceTree = ""; }; FD3AABE828306BBD00E5099A /* ThreadPickerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadPickerViewModel.swift; sourceTree = ""; }; FD3C906627E416AF00CD579F /* BlindedIdLookupSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlindedIdLookupSpec.swift; sourceTree = ""; }; FD3E0C83283B5835002A425C /* SessionThreadViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionThreadViewModel.swift; sourceTree = ""; }; FD3F2EE62DE6CC3B00FD6849 /* NotificationsManagerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsManagerSpec.swift; sourceTree = ""; }; FD3F2EF12DF273D100FD6849 /* ThemedAttributedString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemedAttributedString.swift; sourceTree = ""; }; - FD3FAB582ADF906300DC5421 /* Profile+CurrentUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Profile+CurrentUser.swift"; sourceTree = ""; }; + FD3FAB582ADF906300DC5421 /* Profile+Updating.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Profile+Updating.swift"; sourceTree = ""; }; FD3FAB5E2AE9BC2200DC5421 /* EditGroupViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditGroupViewModel.swift; sourceTree = ""; }; FD3FAB602AEA194E00DC5421 /* UserListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserListViewModel.swift; sourceTree = ""; }; FD3FAB622AEB9A1500DC5421 /* ToastController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastController.swift; sourceTree = ""; }; @@ -2000,7 +2010,7 @@ FD428B182B4B576F006D0888 /* AppContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppContext.swift; sourceTree = ""; }; FD428B1A2B4B6098006D0888 /* Notifications+Lifecycle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Notifications+Lifecycle.swift"; sourceTree = ""; }; FD428B1E2B4B758B006D0888 /* AppReadiness.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReadiness.swift; sourceTree = ""; }; - FD428B222B4B9969006D0888 /* _017_RebuildFTSIfNeeded_2_4_5.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _017_RebuildFTSIfNeeded_2_4_5.swift; sourceTree = ""; }; + FD428B222B4B9969006D0888 /* _031_RebuildFTSIfNeeded_2_4_5.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _031_RebuildFTSIfNeeded_2_4_5.swift; sourceTree = ""; }; FD42ECCD2E287CD1002D03EA /* ThemeColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeColor.swift; sourceTree = ""; }; FD42ECCF2E289257002D03EA /* ThemeLinearGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeLinearGradient.swift; sourceTree = ""; }; FD42ECD12E3071DC002D03EA /* ThemeText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeText.swift; sourceTree = ""; }; @@ -2019,7 +2029,7 @@ FD4B200D283492210034334B /* InsetLockableTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsetLockableTableView.swift; sourceTree = ""; }; FD4BB22A2D63F20600D0DC3D /* MigrationHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationHelper.swift; sourceTree = ""; }; FD4C4E9B2B02E2A300C72199 /* DisplayPictureError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DisplayPictureError.swift; sourceTree = ""; }; - FD4C53AE2CC1D61E003B10F4 /* _021_ReworkRecipientState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _021_ReworkRecipientState.swift; sourceTree = ""; }; + FD4C53AE2CC1D61E003B10F4 /* _035_ReworkRecipientState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _035_ReworkRecipientState.swift; sourceTree = ""; }; FD52090228B4680F006098F6 /* RadioButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButton.swift; sourceTree = ""; }; FD52090428B4915F006098F6 /* PrivacySettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacySettingsViewModel.swift; sourceTree = ""; }; FD52090628B49738006098F6 /* ConfirmationModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmationModal.swift; sourceTree = ""; }; @@ -2042,22 +2052,42 @@ FD5CE3442A3C5D96001A6DE3 /* DecryptExportedKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecryptExportedKey.swift; sourceTree = ""; }; FD5D201D27B0D87C00FEA984 /* SessionId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionId.swift; sourceTree = ""; }; FD61FCF82D308CC5005752DE /* GroupMemberSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberSpec.swift; sourceTree = ""; }; - FD61FCFA2D34A5DE005752DE /* _007_SplitSnodeReceivedMessageInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _007_SplitSnodeReceivedMessageInfo.swift; sourceTree = ""; }; + FD61FCFA2D34A5DE005752DE /* _023_SplitSnodeReceivedMessageInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _023_SplitSnodeReceivedMessageInfo.swift; sourceTree = ""; }; FD6531892AA025C500DFEEAA /* TestDependencies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestDependencies.swift; sourceTree = ""; }; FD6673FC2D77F54400041530 /* ScreenLockViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenLockViewController.swift; sourceTree = ""; }; FD6673FE2D77F9BE00041530 /* ScreenLock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenLock.swift; sourceTree = ""; }; - FD66CB272BF3449B00268FAB /* SessionSnodeKit.xctestplan */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = SessionSnodeKit.xctestplan; sourceTree = ""; }; + FD66CB272BF3449B00268FAB /* SessionNetworkingKit.xctestplan */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = SessionNetworkingKit.xctestplan; sourceTree = ""; }; FD66CB2B2BF344C600268FAB /* SessionMessageKit.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = SessionMessageKit.xctestplan; sourceTree = ""; }; FD6A38F02C2A66B100762359 /* KeychainStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStorage.swift; sourceTree = ""; }; - FD6A7A6C2818C61500035AC1 /* _002_SetupStandardJobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _002_SetupStandardJobs.swift; sourceTree = ""; }; + FD6A7A6C2818C61500035AC1 /* _005_SNK_SetupStandardJobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _005_SNK_SetupStandardJobs.swift; sourceTree = ""; }; + FD6B928B2E779DC8004463B5 /* FileServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileServer.swift; sourceTree = ""; }; + FD6B928D2E779E95004463B5 /* FileServerEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileServerEndpoint.swift; sourceTree = ""; }; + FD6B928F2E779EDA004463B5 /* FileServerAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileServerAPI.swift; sourceTree = ""; }; + FD6B92912E779FC6004463B5 /* SessionNetwork.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionNetwork.swift; sourceTree = ""; }; + FD6B92932E779FFE004463B5 /* SessionNetworkEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionNetworkEndpoint.swift; sourceTree = ""; }; + FD6B92962E77A042004463B5 /* Price.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Price.swift; sourceTree = ""; }; + FD6B92982E77A06C004463B5 /* Token.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Token.swift; sourceTree = ""; }; + FD6B929A2E77A083004463B5 /* NetworkInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkInfo.swift; sourceTree = ""; }; + FD6B929C2E77A095004463B5 /* Info.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Info.swift; sourceTree = ""; }; + FD6B92A22E77A189004463B5 /* SnodeAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnodeAPI.swift; sourceTree = ""; }; + FD6B92A92E77A8F8004463B5 /* SOGS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOGS.swift; sourceTree = ""; }; + FD6B92C52E77AD0B004463B5 /* Crypto+FileServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Crypto+FileServer.swift"; sourceTree = ""; }; + FD6B92C72E77AD35004463B5 /* Crypto+SOGS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Crypto+SOGS.swift"; sourceTree = ""; }; + FD6B92CB2E77B1E5004463B5 /* CapabilitiesResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapabilitiesResponse.swift; sourceTree = ""; }; + FD6B92DA2E77B592004463B5 /* CryptoSOGSAPISpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoSOGSAPISpec.swift; sourceTree = ""; }; + FD6B92DC2E77BB7E004463B5 /* Authentication+SOGS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Authentication+SOGS.swift"; sourceTree = ""; }; + FD6B92E02E77C1DC004463B5 /* PushNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotification.swift; sourceTree = ""; }; + FD6B92E32E77C250004463B5 /* PushNotificationAPI+SessionMessagingKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PushNotificationAPI+SessionMessagingKit.swift"; sourceTree = ""; }; + FD6B92F62E77C6D3004463B5 /* Crypto+PushNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Crypto+PushNotification.swift"; sourceTree = ""; }; FD6C67232CF6E72900B350A7 /* NoopSessionCallManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoopSessionCallManager.swift; sourceTree = ""; }; - FD6DF00A2ACFE40D0084BA4C /* _005_AddSnodeReveivedMessageInfoPrimaryKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _005_AddSnodeReveivedMessageInfoPrimaryKey.swift; sourceTree = ""; }; - FD6E4C892A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyUnsubscribeRequest.swift; sourceTree = ""; }; + FD6DF00A2ACFE40D0084BA4C /* _021_AddSnodeReveivedMessageInfoPrimaryKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _021_AddSnodeReveivedMessageInfoPrimaryKey.swift; sourceTree = ""; }; + FD6F5B5D2E657A24009A8D01 /* CancellationAwareAsyncStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancellationAwareAsyncStream.swift; sourceTree = ""; }; + FD6F5B5F2E657A32009A8D01 /* StreamLifecycleManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamLifecycleManager.swift; sourceTree = ""; }; FD705A91278D051200F16121 /* ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReusableView.swift; sourceTree = ""; }; - FD70F25B2DC1F176003729B7 /* _026_MessageDeduplicationTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _026_MessageDeduplicationTable.swift; sourceTree = ""; }; + FD70F25B2DC1F176003729B7 /* _040_MessageDeduplicationTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _040_MessageDeduplicationTable.swift; sourceTree = ""; }; FD7115EA28C5D78E00B47552 /* ThreadSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadSettingsViewModel.swift; sourceTree = ""; }; FD7115EF28C5D7DE00B47552 /* SessionHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionHeaderView.swift; sourceTree = ""; }; - FD7115F128C6CB3900B47552 /* _010_AddThreadIdToFTS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _010_AddThreadIdToFTS.swift; sourceTree = ""; }; + FD7115F128C6CB3900B47552 /* _019_AddThreadIdToFTS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _019_AddThreadIdToFTS.swift; sourceTree = ""; }; FD7115F328C71EB200B47552 /* ThreadDisappearingMessagesSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadDisappearingMessagesSettingsViewModel.swift; sourceTree = ""; }; FD7115F728C8151C00B47552 /* DisposableBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisposableBarButtonItem.swift; sourceTree = ""; }; FD7115F928C8153400B47552 /* UIBarButtonItem+Combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIBarButtonItem+Combine.swift"; sourceTree = ""; }; @@ -2091,7 +2121,7 @@ FD716E7028505E5100C96BF4 /* MessageRequestsCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageRequestsCell.swift; sourceTree = ""; }; FD72BDA02BE368C800CF6CF6 /* UIWindowLevel+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIWindowLevel+Utilities.swift"; sourceTree = ""; }; FD72BDA32BE3690B00CF6CF6 /* CryptoSMKSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoSMKSpec.swift; sourceTree = ""; }; - FD72BDA62BE369DC00CF6CF6 /* CryptoOpenGroupAPISpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoOpenGroupAPISpec.swift; sourceTree = ""; }; + FD72BDA62BE369DC00CF6CF6 /* CryptoOpenGroupSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoOpenGroupSpec.swift; sourceTree = ""; }; FD74433F2D07A25C00862443 /* PushRegistrationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushRegistrationManager.swift; sourceTree = ""; }; FD7443412D07A27E00862443 /* SyncPushTokensJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncPushTokensJob.swift; sourceTree = ""; }; FD7443452D07CA9F00862443 /* CGFloat+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGFloat+Utilities.swift"; sourceTree = ""; }; @@ -2102,18 +2132,18 @@ FD7692F62A53A2ED000E4B70 /* SessionThreadViewModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionThreadViewModelSpec.swift; sourceTree = ""; }; FD7728952849E7E90018502F /* String+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Utilities.swift"; sourceTree = ""; }; FD7728972849E8110018502F /* UITableView+ReusableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UITableView+ReusableView.swift"; sourceTree = ""; }; - FD778B6329B189FF001BAC6B /* _014_GenerateInitialUserConfigDumps.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _014_GenerateInitialUserConfigDumps.swift; sourceTree = ""; }; + FD778B6329B189FF001BAC6B /* _028_GenerateInitialUserConfigDumps.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _028_GenerateInitialUserConfigDumps.swift; sourceTree = ""; }; FD78E9F12DDA9E9B00D55B50 /* MockImageDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockImageDataManager.swift; sourceTree = ""; }; FD78E9F32DDABA4200D55B50 /* AttachmentUploader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentUploader.swift; sourceTree = ""; }; FD78E9F52DDD43AB00D55B50 /* Mutation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mutation.swift; sourceTree = ""; }; - FD78E9F72DDD742100D55B50 /* _027_MoveSettingsToLibSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _027_MoveSettingsToLibSession.swift; sourceTree = ""; }; + FD78E9F72DDD742100D55B50 /* _042_MoveSettingsToLibSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _042_MoveSettingsToLibSession.swift; sourceTree = ""; }; FD78E9FC2DDD97F000D55B50 /* Setting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Setting.swift; sourceTree = ""; }; FD78EA012DDEBC2C00D55B50 /* DebounceTaskManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebounceTaskManager.swift; sourceTree = ""; }; FD78EA032DDEC3C000D55B50 /* MultiTaskManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiTaskManager.swift; sourceTree = ""; }; FD78EA052DDEC8F100D55B50 /* AsyncSequence+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AsyncSequence+Utilities.swift"; sourceTree = ""; }; FD78EA092DDFE45900D55B50 /* Interaction+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Interaction+UI.swift"; sourceTree = ""; }; FD78EA0C2DDFEDDF00D55B50 /* LibSession+Local.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LibSession+Local.swift"; sourceTree = ""; }; - FD7F74562BAA9D31006DDFD8 /* _006_DropSnodeCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _006_DropSnodeCache.swift; sourceTree = ""; }; + FD7F74562BAA9D31006DDFD8 /* _022_DropSnodeCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _022_DropSnodeCache.swift; sourceTree = ""; }; FD7F745A2BAAA35E006DDFD8 /* LibSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibSession.swift; sourceTree = ""; }; FD7F745E2BAAA3B4006DDFD8 /* TypeConversion+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TypeConversion+Utilities.swift"; sourceTree = ""; }; FD7F74692BAB8A6D006DDFD8 /* LibSession+Networking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LibSession+Networking.swift"; sourceTree = ""; }; @@ -2121,7 +2151,7 @@ FD83B9BA27CF20AF005E1583 /* SessionIdSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionIdSpec.swift; sourceTree = ""; }; FD83B9BD27CF2243005E1583 /* TestConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestConstants.swift; sourceTree = ""; }; FD83B9C427CF3E2A005E1583 /* OpenGroupSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupSpec.swift; sourceTree = ""; }; - FD83B9C627CF3F10005E1583 /* CapabilitiesSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapabilitiesSpec.swift; sourceTree = ""; }; + FD83B9C627CF3F10005E1583 /* CapabilitySpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapabilitySpec.swift; sourceTree = ""; }; FD83B9C827D0487A005E1583 /* SendDirectMessageResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendDirectMessageResponse.swift; sourceTree = ""; }; FD83B9D127D59495005E1583 /* MockUserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUserDefaults.swift; sourceTree = ""; }; FD83DCDC2A739D350065FFAE /* RetryWithDependencies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryWithDependencies.swift; sourceTree = ""; }; @@ -2139,7 +2169,7 @@ FD860CB52D66913B00BBE29C /* ThemePreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemePreviewView.swift; sourceTree = ""; }; FD860CB72D66BC9500BBE29C /* AppIconViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconViewModel.swift; sourceTree = ""; }; FD860CB92D66BF2300BBE29C /* AppIconGridView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconGridView.swift; sourceTree = ""; }; - FD860CBB2D6E7A9400BBE29C /* _024_FixBustedInteractionVariant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _024_FixBustedInteractionVariant.swift; sourceTree = ""; }; + FD860CBB2D6E7A9400BBE29C /* _038_FixBustedInteractionVariant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _038_FixBustedInteractionVariant.swift; sourceTree = ""; }; FD860CBD2D6E7DA000BBE29C /* DeveloperSettingsViewModel+Testing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DeveloperSettingsViewModel+Testing.swift"; sourceTree = ""; }; FD86FDA22BC5020600EC251B /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; FD87DCF928B74DB300AF0F98 /* ConversationSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationSettingsViewModel.swift; sourceTree = ""; }; @@ -2151,12 +2181,12 @@ FD8A5B242DC05B16004C689B /* Number+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Number+Utilities.swift"; sourceTree = ""; }; FD8A5B282DC060DD004C689B /* Double+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+Utilities.swift"; sourceTree = ""; }; FD8A5B2F2DC18D5E004C689B /* GeneralCacheSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralCacheSpec.swift; sourceTree = ""; }; - FD8A5B312DC191AB004C689B /* _025_DropLegacyClosedGroupKeyPairTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _025_DropLegacyClosedGroupKeyPairTable.swift; sourceTree = ""; }; - FD8A5B332DC1A726004C689B /* _008_ResetUserConfigLastHashes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _008_ResetUserConfigLastHashes.swift; sourceTree = ""; }; + FD8A5B312DC191AB004C689B /* _039_DropLegacyClosedGroupKeyPairTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _039_DropLegacyClosedGroupKeyPairTable.swift; sourceTree = ""; }; + FD8A5B332DC1A726004C689B /* _024_ResetUserConfigLastHashes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _024_ResetUserConfigLastHashes.swift; sourceTree = ""; }; FD8ECF7A29340FFD00C0D1BB /* LibSession+SessionMessagingKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LibSession+SessionMessagingKit.swift"; sourceTree = ""; }; - FD8ECF7C2934293A00C0D1BB /* _013_SessionUtilChanges.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _013_SessionUtilChanges.swift; sourceTree = ""; }; + FD8ECF7C2934293A00C0D1BB /* _027_SessionUtilChanges.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _027_SessionUtilChanges.swift; sourceTree = ""; }; FD8ECF7E2934298100C0D1BB /* ConfigDump.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigDump.swift; sourceTree = ""; }; - FD9004132818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _002_SetupStandardJobs.swift; sourceTree = ""; }; + FD9004132818AD0B00ABAAF6 /* _002_SUK_SetupStandardJobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _002_SUK_SetupStandardJobs.swift; sourceTree = ""; }; FD9401CE2ABD04AC003A4834 /* TRANSLATIONS.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = TRANSLATIONS.md; sourceTree = ""; }; FD96F3A429DBC3DC00401309 /* MessageSendJobSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSendJobSpec.swift; sourceTree = ""; }; FD97B23F2A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ARC4RandomNumberGenerator.swift; sourceTree = ""; }; @@ -2199,7 +2229,7 @@ FDB3DA8C2E24881200148F8D /* ImageLoading+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ImageLoading+Convenience.swift"; sourceTree = ""; }; FDB4BBC62838B91E00B7C95D /* LinkPreviewError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreviewError.swift; sourceTree = ""; }; FDB5DAC02A9443A5002C8721 /* MessageSender+Groups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageSender+Groups.swift"; sourceTree = ""; }; - FDB5DAC62A9447E7002C8721 /* _022_GroupsRebuildChanges.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _022_GroupsRebuildChanges.swift; sourceTree = ""; }; + FDB5DAC62A9447E7002C8721 /* _036_GroupsRebuildChanges.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _036_GroupsRebuildChanges.swift; sourceTree = ""; }; FDB5DAD32A9483F3002C8721 /* GroupUpdateInviteMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupUpdateInviteMessage.swift; sourceTree = ""; }; FDB5DAD92A95D839002C8721 /* GroupUpdateInfoChangeMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupUpdateInfoChangeMessage.swift; sourceTree = ""; }; FDB5DADB2A95D840002C8721 /* GroupUpdateMemberChangeMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupUpdateMemberChangeMessage.swift; sourceTree = ""; }; @@ -2209,13 +2239,13 @@ FDB5DAE52A95D8B0002C8721 /* GroupUpdateDeleteMemberContentMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupUpdateDeleteMemberContentMessage.swift; sourceTree = ""; }; FDB5DAE72A95D96C002C8721 /* MessageReceiver+Groups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+Groups.swift"; sourceTree = ""; }; FDB5DAF22A96DD4F002C8721 /* PreparedRequest+Sending.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PreparedRequest+Sending.swift"; sourceTree = ""; }; - FDB5DAFA2A981C42002C8721 /* SessionSnodeKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SessionSnodeKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + FDB5DAFA2A981C42002C8721 /* SessionNetworkingKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SessionNetworkingKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; FDB5DB052A981C67002C8721 /* PreparedRequestSendingSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreparedRequestSendingSpec.swift; sourceTree = ""; }; FDB6A87B2AD75B7F002D4F96 /* PhotosUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PhotosUI.framework; path = System/Library/Frameworks/PhotosUI.framework; sourceTree = SDKROOT; }; FDB7400A28EB99A70094D718 /* TimeInterval+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimeInterval+Utilities.swift"; sourceTree = ""; }; FDB7400C28EBEC240094D718 /* DateHeaderCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateHeaderCell.swift; sourceTree = ""; }; FDBA8A832D59796F007C19C0 /* FailedGroupInvitesAndPromotionsJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FailedGroupInvitesAndPromotionsJob.swift; sourceTree = ""; }; - FDBB25E22988B13800F1508E /* _004_AddJobPriority.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _004_AddJobPriority.swift; sourceTree = ""; }; + FDBB25E22988B13800F1508E /* _012_AddJobPriority.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _012_AddJobPriority.swift; sourceTree = ""; }; FDBB25E62988BBBD00F1508E /* UIContextualAction+Theming.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIContextualAction+Theming.swift"; sourceTree = ""; }; FDBEE52D2B6A18B900C143A0 /* UserDefaultsConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDefaultsConfig.swift; sourceTree = ""; }; FDC0F0032BFECE12002CBFB9 /* TimeUnit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeUnit.swift; sourceTree = ""; }; @@ -2223,18 +2253,16 @@ FDC13D462A16E4CA007267C7 /* SubscribeRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscribeRequest.swift; sourceTree = ""; }; FDC13D482A16EC20007267C7 /* Service.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Service.swift; sourceTree = ""; }; FDC13D4A2A16ECBA007267C7 /* SubscribeResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscribeResponse.swift; sourceTree = ""; }; - FDC13D4F2A16EE50007267C7 /* PushNotificationAPIEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationAPIEndpoint.swift; sourceTree = ""; }; - FDC13D532A16FF29007267C7 /* LegacyGroupRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyGroupRequest.swift; sourceTree = ""; }; + FDC13D4F2A16EE50007267C7 /* PushNotificationEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationEndpoint.swift; sourceTree = ""; }; FDC13D552A171FE4007267C7 /* UnsubscribeRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsubscribeRequest.swift; sourceTree = ""; }; FDC13D572A17207D007267C7 /* UnsubscribeResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsubscribeResponse.swift; sourceTree = ""; }; - FDC13D592A1721C5007267C7 /* LegacyNotifyRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyNotifyRequest.swift; sourceTree = ""; }; FDC1BD652CFD6C4E002CDC71 /* Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = ""; }; FDC1BD672CFE6EEA002CDC71 /* DeveloperSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperSettingsViewModel.swift; sourceTree = ""; }; FDC1BD692CFE7B67002CDC71 /* DirectoryArchiver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectoryArchiver.swift; sourceTree = ""; }; FDC289462C881A3800020BC2 /* MutableIdentifiable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutableIdentifiable.swift; sourceTree = ""; }; FDC2908627D7047F005DAE71 /* RoomSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSpec.swift; sourceTree = ""; }; FDC2908827D70656005DAE71 /* RoomPollInfoSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollInfoSpec.swift; sourceTree = ""; }; - FDC2908A27D707F3005DAE71 /* SendMessageRequestSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageRequestSpec.swift; sourceTree = ""; }; + FDC2908A27D707F3005DAE71 /* SendSOGSMessageRequestSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendSOGSMessageRequestSpec.swift; sourceTree = ""; }; FDC2908C27D70905005DAE71 /* UpdateMessageRequestSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateMessageRequestSpec.swift; sourceTree = ""; }; FDC2908E27D70938005DAE71 /* SendDirectMessageRequestSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendDirectMessageRequestSpec.swift; sourceTree = ""; }; FDC2909027D709CA005DAE71 /* SOGSMessageSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOGSMessageSpec.swift; sourceTree = ""; }; @@ -2243,20 +2271,19 @@ FDC2909727D7129B005DAE71 /* PersonalizationSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersonalizationSpec.swift; sourceTree = ""; }; FDC2909D27D85751005DAE71 /* OpenGroupManagerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupManagerSpec.swift; sourceTree = ""; }; FDC290A727D9B46D005DAE71 /* NimbleExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NimbleExtensions.swift; sourceTree = ""; }; - FDC4380827B31D4E00C60D73 /* OpenGroupAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupAPIError.swift; sourceTree = ""; }; + FDC4380827B31D4E00C60D73 /* SOGSError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOGSError.swift; sourceTree = ""; }; FDC4381627B32EC700C60D73 /* Personalization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Personalization.swift; sourceTree = ""; }; FDC4381F27B36ADC00C60D73 /* SOGSEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOGSEndpoint.swift; sourceTree = ""; }; - FDC4382E27B383AF00C60D73 /* LegacyPushServerResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyPushServerResponse.swift; sourceTree = ""; }; FDC4383727B3863200C60D73 /* AppVersionResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppVersionResponse.swift; sourceTree = ""; }; FDC4385C27B4C18900C60D73 /* Room.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Room.swift; sourceTree = ""; }; FDC4385E27B4C4A200C60D73 /* PinnedMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedMessage.swift; sourceTree = ""; }; FDC4386227B4D94E00C60D73 /* SOGSMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOGSMessage.swift; sourceTree = ""; }; FDC4386427B4DE7600C60D73 /* RoomPollInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollInfo.swift; sourceTree = ""; }; - FDC4386627B4E10E00C60D73 /* Capabilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Capabilities.swift; sourceTree = ""; }; + FDC4386627B4E10E00C60D73 /* CapabilitiesResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapabilitiesResponse.swift; sourceTree = ""; }; FDC4387127B5BB3B00C60D73 /* FileUploadResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUploadResponse.swift; sourceTree = ""; }; - FDC4387727B5C35400C60D73 /* SendMessageRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageRequest.swift; sourceTree = ""; }; + FDC4387727B5C35400C60D73 /* SendSOGSMessageRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendSOGSMessageRequest.swift; sourceTree = ""; }; FDC4388E27B9FFC700C60D73 /* SessionMessagingKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SessionMessagingKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - FDC4389927BA002500C60D73 /* OpenGroupAPISpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupAPISpec.swift; sourceTree = ""; }; + FDC4389927BA002500C60D73 /* SOGSAPISpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOGSAPISpec.swift; sourceTree = ""; }; FDC438A327BB107F00C60D73 /* UserBanRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserBanRequest.swift; sourceTree = ""; }; FDC438A527BB113A00C60D73 /* UserUnbanRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserUnbanRequest.swift; sourceTree = ""; }; FDC438A927BB12BB00C60D73 /* UserModeratorRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserModeratorRequest.swift; sourceTree = ""; }; @@ -2267,7 +2294,6 @@ FDC498B82AC15FE300EDD897 /* AppNotificationAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppNotificationAction.swift; sourceTree = ""; }; FDC6D75F2862B3F600B04575 /* Dependencies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dependencies.swift; sourceTree = ""; }; FDCCC6E82ABA7402002BBEF5 /* EmojiGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiGenerator.swift; sourceTree = ""; }; - FDCD2E022A41294E00964D6A /* LegacyGroupOnlyRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyGroupOnlyRequest.swift; sourceTree = ""; }; FDCDB8DF2811007F00352A0C /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = ""; }; FDD20C152A09E64A003898FB /* GetExpiriesRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetExpiriesRequest.swift; sourceTree = ""; }; FDD20C172A09E7D3003898FB /* GetExpiriesResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetExpiriesResponse.swift; sourceTree = ""; }; @@ -2277,15 +2303,14 @@ FDD383702AFDD0E1001367F2 /* BencodeResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BencodeResponse.swift; sourceTree = ""; }; FDD383722AFDD6D7001367F2 /* BencodeResponseSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BencodeResponseSpec.swift; sourceTree = ""; }; FDD82C3E2A205D0A00425F05 /* ProcessResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessResult.swift; sourceTree = ""; }; - FDDD554D2C1FCB77006CBF03 /* _019_ScheduleAppUpdateCheckJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _019_ScheduleAppUpdateCheckJob.swift; sourceTree = ""; }; + FDDD554D2C1FCB77006CBF03 /* _033_ScheduleAppUpdateCheckJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _033_ScheduleAppUpdateCheckJob.swift; sourceTree = ""; }; FDDF074329C3E3D000E5E8B5 /* FetchRequest+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FetchRequest+Utilities.swift"; sourceTree = ""; }; FDDF074929DAB36900E5E8B5 /* JobRunnerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JobRunnerSpec.swift; sourceTree = ""; }; FDE125222A837E4E002DA685 /* MainAppContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainAppContext.swift; sourceTree = ""; }; FDE33BBB2D5C124300E56F42 /* DispatchTimeInterval+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DispatchTimeInterval+Utilities.swift"; sourceTree = ""; }; - FDE33BBD2D5C3AE800E56F42 /* _023_GroupsExpiredFlag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _023_GroupsExpiredFlag.swift; sourceTree = ""; }; + FDE33BBD2D5C3AE800E56F42 /* _037_GroupsExpiredFlag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _037_GroupsExpiredFlag.swift; sourceTree = ""; }; FDE519F62AB7CDC700450C53 /* Result+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Result+Utilities.swift"; sourceTree = ""; }; FDE5218D2E03A06700061B8E /* AttachmentManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentManager.swift; sourceTree = ""; }; - FDE5218F2E04CCE600061B8E /* AVURLAsset+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVURLAsset+Utilities.swift"; sourceTree = ""; }; FDE521932E050B0800061B8E /* DismissCallbackAVPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DismissCallbackAVPlayerViewController.swift; sourceTree = ""; }; FDE521992E08DBB000061B8E /* ImageLoading+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ImageLoading+Convenience.swift"; sourceTree = ""; }; FDE5219B2E08E76600061B8E /* SessionAsyncImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionAsyncImage.swift; sourceTree = ""; }; @@ -2293,11 +2318,17 @@ FDE5219F2E0D22FD00061B8E /* ObservationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservationManager.swift; sourceTree = ""; }; FDE521A12E0D23A200061B8E /* ObservableKey+SessionMessagingKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ObservableKey+SessionMessagingKit.swift"; sourceTree = ""; }; FDE6E99729F8E63A00F93C5D /* Accessibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Accessibility.swift; sourceTree = ""; }; + FDE71B022E77CCE90023F5F9 /* HTTPHeader+FileServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HTTPHeader+FileServer.swift"; sourceTree = ""; }; + FDE71B042E77E1A00023F5F9 /* ObservableKey+SessionNetworkingKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ObservableKey+SessionNetworkingKit.swift"; sourceTree = ""; }; + FDE71B0A2E7935250023F5F9 /* DeveloperSettingsGroupsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperSettingsGroupsViewModel.swift; sourceTree = ""; }; + FDE71B0C2E793B1F0023F5F9 /* DeveloperSettingsProViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperSettingsProViewModel.swift; sourceTree = ""; }; + FDE71B0E2E7A195B0023F5F9 /* Session - Anonymous Messenger.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = "Session - Anonymous Messenger.storekit"; sourceTree = ""; }; + FDE71B5E2E7A73560023F5F9 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; FDE7214F287E50D50093DF33 /* ProtoWrappers.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = ProtoWrappers.py; sourceTree = ""; }; FDE72150287E50D50093DF33 /* LintLocalizableStrings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LintLocalizableStrings.swift; sourceTree = ""; }; FDE7549A2C940108002A2623 /* MessageViewModel+DeletionActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageViewModel+DeletionActions.swift"; sourceTree = ""; }; FDE7549C2C9961A4002A2623 /* CommunityPoller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CommunityPoller.swift; sourceTree = ""; }; - FDE754A02C9A60A6002A2623 /* Crypto+OpenGroupAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Crypto+OpenGroupAPI.swift"; sourceTree = ""; }; + FDE754A02C9A60A6002A2623 /* Crypto+OpenGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Crypto+OpenGroup.swift"; sourceTree = ""; }; FDE754A22C9A8FD1002A2623 /* SwarmPoller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwarmPoller.swift; sourceTree = ""; }; FDE754A42C9B964D002A2623 /* MessageReceiverGroupsSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageReceiverGroupsSpec.swift; sourceTree = ""; }; FDE754A52C9B964D002A2623 /* MessageSenderGroupsSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageSenderGroupsSpec.swift; sourceTree = ""; }; @@ -2317,7 +2348,6 @@ FDE754C82C9BAF36002A2623 /* UTType+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UTType+Utilities.swift"; sourceTree = ""; }; FDE754C92C9BAF36002A2623 /* ImageFormat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageFormat.swift; sourceTree = ""; }; FDE754CA2C9BAF37002A2623 /* DataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataSource.swift; sourceTree = ""; }; - FDE754CB2C9BAF37002A2623 /* Data+Image.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Data+Image.swift"; sourceTree = ""; }; FDE754D12C9BAF53002A2623 /* JobDependencies.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JobDependencies.swift; sourceTree = ""; }; FDE754D32C9BAF6B002A2623 /* UICollectionView+ReusableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UICollectionView+ReusableView.swift"; sourceTree = ""; }; FDE754D52C9BAF89002A2623 /* Crypto.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Crypto.swift; sourceTree = ""; }; @@ -2326,7 +2356,7 @@ FDE754D82C9BAF89002A2623 /* Crypto+SessionUtilitiesKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Crypto+SessionUtilitiesKit.swift"; sourceTree = ""; }; FDE754D92C9BAF89002A2623 /* KeyPair.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyPair.swift; sourceTree = ""; }; FDE754DA2C9BAF8A002A2623 /* Hex.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Hex.swift; sourceTree = ""; }; - FDE754E12C9BAFF4002A2623 /* Crypto+SessionSnodeKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Crypto+SessionSnodeKit.swift"; sourceTree = ""; }; + FDE754E12C9BAFF4002A2623 /* Crypto+SessionNetworkingKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Crypto+SessionNetworkingKit.swift"; sourceTree = ""; }; FDE754E42C9BB012002A2623 /* BezierPathView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BezierPathView.swift; sourceTree = ""; }; FDE754E62C9BB051002A2623 /* OWSViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSViewController.swift; sourceTree = ""; }; FDE754EC2C9BB08B002A2623 /* Crypto+Attachments.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Crypto+Attachments.swift"; sourceTree = ""; }; @@ -2335,9 +2365,9 @@ FDE754F32C9BB0AF002A2623 /* UserNotificationConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserNotificationConfig.swift; sourceTree = ""; }; FDE754F42C9BB0AF002A2623 /* NotificationActionHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationActionHandler.swift; sourceTree = ""; }; FDE754F52C9BB0AF002A2623 /* NotificationPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationPresenter.swift; sourceTree = ""; }; - FDE754FD2C9BB0D0002A2623 /* Threading+SMK.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Threading+SMK.swift"; sourceTree = ""; }; + FDE754FD2C9BB0D0002A2623 /* Threading+SessionMessagingKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Threading+SessionMessagingKit.swift"; sourceTree = ""; }; FDE754FF2C9BB0FA002A2623 /* SessionEnvironment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionEnvironment.swift; sourceTree = ""; }; - FDE755012C9BB122002A2623 /* _011_AddPendingReadReceipts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _011_AddPendingReadReceipts.swift; sourceTree = ""; }; + FDE755012C9BB122002A2623 /* _025_AddPendingReadReceipts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _025_AddPendingReadReceipts.swift; sourceTree = ""; }; FDE755032C9BB4ED002A2623 /* BencodeDecoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BencodeDecoder.swift; sourceTree = ""; }; FDE755042C9BB4ED002A2623 /* Bencode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Bencode.swift; sourceTree = ""; }; FDE755132C9BC169002A2623 /* UIAlertAction+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIAlertAction+Utilities.swift"; sourceTree = ""; }; @@ -2364,7 +2394,7 @@ FDF01FAC2A9ECC4200CAF969 /* SingletonConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingletonConfig.swift; sourceTree = ""; }; FDF0B73B27FFD3D6004C14C5 /* LinkPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreview.swift; sourceTree = ""; }; FDF0B73F280402C4004C14C5 /* Job.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Job.swift; sourceTree = ""; }; - FDF0B7412804EA4F004C14C5 /* _002_SetupStandardJobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _002_SetupStandardJobs.swift; sourceTree = ""; }; + FDF0B7412804EA4F004C14C5 /* _007_SMK_SetupStandardJobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _007_SMK_SetupStandardJobs.swift; sourceTree = ""; }; FDF0B7432804EF1B004C14C5 /* JobRunner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JobRunner.swift; sourceTree = ""; }; FDF0B74828060D13004C14C5 /* QuotedReplyModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuotedReplyModel.swift; sourceTree = ""; }; FDF0B74A28061F7A004C14C5 /* InteractionAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractionAttachment.swift; sourceTree = ""; }; @@ -2379,13 +2409,12 @@ FDF2220E281B55E6000A4995 /* QueryInterfaceRequest+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "QueryInterfaceRequest+Utilities.swift"; sourceTree = ""; }; FDF22210281B5E0B000A4995 /* TableRecord+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TableRecord+Utilities.swift"; sourceTree = ""; }; FDF2F0212DAE1AEF00491E8A /* MessageReceiver+LegacyClosedGroups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+LegacyClosedGroups.swift"; sourceTree = ""; }; - FDF40CDD2897A1BC006A0CC4 /* _004_RemoveLegacyYDB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _004_RemoveLegacyYDB.swift; sourceTree = ""; }; + FDF40CDD2897A1BC006A0CC4 /* _011_RemoveLegacyYDB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _011_RemoveLegacyYDB.swift; sourceTree = ""; }; FDF71EA22B072C2800A8D6B5 /* LibSessionMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibSessionMessage.swift; sourceTree = ""; }; FDF71EA42B07363500A8D6B5 /* MessageReceiver+LibSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+LibSession.swift"; sourceTree = ""; }; - FDF8487D29405993007DCAE5 /* HTTPHeader+OpenGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "HTTPHeader+OpenGroup.swift"; sourceTree = ""; }; - FDF8487E29405994007DCAE5 /* HTTPQueryParam+OpenGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "HTTPQueryParam+OpenGroup.swift"; sourceTree = ""; }; + FDF8487D29405993007DCAE5 /* HTTPHeader+SOGS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "HTTPHeader+SOGS.swift"; sourceTree = ""; }; + FDF8487E29405994007DCAE5 /* HTTPQueryParam+SOGS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "HTTPQueryParam+SOGS.swift"; sourceTree = ""; }; FDF8489029405C13007DCAE5 /* SnodeAPINamespace.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnodeAPINamespace.swift; sourceTree = ""; }; - FDF8489329405C1B007DCAE5 /* SnodeAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnodeAPI.swift; sourceTree = ""; }; FDF8489A29405C5A007DCAE5 /* SnodeRecursiveResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnodeRecursiveResponse.swift; sourceTree = ""; }; FDF8489B29405C5A007DCAE5 /* GetMessagesRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetMessagesRequest.swift; sourceTree = ""; }; FDF8489D29405C5A007DCAE5 /* SnodeResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnodeResponse.swift; sourceTree = ""; }; @@ -2428,8 +2457,10 @@ FDFDE125282D05380098B17F /* MediaInteractiveDismiss.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaInteractiveDismiss.swift; sourceTree = ""; }; FDFDE127282D05530098B17F /* MediaPresentationContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPresentationContext.swift; sourceTree = ""; }; FDFDE129282D056B0098B17F /* MediaZoomAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaZoomAnimationController.swift; sourceTree = ""; }; - FDFE75B02ABD2D2400655640 /* _016_MakeBrokenProfileTimestampsNullable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _016_MakeBrokenProfileTimestampsNullable.swift; sourceTree = ""; }; + FDFE75B02ABD2D2400655640 /* _030_MakeBrokenProfileTimestampsNullable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _030_MakeBrokenProfileTimestampsNullable.swift; sourceTree = ""; }; FDFF9FDE2A787F57005E0628 /* JSONEncoder+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSONEncoder+Utilities.swift"; sourceTree = ""; }; + FED288F22E4C28CF00C31171 /* AppReviewPromptDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReviewPromptDialog.swift; sourceTree = ""; }; + FED288F72E4C3BE100C31171 /* AppReviewPromptModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReviewPromptModel.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -2438,7 +2469,7 @@ buildActionMask = 2147483647; files = ( FD860CC92D6ED2ED00BBE29C /* DifferenceKit in Frameworks */, - C32824D325C9F9790062D0A7 /* SessionSnodeKit.framework in Frameworks */, + C32824D325C9F9790062D0A7 /* SessionNetworkingKit.framework in Frameworks */, B8D64FC725BA78520029CFC0 /* SessionMessagingKit.framework in Frameworks */, C3D90A5C25773A25002C9DF5 /* SessionUtilitiesKit.framework in Frameworks */, C3402FE52559036600EA6424 /* SessionUIKit.framework in Frameworks */, @@ -2452,7 +2483,7 @@ files = ( B8D64FBB25BA78310029CFC0 /* SessionMessagingKit.framework in Frameworks */, FD8A5B182DBF47E9004C689B /* SessionUIKit.framework in Frameworks */, - B8D64FBD25BA78310029CFC0 /* SessionSnodeKit.framework in Frameworks */, + B8D64FBD25BA78310029CFC0 /* SessionNetworkingKit.framework in Frameworks */, B8D64FBE25BA78310029CFC0 /* SessionUtilitiesKit.framework in Frameworks */, C38EF00C255B61CC007E1867 /* SignalUtilitiesKit.framework in Frameworks */, ); @@ -2475,7 +2506,7 @@ FD6A39222C2AA91D00762359 /* NVActivityIndicatorView in Frameworks */, FD22866F2C38D42300BC06F7 /* DifferenceKit in Frameworks */, C33FD9C2255A54EF00E217F9 /* SessionMessagingKit.framework in Frameworks */, - C33FD9C4255A54EF00E217F9 /* SessionSnodeKit.framework in Frameworks */, + C33FD9C4255A54EF00E217F9 /* SessionNetworkingKit.framework in Frameworks */, C33FD9C5255A54EF00E217F9 /* SessionUtilitiesKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2510,7 +2541,7 @@ FD6673FA2D7021F800041530 /* SessionUtil in Frameworks */, FD2286732C38D43900BC06F7 /* DifferenceKit in Frameworks */, FDC4386C27B4E90300C60D73 /* SessionUtilitiesKit.framework in Frameworks */, - C3C2A70B25539E1E00C340D1 /* SessionSnodeKit.framework in Frameworks */, + C3C2A70B25539E1E00C340D1 /* SessionNetworkingKit.framework in Frameworks */, FD6A39132C2A946A00762359 /* SwiftProtobuf in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2524,7 +2555,7 @@ B8FF8DAE25C0D00F004D1F22 /* SessionMessagingKit.framework in Frameworks */, B8FF8DAF25C0D00F004D1F22 /* SessionUtilitiesKit.framework in Frameworks */, FDB6A87C2AD75B7F002D4F96 /* PhotosUI.framework in Frameworks */, - C37F54DC255BB84A002AEA92 /* SessionSnodeKit.framework in Frameworks */, + C37F54DC255BB84A002AEA92 /* SessionNetworkingKit.framework in Frameworks */, C37F5414255BAFA7002AEA92 /* SignalUtilitiesKit.framework in Frameworks */, 455A16DD1F1FEA0000F86704 /* Metal.framework in Frameworks */, 455A16DE1F1FEA0000F86704 /* MetalKit.framework in Frameworks */, @@ -2546,6 +2577,7 @@ A1C32D5117A06544000A904E /* AddressBook.framework in Frameworks */, FD6DA9CF2D015B440092085A /* Lucide in Frameworks */, A1C32D5017A06538000A904E /* AddressBookUI.framework in Frameworks */, + FDE71B5F2E7A73570023F5F9 /* StoreKit.framework in Frameworks */, D2AEACDC16C426DA00C364C0 /* CFNetwork.framework in Frameworks */, C331FF222558F9D300070591 /* SessionUIKit.framework in Frameworks */, D2179CFE16BB0B480006F3AB /* SystemConfiguration.framework in Frameworks */, @@ -2563,7 +2595,7 @@ files = ( FD0150542CA24471005B08A1 /* Nimble in Frameworks */, FD0150522CA2446D005B08A1 /* Quick in Frameworks */, - FD0150502CA24468005B08A1 /* SessionSnodeKit.framework in Frameworks */, + FD0150502CA24468005B08A1 /* SessionNetworkingKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2851,16 +2883,16 @@ path = SwiftUI; sourceTree = ""; }; - 947D7FD32D509FC900E8E413 /* SessionNetworkAPI */ = { + 947D7FD32D509FC900E8E413 /* SessionNetwork */ = { isa = PBXGroup; children = ( - 941375BA2D5184B60058F244 /* HTTPHeader+SessionNetwork.swift */, + FD6B92952E77A036004463B5 /* Models */, + FD6B929E2E77A0E8004463B5 /* Types */, + FD6B92912E779FC6004463B5 /* SessionNetwork.swift */, 947D7FCF2D509FC900E8E413 /* SessionNetworkAPI.swift */, - 947D7FD02D509FC900E8E413 /* SessionNetworkAPI+Database.swift */, - 947D7FD12D509FC900E8E413 /* SessionNetworkAPI+Models.swift */, - 947D7FD22D509FC900E8E413 /* SessionNetworkAPI+Network.swift */, + FD6B92932E779FFE004463B5 /* SessionNetworkEndpoint.swift */, ); - path = SessionNetworkAPI; + path = SessionNetwork; sourceTree = ""; }; 947D7FDC2D5180F200E8E413 /* SessionNetworkScreen */ = { @@ -2884,9 +2916,10 @@ 94CD963F2E1BABE90097754D /* WebPImages */ = { isa = PBXGroup; children = ( + 94B6BB052E431B6300E718BB /* AnimatedProfileCTAAnimationCropped.webp */, + 94AAB15B2E24C97400A6FA18 /* AnimatedProfileCTA.webp */, 94AAB1562E24BD3700A6FA18 /* PinnedConversationsCTA.webp */, 94CD963C2E1BABE90097754D /* GenericCTA.webp */, - 94CD963D2E1BABE90097754D /* GenericCTAAnimation.webp */, 94CD963E2E1BABE90097754D /* HigherCharLimitCTA.webp */, ); path = WebPImages; @@ -2985,7 +3018,6 @@ B8A582AB258C64E800AFD84C /* Database */ = { isa = PBXGroup; children = ( - FD17D7C827F546CE00122BE0 /* Migrations */, FD17D7CB27F546F500122BE0 /* Models */, FD17D7B427F51E6700122BE0 /* Types */, FD17D7BB27F51F5C00122BE0 /* Utilities */, @@ -3011,7 +3043,6 @@ B8A582AF258C665E00AFD84C /* Media */ = { isa = PBXGroup; children = ( - FDE754CB2C9BAF37002A2623 /* Data+Image.swift */, FDE754CA2C9BAF37002A2623 /* DataSource.swift */, FDE754C92C9BAF36002A2623 /* ImageFormat.swift */, FDE754C72C9BAF36002A2623 /* MediaUtils.swift */, @@ -3301,7 +3332,7 @@ children = ( FD37E9C428A1C701003AE748 /* Themes */, 947AD68F2C8968FF000B2730 /* Constants.swift */, - 94A6B9DA2DD6BF6E00DB4B44 /* Constants+URLs.swift */, + 94A6B9DA2DD6BF6E00DB4B44 /* Constants+Apple.swift */, B8BB82BD2394D4CE00BA5194 /* Fonts.swift */, FDF848F029406A30007DCAE5 /* Format.swift */, FD37E9C228A1C6F3003AE748 /* ThemeManager.swift */, @@ -3313,8 +3344,8 @@ C331FFAE2558FA7700070591 /* Utilities */ = { isa = PBXGroup; children = ( + 949D91212E822D520074F595 /* String+SessionProBadge.swift */, 94CD962F2E1B88430097754D /* CGRect+Utilities.swift */, - FD2272E32C35134B004D8A6C /* Data+Utilities.swift */, FD848B9728422F1A000E298B /* Date+Utilities.swift */, FD8A5B282DC060DD004C689B /* Double+Utilities.swift */, 94E9BC0C2C7BFBDA006984EA /* Localization+Style.swift */, @@ -3432,6 +3463,7 @@ C360968E25AD16E8008B62B2 /* Home */ = { isa = PBXGroup; children = ( + FED288EF2E4C239800C31171 /* App Review */, 7B8C44C328B49DA900FBE25F /* New Conversation */, 7B93D06827CF173D00811CB6 /* Message Requests */, 7BAF54CA27ACCEEC003D12F8 /* GlobalSearch */, @@ -3444,6 +3476,7 @@ C360969125AD1765008B62B2 /* Settings */ = { isa = PBXGroup; children = ( + FDE71B092E7934DC0023F5F9 /* DeveloperSettings */, FD8A5B002DBEFBF9004C689B /* SessionNetworkScreen */, FD37E9CD28A1E682003AE748 /* Views */, 9422569A2C23F8F000C0FDBF /* QRCodeScreen.swift */, @@ -3459,8 +3492,6 @@ FD860CB72D66BC9500BBE29C /* AppIconViewModel.swift */, FD37EA0228A9FDCC003AE748 /* HelpViewModel.swift */, B86BD08523399CEF000F5AE3 /* SeedModal.swift */, - FDC1BD672CFE6EEA002CDC71 /* DeveloperSettingsViewModel.swift */, - FD860CBD2D6E7DA000BBE29C /* DeveloperSettingsViewModel+Testing.swift */, B894D0742339EDCF00B4D94D /* NukeDataModal.swift */, FDF848F229413DB0007DCAE5 /* ImagePickerHandler.swift */, ); @@ -3568,9 +3599,8 @@ isa = PBXGroup; children = ( FDC13D4E2A16EE41007267C7 /* Types */, - FDC4382D27B383A600C60D73 /* Models */, FDF0B7502807BA56004C14C5 /* NotificationsManagerType.swift */, - C33FDBDE255A581900E217F9 /* PushNotificationAPI.swift */, + FD6B92E32E77C250004463B5 /* PushNotificationAPI+SessionMessagingKit.swift */, ); path = Notifications; sourceTree = ""; @@ -3626,9 +3656,7 @@ isa = PBXGroup; children = ( FD23CE202A661CE80000B97C /* Crypto */, - FDC4381827B34EAD00C60D73 /* Models */, - FDC4380727B31D3A00C60D73 /* Types */, - B88FA7B726045D100049422F /* OpenGroupAPI.swift */, + FDC4381827B34EAD00C60D73 /* Types */, C3DB66AB260ACA42001EFC55 /* OpenGroupManager.swift */, ); path = "Open Groups"; @@ -3637,7 +3665,7 @@ C3BBE0B32554F0D30050F1E3 /* Utilities */ = { isa = PBXGroup; children = ( - 94B6BAF92E38454F00E718BB /* SessionProState.swift */, + 94B6BAF52E30A88800E718BB /* SessionProState.swift */, FD428B1E2B4B758B006D0888 /* AppReadiness.swift */, FDE5218D2E03A06700061B8E /* AttachmentManager.swift */, FDE5219D2E0D0B9800061B8E /* AsyncAccessible.swift */, @@ -3660,38 +3688,39 @@ FDAA167C2AC528A200DDBF77 /* Preferences+Sound.swift */, C38EF2FB255B6DBD007E1867 /* OWSWindowManager.h */, C38EF306255B6DBE007E1867 /* OWSWindowManager.m */, - FD3FAB582ADF906300DC5421 /* Profile+CurrentUser.swift */, + FD3FAB582ADF906300DC5421 /* Profile+Updating.swift */, FD16AB602A1DD9B60083D849 /* ProfilePictureView+Convenience.swift */, C38EF2EC255B6DBA007E1867 /* ProximityMonitoringManager.swift */, FDE754FF2C9BB0FA002A2623 /* SessionEnvironment.swift */, C38EEF09255B49A8007E1867 /* SNProtoEnvelope+Conversion.swift */, - FDE754FD2C9BB0D0002A2623 /* Threading+SMK.swift */, + FDE754FD2C9BB0D0002A2623 /* Threading+SessionMessagingKit.swift */, FD72BDA02BE368C800CF6CF6 /* UIWindowLevel+Utilities.swift */, ); path = Utilities; sourceTree = ""; }; - C3C2A5A0255385C100C340D1 /* SessionSnodeKit */ = { + C3C2A5A0255385C100C340D1 /* SessionNetworkingKit */ = { isa = PBXGroup; children = ( - 947D7FD32D509FC900E8E413 /* SessionNetworkAPI */, C3C2A5B0255385C700C340D1 /* Meta */, FDE754E22C9BAFF4002A2623 /* Crypto */, - FD17D79D27F40CAA00122BE0 /* Database */, - FDF8489929405C5A007DCAE5 /* Models */, - FDF8488F29405C13007DCAE5 /* Types */, - FD2272842C33E28D004D8A6C /* SnodeAPI */, + FD6B928A2E779DB6004463B5 /* FileServer */, FD7F74682BAB8A5D006DDFD8 /* LibSession */, + FD6B92DF2E77C1CB004463B5 /* PushNotification */, + 947D7FD32D509FC900E8E413 /* SessionNetwork */, + FD6B92892E779D8D004463B5 /* SOGS */, + FD2272842C33E28D004D8A6C /* StorageServer */, + FD6B92A52E77A3BD004463B5 /* Models */, + FDF8488F29405C13007DCAE5 /* Types */, C3C2A5CD255385F300C340D1 /* Utilities */, - C3C2A5B9255385ED00C340D1 /* Configuration.swift */, ); - path = SessionSnodeKit; + path = SessionNetworkingKit; sourceTree = ""; }; C3C2A5B0255385C700C340D1 /* Meta */ = { isa = PBXGroup; children = ( - C3C2A5A1255385C100C340D1 /* SessionSnodeKit.h */, + C3C2A5A1255385C100C340D1 /* SessionNetworkingKit.h */, C3C2A5A2255385C100C340D1 /* Info.plist */, ); path = Meta; @@ -3701,10 +3730,10 @@ isa = PBXGroup; children = ( C3C2A5D82553860B00C340D1 /* Data+Utilities.swift */, + FDE71B042E77E1A00023F5F9 /* ObservableKey+SessionNetworkingKit.swift */, FD2272BD2C34B710004D8A6C /* Publisher+Utilities.swift */, FD83DCDC2A739D350065FFAE /* RetryWithDependencies.swift */, C3C2A5D22553860900C340D1 /* String+Trimming.swift */, - C3C2A5D42553860A00C340D1 /* Threading+SSK.swift */, FD2272A82C33E337004D8A6C /* URLResponse+Utilities.swift */, ); path = Utilities; @@ -3841,6 +3870,7 @@ FDE125222A837E4E002DA685 /* MainAppContext.swift */, C3CA3AA0255CDA7000F4C6D4 /* Mnemonic */, FD86FDA22BC5020600EC251B /* PrivacyInfo.xcprivacy */, + FDE71B0E2E7A195B0023F5F9 /* Session - Anonymous Messenger.storekit */, B67EBF5C19194AC60084CCFD /* Settings.bundle */, B657DDC91911A40500F45B0C /* Signal.entitlements */, FDF2220A2818F38D000A4995 /* SessionApp.swift */, @@ -3861,18 +3891,17 @@ 7BC01A3C241F40AB00BC7C55 /* SessionNotificationServiceExtension */, C331FF1C2558F9D300070591 /* SessionUIKit */, C3C2A6F125539DE700C340D1 /* SessionMessagingKit */, - C3C2A5A0255385C100C340D1 /* SessionSnodeKit */, + C3C2A5A0255385C100C340D1 /* SessionNetworkingKit */, C3C2A67A255388CC00C340D1 /* SessionUtilitiesKit */, C33FD9AC255A548A00E217F9 /* SignalUtilitiesKit */, FD83B9BC27CF2215005E1583 /* _SharedTestUtilities */, FD71160A28D00BAE00B47552 /* SessionTests */, FDC4388F27B9FFC700C60D73 /* SessionMessagingKitTests */, - FDB5DAFB2A981C43002C8721 /* SessionSnodeKitTests */, + FDB5DAFB2A981C43002C8721 /* SessionNetworkingKitTests */, FD83B9B027CF200A005E1583 /* SessionUtilitiesKitTests */, FDE7214E287E50D50093DF33 /* Scripts */, D221A08C169C9E5E00537ABF /* Frameworks */, D221A08A169C9E5E00537ABF /* Products */, - FD8A5B232DC05A0E004C689B /* Recovered References */, ); sourceTree = ""; }; @@ -3882,7 +3911,7 @@ D221A089169C9E5E00537ABF /* Session.app */, 453518681FC635DD00210559 /* SessionShareExtension.appex */, 7BC01A3B241F40AB00BC7C55 /* SessionNotificationServiceExtension.appex */, - C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */, + C3C2A59F255385C100C340D1 /* SessionNetworkingKit.framework */, C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */, C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */, C331FF1B2558F9D300070591 /* SessionUIKit.framework */, @@ -3890,7 +3919,7 @@ FDC4388E27B9FFC700C60D73 /* SessionMessagingKitTests.xctest */, FD83B9AF27CF200A005E1583 /* SessionUtilitiesKitTests.xctest */, FD71160928D00BAE00B47552 /* SessionTests.xctest */, - FDB5DAFA2A981C42002C8721 /* SessionSnodeKitTests.xctest */, + FDB5DAFA2A981C42002C8721 /* SessionNetworkingKitTests.xctest */, ); name = Products; sourceTree = ""; @@ -3898,6 +3927,7 @@ D221A08C169C9E5E00537ABF /* Frameworks */ = { isa = PBXGroup; children = ( + FDE71B5E2E7A73560023F5F9 /* StoreKit.framework */, FDB6A87B2AD75B7F002D4F96 /* PhotosUI.framework */, 3496955F21A2FC8100DCFE74 /* CloudKit.framework */, 455A16DB1F1FEA0000F86704 /* Metal.framework */, @@ -3957,7 +3987,6 @@ isa = PBXGroup; children = ( FDB3DA892E2482A400148F8D /* AVURLAsset+Utilities.swift */, - FDE5218F2E04CCE600061B8E /* AVURLAsset+Utilities.swift */, 94C58AC82D2E036E00609195 /* Permissions.swift */, FD97B23F2A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift */, FD78EA052DDEC8F100D55B50 /* AsyncSequence+Utilities.swift */, @@ -4021,69 +4050,61 @@ FD17D79427F3E03300122BE0 /* Migrations */ = { isa = PBXGroup; children = ( - FD17D79527F3E04600122BE0 /* _001_InitialSetupMigration.swift */, - FDF0B7412804EA4F004C14C5 /* _002_SetupStandardJobs.swift */, - FD17D79827F40AB800122BE0 /* _003_YDBToGRDBMigration.swift */, - FDF40CDD2897A1BC006A0CC4 /* _004_RemoveLegacyYDB.swift */, - FD37EA0C28AB2A45003AE748 /* _005_FixDeletedMessageReadState.swift */, - FD37EA0E28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift */, - 7B81682728B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift */, - FD09B7E4288670BB00ED0B66 /* _008_EmojiReacts.swift */, - 7BAA7B6528D2DE4700AE1489 /* _009_OpenGroupPermission.swift */, - FD7115F128C6CB3900B47552 /* _010_AddThreadIdToFTS.swift */, - FDE755012C9BB122002A2623 /* _011_AddPendingReadReceipts.swift */, - FD368A6729DE8F9B000DBF1E /* _012_AddFTSIfNeeded.swift */, - FD8ECF7C2934293A00C0D1BB /* _013_SessionUtilChanges.swift */, - FD778B6329B189FF001BAC6B /* _014_GenerateInitialUserConfigDumps.swift */, - FD1D732D2A86114600E3F410 /* _015_BlockCommunityMessageRequests.swift */, - FDFE75B02ABD2D2400655640 /* _016_MakeBrokenProfileTimestampsNullable.swift */, - FD428B222B4B9969006D0888 /* _017_RebuildFTSIfNeeded_2_4_5.swift */, - 7B5233C5290636D700F8F375 /* _018_DisappearingMessagesConfiguration.swift */, - FDDD554D2C1FCB77006CBF03 /* _019_ScheduleAppUpdateCheckJob.swift */, - FD3559452CC1FF140088F2A9 /* _020_AddMissingWhisperFlag.swift */, - FD4C53AE2CC1D61E003B10F4 /* _021_ReworkRecipientState.swift */, - FDB5DAC62A9447E7002C8721 /* _022_GroupsRebuildChanges.swift */, - FDE33BBD2D5C3AE800E56F42 /* _023_GroupsExpiredFlag.swift */, - FD860CBB2D6E7A9400BBE29C /* _024_FixBustedInteractionVariant.swift */, - FD8A5B312DC191AB004C689B /* _025_DropLegacyClosedGroupKeyPairTable.swift */, - FD70F25B2DC1F176003729B7 /* _026_MessageDeduplicationTable.swift */, - FD78E9F72DDD742100D55B50 /* _027_MoveSettingsToLibSession.swift */, - FD05594D2E012D1A00DC48CE /* _028_RenameAttachments.swift */, - 94CD95C02E0CBF1C0097754D /* _029_AddProMessageFlag.swift */, + FD17D7C927F546D900122BE0 /* _001_SUK_InitialSetupMigration.swift */, + FD9004132818AD0B00ABAAF6 /* _002_SUK_SetupStandardJobs.swift */, + FD17D7E627F6A16700122BE0 /* _003_SUK_YDBToGRDBMigration.swift */, + FD17D79F27F40CC800122BE0 /* _004_SNK_InitialSetupMigration.swift */, + FD6A7A6C2818C61500035AC1 /* _005_SNK_SetupStandardJobs.swift */, + FD17D79527F3E04600122BE0 /* _006_SMK_InitialSetupMigration.swift */, + FDF0B7412804EA4F004C14C5 /* _007_SMK_SetupStandardJobs.swift */, + FD17D7A327F40F8100122BE0 /* _008_SNK_YDBToGRDBMigration.swift */, + FD17D79827F40AB800122BE0 /* _009_SMK_YDBToGRDBMigration.swift */, + FD39353528F7C3390084DADA /* _010_FlagMessageHashAsDeletedOrInvalid.swift */, + FDF40CDD2897A1BC006A0CC4 /* _011_RemoveLegacyYDB.swift */, + FDBB25E22988B13800F1508E /* _012_AddJobPriority.swift */, + FD37EA0C28AB2A45003AE748 /* _013_FixDeletedMessageReadState.swift */, + FD37EA0E28AB3330003AE748 /* _014_FixHiddenModAdminSupport.swift */, + 7B81682728B310D50069F315 /* _015_HomeQueryOptimisationIndexes.swift */, + FD37E9F828A5F14A003AE748 /* _016_ThemePreferences.swift */, + FD09B7E4288670BB00ED0B66 /* _017_EmojiReacts.swift */, + 7BAA7B6528D2DE4700AE1489 /* _018_OpenGroupPermission.swift */, + FD7115F128C6CB3900B47552 /* _019_AddThreadIdToFTS.swift */, + 945D9C572D6FDBE7003C4C0C /* _020_AddJobUniqueHash.swift */, + FD6DF00A2ACFE40D0084BA4C /* _021_AddSnodeReveivedMessageInfoPrimaryKey.swift */, + FD7F74562BAA9D31006DDFD8 /* _022_DropSnodeCache.swift */, + FD61FCFA2D34A5DE005752DE /* _023_SplitSnodeReceivedMessageInfo.swift */, + FD8A5B332DC1A726004C689B /* _024_ResetUserConfigLastHashes.swift */, + FDE755012C9BB122002A2623 /* _025_AddPendingReadReceipts.swift */, + FD368A6729DE8F9B000DBF1E /* _026_AddFTSIfNeeded.swift */, + FD8ECF7C2934293A00C0D1BB /* _027_SessionUtilChanges.swift */, + FD778B6329B189FF001BAC6B /* _028_GenerateInitialUserConfigDumps.swift */, + FD1D732D2A86114600E3F410 /* _029_BlockCommunityMessageRequests.swift */, + FDFE75B02ABD2D2400655640 /* _030_MakeBrokenProfileTimestampsNullable.swift */, + FD428B222B4B9969006D0888 /* _031_RebuildFTSIfNeeded_2_4_5.swift */, + 7B5233C5290636D700F8F375 /* _032_DisappearingMessagesConfiguration.swift */, + FDDD554D2C1FCB77006CBF03 /* _033_ScheduleAppUpdateCheckJob.swift */, + FD3559452CC1FF140088F2A9 /* _034_AddMissingWhisperFlag.swift */, + FD4C53AE2CC1D61E003B10F4 /* _035_ReworkRecipientState.swift */, + FDB5DAC62A9447E7002C8721 /* _036_GroupsRebuildChanges.swift */, + FDE33BBD2D5C3AE800E56F42 /* _037_GroupsExpiredFlag.swift */, + FD860CBB2D6E7A9400BBE29C /* _038_FixBustedInteractionVariant.swift */, + FD8A5B312DC191AB004C689B /* _039_DropLegacyClosedGroupKeyPairTable.swift */, + FD70F25B2DC1F176003729B7 /* _040_MessageDeduplicationTable.swift */, + 947D7FE22D5181F400E8E413 /* _041_RenameTableSettingToKeyValueStore.swift */, + FD78E9F72DDD742100D55B50 /* _042_MoveSettingsToLibSession.swift */, + FD05594D2E012D1A00DC48CE /* _043_RenameAttachments.swift */, + 94CD95C02E0CBF1C0097754D /* _044_AddProMessageFlag.swift */, + 942BA9BE2E4ABB9F007C4595 /* _045_LastProfileUpdateTimestamp.swift */, ); path = Migrations; sourceTree = ""; }; FD17D79D27F40CAA00122BE0 /* Database */ = { - isa = PBXGroup; - children = ( - FD17D79E27F40CC000122BE0 /* Migrations */, - FD17D7A827F41BE300122BE0 /* Models */, - ); - path = Database; - sourceTree = ""; - }; - FD17D79E27F40CC000122BE0 /* Migrations */ = { - isa = PBXGroup; - children = ( - FD17D79F27F40CC800122BE0 /* _001_InitialSetupMigration.swift */, - FD6A7A6C2818C61500035AC1 /* _002_SetupStandardJobs.swift */, - FD17D7A327F40F8100122BE0 /* _003_YDBToGRDBMigration.swift */, - FD39353528F7C3390084DADA /* _004_FlagMessageHashAsDeletedOrInvalid.swift */, - FD6DF00A2ACFE40D0084BA4C /* _005_AddSnodeReveivedMessageInfoPrimaryKey.swift */, - FD7F74562BAA9D31006DDFD8 /* _006_DropSnodeCache.swift */, - FD61FCFA2D34A5DE005752DE /* _007_SplitSnodeReceivedMessageInfo.swift */, - FD8A5B332DC1A726004C689B /* _008_ResetUserConfigLastHashes.swift */, - ); - path = Migrations; - sourceTree = ""; - }; - FD17D7A827F41BE300122BE0 /* Models */ = { isa = PBXGroup; children = ( FD17D7AD27F41C4300122BE0 /* SnodeReceivedMessageInfo.swift */, ); - path = Models; + path = Database; sourceTree = ""; }; FD17D7B427F51E6700122BE0 /* Types */ = { @@ -4092,7 +4113,6 @@ FD17D7BE27F51F8200122BE0 /* ColumnExpressible.swift */, FD17D7B727F51ECA00122BE0 /* Migration.swift */, FD4BB22A2D63F20600D0DC3D /* MigrationHelper.swift */, - FD17D7B927F51F2100122BE0 /* TargetMigrations.swift */, FD7162DA281B6C440060647B /* TypedTableAlias.swift */, FD848B8A283DC509000E298B /* PagedDatabaseObserver.swift */, FD1A553D2E14BE0E003761E4 /* PagedData.swift */, @@ -4103,7 +4123,6 @@ FD17D7BB27F51F5C00122BE0 /* Utilities */ = { isa = PBXGroup; children = ( - FD17D7C627F5207C00122BE0 /* DatabaseMigrator+Utilities.swift */, FDF22210281B5E0B000A4995 /* TableRecord+Utilities.swift */, FDDF074329C3E3D000E5E8B5 /* FetchRequest+Utilities.swift */, FDF2220E281B55E6000A4995 /* QueryInterfaceRequest+Utilities.swift */, @@ -4114,19 +4133,6 @@ path = Utilities; sourceTree = ""; }; - FD17D7C827F546CE00122BE0 /* Migrations */ = { - isa = PBXGroup; - children = ( - FD17D7C927F546D900122BE0 /* _001_InitialSetupMigration.swift */, - FD9004132818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift */, - FD17D7E627F6A16700122BE0 /* _003_YDBToGRDBMigration.swift */, - FDBB25E22988B13800F1508E /* _004_AddJobPriority.swift */, - 945D9C572D6FDBE7003C4C0C /* _005_AddJobUniqueHash.swift */, - 947D7FE22D5181F400E8E413 /* _006_RenameTableSettingToKeyValueStore.swift */, - ); - path = Migrations; - sourceTree = ""; - }; FD17D7CB27F546F500122BE0 /* Models */ = { isa = PBXGroup; children = ( @@ -4146,17 +4152,18 @@ path = Database; sourceTree = ""; }; - FD2272842C33E28D004D8A6C /* SnodeAPI */ = { + FD2272842C33E28D004D8A6C /* StorageServer */ = { isa = PBXGroup; children = ( - FDF8489329405C1B007DCAE5 /* SnodeAPI.swift */, + FD17D79D27F40CAA00122BE0 /* Database */, + FDF8489929405C5A007DCAE5 /* Models */, + FD6B92A12E77A153004463B5 /* Types */, + FD6B92A22E77A189004463B5 /* SnodeAPI.swift */, FD2272D72C34EDE6004D8A6C /* SnodeAPIEndpoint.swift */, FDF848E029405D6E007DCAE5 /* SnodeAPIError.swift */, FDF8489029405C13007DCAE5 /* SnodeAPINamespace.swift */, - FD47E0B42AA6D7AA00A55E41 /* Request+SnodeAPI.swift */, - FD19363B2ACA3134004BCF0F /* ResponseInfo+SnodeAPI.swift */, ); - path = SnodeAPI; + path = StorageServer; sourceTree = ""; }; FD2272C52C34E9D1004D8A6C /* Types */ = { @@ -4177,15 +4184,18 @@ FD2272D22C34ECBB004D8A6C /* Types */ = { isa = PBXGroup; children = ( + FD39370B2E4D7BBE00571F17 /* DocumentPickerHandler.swift */, FD0E353A2AB98773006A81F7 /* AppVersion.swift */, FDB3486D2BE8457F00B716C2 /* BackgroundTaskManager.swift */, FDE755042C9BB4ED002A2623 /* Bencode.swift */, FDE755032C9BB4ED002A2623 /* BencodeDecoder.swift */, FD2272D32C34ECE1004D8A6C /* BencodeEncoder.swift */, + FD6F5B5D2E657A24009A8D01 /* CancellationAwareAsyncStream.swift */, FDB11A5A2DD1900B00BEF49F /* CurrentValueAsyncStream.swift */, FD3FAB682AF1ADCA00DC5421 /* FileManager.swift */, FD6A38F02C2A66B100762359 /* KeychainStorage.swift */, FD78EA032DDEC3C000D55B50 /* MultiTaskManager.swift */, + FD6F5B5F2E657A32009A8D01 /* StreamLifecycleManager.swift */, FD2272E92C351CA7004D8A6C /* Threading.swift */, FDAA16752AC28A3B00DDBF77 /* UserDefaultsType.swift */, ); @@ -4195,7 +4205,7 @@ FD23CE202A661CE80000B97C /* Crypto */ = { isa = PBXGroup; children = ( - FDE754A02C9A60A6002A2623 /* Crypto+OpenGroupAPI.swift */, + FDE754A02C9A60A6002A2623 /* Crypto+OpenGroup.swift */, ); path = Crypto; sourceTree = ""; @@ -4268,7 +4278,6 @@ FD37E9F728A5F143003AE748 /* Migrations */ = { isa = PBXGroup; children = ( - FD37E9F828A5F14A003AE748 /* _001_ThemePreferences.swift */, ); path = Migrations; sourceTree = ""; @@ -4356,6 +4365,207 @@ path = Models; sourceTree = ""; }; + FD6B92892E779D8D004463B5 /* SOGS */ = { + isa = PBXGroup; + children = ( + FD6B92C32E77ACF2004463B5 /* Crypto */, + FD6B92A82E77A8B2004463B5 /* Models */, + FD6B92A72E77A875004463B5 /* Types */, + FD6B92A92E77A8F8004463B5 /* SOGS.swift */, + B88FA7B726045D100049422F /* SOGSAPI.swift */, + FDC4381F27B36ADC00C60D73 /* SOGSEndpoint.swift */, + FDC4380827B31D4E00C60D73 /* SOGSError.swift */, + ); + path = SOGS; + sourceTree = ""; + }; + FD6B928A2E779DB6004463B5 /* FileServer */ = { + isa = PBXGroup; + children = ( + FD6B92C42E77AD01004463B5 /* Crypto */, + FD6B92A42E77A37A004463B5 /* Models */, + FDE71B012E77CCE30023F5F9 /* Types */, + FD6B928B2E779DC8004463B5 /* FileServer.swift */, + FD6B928F2E779EDA004463B5 /* FileServerAPI.swift */, + FD6B928D2E779E95004463B5 /* FileServerEndpoint.swift */, + ); + path = FileServer; + sourceTree = ""; + }; + FD6B92952E77A036004463B5 /* Models */ = { + isa = PBXGroup; + children = ( + FD6B929C2E77A095004463B5 /* Info.swift */, + FD6B929A2E77A083004463B5 /* NetworkInfo.swift */, + FD6B92962E77A042004463B5 /* Price.swift */, + FD6B92982E77A06C004463B5 /* Token.swift */, + ); + path = Models; + sourceTree = ""; + }; + FD6B929E2E77A0E8004463B5 /* Types */ = { + isa = PBXGroup; + children = ( + 941375BA2D5184B60058F244 /* HTTPHeader+SessionNetwork.swift */, + 947D7FD22D509FC900E8E413 /* HTTPClient.swift */, + 947D7FD02D509FC900E8E413 /* KeyValueStore+SessionNetwork.swift */, + ); + path = Types; + sourceTree = ""; + }; + FD6B92A12E77A153004463B5 /* Types */ = { + isa = PBXGroup; + children = ( + FD19363B2ACA3134004BCF0F /* ResponseInfo+SnodeAPI.swift */, + FD47E0B42AA6D7AA00A55E41 /* Request+SnodeAPI.swift */, + FDF848B429405C5A007DCAE5 /* SnodeMessage.swift */, + FDF848AA29405C5A007DCAE5 /* SnodeReceivedMessage.swift */, + ); + path = Types; + sourceTree = ""; + }; + FD6B92A42E77A37A004463B5 /* Models */ = { + isa = PBXGroup; + children = ( + FDC4383727B3863200C60D73 /* AppVersionResponse.swift */, + ); + path = Models; + sourceTree = ""; + }; + FD6B92A52E77A3BD004463B5 /* Models */ = { + isa = PBXGroup; + children = ( + FDC4387127B5BB3B00C60D73 /* FileUploadResponse.swift */, + ); + path = Models; + sourceTree = ""; + }; + FD6B92A72E77A875004463B5 /* Types */ = { + isa = PBXGroup; + children = ( + FDF8487D29405993007DCAE5 /* HTTPHeader+SOGS.swift */, + FDF8487E29405994007DCAE5 /* HTTPQueryParam+SOGS.swift */, + FDC4381627B32EC700C60D73 /* Personalization.swift */, + FD02CC132C3677E6009AB976 /* Request+SOGS.swift */, + 7B81682228A4C1210069F315 /* UpdateTypes.swift */, + ); + path = Types; + sourceTree = ""; + }; + FD6B92A82E77A8B2004463B5 /* Models */ = { + isa = PBXGroup; + children = ( + FDC4386627B4E10E00C60D73 /* CapabilitiesResponse.swift */, + FDC4385C27B4C18900C60D73 /* Room.swift */, + FDC4386427B4DE7600C60D73 /* RoomPollInfo.swift */, + FDC4385E27B4C4A200C60D73 /* PinnedMessage.swift */, + FDC4387727B5C35400C60D73 /* SendSOGSMessageRequest.swift */, + FDC438CA27BB7DB100C60D73 /* UpdateMessageRequest.swift */, + FDC4386227B4D94E00C60D73 /* SOGSMessage.swift */, + FDC438C627BB6DF000C60D73 /* DirectMessage.swift */, + FDC438C827BB706500C60D73 /* SendDirectMessageRequest.swift */, + FD83B9C827D0487A005E1583 /* SendDirectMessageResponse.swift */, + FDD20C192A0A03AC003898FB /* DeleteInboxResponse.swift */, + FDC438A327BB107F00C60D73 /* UserBanRequest.swift */, + FDC438A527BB113A00C60D73 /* UserUnbanRequest.swift */, + FDC438A927BB12BB00C60D73 /* UserModeratorRequest.swift */, + 7B81682928B6F1420069F315 /* ReactionResponse.swift */, + ); + path = Models; + sourceTree = ""; + }; + FD6B92C32E77ACF2004463B5 /* Crypto */ = { + isa = PBXGroup; + children = ( + FD6B92C72E77AD35004463B5 /* Crypto+SOGS.swift */, + ); + path = Crypto; + sourceTree = ""; + }; + FD6B92C42E77AD01004463B5 /* Crypto */ = { + isa = PBXGroup; + children = ( + FD6B92C52E77AD0B004463B5 /* Crypto+FileServer.swift */, + ); + path = Crypto; + sourceTree = ""; + }; + FD6B92C92E77B1A7004463B5 /* SOGS */ = { + isa = PBXGroup; + children = ( + FD6B92D92E77B58B004463B5 /* Crypto */, + FD6B92CA2E77B1AE004463B5 /* Models */, + FD6B92D52E77B54B004463B5 /* Types */, + FDC4389927BA002500C60D73 /* SOGSAPISpec.swift */, + ); + path = SOGS; + sourceTree = ""; + }; + FD6B92CA2E77B1AE004463B5 /* Models */ = { + isa = PBXGroup; + children = ( + FD6B92CB2E77B1E5004463B5 /* CapabilitiesResponse.swift */, + FDC2908627D7047F005DAE71 /* RoomSpec.swift */, + FDC2908827D70656005DAE71 /* RoomPollInfoSpec.swift */, + FDC2909027D709CA005DAE71 /* SOGSMessageSpec.swift */, + FDC2908A27D707F3005DAE71 /* SendSOGSMessageRequestSpec.swift */, + FDC2908E27D70938005DAE71 /* SendDirectMessageRequestSpec.swift */, + FDC2908C27D70905005DAE71 /* UpdateMessageRequestSpec.swift */, + ); + path = Models; + sourceTree = ""; + }; + FD6B92D52E77B54B004463B5 /* Types */ = { + isa = PBXGroup; + children = ( + FDC2909727D7129B005DAE71 /* PersonalizationSpec.swift */, + FDC2909327D710B4005DAE71 /* SOGSEndpointSpec.swift */, + FDC2909527D71252005DAE71 /* SOGSErrorSpec.swift */, + ); + path = Types; + sourceTree = ""; + }; + FD6B92D92E77B58B004463B5 /* Crypto */ = { + isa = PBXGroup; + children = ( + FD6B92DC2E77BB7E004463B5 /* Authentication+SOGS.swift */, + FD6B92DA2E77B592004463B5 /* CryptoSOGSAPISpec.swift */, + ); + path = Crypto; + sourceTree = ""; + }; + FD6B92DF2E77C1CB004463B5 /* PushNotification */ = { + isa = PBXGroup; + children = ( + FD6B92F52E77C6AF004463B5 /* Crypto */, + FDC4382D27B383A600C60D73 /* Models */, + FD6B92E52E77C33B004463B5 /* Types */, + FD6B92E02E77C1DC004463B5 /* PushNotification.swift */, + C33FDBDE255A581900E217F9 /* PushNotificationAPI.swift */, + FDC13D4F2A16EE50007267C7 /* PushNotificationEndpoint.swift */, + ); + path = PushNotification; + sourceTree = ""; + }; + FD6B92E52E77C33B004463B5 /* Types */ = { + isa = PBXGroup; + children = ( + FDD82C3E2A205D0A00425F05 /* ProcessResult.swift */, + FD02CC112C367761009AB976 /* Request+PushNotificationAPI.swift */, + FDC13D482A16EC20007267C7 /* Service.swift */, + FD3765F52ADE5BA500DC1489 /* ServiceInfo.swift */, + ); + path = Types; + sourceTree = ""; + }; + FD6B92F52E77C6AF004463B5 /* Crypto */ = { + isa = PBXGroup; + children = ( + FD6B92F62E77C6D3004463B5 /* Crypto+PushNotification.swift */, + ); + path = Crypto; + sourceTree = ""; + }; FD7115F528C8150600B47552 /* Combine */ = { isa = PBXGroup; children = ( @@ -4445,7 +4655,6 @@ isa = PBXGroup; children = ( FD71164928E3EA5B00B47552 /* DismissType.swift */, - FD12A83C2AD63BCC00EEBA0D /* EditableState.swift */, FD12A83E2AD63BDF00EEBA0D /* Navigatable.swift */, FD12A8402AD63BEA00EEBA0D /* NavigatableState.swift */, FD12A8422AD63BF600EEBA0D /* ObservableTableSource.swift */, @@ -4480,7 +4689,7 @@ FD72BDA52BE369B600CF6CF6 /* Crypto */ = { isa = PBXGroup; children = ( - FD72BDA62BE369DC00CF6CF6 /* CryptoOpenGroupAPISpec.swift */, + FD72BDA62BE369DC00CF6CF6 /* CryptoOpenGroupSpec.swift */, ); path = Crypto; sourceTree = ""; @@ -4607,14 +4816,8 @@ FD83B9C127CF33EE005E1583 /* Models */ = { isa = PBXGroup; children = ( - FD83B9C627CF3F10005E1583 /* CapabilitiesSpec.swift */, + FD83B9C627CF3F10005E1583 /* CapabilitySpec.swift */, FD83B9C427CF3E2A005E1583 /* OpenGroupSpec.swift */, - FDC2908627D7047F005DAE71 /* RoomSpec.swift */, - FDC2908827D70656005DAE71 /* RoomPollInfoSpec.swift */, - FDC2909027D709CA005DAE71 /* SOGSMessageSpec.swift */, - FDC2908A27D707F3005DAE71 /* SendMessageRequestSpec.swift */, - FDC2908E27D70938005DAE71 /* SendDirectMessageRequestSpec.swift */, - FDC2908C27D70905005DAE71 /* UpdateMessageRequestSpec.swift */, ); path = Models; sourceTree = ""; @@ -4643,14 +4846,6 @@ path = SessionNetworkScreen; sourceTree = ""; }; - FD8A5B232DC05A0E004C689B /* Recovered References */ = { - isa = PBXGroup; - children = ( - 941375BE2D5196D10058F244 /* Number+Utilities.swift */, - ); - name = "Recovered References"; - sourceTree = ""; - }; FD8ECF7529340F4800C0D1BB /* LibSession */ = { isa = PBXGroup; children = ( @@ -4752,15 +4947,16 @@ path = "Group Update Messages"; sourceTree = ""; }; - FDB5DAFB2A981C43002C8721 /* SessionSnodeKitTests */ = { + FDB5DAFB2A981C43002C8721 /* SessionNetworkingKitTests */ = { isa = PBXGroup; children = ( - FD66CB272BF3449B00268FAB /* SessionSnodeKit.xctestplan */, + FD66CB272BF3449B00268FAB /* SessionNetworkingKit.xctestplan */, FD3765DD2AD8F02300DC1489 /* _TestUtilities */, + FD6B92C92E77B1A7004463B5 /* SOGS */, FDAA16792AC28E2200DDBF77 /* Models */, FD2272C52C34E9D1004D8A6C /* Types */, ); - path = SessionSnodeKitTests; + path = SessionNetworkingKitTests; sourceTree = ""; }; FDC13D4E2A16EE41007267C7 /* Types */ = { @@ -4770,11 +4966,6 @@ FD981BD62DC9A61600564172 /* NotificationCategory.swift */, FDB11A4B2DCC527900BEF49F /* NotificationContent.swift */, FD981BD82DC9A69000564172 /* NotificationUserInfoKey.swift */, - FDC13D482A16EC20007267C7 /* Service.swift */, - FD3765F52ADE5BA500DC1489 /* ServiceInfo.swift */, - FDD82C3E2A205D0A00425F05 /* ProcessResult.swift */, - FDC13D4F2A16EE50007267C7 /* PushNotificationAPIEndpoint.swift */, - FD02CC112C367761009AB976 /* Request+PushNotificationAPI.swift */, ); path = Types; sourceTree = ""; @@ -4798,51 +4989,12 @@ path = LibSession; sourceTree = ""; }; - FDC2909227D710A9005DAE71 /* Types */ = { - isa = PBXGroup; - children = ( - FDC2909727D7129B005DAE71 /* PersonalizationSpec.swift */, - FDC2909327D710B4005DAE71 /* SOGSEndpointSpec.swift */, - FDC2909527D71252005DAE71 /* SOGSErrorSpec.swift */, - ); - path = Types; - sourceTree = ""; - }; - FDC4380727B31D3A00C60D73 /* Types */ = { - isa = PBXGroup; - children = ( - FDF8487D29405993007DCAE5 /* HTTPHeader+OpenGroup.swift */, - FDF8487E29405994007DCAE5 /* HTTPQueryParam+OpenGroup.swift */, - FDC4380827B31D4E00C60D73 /* OpenGroupAPIError.swift */, - FDC4381627B32EC700C60D73 /* Personalization.swift */, - FD02CC132C3677E6009AB976 /* Request+OpenGroupAPI.swift */, - FDC4381F27B36ADC00C60D73 /* SOGSEndpoint.swift */, - 7B81682228A4C1210069F315 /* UpdateTypes.swift */, - ); - path = Types; - sourceTree = ""; - }; - FDC4381827B34EAD00C60D73 /* Models */ = { + FDC4381827B34EAD00C60D73 /* Types */ = { isa = PBXGroup; children = ( - FDC4386627B4E10E00C60D73 /* Capabilities.swift */, - FDC4385C27B4C18900C60D73 /* Room.swift */, - FDC4386427B4DE7600C60D73 /* RoomPollInfo.swift */, - FDC4385E27B4C4A200C60D73 /* PinnedMessage.swift */, - FDC4387727B5C35400C60D73 /* SendMessageRequest.swift */, - FDC438CA27BB7DB100C60D73 /* UpdateMessageRequest.swift */, - FDC4386227B4D94E00C60D73 /* SOGSMessage.swift */, - FDC438C627BB6DF000C60D73 /* DirectMessage.swift */, - FDC438C827BB706500C60D73 /* SendDirectMessageRequest.swift */, - FD83B9C827D0487A005E1583 /* SendDirectMessageResponse.swift */, - FDD20C192A0A03AC003898FB /* DeleteInboxResponse.swift */, - FDC438A327BB107F00C60D73 /* UserBanRequest.swift */, - FDC438A527BB113A00C60D73 /* UserUnbanRequest.swift */, - FDC438A927BB12BB00C60D73 /* UserModeratorRequest.swift */, - 7B81682928B6F1420069F315 /* ReactionResponse.swift */, 7B81682B28B72F480069F315 /* PendingChange.swift */, ); - path = Models; + path = Types; sourceTree = ""; }; FDC4382D27B383A600C60D73 /* Models */ = { @@ -4853,11 +5005,6 @@ FDC13D4A2A16ECBA007267C7 /* SubscribeResponse.swift */, FDC13D552A171FE4007267C7 /* UnsubscribeRequest.swift */, FDC13D572A17207D007267C7 /* UnsubscribeResponse.swift */, - FD6E4C892A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift */, - FDC13D532A16FF29007267C7 /* LegacyGroupRequest.swift */, - FDCD2E022A41294E00964D6A /* LegacyGroupOnlyRequest.swift */, - FDC4382E27B383AF00C60D73 /* LegacyPushServerResponse.swift */, - FDC13D592A1721C5007267C7 /* LegacyNotifyRequest.swift */, FDFBB74C2A1F3C4E00CA7350 /* NotificationMetadata.swift */, ); path = Models; @@ -4886,8 +5033,6 @@ children = ( FD72BDA52BE369B600CF6CF6 /* Crypto */, FD83B9C127CF33EE005E1583 /* Models */, - FDC2909227D710A9005DAE71 /* Types */, - FDC4389927BA002500C60D73 /* OpenGroupAPISpec.swift */, FDC2909D27D85751005DAE71 /* OpenGroupManagerSpec.swift */, ); path = "Open Groups"; @@ -4897,7 +5042,7 @@ isa = PBXGroup; children = ( FD336F562CAA28CF00C0B51B /* CommonSMKMockExtensions.swift */, - FD336F572CAA28CF00C0B51B /* CustomArgSummaryDescribable+SMK.swift */, + FD336F572CAA28CF00C0B51B /* CustomArgSummaryDescribable+SessionMessagingKit.swift */, FD336F6E2CAA37CB00C0B51B /* MockCommunityPoller.swift */, FD336F582CAA28CF00C0B51B /* MockCommunityPollerCache.swift */, FD336F592CAA28CF00C0B51B /* MockDisplayPictureCache.swift */, @@ -4937,6 +5082,25 @@ path = JobRunner; sourceTree = ""; }; + FDE71B012E77CCE30023F5F9 /* Types */ = { + isa = PBXGroup; + children = ( + FDE71B022E77CCE90023F5F9 /* HTTPHeader+FileServer.swift */, + ); + path = Types; + sourceTree = ""; + }; + FDE71B092E7934DC0023F5F9 /* DeveloperSettings */ = { + isa = PBXGroup; + children = ( + FDC1BD672CFE6EEA002CDC71 /* DeveloperSettingsViewModel.swift */, + FD860CBD2D6E7DA000BBE29C /* DeveloperSettingsViewModel+Testing.swift */, + FDE71B0A2E7935250023F5F9 /* DeveloperSettingsGroupsViewModel.swift */, + FDE71B0C2E793B1F0023F5F9 /* DeveloperSettingsProViewModel.swift */, + ); + path = DeveloperSettings; + sourceTree = ""; + }; FDE7214E287E50D50093DF33 /* Scripts */ = { isa = PBXGroup; children = ( @@ -4966,7 +5130,7 @@ FDE754E22C9BAFF4002A2623 /* Crypto */ = { isa = PBXGroup; children = ( - FDE754E12C9BAFF4002A2623 /* Crypto+SessionSnodeKit.swift */, + FDE754E12C9BAFF4002A2623 /* Crypto+SessionNetworkingKit.swift */, ); path = Crypto; sourceTree = ""; @@ -5015,6 +5179,7 @@ FDF01FAE2A9ED0C800CAF969 /* Dependency Injection */ = { isa = PBXGroup; children = ( + FD3937072E4AD3F800571F17 /* NoopDependency.swift */, FDE7551F2C9BC1A6002A2623 /* CacheConfig.swift */, FDC6D75F2862B3F600B04575 /* Dependencies.swift */, FD2272ED2C3521D6004D8A6C /* FeatureConfig.swift */, @@ -5092,14 +5257,10 @@ FD3765E82ADE1AAE00DC1489 /* UnrevokeSubaccountResponse.swift */, FDF848BA29405C5A007DCAE5 /* RevokeSubaccountRequest.swift */, FD02CC152C3681EF009AB976 /* RevokeSubaccountResponse.swift */, - FDF848AA29405C5A007DCAE5 /* SnodeReceivedMessage.swift */, - FDF848B429405C5A007DCAE5 /* SnodeMessage.swift */, FDF848AB29405C5A007DCAE5 /* GetNetworkTimestampResponse.swift */, FDF848A029405C5A007DCAE5 /* OxenDaemonRPCRequest.swift */, FDF848A629405C5A007DCAE5 /* ONSResolveRequest.swift */, FDF8489E29405C5A007DCAE5 /* ONSResolveResponse.swift */, - FDC4387127B5BB3B00C60D73 /* FileUploadResponse.swift */, - FDC4383727B3863200C60D73 /* AppVersionResponse.swift */, ); path = Models; sourceTree = ""; @@ -5127,6 +5288,23 @@ path = Transitions; sourceTree = ""; }; + FED288EF2E4C239800C31171 /* App Review */ = { + isa = PBXGroup; + children = ( + FED288F42E4C3B5A00C31171 /* View */, + FED288F72E4C3BE100C31171 /* AppReviewPromptModel.swift */, + ); + path = "App Review"; + sourceTree = ""; + }; + FED288F42E4C3B5A00C31171 /* View */ = { + isa = PBXGroup; + children = ( + FED288F22E4C28CF00C31171 /* AppReviewPromptDialog.swift */, + ); + path = View; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -5149,7 +5327,7 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - C3C2A5A3255385C100C340D1 /* SessionSnodeKit.h in Headers */, + C3C2A5A3255385C100C340D1 /* SessionNetworkingKit.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -5270,9 +5448,9 @@ productReference = C33FD9AB255A548A00E217F9 /* SignalUtilitiesKit.framework */; productType = "com.apple.product-type.framework"; }; - C3C2A59E255385C100C340D1 /* SessionSnodeKit */ = { + C3C2A59E255385C100C340D1 /* SessionNetworkingKit */ = { isa = PBXNativeTarget; - buildConfigurationList = C3C2A5AA255385C100C340D1 /* Build configuration list for PBXNativeTarget "SessionSnodeKit" */; + buildConfigurationList = C3C2A5AA255385C100C340D1 /* Build configuration list for PBXNativeTarget "SessionNetworkingKit" */; buildPhases = ( C3C2A59A255385C100C340D1 /* Headers */, C3C2A59B255385C100C340D1 /* Sources */, @@ -5285,12 +5463,12 @@ FDB348822BE86A4400B716C2 /* PBXTargetDependency */, FD7F74622BAAA4C7006DDFD8 /* PBXTargetDependency */, ); - name = SessionSnodeKit; + name = SessionNetworkingKit; packageProductDependencies = ( FD6673F72D7021F200041530 /* SessionUtil */, ); productName = SessionSnodeKit; - productReference = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; + productReference = C3C2A59F255385C100C340D1 /* SessionNetworkingKit.framework */; productType = "com.apple.product-type.framework"; }; C3C2A678255388CC00C340D1 /* SessionUtilitiesKit */ = { @@ -5428,9 +5606,9 @@ productReference = FD83B9AF27CF200A005E1583 /* SessionUtilitiesKitTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; - FDB5DAF92A981C42002C8721 /* SessionSnodeKitTests */ = { + FDB5DAF92A981C42002C8721 /* SessionNetworkingKitTests */ = { isa = PBXNativeTarget; - buildConfigurationList = FD2272522C32910F004D8A6C /* Build configuration list for PBXNativeTarget "SessionSnodeKitTests" */; + buildConfigurationList = FD2272522C32910F004D8A6C /* Build configuration list for PBXNativeTarget "SessionNetworkingKitTests" */; buildPhases = ( FDB5DAF62A981C42002C8721 /* Sources */, FD01504F2CA2445E005B08A1 /* Frameworks */, @@ -5441,9 +5619,9 @@ dependencies = ( FDB5DB002A981C43002C8721 /* PBXTargetDependency */, ); - name = SessionSnodeKitTests; + name = SessionNetworkingKitTests; productName = SessionSnodeKitTests; - productReference = FDB5DAFA2A981C42002C8721 /* SessionSnodeKitTests.xctest */; + productReference = FDB5DAFA2A981C42002C8721 /* SessionNetworkingKitTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; FDC4388D27B9FFC700C60D73 /* SessionMessagingKitTests */ = { @@ -5614,11 +5792,11 @@ C33FD9AA255A548A00E217F9 /* SignalUtilitiesKit */, C331FF1A2558F9D300070591 /* SessionUIKit */, C3C2A6EF25539DE700C340D1 /* SessionMessagingKit */, - C3C2A59E255385C100C340D1 /* SessionSnodeKit */, + C3C2A59E255385C100C340D1 /* SessionNetworkingKit */, C3C2A678255388CC00C340D1 /* SessionUtilitiesKit */, FD71160828D00BAE00B47552 /* SessionTests */, FDC4388D27B9FFC700C60D73 /* SessionMessagingKitTests */, - FDB5DAF92A981C42002C8721 /* SessionSnodeKitTests */, + FDB5DAF92A981C42002C8721 /* SessionNetworkingKitTests */, FD83B9AE27CF200A005E1583 /* SessionUtilitiesKitTests */, ); }; @@ -5630,9 +5808,10 @@ buildActionMask = 2147483647; files = ( 94CD96432E1BAC0F0097754D /* GenericCTA.webp in Resources */, - 94CD96442E1BAC0F0097754D /* GenericCTAAnimation.webp in Resources */, + 94AAB15E2E24C97400A6FA18 /* AnimatedProfileCTA.webp in Resources */, 94CD96452E1BAC0F0097754D /* HigherCharLimitCTA.webp in Resources */, 94AAB1582E24BD3700A6FA18 /* PinnedConversationsCTA.webp in Resources */, + 94B6BB062E431B6300E718BB /* AnimatedProfileCTAAnimationCropped.webp in Resources */, 4535186E1FC635DD00210559 /* MainInterface.storyboard in Resources */, B8D07406265C683A00F77E07 /* ElegantIcons.ttf in Resources */, FD86FDA42BC51C5400EC251B /* PrivacyInfo.xcprivacy in Resources */, @@ -5716,7 +5895,10 @@ C34C8F7423A7830B00D82669 /* SpaceMono-Bold.ttf in Resources */, FDEF57712C44D2D300131302 /* GeoLite2-Country-Blocks-IPv4 in Resources */, B8D07405265C683300F77E07 /* ElegantIcons.ttf in Resources */, + 94B6BB072E431B6300E718BB /* AnimatedProfileCTAAnimationCropped.webp in Resources */, + FDE71B0F2E7A195B0023F5F9 /* Session - Anonymous Messenger.storekit in Resources */, 45B74A7E2044AAB600CD42F8 /* complete.aifc in Resources */, + 94AAB1602E24C97400A6FA18 /* AnimatedProfileCTA.webp in Resources */, 9473386E2BDF5F3E00B9E169 /* InfoPlist.xcstrings in Resources */, B8CCF6352396005F0091D419 /* SpaceMono-Regular.ttf in Resources */, 45B74A872044AAB600CD42F8 /* complete-quiet.aifc in Resources */, @@ -5737,7 +5919,6 @@ C3CA3AC8255CDB2900F4C6D4 /* spanish.txt in Resources */, 94CD96402E1BABE90097754D /* GenericCTA.webp in Resources */, 94CD96412E1BABE90097754D /* HigherCharLimitCTA.webp in Resources */, - 94CD96422E1BABE90097754D /* GenericCTAAnimation.webp in Resources */, 45B74A802044AAB600CD42F8 /* pulse-quiet.aifc in Resources */, 45B74A8B2044AAB600CD42F8 /* synth.aifc in Resources */, 45B74A752044AAB600CD42F8 /* synth-quiet.aifc in Resources */, @@ -5763,7 +5944,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - FD66CB2A2BF3449B00268FAB /* SessionSnodeKit.xctestplan in Resources */, + FD66CB2A2BF3449B00268FAB /* SessionNetworkingKit.xctestplan in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -6037,7 +6218,6 @@ 7BAF54D427ACCF01003D12F8 /* SAEScreenLockViewController.swift in Sources */, B817AD9C26436F73009DF825 /* ThreadPickerVC.swift in Sources */, FD78EA0A2DDFE45E00D55B50 /* Interaction+UI.swift in Sources */, - FD2272DE2C34F11F004D8A6C /* _001_ThemePreferences.swift in Sources */, 7BAF54D327ACCF01003D12F8 /* ShareAppExtensionContext.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -6076,7 +6256,6 @@ FDE5219C2E08E76C00061B8E /* SessionAsyncImage.swift in Sources */, FD09B7E328865FDA00ED0B66 /* HighlightMentionBackgroundView.swift in Sources */, FD3FAB632AEB9A1500DC5421 /* ToastController.swift in Sources */, - FD2272E42C35134B004D8A6C /* Data+Utilities.swift in Sources */, C331FFE72558FB0000070591 /* SNTextField.swift in Sources */, 942256962C23F8DD00C0FDBF /* CompatibleScrollingVStack.swift in Sources */, FD71165B28E6DDBC00B47552 /* StyledNavigationController.swift in Sources */, @@ -6107,6 +6286,7 @@ FD37E9F628A5F106003AE748 /* Configuration.swift in Sources */, 94AAB1512E1F753500A6FA18 /* CyclicGradientView.swift in Sources */, FD8A5B1E2DBF4BBC004C689B /* ScreenLock+Errors.swift in Sources */, + 949D91222E822D5A0074F595 /* String+SessionProBadge.swift in Sources */, FD2272E62C351378004D8A6C /* SUIKImageFormat.swift in Sources */, 7BF8D1FB2A70AF57005F1D6E /* SwiftUI+Theme.swift in Sources */, FDB11A612DD5BDCC00BEF49F /* ImageDataManager.swift in Sources */, @@ -6132,7 +6312,7 @@ 94AAB14B2E1E198200A6FA18 /* Modal+SwiftUI.swift in Sources */, 94AAB1532E1F8AE200A6FA18 /* ShineButton.swift in Sources */, FD37E9D728A20B5D003AE748 /* UIColor+Utilities.swift in Sources */, - 94A6B9DB2DD6BF7C00DB4B44 /* Constants+URLs.swift in Sources */, + 94A6B9DB2DD6BF7C00DB4B44 /* Constants+Apple.swift in Sources */, FD37E9C328A1C6F3003AE748 /* ThemeManager.swift in Sources */, FD8A5B0A2DBF246A004C689B /* Constants.swift in Sources */, C331FF9A2558FA6B00070591 /* Values.swift in Sources */, @@ -6200,80 +6380,121 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + FD6B92E12E77C1E1004463B5 /* PushNotification.swift in Sources */, FD2272B12C33E337004D8A6C /* ProxiedContentDownloader.swift in Sources */, FDF848C329405C5A007DCAE5 /* DeleteMessagesRequest.swift in Sources */, + FD6B928E2E779E99004463B5 /* FileServerEndpoint.swift in Sources */, FDF8489129405C13007DCAE5 /* SnodeAPINamespace.swift in Sources */, FD2272AB2C33E337004D8A6C /* ValidatableResponse.swift in Sources */, FD2272B72C33E337004D8A6C /* Request.swift in Sources */, FD2272B92C33E337004D8A6C /* ResponseInfo.swift in Sources */, - C3C2A5E02553860B00C340D1 /* Threading+SSK.swift in Sources */, + FD6B92F82E77C725004463B5 /* ProcessResult.swift in Sources */, + FD6B92902E779EDD004463B5 /* FileServerAPI.swift in Sources */, FDF848D729405C5B007DCAE5 /* SnodeBatchRequest.swift in Sources */, FDF848CE29405C5B007DCAE5 /* UpdateExpiryAllRequest.swift in Sources */, FDF848C229405C5A007DCAE5 /* OxenDaemonRPCRequest.swift in Sources */, FDF848DC29405C5B007DCAE5 /* RevokeSubaccountRequest.swift in Sources */, - FD17D7A027F40CC800122BE0 /* _001_InitialSetupMigration.swift in Sources */, FDF848D029405C5B007DCAE5 /* UpdateExpiryResponse.swift in Sources */, + FD6B92E82E77C5B7004463B5 /* PushNotificationEndpoint.swift in Sources */, + FDE71B032E77CCEE0023F5F9 /* HTTPHeader+FileServer.swift in Sources */, + FD6B92AC2E77A993004463B5 /* SOGSEndpoint.swift in Sources */, + FD6B92922E779FC8004463B5 /* SessionNetwork.swift in Sources */, FDF848D329405C5B007DCAE5 /* UpdateExpiryAllResponse.swift in Sources */, FDD20C162A09E64A003898FB /* GetExpiriesRequest.swift in Sources */, FDF848BC29405C5A007DCAE5 /* SnodeRecursiveResponse.swift in Sources */, FDF848C029405C5A007DCAE5 /* ONSResolveResponse.swift in Sources */, FD2272AC2C33E337004D8A6C /* IPv4.swift in Sources */, - FD17D7A427F40F8100122BE0 /* _003_YDBToGRDBMigration.swift in Sources */, FD2272D82C34EDE7004D8A6C /* SnodeAPIEndpoint.swift in Sources */, FDFC4D9A29F0C51500992FB6 /* String+Trimming.swift in Sources */, + FD6B92992E77A06E004463B5 /* Token.swift in Sources */, FDB5DAF32A96DD4F002C8721 /* PreparedRequest+Sending.swift in Sources */, FDF848C629405C5B007DCAE5 /* DeleteAllMessagesRequest.swift in Sources */, FDF848D429405C5B007DCAE5 /* DeleteAllBeforeResponse.swift in Sources */, + FD6B92AF2E77AA03004463B5 /* HTTPQueryParam+SOGS.swift in Sources */, + FD6B92B02E77AA03004463B5 /* Request+SOGS.swift in Sources */, + FD6B92B12E77AA03004463B5 /* HTTPHeader+SOGS.swift in Sources */, + FD6B92F72E77C6D7004463B5 /* Crypto+PushNotification.swift in Sources */, + FD6B92B22E77AA03004463B5 /* UpdateTypes.swift in Sources */, + FD6B92B32E77AA03004463B5 /* Personalization.swift in Sources */, + FD6B929B2E77A084004463B5 /* NetworkInfo.swift in Sources */, FD47E0B52AA6D7AA00A55E41 /* Request+SnodeAPI.swift in Sources */, + FDE71B052E77E1AA0023F5F9 /* ObservableKey+SessionNetworkingKit.swift in Sources */, FD5E93D12C100FD70038C25A /* FileUploadResponse.swift in Sources */, FDF848D629405C5B007DCAE5 /* SnodeMessage.swift in Sources */, FD02CC162C3681EF009AB976 /* RevokeSubaccountResponse.swift in Sources */, - FDE754E32C9BAFF4002A2623 /* Crypto+SessionSnodeKit.swift in Sources */, + FDE754E32C9BAFF4002A2623 /* Crypto+SessionNetworkingKit.swift in Sources */, FD2272AD2C33E337004D8A6C /* Network.swift in Sources */, FD2272B32C33E337004D8A6C /* BatchRequest.swift in Sources */, FDF848D129405C5B007DCAE5 /* SnodeSwarmItem.swift in Sources */, FDF848DD29405C5B007DCAE5 /* LegacySendMessageRequest.swift in Sources */, FDF848BD29405C5A007DCAE5 /* GetMessagesRequest.swift in Sources */, FD2272B02C33E337004D8A6C /* NetworkError.swift in Sources */, + FD6B92AB2E77A920004463B5 /* SOGS.swift in Sources */, FD19363C2ACA3134004BCF0F /* ResponseInfo+SnodeAPI.swift in Sources */, - 947D7FD42D509FC900E8E413 /* SessionNetworkAPI+Models.swift in Sources */, + FD6B92E92E77C5D1004463B5 /* SubscribeResponse.swift in Sources */, + FD6B92EA2E77C5D1004463B5 /* NotificationMetadata.swift in Sources */, + FD6B92EB2E77C5D1004463B5 /* AuthenticatedRequest.swift in Sources */, + FD6B92EF2E77C5D1004463B5 /* UnsubscribeRequest.swift in Sources */, + FD6B92F02E77C5D1004463B5 /* SubscribeRequest.swift in Sources */, + FD6B92F22E77C5D1004463B5 /* UnsubscribeResponse.swift in Sources */, 947D7FD62D509FC900E8E413 /* SessionNetworkAPI.swift in Sources */, - 947D7FD72D509FC900E8E413 /* SessionNetworkAPI+Network.swift in Sources */, - 947D7FD82D509FC900E8E413 /* SessionNetworkAPI+Database.swift in Sources */, + 947D7FD72D509FC900E8E413 /* HTTPClient.swift in Sources */, + FD6B92B42E77AA11004463B5 /* PinnedMessage.swift in Sources */, + FD6B92B52E77AA11004463B5 /* SendDirectMessageResponse.swift in Sources */, + FD6B92B62E77AA11004463B5 /* UserUnbanRequest.swift in Sources */, + FD6B92B72E77AA11004463B5 /* UserModeratorRequest.swift in Sources */, + FD6B92B82E77AA11004463B5 /* SendSOGSMessageRequest.swift in Sources */, + FD6B92B92E77AA11004463B5 /* Room.swift in Sources */, + FD6B92BA2E77AA11004463B5 /* RoomPollInfo.swift in Sources */, + FD6B92BB2E77AA11004463B5 /* UpdateMessageRequest.swift in Sources */, + FD6B92BC2E77AA11004463B5 /* ReactionResponse.swift in Sources */, + FD6B92BD2E77AA11004463B5 /* UserBanRequest.swift in Sources */, + FD6B92BE2E77AA11004463B5 /* DirectMessage.swift in Sources */, + FD6B92BF2E77AA11004463B5 /* DeleteInboxResponse.swift in Sources */, + FD6B92C02E77AA11004463B5 /* SendDirectMessageRequest.swift in Sources */, + FD6B92C12E77AA11004463B5 /* CapabilitiesResponse.swift in Sources */, + FD6B92C22E77AA11004463B5 /* SOGSMessage.swift in Sources */, + 947D7FD82D509FC900E8E413 /* KeyValueStore+SessionNetwork.swift in Sources */, FDF848DB29405C5B007DCAE5 /* DeleteMessagesResponse.swift in Sources */, + FD6B92F42E77C61A004463B5 /* ServiceInfo.swift in Sources */, FDF848E629405D6E007DCAE5 /* Destination.swift in Sources */, + FD6B92A32E77A18B004463B5 /* SnodeAPI.swift in Sources */, FDF848CC29405C5B007DCAE5 /* SnodeReceivedMessage.swift in Sources */, FDF848C129405C5A007DCAE5 /* UpdateExpiryRequest.swift in Sources */, + FD6B92E22E77C21D004463B5 /* PushNotificationAPI.swift in Sources */, FDF848C729405C5B007DCAE5 /* SendMessageResponse.swift in Sources */, FDF848CA29405C5B007DCAE5 /* DeleteAllBeforeRequest.swift in Sources */, FD2272AF2C33E337004D8A6C /* JSON.swift in Sources */, FD2272D62C34ED6A004D8A6C /* RetryWithDependencies.swift in Sources */, FDF848D229405C5B007DCAE5 /* LegacyGetMessagesRequest.swift in Sources */, - FD8A5B342DC1A732004C689B /* _008_ResetUserConfigLastHashes.swift in Sources */, FDF848E529405D6E007DCAE5 /* SnodeAPIError.swift in Sources */, - FD61FCFB2D34A5EA005752DE /* _007_SplitSnodeReceivedMessageInfo.swift in Sources */, + FD6B928C2E779DCC004463B5 /* FileServer.swift in Sources */, FDF848D529405C5B007DCAE5 /* DeleteAllMessagesResponse.swift in Sources */, FD2272B22C33E337004D8A6C /* PreparedRequest.swift in Sources */, FDF848BF29405C5A007DCAE5 /* SnodeResponse.swift in Sources */, + FD6B92C82E77AD39004463B5 /* Crypto+SOGS.swift in Sources */, + FD6B92942E77A003004463B5 /* SessionNetworkEndpoint.swift in Sources */, FDD20C182A09E7D3003898FB /* GetExpiriesResponse.swift in Sources */, FD2272BB2C33E337004D8A6C /* HTTPMethod.swift in Sources */, + FD6B92AE2E77A9F7004463B5 /* SOGSAPI.swift in Sources */, + FD6B92972E77A047004463B5 /* Price.swift in Sources */, C3C2A5E42553860B00C340D1 /* Data+Utilities.swift in Sources */, FDF848D929405C5B007DCAE5 /* SnodeAuthenticatedRequestBody.swift in Sources */, + FD6B92AD2E77A9F1004463B5 /* SOGSError.swift in Sources */, FD2272BA2C33E337004D8A6C /* HTTPHeader.swift in Sources */, FDF848CD29405C5B007DCAE5 /* GetNetworkTimestampResponse.swift in Sources */, FDF848DA29405C5B007DCAE5 /* GetMessagesResponse.swift in Sources */, - FD39353628F7C3390084DADA /* _004_FlagMessageHashAsDeletedOrInvalid.swift in Sources */, - FD7F74572BAA9D31006DDFD8 /* _006_DropSnodeCache.swift in Sources */, - FDF8489429405C1B007DCAE5 /* SnodeAPI.swift in Sources */, + FD6B92C62E77AD0F004463B5 /* Crypto+FileServer.swift in Sources */, FD2286682C37DA3B00BC06F7 /* LibSession+Networking.swift in Sources */, FD2272A92C33E337004D8A6C /* ContentProxy.swift in Sources */, + FD6B92E62E77C5A2004463B5 /* Service.swift in Sources */, + FD6B92E72E77C5A2004463B5 /* Request+PushNotificationAPI.swift in Sources */, + FD6B92DE2E77BDE2004463B5 /* Authentication+SOGS.swift in Sources */, FDF848C829405C5B007DCAE5 /* ONSResolveRequest.swift in Sources */, - FD6DF00B2ACFE40D0084BA4C /* _005_AddSnodeReveivedMessageInfoPrimaryKey.swift in Sources */, - C3C2A5C2255385EE00C340D1 /* Configuration.swift in Sources */, + FD6B929D2E77A096004463B5 /* Info.swift in Sources */, FDF848C929405C5B007DCAE5 /* SnodeRequest.swift in Sources */, FDF848CF29405C5B007DCAE5 /* SendMessageRequest.swift in Sources */, FD2272BE2C34B710004D8A6C /* Publisher+Utilities.swift in Sources */, - FD6A7A6D2818C61500035AC1 /* _002_SetupStandardJobs.swift in Sources */, FD3765E72ADE1AA300DC1489 /* UnrevokeSubaccountRequest.swift in Sources */, FD2272BC2C33E337004D8A6C /* URLResponse+Utilities.swift in Sources */, FD2272B52C33E337004D8A6C /* BatchResponse.swift in Sources */, @@ -6294,7 +6515,6 @@ files = ( FD2272C82C34EB0A004D8A6C /* Job.swift in Sources */, FD2272C72C34EAF5004D8A6C /* ColumnExpressible.swift in Sources */, - 945D9C582D6FDBE7003C4C0C /* _005_AddJobUniqueHash.swift in Sources */, FDB3486E2BE8457F00B716C2 /* BackgroundTaskManager.swift in Sources */, 7B1D74B027C365960030B423 /* Timer+MainThread.swift in Sources */, FD428B192B4B576F006D0888 /* AppContext.swift in Sources */, @@ -6307,7 +6527,6 @@ FD42ECD62E3308B5002D03EA /* ObservableKey+SessionUtilitiesKit.swift in Sources */, FDE754D22C9BAF53002A2623 /* JobDependencies.swift in Sources */, FDE755242C9BC1D1002A2623 /* Publisher+Utilities.swift in Sources */, - FDBB25E32988B13800F1508E /* _004_AddJobPriority.swift in Sources */, 7B7CB192271508AD0079FF93 /* CallRingTonePlayer.swift in Sources */, FD00CDCB2D5317A7006B96D3 /* Scheduler+Utilities.swift in Sources */, FD848B8B283DC509000E298B /* PagedDatabaseObserver.swift in Sources */, @@ -6333,10 +6552,8 @@ FD5931A72A8DA5DA0040147D /* SQLInterpolation+Utilities.swift in Sources */, FD9004152818B46300ABAAF6 /* JobRunner.swift in Sources */, FD3765EA2ADE37B400DC1489 /* Authentication.swift in Sources */, - FD17D7CA27F546D900122BE0 /* _001_InitialSetupMigration.swift in Sources */, FDE755202C9BC1A6002A2623 /* CacheConfig.swift in Sources */, FD1A553E2E14BE11003761E4 /* PagedData.swift in Sources */, - FD4BB22C2D63FA8600D0DC3D /* (null) in Sources */, FDE755192C9BC169002A2623 /* UIImage+Utilities.swift in Sources */, C3BBE0AA2554D4DE0050F1E3 /* Dictionary+Utilities.swift in Sources */, FD97B2402A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift in Sources */, @@ -6350,11 +6567,8 @@ FDB7400B28EB99A70094D718 /* TimeInterval+Utilities.swift in Sources */, FD09797D27FBDB2000936362 /* Notification+Utilities.swift in Sources */, FDC6D7602862B3F600B04575 /* Dependencies.swift in Sources */, - FDE521902E04CCEB00061B8E /* AVURLAsset+Utilities.swift in Sources */, FD6673FF2D77F9C100041530 /* ScreenLock.swift in Sources */, - FD78E9F02DD6D61200D55B50 /* Data+Image.swift in Sources */, FDB3DA8B2E24834000148F8D /* AVURLAsset+Utilities.swift in Sources */, - FD17D7C727F5207C00122BE0 /* DatabaseMigrator+Utilities.swift in Sources */, FD848B9328420164000E298B /* UnicodeScalar+Utilities.swift in Sources */, FDE754CE2C9BAF37002A2623 /* ImageFormat.swift in Sources */, FDB11A542DCD7A7F00BEF49F /* Task+Utilities.swift in Sources */, @@ -6364,11 +6578,9 @@ FDE33BBC2D5C124900E56F42 /* DispatchTimeInterval+Utilities.swift in Sources */, FD7115FA28C8153400B47552 /* UIBarButtonItem+Combine.swift in Sources */, FD705A92278D051200F16121 /* ReusableView.swift in Sources */, - FD17D7BA27F51F2100122BE0 /* TargetMigrations.swift in Sources */, FDE754DD2C9BAF8A002A2623 /* Mnemonic.swift in Sources */, FD52CB652E13B6E900A4DA70 /* ObservationBuilder.swift in Sources */, FDBEE52E2B6A18B900C143A0 /* UserDefaultsConfig.swift in Sources */, - 947D7FE32D5181F400E8E413 /* _006_RenameTableSettingToKeyValueStore.swift in Sources */, FD78EA042DDEC3C500D55B50 /* MultiTaskManager.swift in Sources */, FD78EA062DDEC8F600D55B50 /* AsyncSequence+Utilities.swift in Sources */, FDC438CD27BC641200C60D73 /* Set+Utilities.swift in Sources */, @@ -6398,6 +6610,7 @@ FD7728962849E7E90018502F /* String+Utilities.swift in Sources */, C35D0DB525AE5F1200B6BF49 /* UIEdgeInsets.swift in Sources */, FDF222092818D2B0000A4995 /* NSAttributedString+Utilities.swift in Sources */, + FD39370C2E4D7BCA00571F17 /* DocumentPickerHandler.swift in Sources */, C32C5E0C256DDAFA003C73A2 /* NSRegularExpression+SSK.swift in Sources */, FD29598D2A43BC0B00888A17 /* Version.swift in Sources */, FD71160028C8253500B47552 /* UIView+Combine.swift in Sources */, @@ -6405,18 +6618,20 @@ FDE754D42C9BAF6B002A2623 /* UICollectionView+ReusableView.swift in Sources */, FDE754C02C9BAEF6002A2623 /* Array+Utilities.swift in Sources */, FD17D7E527F6A09900122BE0 /* Identity.swift in Sources */, - FD9004142818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift in Sources */, FDAA16762AC28A3B00DDBF77 /* UserDefaultsType.swift in Sources */, FDDF074429C3E3D000E5E8B5 /* FetchRequest+Utilities.swift in Sources */, FD7F745B2BAAA35E006DDFD8 /* LibSession.swift in Sources */, + FD3937082E4AD3FE00571F17 /* NoopDependency.swift in Sources */, FD74434A2D07CA9F00862443 /* Codable+Utilities.swift in Sources */, FD0559562E026E1B00DC48CE /* ObservingDatabase.swift in Sources */, FD74434B2D07CA9F00862443 /* CGFloat+Utilities.swift in Sources */, + FD6F5B602E657A33009A8D01 /* StreamLifecycleManager.swift in Sources */, FD52CB632E13B61700A4DA70 /* ObservableKey.swift in Sources */, FD74434C2D07CA9F00862443 /* CGSize+Utilities.swift in Sources */, FD74434D2D07CA9F00862443 /* CGPoint+Utilities.swift in Sources */, FD74434E2D07CA9F00862443 /* CGRect+Utilities.swift in Sources */, FDC289472C881A3800020BC2 /* MutableIdentifiable.swift in Sources */, + FD6F5B5E2E657A24009A8D01 /* CancellationAwareAsyncStream.swift in Sources */, C300A60D2554B31900555489 /* Logging.swift in Sources */, FD9004162818B46700ABAAF6 /* JobRunnerError.swift in Sources */, FDE7551C2C9BC169002A2623 /* UIBezierPath+Utilities.swift in Sources */, @@ -6426,7 +6641,6 @@ FDF01FAD2A9ECC4200CAF969 /* SingletonConfig.swift in Sources */, FDE755182C9BC169002A2623 /* UIAlertAction+Utilities.swift in Sources */, FD7115F828C8151C00B47552 /* DisposableBarButtonItem.swift in Sources */, - FD17D7E727F6A16700122BE0 /* _003_YDBToGRDBMigration.swift in Sources */, FDE7551B2C9BC169002A2623 /* UINavigationController+Utilities.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -6436,20 +6650,20 @@ buildActionMask = 2147483647; files = ( FD8ECF7B29340FFD00C0D1BB /* LibSession+SessionMessagingKit.swift in Sources */, - 7B81682828B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift in Sources */, + FDD23AE62E458CAA0057E853 /* _023_SplitSnodeReceivedMessageInfo.swift in Sources */, + 7B81682828B310D50069F315 /* _015_HomeQueryOptimisationIndexes.swift in Sources */, FD245C52285065D500B966DD /* SignalAttachment.swift in Sources */, FD2273002C352D8E004D8A6C /* LibSession+GroupKeys.swift in Sources */, - FD70F25C2DC1F184003729B7 /* _026_MessageDeduplicationTable.swift in Sources */, + FD70F25C2DC1F184003729B7 /* _040_MessageDeduplicationTable.swift in Sources */, FD2272FB2C352D8E004D8A6C /* LibSession+UserGroups.swift in Sources */, B8856D08256F10F1001CE70E /* DeviceSleepManager.swift in Sources */, FD22726F2C32911C004D8A6C /* ProcessPendingGroupMemberRemovalsJob.swift in Sources */, FD981BD72DC9A61A00564172 /* NotificationCategory.swift in Sources */, C300A5D32554B05A00555489 /* TypingIndicator.swift in Sources */, FDF71EA52B07363500A8D6B5 /* MessageReceiver+LibSession.swift in Sources */, - FDC13D582A17207D007267C7 /* UnsubscribeResponse.swift in Sources */, FD09799927FFC1A300936362 /* Attachment.swift in Sources */, FD245C5F2850662200B966DD /* OWSWindowManager.m in Sources */, - FDF40CDE2897A1BC006A0CC4 /* _004_RemoveLegacyYDB.swift in Sources */, + FDF40CDE2897A1BC006A0CC4 /* _011_RemoveLegacyYDB.swift in Sources */, FDF0B74928060D13004C14C5 /* QuotedReplyModel.swift in Sources */, FD78E9F42DDABA4F00D55B50 /* AttachmentUploader.swift in Sources */, FDE754A32C9A8FD1002A2623 /* SwarmPoller.swift in Sources */, @@ -6463,80 +6677,71 @@ FD47E0B12AA6A05800A55E41 /* Authentication+SessionMessagingKit.swift in Sources */, FD2272832C337830004D8A6C /* GroupPoller.swift in Sources */, FD22726C2C32911C004D8A6C /* GroupLeavingJob.swift in Sources */, - FDE33BBE2D5C3AF100E56F42 /* _023_GroupsExpiredFlag.swift in Sources */, + FDE33BBE2D5C3AF100E56F42 /* _037_GroupsExpiredFlag.swift in Sources */, FDF848F729414477007DCAE5 /* CurrentUserPoller.swift in Sources */, C3C2A74D2553A39700C340D1 /* VisibleMessage.swift in Sources */, FD2272FD2C352D8E004D8A6C /* LibSession+ConvoInfoVolatile.swift in Sources */, FD22727E2C32911C004D8A6C /* GarbageCollectionJob.swift in Sources */, FD09B7E7288670FD00ED0B66 /* Reaction.swift in Sources */, FD245C5A2850660100B966DD /* LinkPreviewDraft.swift in Sources */, - FDD82C3F2A205D0A00425F05 /* ProcessResult.swift in Sources */, FDE5219E2E0D0B9B00061B8E /* AsyncAccessible.swift in Sources */, FDF0B75C2807F41D004C14C5 /* MessageSender+Convenience.swift in Sources */, FD22726D2C32911C004D8A6C /* CheckForAppUpdatesJob.swift in Sources */, - 7B81682A28B6F1420069F315 /* ReactionResponse.swift in Sources */, FD2273082C353109004D8A6C /* DisplayPictureManager.swift in Sources */, FDE521A22E0D23AB00061B8E /* ObservableKey+SessionMessagingKit.swift in Sources */, FD2273022C352D8E004D8A6C /* LibSession+GroupInfo.swift in Sources */, - FDDD554E2C1FCB77006CBF03 /* _019_ScheduleAppUpdateCheckJob.swift in Sources */, + FDDD554E2C1FCB77006CBF03 /* _033_ScheduleAppUpdateCheckJob.swift in Sources */, FD09798927FD1C5A00936362 /* OpenGroup.swift in Sources */, - 94CD95C12E0CBF430097754D /* _029_AddProMessageFlag.swift in Sources */, + 94CD95C12E0CBF430097754D /* _044_AddProMessageFlag.swift in Sources */, FD2272FC2C352D8E004D8A6C /* LibSession+Contacts.swift in Sources */, FD848B9628422A2A000E298B /* MessageViewModel.swift in Sources */, FD05593D2DFA3A2800DC48CE /* VoipPayloadKey.swift in Sources */, FD2272782C32911C004D8A6C /* AttachmentDownloadJob.swift in Sources */, FDF0B73C27FFD3D6004C14C5 /* LinkPreview.swift in Sources */, - FD428B232B4B9969006D0888 /* _017_RebuildFTSIfNeeded_2_4_5.swift in Sources */, + FD428B232B4B9969006D0888 /* _031_RebuildFTSIfNeeded_2_4_5.swift in Sources */, FD2272742C32911C004D8A6C /* ConfigMessageReceiveJob.swift in Sources */, - FDFBB74D2A1F3C4E00CA7350 /* NotificationMetadata.swift in Sources */, FD716E6628502EE200C96BF4 /* CurrentCallProtocol.swift in Sources */, FD22727A2C32911C004D8A6C /* GroupInviteMemberJob.swift in Sources */, - FDB5DAC72A9447E7002C8721 /* _022_GroupsRebuildChanges.swift in Sources */, - FD09B7E5288670BB00ED0B66 /* _008_EmojiReacts.swift in Sources */, - FDC4385F27B4C4A200C60D73 /* PinnedMessage.swift in Sources */, - FDD20C1A2A0A03AC003898FB /* DeleteInboxResponse.swift in Sources */, + FDB5DAC72A9447E7002C8721 /* _036_GroupsRebuildChanges.swift in Sources */, + FD09B7E5288670BB00ED0B66 /* _017_EmojiReacts.swift in Sources */, 7B8D5FC428332600008324D9 /* VisibleMessage+Reaction.swift in Sources */, - FDC4386527B4DE7600C60D73 /* RoomPollInfo.swift in Sources */, FD245C6B2850667400B966DD /* VisibleMessage+Profile.swift in Sources */, FD2272FA2C352D8E004D8A6C /* LibSession+SharedGroup.swift in Sources */, - FD37EA0F28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift in Sources */, + FD37EA0F28AB3330003AE748 /* _014_FixHiddenModAdminSupport.swift in Sources */, FD2272772C32911C004D8A6C /* AttachmentUploadJob.swift in Sources */, - 7B81682328A4C1210069F315 /* UpdateTypes.swift in Sources */, - FDC13D472A16E4CA007267C7 /* SubscribeRequest.swift in Sources */, - FDC438A627BB113A00C60D73 /* UserUnbanRequest.swift in Sources */, FD22727C2C32911C004D8A6C /* GroupPromoteMemberJob.swift in Sources */, FD5C72FB284F0EA10029977D /* MessageReceiver+DataExtractionNotification.swift in Sources */, - FDC4386727B4E10E00C60D73 /* Capabilities.swift in Sources */, - FDC438A427BB107F00C60D73 /* UserBanRequest.swift in Sources */, FDE754F22C9BB08B002A2623 /* Crypto+SessionMessagingKit.swift in Sources */, - FD4C53AF2CC1D62E003B10F4 /* _021_ReworkRecipientState.swift in Sources */, + FD4C53AF2CC1D62E003B10F4 /* _035_ReworkRecipientState.swift in Sources */, C379DCF4256735770002D4EB /* VisibleMessage+Attachment.swift in Sources */, FD6C67242CF6E72E00B350A7 /* NoopSessionCallManager.swift in Sources */, FDB4BBC72838B91E00B7C95D /* LinkPreviewError.swift in Sources */, FD09798327FD1A1500936362 /* ClosedGroup.swift in Sources */, - FDC13D542A16FF29007267C7 /* LegacyGroupRequest.swift in Sources */, B8B320B7258C30D70020074B /* HTMLMetadata.swift in Sources */, FD09798727FD1B7800936362 /* GroupMember.swift in Sources */, FD78EA0D2DDFEDE200D55B50 /* LibSession+Local.swift in Sources */, - FDCD2E032A41294E00964D6A /* LegacyGroupOnlyRequest.swift in Sources */, + FDD23AE12E457CDE0057E853 /* _005_SNK_SetupStandardJobs.swift in Sources */, FD3E0C84283B5835002A425C /* SessionThreadViewModel.swift in Sources */, FD09C5EC282B8F18000CE219 /* AttachmentError.swift in Sources */, FDE754F02C9BB08B002A2623 /* Crypto+Attachments.swift in Sources */, - FD17D79927F40AB800122BE0 /* _003_YDBToGRDBMigration.swift in Sources */, - FDE754A12C9A60A6002A2623 /* Crypto+OpenGroupAPI.swift in Sources */, + FD17D79927F40AB800122BE0 /* _009_SMK_YDBToGRDBMigration.swift in Sources */, + FDE754A12C9A60A6002A2623 /* Crypto+OpenGroup.swift in Sources */, FDF0B7512807BA56004C14C5 /* NotificationsManagerType.swift in Sources */, FD2272722C32911C004D8A6C /* FailedAttachmentDownloadsJob.swift in Sources */, - FDC13D5A2A1721C5007267C7 /* LegacyNotifyRequest.swift in Sources */, + FDD23AEA2E458EB00057E853 /* _012_AddJobPriority.swift in Sources */, FD245C59285065FC00B966DD /* ControlMessage.swift in Sources */, - FD6E4C8A2A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift in Sources */, B8DE1FB626C22FCB0079C9CE /* CallMessage.swift in Sources */, + FDD23AEC2E458F980057E853 /* _024_ResetUserConfigLastHashes.swift in Sources */, FD245C50285065C700B966DD /* VisibleMessage+Quote.swift in Sources */, + FDD23AE82E458DD40057E853 /* _002_SUK_SetupStandardJobs.swift in Sources */, FD72BDA12BE368C800CF6CF6 /* UIWindowLevel+Utilities.swift in Sources */, FD4C4E9C2B02E2A300C72199 /* DisplayPictureError.swift in Sources */, + FDD23AE22E457CE50057E853 /* _008_SNK_YDBToGRDBMigration.swift in Sources */, FD5C7307284F103B0029977D /* MessageReceiver+MessageRequests.swift in Sources */, C3A71D0B2558989C0043A11F /* MessageWrapper.swift in Sources */, - FD3FAB592ADF906300DC5421 /* Profile+CurrentUser.swift in Sources */, + FD3FAB592ADF906300DC5421 /* Profile+Updating.swift in Sources */, FDEF573E2C40F2A100131302 /* GroupUpdateMemberLeftNotificationMessage.swift in Sources */, + FDD23ADF2E457CAA0057E853 /* _016_ThemePreferences.swift in Sources */, FDB11A5D2DD300D300BEF49F /* SNProtoContent+Utilities.swift in Sources */, FDE755002C9BB0FA002A2623 /* SessionEnvironment.swift in Sources */, FDB5DADC2A95D840002C8721 /* GroupUpdateMemberChangeMessage.swift in Sources */, @@ -6544,50 +6749,40 @@ FDB5DAE02A95D84D002C8721 /* GroupUpdateMemberLeftMessage.swift in Sources */, FD8FD7622C37B7BD001E38C7 /* Position.swift in Sources */, 7B93D07127CF194000811CB6 /* MessageRequestResponse.swift in Sources */, + 94B6BAF62E30A88800E718BB /* SessionProState.swift in Sources */, FDB5DADE2A95D847002C8721 /* GroupUpdatePromoteMessage.swift in Sources */, - FD245C662850665900B966DD /* OpenGroupAPI.swift in Sources */, FD245C5B2850660500B966DD /* ReadReceipt.swift in Sources */, FD428B1F2B4B758B006D0888 /* AppReadiness.swift in Sources */, FD22726B2C32911C004D8A6C /* SendReadReceiptsJob.swift in Sources */, B8F5F60325EDE16F003BF8D4 /* DataExtractionNotification.swift in Sources */, C3A71D1E25589AC30043A11F /* WebSocketProto.swift in Sources */, C3C2A7852553AAF300C340D1 /* SessionProtos.pb.swift in Sources */, - FDF0B7422804EA4F004C14C5 /* _002_SetupStandardJobs.swift in Sources */, + FDF0B7422804EA4F004C14C5 /* _007_SMK_SetupStandardJobs.swift in Sources */, B8EB20EE2640F28000773E52 /* VisibleMessage+OpenGroupInvitation.swift in Sources */, - FDF8487F29405994007DCAE5 /* HTTPHeader+OpenGroup.swift in Sources */, - FD8ECF7D2934293A00C0D1BB /* _013_SessionUtilChanges.swift in Sources */, - FD17D7A227F40F0500122BE0 /* _001_InitialSetupMigration.swift in Sources */, + FD8ECF7D2934293A00C0D1BB /* _027_SessionUtilChanges.swift in Sources */, + FD17D7A227F40F0500122BE0 /* _006_SMK_InitialSetupMigration.swift in Sources */, FD245C5D2850660F00B966DD /* OWSAudioPlayer.m in Sources */, FD2272FE2C352D8E004D8A6C /* LibSession+GroupMembers.swift in Sources */, FDF0B7582807F368004C14C5 /* MessageReceiverError.swift in Sources */, FD09798D27FD1D8900936362 /* DisappearingMessageConfiguration.swift in Sources */, FD2272732C32911C004D8A6C /* ConfigurationSyncJob.swift in Sources */, FDF0B75A2807F3A3004C14C5 /* MessageSenderError.swift in Sources */, - FDC4382F27B383AF00C60D73 /* LegacyPushServerResponse.swift in Sources */, - 94B6BAFA2E38454F00E718BB /* SessionProState.swift in Sources */, - FDC4386327B4D94E00C60D73 /* SOGSMessage.swift in Sources */, FD245C692850666800B966DD /* ExpirationTimerUpdate.swift in Sources */, - FD42F9A8285064B800A0C77D /* PushNotificationAPI.swift in Sources */, FD2272752C32911C004D8A6C /* RetrieveDefaultOpenGroupRoomsJob.swift in Sources */, FD2272712C32911C004D8A6C /* MessageReceiveJob.swift in Sources */, FDB5DAC12A9443A5002C8721 /* MessageSender+Groups.swift in Sources */, - FDC13D4B2A16ECBA007267C7 /* SubscribeResponse.swift in Sources */, FD2272702C32911C004D8A6C /* DisappearingMessagesJob.swift in Sources */, - FD7115F228C6CB3900B47552 /* _010_AddThreadIdToFTS.swift in Sources */, + FD7115F228C6CB3900B47552 /* _019_AddThreadIdToFTS.swift in Sources */, FD716E6428502DDD00C96BF4 /* CallManagerProtocol.swift in Sources */, 943C6D822B75E061004ACE64 /* Message+DisappearingMessages.swift in Sources */, - FDC438C727BB6DF000C60D73 /* DirectMessage.swift in Sources */, - FD3765F42ADE5A0800DC1489 /* AuthenticatedRequest.swift in Sources */, - FDC13D502A16EE50007267C7 /* PushNotificationAPIEndpoint.swift in Sources */, FD432434299C6985008A0213 /* PendingReadReceipt.swift in Sources */, - FDC4381727B32EC700C60D73 /* Personalization.swift in Sources */, FD78E9F62DDD43AD00D55B50 /* Mutation.swift in Sources */, FDB11A5F2DD5B77800BEF49F /* Message+Origin.swift in Sources */, FD245C51285065CC00B966DD /* MessageReceiver.swift in Sources */, - FDC4387827B5C35400C60D73 /* SendMessageRequest.swift in Sources */, FDB5DAE62A95D8B0002C8721 /* GroupUpdateDeleteMemberContentMessage.swift in Sources */, - 7B5233C6290636D700F8F375 /* _018_DisappearingMessagesConfiguration.swift in Sources */, + 7B5233C6290636D700F8F375 /* _032_DisappearingMessagesConfiguration.swift in Sources */, FD5C72FD284F0EC90029977D /* MessageReceiver+ExpirationTimers.swift in Sources */, + FDD23AEB2E458F4D0057E853 /* _020_AddJobUniqueHash.swift in Sources */, FDB11A502DCC6ADE00BEF49F /* ThreadUpdateInfo.swift in Sources */, B8D0A25925E367AC00C1835E /* Notification+MessageReceiver.swift in Sources */, FDC1BD662CFD6C4F002CDC71 /* Config.swift in Sources */, @@ -6596,86 +6791,81 @@ C32C599E256DB02B003C73A2 /* TypingIndicators.swift in Sources */, FDE7549B2C940108002A2623 /* MessageViewModel+DeletionActions.swift in Sources */, FD09799527FE7B8E00936362 /* Interaction.swift in Sources */, - FD37EA0D28AB2A45003AE748 /* _005_FixDeletedMessageReadState.swift in Sources */, - 7BAA7B6628D2DE4700AE1489 /* _009_OpenGroupPermission.swift in Sources */, - FDC4380927B31D4E00C60D73 /* OpenGroupAPIError.swift in Sources */, + FD37EA0D28AB2A45003AE748 /* _013_FixDeletedMessageReadState.swift in Sources */, + FDD23AE32E457CFE0057E853 /* _010_FlagMessageHashAsDeletedOrInvalid.swift in Sources */, + 7BAA7B6628D2DE4700AE1489 /* _018_OpenGroupPermission.swift in Sources */, FD2286692C37DA5500BC06F7 /* PollerType.swift in Sources */, - FD860CBC2D6E7A9F00BBE29C /* _024_FixBustedInteractionVariant.swift in Sources */, + FD860CBC2D6E7A9F00BBE29C /* _038_FixBustedInteractionVariant.swift in Sources */, + FDD23AE72E458DBC0057E853 /* _001_SUK_InitialSetupMigration.swift in Sources */, + FDD23AE42E458C810057E853 /* _021_AddSnodeReveivedMessageInfoPrimaryKey.swift in Sources */, FD22727B2C32911C004D8A6C /* MessageSendJob.swift in Sources */, FD78E9EE2DD6D32500D55B50 /* ImageDataManager+Singleton.swift in Sources */, - FDC4382027B36ADC00C60D73 /* SOGSEndpoint.swift in Sources */, - FDC438C927BB706500C60D73 /* SendDirectMessageRequest.swift in Sources */, C3A71D1F25589AC30043A11F /* WebSocketResources.pb.swift in Sources */, FD981BC42DC304E600564172 /* MessageDeduplication.swift in Sources */, FDF0B74B28061F7A004C14C5 /* InteractionAttachment.swift in Sources */, FD09796E27FA6D0000936362 /* Contact.swift in Sources */, - FD02CC142C3677E6009AB976 /* Request+OpenGroupAPI.swift in Sources */, FD5C72F7284F0E560029977D /* MessageReceiver+ReadReceipts.swift in Sources */, - FDC13D492A16EC20007267C7 /* Service.swift in Sources */, FDBA8A842D597975007C19C0 /* FailedGroupInvitesAndPromotionsJob.swift in Sources */, - FD778B6429B189FF001BAC6B /* _014_GenerateInitialUserConfigDumps.swift in Sources */, - FDC13D562A171FE4007267C7 /* UnsubscribeRequest.swift in Sources */, + FD778B6429B189FF001BAC6B /* _028_GenerateInitialUserConfigDumps.swift in Sources */, FD1A55432E179AED003761E4 /* ObservableKeyEvent+Utilities.swift in Sources */, C32C598A256D0664003C73A2 /* SNProtoEnvelope+Conversion.swift in Sources */, - FDC438CB27BB7DB100C60D73 /* UpdateMessageRequest.swift in Sources */, + FDD23AE92E458E020057E853 /* _003_SUK_YDBToGRDBMigration.swift in Sources */, FD8ECF7F2934298100C0D1BB /* ConfigDump.swift in Sources */, FD22727F2C32911C004D8A6C /* GetExpirationJob.swift in Sources */, FD2272792C32911C004D8A6C /* DisplayPictureDownloadJob.swift in Sources */, FD981BD92DC9A69600564172 /* NotificationUserInfoKey.swift in Sources */, - C352A2FF25574B6300338F3E /* (null) in Sources */, + FDD23AE52E458C940057E853 /* _022_DropSnodeCache.swift in Sources */, FD16AB612A1DD9B60083D849 /* ProfilePictureView+Convenience.swift in Sources */, B8856D11256F112A001CE70E /* OWSAudioSession.swift in Sources */, FD2272762C32911C004D8A6C /* ExpirationUpdateJob.swift in Sources */, FD716E722850647600C96BF4 /* Data+Utilities.swift in Sources */, - FD368A6829DE8F9C000DBF1E /* _012_AddFTSIfNeeded.swift in Sources */, + FD368A6829DE8F9C000DBF1E /* _026_AddFTSIfNeeded.swift in Sources */, FD5C7301284F0F7A0029977D /* MessageReceiver+UnsendRequests.swift in Sources */, C3C2A75F2553A3C500C340D1 /* VisibleMessage+LinkPreview.swift in Sources */, FD848B8D283E0B26000E298B /* MessageInputTypes.swift in Sources */, - FDFE75B12ABD2D2400655640 /* _016_MakeBrokenProfileTimestampsNullable.swift in Sources */, + FDFE75B12ABD2D2400655640 /* _030_MakeBrokenProfileTimestampsNullable.swift in Sources */, FD09799B27FFC82D00936362 /* Quote.swift in Sources */, FD2273012C352D8E004D8A6C /* LibSession+Shared.swift in Sources */, C3C2A74425539EB700C340D1 /* Message.swift in Sources */, FD245C682850666300B966DD /* Message+Destination.swift in Sources */, - FDF8488029405994007DCAE5 /* HTTPQueryParam+OpenGroup.swift in Sources */, - FD8A5B322DC191B4004C689B /* _025_DropLegacyClosedGroupKeyPairTable.swift in Sources */, + FD8A5B322DC191B4004C689B /* _039_DropLegacyClosedGroupKeyPairTable.swift in Sources */, FD245C632850664600B966DD /* Configuration.swift in Sources */, FD981BC62DC3310B00564172 /* ExtensionHelper.swift in Sources */, - FD78E9FA2DDD74D200D55B50 /* _027_MoveSettingsToLibSession.swift in Sources */, + FD78E9FA2DDD74D200D55B50 /* _042_MoveSettingsToLibSession.swift in Sources */, FD2272FF2C352D8E004D8A6C /* LibSession+UserProfile.swift in Sources */, FD5C7305284F0FF30029977D /* MessageReceiver+VisibleMessages.swift in Sources */, FDB5DAE82A95D96C002C8721 /* MessageReceiver+Groups.swift in Sources */, 94CD95BD2E09083C0097754D /* LibSession+Pro.swift in Sources */, - FD1D732E2A86114600E3F410 /* _015_BlockCommunityMessageRequests.swift in Sources */, + FD1D732E2A86114600E3F410 /* _029_BlockCommunityMessageRequests.swift in Sources */, FD2B4B042949887A00AB4848 /* QueryInterfaceRequest+Utilities.swift in Sources */, FDAA167F2AC5290000DDBF77 /* Preferences+NotificationPreviewType.swift in Sources */, FD09797027FA6FF300936362 /* Profile.swift in Sources */, FD245C56285065EA00B966DD /* SNProto.swift in Sources */, FDE7549D2C9961A4002A2623 /* CommunityPoller.swift in Sources */, + 942BA9BF2E4ABBA1007C4595 /* _045_LastProfileUpdateTimestamp.swift in Sources */, FDE754F12C9BB08B002A2623 /* Crypto+LibSession.swift in Sources */, FD09798B27FD1CFE00936362 /* Capability.swift in Sources */, C3BBE0C72554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift in Sources */, FD09798127FCFEE800936362 /* SessionThread.swift in Sources */, - FD3765F62ADE5BA500DC1489 /* ServiceInfo.swift in Sources */, FD09C5EA282A1BB2000CE219 /* ThreadTypingIndicator.swift in Sources */, FDB5DADA2A95D839002C8721 /* GroupUpdateInfoChangeMessage.swift in Sources */, FDF71EA32B072C2800A8D6B5 /* LibSessionMessage.swift in Sources */, FDAA167D2AC528A200DDBF77 /* Preferences+Sound.swift in Sources */, - FDE754FE2C9BB0D0002A2623 /* Threading+SMK.swift in Sources */, + FDD23AED2E4590A10057E853 /* _041_RenameTableSettingToKeyValueStore.swift in Sources */, + FDE754FE2C9BB0D0002A2623 /* Threading+SessionMessagingKit.swift in Sources */, FDF0B75E280AAF35004C14C5 /* Preferences.swift in Sources */, FDF2F0222DAE1AF500491E8A /* MessageReceiver+LegacyClosedGroups.swift in Sources */, - FD02CC122C367762009AB976 /* Request+PushNotificationAPI.swift in Sources */, - FD05594E2E012D2700DC48CE /* _028_RenameAttachments.swift in Sources */, + FD05594E2E012D2700DC48CE /* _043_RenameAttachments.swift in Sources */, FD22726E2C32911C004D8A6C /* FailedMessageSendsJob.swift in Sources */, + FD6B92E42E77C256004463B5 /* PushNotificationAPI+SessionMessagingKit.swift in Sources */, FDB5DAD42A9483F3002C8721 /* GroupUpdateInviteMessage.swift in Sources */, FDB5DAE22A95D8A0002C8721 /* GroupUpdateInviteResponseMessage.swift in Sources */, FDB11A522DCC6B0000BEF49F /* OpenGroupUrlInfo.swift in Sources */, - FD3559462CC1FF200088F2A9 /* _020_AddMissingWhisperFlag.swift in Sources */, + FD3559462CC1FF200088F2A9 /* _034_AddMissingWhisperFlag.swift in Sources */, FD5C72F9284F0E880029977D /* MessageReceiver+TypingIndicators.swift in Sources */, + FDD23AE02E457CD40057E853 /* _004_SNK_InitialSetupMigration.swift in Sources */, FD5C7303284F0FA50029977D /* MessageReceiver+Calls.swift in Sources */, - FD83B9C927D0487A005E1583 /* SendDirectMessageResponse.swift in Sources */, - FDC438AA27BB12BB00C60D73 /* UserModeratorRequest.swift in Sources */, - FDC4385D27B4C18900C60D73 /* Room.swift in Sources */, - FDE755022C9BB122002A2623 /* _011_AddPendingReadReceipts.swift in Sources */, + FDE755022C9BB122002A2623 /* _025_AddPendingReadReceipts.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -6690,7 +6880,6 @@ FD7115EB28C5D78E00B47552 /* ThreadSettingsViewModel.swift in Sources */, B8041AA725C90927003C2166 /* TypingIndicatorCell.swift in Sources */, 7B4EF25A2934743000CB351D /* SessionTableViewTitleView.swift in Sources */, - FD2272D92C34EED6004D8A6C /* _001_ThemePreferences.swift in Sources */, 942256A12C23F90700C0FDBF /* CustomTopTabBar.swift in Sources */, FDFDE12A282D056B0098B17F /* MediaZoomAnimationController.swift in Sources */, 4C1885D2218F8E1C00B67051 /* PhotoGridViewCell.swift in Sources */, @@ -6707,10 +6896,8 @@ B877E24626CA13BA0007970A /* CallVC+Camera.swift in Sources */, 454A84042059C787008B8C75 /* MediaTileViewController.swift in Sources */, FD3FAB5F2AE9BC2200DC5421 /* EditGroupViewModel.swift in Sources */, - FDEF57222C3CF03D00131302 /* (null) in Sources */, 7BA37AFB2AEB64CA002438F8 /* DisappearingMessageTimerView.swift in Sources */, FD12A8492AD63C4700EEBA0D /* SessionNavItem.swift in Sources */, - FD12A83D2AD63BCC00EEBA0D /* EditableState.swift in Sources */, FD37EA0528AA00C1003AE748 /* NotificationSettingsViewModel.swift in Sources */, C328255225CA64470062D0A7 /* ContextMenuVC+ActionView.swift in Sources */, C3548F0824456AB6009433A8 /* UIView+Wrapping.swift in Sources */, @@ -6722,6 +6909,7 @@ FDB3DA842E1CA22400148F8D /* UIActivityViewController+Utilities.swift in Sources */, FDCDB8E02811007F00352A0C /* HomeViewModel.swift in Sources */, FDE754F82C9BB0B0002A2623 /* UserNotificationConfig.swift in Sources */, + FED288F32E4C28CF00C31171 /* AppReviewPromptDialog.swift in Sources */, 7B0EFDF62755CC5400FFAAE7 /* CallMissedTipsModal.swift in Sources */, C374EEF425DB31D40073A857 /* VoiceMessageRecordingView.swift in Sources */, 7B1581E6271FD2A100848B49 /* VideoPreviewVC.swift in Sources */, @@ -6736,7 +6924,6 @@ B886B4A92398BA1500211ABE /* QRCode.swift in Sources */, 34A8B3512190A40E00218A25 /* MediaAlbumView.swift in Sources */, FD09C5E828264937000CE219 /* MediaDetailViewController.swift in Sources */, - FDEF57262C3CF05F00131302 /* (null) in Sources */, 3496955E219B605E00DCFE74 /* PhotoLibrary.swift in Sources */, 7BA37AFD2AEF7C3D002438F8 /* VoiceMessageView_SwiftUI.swift in Sources */, 7B1B52E028580D51006069F2 /* EmojiSkinTonePicker.swift in Sources */, @@ -6780,10 +6967,10 @@ 7BBBDC462875600700747E59 /* DocumentTitleViewController.swift in Sources */, FD71163F28E2C82C00B47552 /* SessionHeaderView.swift in Sources */, B877E24226CA12910007970A /* CallVC.swift in Sources */, - FDEF57232C3CF04300131302 /* (null) in Sources */, FDC498B92AC15FE300EDD897 /* AppNotificationAction.swift in Sources */, FD7443402D07A25C00862443 /* PushRegistrationManager.swift in Sources */, 7BA6890D27325CCC00EFC32F /* SessionCallManager+CXCallController.swift in Sources */, + FDE71B0B2E79352D0023F5F9 /* DeveloperSettingsGroupsViewModel.swift in Sources */, 7B71A98F2925E2A600E54854 /* SessionFooterView.swift in Sources */, C374EEEB25DA3CA70073A857 /* ConversationTitleView.swift in Sources */, FDE754E52C9BB012002A2623 /* BezierPathView.swift in Sources */, @@ -6814,7 +7001,6 @@ 7BAF54CF27ACCEEC003D12F8 /* GlobalSearchViewController.swift in Sources */, FD37EA1728AC5605003AE748 /* NotificationContentViewModel.swift in Sources */, FD3FAB612AEA194E00DC5421 /* UserListViewModel.swift in Sources */, - B886B4A72398B23E00211ABE /* (null) in Sources */, 94CD96322E1B88C20097754D /* ExpandingAttachmentsButton.swift in Sources */, 94B3DC172AF8592200C88531 /* QuoteView_SwiftUI.swift in Sources */, 4C4AE6A1224AF35700D4AF6F /* SendMediaNavigationController.swift in Sources */, @@ -6852,15 +7038,14 @@ 45F32C232057297A00A300D5 /* MediaPageViewController.swift in Sources */, 7B81FB5A2AB01B17002FB267 /* LoadingIndicatorView.swift in Sources */, 7B9F71D42852EEE2006DFE7B /* Emoji+Name.swift in Sources */, + FDE71B0D2E793B250023F5F9 /* DeveloperSettingsProViewModel.swift in Sources */, 4CA46F4C219CCC630038ABDE /* CaptionView.swift in Sources */, C328253025CA55370062D0A7 /* ContextMenuWindow.swift in Sources */, - FDEF57242C3CF04700131302 /* (null) in Sources */, 9479981C2DD44ADC008F5CD5 /* ThreadNotificationSettingsViewModel.swift in Sources */, 34BECE2E1F7ABCE000D7438D /* GifPickerViewController.swift in Sources */, 9422568C2C23F8C800C0FDBF /* DisplayNameScreen.swift in Sources */, 7B9F71D72853100A006DFE7B /* Emoji+Available.swift in Sources */, FD09C5E628260FF9000CE219 /* MediaGalleryViewModel.swift in Sources */, - FDEF57212C3CF03A00131302 /* (null) in Sources */, 7B9F71D32852EEE2006DFE7B /* Emoji.swift in Sources */, C328250F25CA06020062D0A7 /* VoiceMessageView.swift in Sources */, 3488F9362191CC4000E524CC /* MediaView.swift in Sources */, @@ -6900,7 +7085,6 @@ 940943402C7ED62300D9D2E0 /* StartupError.swift in Sources */, FD368A6A29DE9E30000DBF1E /* UIContextualAction+Utilities.swift in Sources */, 947D7FDE2D5180F200E8E413 /* SessionNetworkScreen+ViewModel.swift in Sources */, - FDEF57252C3CF04C00131302 /* (null) in Sources */, 7B4C75CD26BB92060000AC89 /* DeletedMessageView.swift in Sources */, FDD250722837234B00198BDA /* MediaGalleryNavigationController.swift in Sources */, FDD2506E283711D600198BDA /* DifferenceKit+Utilities.swift in Sources */, @@ -6917,6 +7101,7 @@ B8EB20F02640F7F000773E52 /* OpenGroupInvitationView.swift in Sources */, C328254025CA55880062D0A7 /* ContextMenuVC.swift in Sources */, 9409433E2C7EB81800D9D2E0 /* WebRTCSession+Constants.swift in Sources */, + FED288F82E4C3BE100C31171 /* AppReviewPromptModel.swift in Sources */, FDE754B12C9B96B4002A2623 /* TurnServerInfo.swift in Sources */, 7BD687D12A5D0D1200D8E455 /* MessageInfoScreen.swift in Sources */, B8269D2925C7A4B400488AB4 /* InputView.swift in Sources */, @@ -6940,7 +7125,7 @@ FD71161528D00D6700B47552 /* ThreadDisappearingMessagesViewModelSpec.swift in Sources */, FD01503A2CA24328005B08A1 /* MockJobRunner.swift in Sources */, FD23EA5E28ED00FD0058676E /* NimbleExtensions.swift in Sources */, - FD481A9B2CB4CAF100ECC4CF /* CustomArgSummaryDescribable+SMK.swift in Sources */, + FD481A9B2CB4CAF100ECC4CF /* CustomArgSummaryDescribable+SessionMessagingKit.swift in Sources */, FD23EA5D28ED00FA0058676E /* TestConstants.swift in Sources */, FD71161A28D00E1100B47552 /* NotificationContentViewModelSpec.swift in Sources */, FDFE75B52ABD46B700655640 /* MockUserDefaults.swift in Sources */, @@ -6971,12 +7156,14 @@ FD65318D2AA025C500DFEEAA /* TestDependencies.swift in Sources */, FD49E2492B05C1D500FFBBB5 /* MockKeychain.swift in Sources */, FD99D0922D10F5EE005D2E15 /* ThreadSafeSpec.swift in Sources */, + FDD23AF02E459EDD0057E853 /* _020_AddJobUniqueHash.swift in Sources */, FD83B9BF27CF2294005E1583 /* TestConstants.swift in Sources */, FD9DD2732A72516D00ECB68E /* TestExtensions.swift in Sources */, FD3FAB6E2AF1B28C00DC5421 /* MockFileManager.swift in Sources */, FD83B9BB27CF20AF005E1583 /* SessionIdSpec.swift in Sources */, FDB11A592DD17D0600BEF49F /* MockLogger.swift in Sources */, FD8A5B302DC18D61004C689B /* GeneralCacheSpec.swift in Sources */, + FDD23AEE2E459E470057E853 /* _001_SUK_InitialSetupMigration.swift in Sources */, FDC290A927D9B46D005DAE71 /* NimbleExtensions.swift in Sources */, FD0150402CA2433D005B08A1 /* BencodeDecoderSpec.swift in Sources */, FD0150412CA2433D005B08A1 /* BencodeEncoderSpec.swift in Sources */, @@ -6994,6 +7181,7 @@ FDDF074A29DAB36900E5E8B5 /* JobRunnerSpec.swift in Sources */, FD0969FB2A6A00B100C5C365 /* Mocked.swift in Sources */, FD0150292CA23DB7005B08A1 /* GRDBExtensions.swift in Sources */, + FDD23AEF2E459EC90057E853 /* _012_AddJobPriority.swift in Sources */, FD481A942CAE0AE000ECC4CF /* MockAppContext.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -7005,6 +7193,8 @@ FDB5DB062A981C67002C8721 /* PreparedRequestSendingSpec.swift in Sources */, FD49E2482B05C1D500FFBBB5 /* MockKeychain.swift in Sources */, FDB5DB082A981F8B002C8721 /* Mocked.swift in Sources */, + FD6B92CD2E77B22D004463B5 /* SOGSMessageSpec.swift in Sources */, + FD6B92DB2E77B597004463B5 /* CryptoSOGSAPISpec.swift in Sources */, FD336F702CABB96C00C0B51B /* BatchRequestSpec.swift in Sources */, FD3765E22AD8F53B00DC1489 /* CommonSSKMockExtensions.swift in Sources */, FDB5DB142A981FAE002C8721 /* CombineExtensions.swift in Sources */, @@ -7013,11 +7203,19 @@ FD3765DF2AD8F03100DC1489 /* MockSnodeAPICache.swift in Sources */, FDB11A582DD17D0600BEF49F /* MockLogger.swift in Sources */, FDB5DB112A981FA6002C8721 /* TestExtensions.swift in Sources */, + FD6B92D22E77B270004463B5 /* SendDirectMessageRequestSpec.swift in Sources */, + FD6B92D32E77B270004463B5 /* UpdateMessageRequestSpec.swift in Sources */, FDB5DB092A981F8D002C8721 /* MockCrypto.swift in Sources */, FDAA167B2AC28E2F00DDBF77 /* SnodeRequestSpec.swift in Sources */, + FD6B92D02E77B23B004463B5 /* CapabilitiesResponse.swift in Sources */, + FD6B92D42E77B2C7004463B5 /* SOGSAPISpec.swift in Sources */, FD65318C2AA025C500DFEEAA /* TestDependencies.swift in Sources */, FDB5DB102A981FA3002C8721 /* TestConstants.swift in Sources */, FDB5DB0C2A981F96002C8721 /* MockNetwork.swift in Sources */, + FD6B92D12E77B253004463B5 /* SendSOGSMessageRequestSpec.swift in Sources */, + FD6B92D62E77B55D004463B5 /* SOGSEndpointSpec.swift in Sources */, + FD6B92D72E77B55D004463B5 /* SOGSErrorSpec.swift in Sources */, + FD6B92D82E77B55D004463B5 /* PersonalizationSpec.swift in Sources */, FD336F712CABB97800C0B51B /* DestinationSpec.swift in Sources */, FD336F722CABB97800C0B51B /* BatchResponseSpec.swift in Sources */, FD336F732CABB97800C0B51B /* HeaderSpec.swift in Sources */, @@ -7026,6 +7224,8 @@ FD336F762CABB97800C0B51B /* BencodeResponseSpec.swift in Sources */, FDB5DB0B2A981F92002C8721 /* MockGeneralCache.swift in Sources */, FD0150492CA243CB005B08A1 /* Mock.swift in Sources */, + FD6B92CE2E77B234004463B5 /* RoomPollInfoSpec.swift in Sources */, + FD6B92CF2E77B234004463B5 /* RoomSpec.swift in Sources */, FDB5DB122A981FA8002C8721 /* NimbleExtensions.swift in Sources */, FD0150282CA23DB7005B08A1 /* GRDBExtensions.swift in Sources */, FDB5DB152A981FB0002C8721 /* SynchronousStorage.swift in Sources */, @@ -7037,28 +7237,23 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - FD72BDA72BE369DC00CF6CF6 /* CryptoOpenGroupAPISpec.swift in Sources */, - FDC2909427D710B4005DAE71 /* SOGSEndpointSpec.swift in Sources */, + FD72BDA72BE369DC00CF6CF6 /* CryptoOpenGroupSpec.swift in Sources */, FD96F3A529DBC3DC00401309 /* MessageSendJobSpec.swift in Sources */, - FDC2909127D709CA005DAE71 /* SOGSMessageSpec.swift in Sources */, - FDC2909627D71252005DAE71 /* SOGSErrorSpec.swift in Sources */, - FDC2908727D7047F005DAE71 /* RoomSpec.swift in Sources */, FDE754A82C9B964D002A2623 /* MessageReceiverGroupsSpec.swift in Sources */, FD01504C2CA243CB005B08A1 /* Mock.swift in Sources */, FD981BC92DC4641100564172 /* ExtensionHelperSpec.swift in Sources */, FD981BCB2DC4A21C00564172 /* MessageDeduplicationSpec.swift in Sources */, - FD83B9C727CF3F10005E1583 /* CapabilitiesSpec.swift in Sources */, + FD83B9C727CF3F10005E1583 /* CapabilitySpec.swift in Sources */, FD2AAAF128ED57B500A49611 /* SynchronousStorage.swift in Sources */, FD23CE2A2A6775660000B97C /* MockCrypto.swift in Sources */, FD336F6F2CAA37CB00C0B51B /* MockCommunityPoller.swift in Sources */, - FDC2909827D7129B005DAE71 /* PersonalizationSpec.swift in Sources */, FD481A962CAE0AE000ECC4CF /* MockAppContext.swift in Sources */, FDB11A562DD17C3300BEF49F /* MockLogger.swift in Sources */, FD0150452CA243BB005B08A1 /* LibSessionUtilSpec.swift in Sources */, FD981BCD2DC81ABF00564172 /* MockExtensionHelper.swift in Sources */, FD336F602CAA28CF00C0B51B /* CommonSMKMockExtensions.swift in Sources */, FD336F612CAA28CF00C0B51B /* MockNotificationsManager.swift in Sources */, - FD336F622CAA28CF00C0B51B /* CustomArgSummaryDescribable+SMK.swift in Sources */, + FD336F622CAA28CF00C0B51B /* CustomArgSummaryDescribable+SessionMessagingKit.swift in Sources */, FD336F632CAA28CF00C0B51B /* MockOGMCache.swift in Sources */, FD481A922CAD17DE00ECC4CF /* LibSessionGroupMembersSpec.swift in Sources */, FD336F642CAA28CF00C0B51B /* MockCommunityPollerCache.swift in Sources */, @@ -7075,26 +7270,21 @@ FD65318B2AA025C500DFEEAA /* TestDependencies.swift in Sources */, FD49E2472B05C1D500FFBBB5 /* MockKeychain.swift in Sources */, FD3765E32AD8F56200DC1489 /* CommonSSKMockExtensions.swift in Sources */, - FDC4389A27BA002500C60D73 /* OpenGroupAPISpec.swift in Sources */, FD23EA6228ED0B260058676E /* CombineExtensions.swift in Sources */, FD83B9C527CF3E2A005E1583 /* OpenGroupSpec.swift in Sources */, FD23CE342A67C4D90000B97C /* MockNetwork.swift in Sources */, FD61FCF92D308CC9005752DE /* GroupMemberSpec.swift in Sources */, - FDC2908B27D707F3005DAE71 /* SendMessageRequestSpec.swift in Sources */, FDC290A827D9B46D005DAE71 /* NimbleExtensions.swift in Sources */, FD7692F72A53A2ED000E4B70 /* SessionThreadViewModelSpec.swift in Sources */, FD0969F92A69FFE700C5C365 /* Mocked.swift in Sources */, FD481A902CAD16F100ECC4CF /* LibSessionGroupInfoSpec.swift in Sources */, FDE754A92C9B964D002A2623 /* MessageSenderGroupsSpec.swift in Sources */, - FDC2908F27D70938005DAE71 /* SendDirectMessageRequestSpec.swift in Sources */, FD3FAB672AF0C47000DC5421 /* DisplayPictureDownloadJobSpec.swift in Sources */, FD72BDA42BE3690B00CF6CF6 /* CryptoSMKSpec.swift in Sources */, - FDC2908927D70656005DAE71 /* RoomPollInfoSpec.swift in Sources */, FD336F6C2CAA29C600C0B51B /* CommunityPollerSpec.swift in Sources */, FD481AA32CB889AE00ECC4CF /* RetrieveDefaultOpenGroupRoomsJobSpec.swift in Sources */, FDFD645D27F273F300808CA1 /* MockGeneralCache.swift in Sources */, FD01502A2CA23DB7005B08A1 /* GRDBExtensions.swift in Sources */, - FDC2908D27D70905005DAE71 /* UpdateMessageRequestSpec.swift in Sources */, FD01503B2CA24328005B08A1 /* MockJobRunner.swift in Sources */, FD3F2EE72DE6CC4100FD6849 /* NotificationsManagerSpec.swift in Sources */, FD078E5427E197CA000769AF /* OpenGroupManagerSpec.swift in Sources */, @@ -7125,7 +7315,7 @@ }; B8D64FB825BA78270029CFC0 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = C3C2A59E255385C100C340D1 /* SessionSnodeKit */; + target = C3C2A59E255385C100C340D1 /* SessionNetworkingKit */; targetProxy = B8D64FB725BA78270029CFC0 /* PBXContainerItemProxy */; }; B8D64FBA25BA78270029CFC0 /* PBXTargetDependency */ = { @@ -7140,7 +7330,7 @@ }; B8D64FC425BA784A0029CFC0 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = C3C2A59E255385C100C340D1 /* SessionSnodeKit */; + target = C3C2A59E255385C100C340D1 /* SessionNetworkingKit */; targetProxy = B8D64FC325BA784A0029CFC0 /* PBXContainerItemProxy */; }; B8D64FC625BA784A0029CFC0 /* PBXTargetDependency */ = { @@ -7160,7 +7350,7 @@ }; C3C2A5A5255385C100C340D1 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = C3C2A59E255385C100C340D1 /* SessionSnodeKit */; + target = C3C2A59E255385C100C340D1 /* SessionNetworkingKit */; targetProxy = C3C2A5A4255385C100C340D1 /* PBXContainerItemProxy */; }; C3C2A67F255388CC00C340D1 /* PBXTargetDependency */ = { @@ -7222,7 +7412,7 @@ FDB5DB002A981C43002C8721 /* PBXTargetDependency */ = { isa = PBXTargetDependency; platformFilter = ios; - target = C3C2A59E255385C100C340D1 /* SessionSnodeKit */; + target = C3C2A59E255385C100C340D1 /* SessionNetworkingKit */; targetProxy = FDB5DAFF2A981C43002C8721 /* PBXContainerItemProxy */; }; FDC4389427B9FFC700C60D73 /* PBXTargetDependency */ = { @@ -7777,7 +7967,7 @@ GCC_DYNAMIC_NO_PIC = NO; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - INFOPLIST_FILE = SessionSnodeKit/Meta/Info.plist; + INFOPLIST_FILE = SessionNetworkingKit/Meta/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -7788,7 +7978,7 @@ MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionSnodeKit"; + PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionNetworkingKit"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = NO; @@ -7850,7 +8040,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - INFOPLIST_FILE = SessionSnodeKit/Meta/Info.plist; + INFOPLIST_FILE = SessionNetworkingKit/Meta/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -7861,7 +8051,7 @@ MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionSnodeKit"; + PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionNetworkingKit"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; SKIP_INSTALL = YES; @@ -8156,7 +8346,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; COMPILE_LIB_SESSION = ""; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 622; + CURRENT_PROJECT_VERSION = 640; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -8196,7 +8386,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 15.6; LIB_SESSION_SOURCE_DIR = "${SRCROOT}/../LibSession-Util"; LOCALIZED_STRING_SWIFTUI_SUPPORT = NO; - MARKETING_VERSION = 2.13.2; + MARKETING_VERSION = 2.14.4; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "-Werror=protocol"; OTHER_SWIFT_FLAGS = "-D DEBUG -Xfrontend -warn-long-expression-type-checking=100"; @@ -8237,7 +8427,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; COMPILE_LIB_SESSION = ""; - CURRENT_PROJECT_VERSION = 622; + CURRENT_PROJECT_VERSION = 640; ENABLE_BITCODE = NO; ENABLE_MODULE_VERIFIER = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -8272,7 +8462,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 15.6; LIB_SESSION_SOURCE_DIR = "${SRCROOT}/../LibSession-Util"; LOCALIZED_STRING_SWIFTUI_SUPPORT = NO; - MARKETING_VERSION = 2.13.2; + MARKETING_VERSION = 2.14.4; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = ( "-DNS_BLOCK_ASSERTIONS=1", @@ -8383,11 +8573,12 @@ FD2272502C32910F004D8A6C /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_MODULE_VERIFIER = NO; GCC_DYNAMIC_NO_PIC = NO; GENERATE_INFOPLIST_FILE = YES; - PRODUCT_BUNDLE_IDENTIFIER = io.oxen.SessionSnodeKitTests; - PRODUCT_NAME = SessionSnodeKitTests; + PRODUCT_BUNDLE_IDENTIFIER = io.oxen.SessionNetworkingKitTests; + PRODUCT_NAME = SessionNetworkingKitTests; }; name = Debug; }; @@ -8396,8 +8587,8 @@ buildSettings = { ENABLE_MODULE_VERIFIER = NO; GENERATE_INFOPLIST_FILE = YES; - PRODUCT_BUNDLE_IDENTIFIER = io.oxen.SessionSnodeKitTests; - PRODUCT_NAME = SessionSnodeKitTests; + PRODUCT_BUNDLE_IDENTIFIER = io.oxen.SessionNetworkingKitTests; + PRODUCT_NAME = SessionNetworkingKitTests; }; name = App_Store_Release; }; @@ -8417,6 +8608,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = "$(inherited)"; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_MODULE_VERIFIER = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; @@ -8508,6 +8700,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = "$(inherited)"; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_MODULE_VERIFIER = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; @@ -8584,9 +8777,10 @@ FD860CBF2D6E981900BBE29C /* Debug_Compile_LibSession */ = { isa = XCBuildConfiguration; buildSettings = { + DEVELOPMENT_TEAM = SUQ8J2PCT7; GENERATE_INFOPLIST_FILE = YES; - PRODUCT_BUNDLE_IDENTIFIER = io.oxen.SessionSnodeKitTests; - PRODUCT_NAME = SessionSnodeKitTests; + PRODUCT_BUNDLE_IDENTIFIER = io.oxen.SessionNetworkingKitTests; + PRODUCT_NAME = SessionNetworkingKitTests; }; name = Debug_Compile_LibSession; }; @@ -8594,8 +8788,8 @@ isa = XCBuildConfiguration; buildSettings = { GENERATE_INFOPLIST_FILE = YES; - PRODUCT_BUNDLE_IDENTIFIER = io.oxen.SessionSnodeKitTests; - PRODUCT_NAME = SessionSnodeKitTests; + PRODUCT_BUNDLE_IDENTIFIER = io.oxen.SessionNetworkingKitTests; + PRODUCT_NAME = SessionNetworkingKitTests; }; name = App_Store_Release_Compile_LibSession; }; @@ -8615,6 +8809,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = "$(inherited)"; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_MODULE_VERIFIER = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; @@ -8718,7 +8913,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; COMPILE_LIB_SESSION = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 622; + CURRENT_PROJECT_VERSION = 640; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -8757,7 +8952,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 15.6; LIB_SESSION_SOURCE_DIR = "${SRCROOT}/../LibSession-Util"; LOCALIZED_STRING_SWIFTUI_SUPPORT = NO; - MARKETING_VERSION = 2.13.2; + MARKETING_VERSION = 2.14.4; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = ( "-fobjc-arc-exceptions", @@ -9107,7 +9302,7 @@ GCC_DYNAMIC_NO_PIC = NO; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - INFOPLIST_FILE = SessionSnodeKit/Meta/Info.plist; + INFOPLIST_FILE = SessionNetworkingKit/Meta/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -9118,7 +9313,7 @@ MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionSnodeKit"; + PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionNetworkingKit"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = NO; @@ -9198,6 +9393,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = "$(inherited)"; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = SUQ8J2PCT7; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; @@ -9230,6 +9426,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = "$(inherited)"; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = SUQ8J2PCT7; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; @@ -9261,6 +9458,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = "$(inherited)"; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = SUQ8J2PCT7; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; @@ -9305,7 +9503,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; COMPILE_LIB_SESSION = YES; - CURRENT_PROJECT_VERSION = 622; + CURRENT_PROJECT_VERSION = 640; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; @@ -9338,7 +9536,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 15.6; LIB_SESSION_SOURCE_DIR = "${SRCROOT}/../LibSession-Util"; LOCALIZED_STRING_SWIFTUI_SUPPORT = NO; - MARKETING_VERSION = 2.13.2; + MARKETING_VERSION = 2.14.4; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = ( "-DNS_BLOCK_ASSERTIONS=1", @@ -9826,7 +10024,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - INFOPLIST_FILE = SessionSnodeKit/Meta/Info.plist; + INFOPLIST_FILE = SessionNetworkingKit/Meta/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -9837,7 +10035,7 @@ MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionSnodeKit"; + PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionNetworkingKit"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; SKIP_INSTALL = YES; @@ -10143,7 +10341,7 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = App_Store_Release; }; - C3C2A5AA255385C100C340D1 /* Build configuration list for PBXNativeTarget "SessionSnodeKit" */ = { + C3C2A5AA255385C100C340D1 /* Build configuration list for PBXNativeTarget "SessionNetworkingKit" */ = { isa = XCConfigurationList; buildConfigurations = ( C3C2A5A8255385C100C340D1 /* Debug */, @@ -10198,7 +10396,7 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = App_Store_Release; }; - FD2272522C32910F004D8A6C /* Build configuration list for PBXNativeTarget "SessionSnodeKitTests" */ = { + FD2272522C32910F004D8A6C /* Build configuration list for PBXNativeTarget "SessionNetworkingKitTests" */ = { isa = XCConfigurationList; buildConfigurations = ( FD2272502C32910F004D8A6C /* Debug */, @@ -10258,7 +10456,7 @@ repositoryURL = "https://github.com/session-foundation/libsession-util-spm"; requirement = { kind = exactVersion; - version = 1.5.1; + version = 1.5.6; }; }; FD6A38E72C2A630E00762359 /* XCRemoteSwiftPackageReference "CocoaLumberjack" */ = { diff --git a/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 40c1f21016..36a72ad9fd 100644 --- a/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -51,8 +51,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/session-foundation/libsession-util-spm", "state" : { - "revision" : "ba7d5f08e4eb71a2efe744df2ad677d8c180c6bb", - "version" : "1.5.1" + "revision" : "a092eb8fa4bbc93756530e08b6c281d9eda06c61", + "version" : "1.5.6" } }, { diff --git a/Session.xcodeproj/xcshareddata/xcschemes/Session.xcscheme b/Session.xcodeproj/xcshareddata/xcschemes/Session.xcscheme index ad1576e055..e20af5e4f5 100644 --- a/Session.xcodeproj/xcshareddata/xcschemes/Session.xcscheme +++ b/Session.xcodeproj/xcshareddata/xcschemes/Session.xcscheme @@ -77,8 +77,8 @@ @@ -55,8 +55,8 @@ diff --git a/Session/Calls/Call Management/SessionCall.swift b/Session/Calls/Call Management/SessionCall.swift index d747fbcdfd..f08656626c 100644 --- a/Session/Calls/Call Management/SessionCall.swift +++ b/Session/Calls/Call Management/SessionCall.swift @@ -9,7 +9,7 @@ import SessionUIKit import SignalUtilitiesKit import SessionMessagingKit import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { private let dependencies: Dependencies diff --git a/Session/Calls/Call Management/SessionCallManager+CXCallController.swift b/Session/Calls/Call Management/SessionCallManager+CXCallController.swift index 36ae7cb867..07e47ac35a 100644 --- a/Session/Calls/Call Management/SessionCallManager+CXCallController.swift +++ b/Session/Calls/Call Management/SessionCallManager+CXCallController.swift @@ -16,7 +16,10 @@ extension SessionCallManager { reportOutgoingCall(call) if callController != nil { - let handle = CXHandle(type: .generic, value: call.sessionId) + // Show contact name + session id (truncated...) opening outgoing call in apple watch + let callDisplay = generateDisplayForCall(call) + + let handle = CXHandle(type: .generic, value: callDisplay) let startCallAction = CXStartCallAction(call: call.callId, handle: handle) startCallAction.isVideo = false diff --git a/Session/Calls/Call Management/SessionCallManager.swift b/Session/Calls/Call Management/SessionCallManager.swift index f484f6c066..2d7f8eb9dc 100644 --- a/Session/Calls/Call Management/SessionCallManager.swift +++ b/Session/Calls/Call Management/SessionCallManager.swift @@ -114,10 +114,13 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { return } + // Show contact name + session id (truncated...) opening outgoing call in apple watch + let callDisplay = generateDisplayForCall(call) + // Construct a CXCallUpdate describing the incoming call, including the caller. let update = CXCallUpdate() update.localizedCallerName = callerName - update.remoteHandle = CXHandle(type: .generic, value: call.sessionId) + update.remoteHandle = CXHandle(type: .generic, value: callDisplay) update.hasVideo = false disableUnsupportedFeatures(callUpdate: update) @@ -296,4 +299,20 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { Log.flush() } } + + func generateDisplayForCall(_ call: CurrentCallProtocol) -> String { + guard + let sessionCall = call as? SessionCall, + sessionCall.contactName.isEmpty == false + else { + /// When contact name is empty display + /// ex. 1234...7890 + return call.sessionId.truncated(prefix: 4, suffix: 4) + } + + /// Display contact name + truncated session id prefix 4 + /// ex. John 1234... + let truncatedSessionId = sessionCall.sessionId.truncated(prefix: 4, suffix: 0) + return "\(sessionCall.contactName) \(truncatedSessionId)" + } } diff --git a/Session/Calls/WebRTC/WebRTCSession.swift b/Session/Calls/WebRTC/WebRTCSession.swift index 99e48d1e25..d579d8a1a5 100644 --- a/Session/Calls/WebRTC/WebRTCSession.swift +++ b/Session/Calls/WebRTC/WebRTCSession.swift @@ -4,7 +4,7 @@ import Foundation import Combine import GRDB import WebRTC -import SessionSnodeKit +import SessionNetworkingKit import SessionMessagingKit import SessionUtilitiesKit diff --git a/Session/Closed Groups/EditGroupViewModel.swift b/Session/Closed Groups/EditGroupViewModel.swift index 01916f533c..a0dd9dba89 100644 --- a/Session/Closed Groups/EditGroupViewModel.swift +++ b/Session/Closed Groups/EditGroupViewModel.swift @@ -5,15 +5,14 @@ import Combine import GRDB import DifferenceKit import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionMessagingKit import SessionUtilitiesKit import SignalUtilitiesKit -class EditGroupViewModel: SessionTableViewModel, NavigatableStateHolder, EditableStateHolder, ObservableTableSource { +class EditGroupViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTableSource { public let dependencies: Dependencies public let navigatableState: NavigatableState = NavigatableState() - public let editableState: EditableState = EditableState() public let state: TableDataState = TableDataState() public let observableState: ObservableTableSourceState = ObservableTableSourceState() private let selectedIdsSubject: CurrentValueSubject<(name: String, ids: Set), Never> = CurrentValueSubject(("", [])) @@ -534,7 +533,7 @@ class EditGroupViewModel: SessionTableViewModel, NavigatableStateHolder, Editabl case (.some(let inviteByIdValue), _): // This could be an ONS name let viewController = ModalActivityIndicatorViewController() { modalActivityIndicator in - SnodeAPI + Network.SnodeAPI .getSessionID(for: inviteByIdValue, using: dependencies) .subscribe(on: DispatchQueue.global(qos: .userInitiated), using: dependencies) .receive(on: DispatchQueue.main, using: dependencies) diff --git a/Session/Conversations/Context Menu/ContextMenuVC+Action.swift b/Session/Conversations/Context Menu/ContextMenuVC+Action.swift index 15d0c0a8b9..53b44ea9b5 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC+Action.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC+Action.swift @@ -225,17 +225,30 @@ extension ContextMenuVC { ) ) ) - let canSave: Bool = ( - cellViewModel.cellType == .mediaMessage && - (cellViewModel.attachments ?? []) - .filter { attachment in - attachment.isValid && - attachment.isVisualMedia && ( - attachment.state == .downloaded || - attachment.state == .uploaded - ) - }.isEmpty == false - ) + let canSave: Bool = { + switch cellViewModel.cellType { + case .mediaMessage: + return (cellViewModel.attachments ?? []) + .filter { attachment in + attachment.isValid && + attachment.isVisualMedia && ( + attachment.state == .downloaded || + attachment.state == .uploaded + ) + }.isEmpty == false + + case .audio, .genericAttachment: + return (cellViewModel.attachments ?? []) + .filter { attachment in + attachment.isValid && ( + attachment.state == .downloaded || + attachment.state == .uploaded + ) + }.isEmpty == false + + default: return false + } + }() let canCopySessionId: Bool = ( cellViewModel.variant == .standardIncoming && cellViewModel.threadVariant != .community diff --git a/Session/Conversations/Context Menu/ContextMenuVC+ActionView.swift b/Session/Conversations/Context Menu/ContextMenuVC+ActionView.swift index 54ae63cbc3..8ae261fc79 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC+ActionView.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC+ActionView.swift @@ -3,7 +3,7 @@ import UIKit import SessionUIKit import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit extension ContextMenuVC { final class ActionView: UIView { @@ -119,9 +119,10 @@ extension ContextMenuVC { } private func setUpSubtitle() { - guard - let expiresInSeconds = self.action.expirationInfo?.expiresInSeconds, - let expiresStartedAtMs = self.action.expirationInfo?.expiresStartedAtMs + guard + let expirationInfo = self.action.expirationInfo, + let expiresStartedAtMs = expirationInfo.expiresStartedAtMs, + let expiresInSeconds = expirationInfo.expiresInSeconds else { subtitleLabel.isHidden = true subtitleWidthConstraint.isActive = false @@ -130,10 +131,12 @@ extension ContextMenuVC { subtitleLabel.isHidden = false subtitleWidthConstraint.isActive = true + // To prevent a negative timer - let timeToExpireInSeconds: TimeInterval = max(0, (expiresStartedAtMs + expiresInSeconds * 1000 - dependencies[cache: .snodeAPI].currentOffsetTimestampMs()) / 1000) + let timeToExpireInSeconds = max(0, (expiresStartedAtMs + expiresInSeconds * 1000 - dependencies[cache: .snodeAPI].currentOffsetTimestampMs()) / 1000) + subtitleLabel.text = "disappearingMessagesCountdownBigMobile" - .put(key: "time_large", value: timeToExpireInSeconds.formatted(format: .twoUnits)) + .put(key: "time_large", value: timeToExpireInSeconds.formatted(format: .twoUnits, minimumUnit: .second)) .localized() timer = Timer.scheduledTimerOnMainThread(withTimeInterval: 1, repeats: true, using: dependencies, block: { [weak self, dependencies] _ in diff --git a/Session/Conversations/ConversationSearch.swift b/Session/Conversations/ConversationSearch.swift index f8dd4c2a5b..69e80a7b90 100644 --- a/Session/Conversations/ConversationSearch.swift +++ b/Session/Conversations/ConversationSearch.swift @@ -170,7 +170,7 @@ public final class SearchResultsBar: UIView { private lazy var loadingIndicator: UIActivityIndicatorView = { let result = UIActivityIndicatorView(style: .medium) - result.themeTintColor = .textPrimary + result.themeColor = .textPrimary result.alpha = 0.5 result.hidesWhenStopped = true diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 62baf8fb45..bda6c2aa00 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -14,27 +14,32 @@ import SessionMessagingKit import SessionUtilitiesKit import SignalUtilitiesKit import SwiftUI -import SessionSnodeKit +import SessionNetworkingKit extension ConversationVC: InputViewDelegate, MessageCellDelegate, ContextMenuActionDelegate, SendMediaNavDelegate, - UIDocumentPickerDelegate, AttachmentApprovalViewControllerDelegate, - GifPickerViewControllerDelegate + GifPickerViewControllerDelegate, + UIGestureRecognizerDelegate { // MARK: - Open Settings - @objc func handleTitleViewTapped() { + @MainActor @objc func handleTitleViewTapped() { // Don't take the user to settings for unapproved threads guard viewModel.threadData.threadRequiresApproval == false else { return } openSettingsFromTitleView() } - func openSettingsFromTitleView() { + // Handle taps outside of tableview cell to dismiss keyboard + @MainActor @objc func dismissKeyboardOnTap() { + _ = self.snInputView.resignFirstResponder() + } + + @MainActor func openSettingsFromTitleView() { // If we shouldn't be able to access settings then disable the title view shortcuts guard viewModel.threadData.canAccessSettings(using: viewModel.dependencies) else { return } @@ -234,7 +239,7 @@ extension ConversationVC: // MARK: - Session Pro CTA - @discardableResult func showSessionProCTAIfNeeded() -> Bool { + @discardableResult @MainActor func showSessionProCTAIfNeeded() -> Bool { let dependencies: Dependencies = viewModel.dependencies guard dependencies[feature: .sessionProEnabled] && (!viewModel.isSessionPro) else { return false @@ -244,7 +249,7 @@ extension ConversationVC: modal: ProCTAModal( delegate: dependencies[singleton: .sessionProState], variant: .longerMessages, - dataManager: viewModel.dependencies[singleton: .imageDataManager], + dataManager: dependencies[singleton: .imageDataManager], afterClosed: { [weak self] in self?.showInputAccessoryView() self?.snInputView.updateNumberOfCharactersLeft(self?.snInputView.text ?? "") @@ -255,6 +260,11 @@ extension ConversationVC: return true } + + // MARK: - UIGestureRecognizerDelegate + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } // MARK: - SendMediaNavDelegate @@ -362,9 +372,74 @@ extension ConversationVC: // UIDocumentPickerModeImport copies to a temp file within our container. // It uses more memory than "open" but lets us avoid working with security scoped URLs. let documentPickerVC = UIDocumentPickerViewController(forOpeningContentTypes: [.item], asCopy: true) - documentPickerVC.delegate = self documentPickerVC.modalPresentationStyle = .fullScreen + self.documentHandler = DocumentPickerHandler( + didPickDocumentsAt: { [weak self, dependencies = viewModel.dependencies] _, urls in + defer { + self?.showInputAccessoryView() + self?.becomeFirstResponder() + self?.documentHandler = nil + } + + guard let url: URL = urls.first else { return } + + let urlResourceValues: URLResourceValues + do { + urlResourceValues = try url.resourceValues(forKeys: [ .typeIdentifierKey, .isDirectoryKey, .nameKey ]) + } + catch { + DispatchQueue.main.async { [weak self] in + self?.viewModel.showToast(text: "attachmentsErrorLoad".localized()) + } + return + } + + let type: UTType = (urlResourceValues.typeIdentifier.map({ UTType($0) }) ?? .data) + guard urlResourceValues.isDirectory != true else { + DispatchQueue.main.async { [weak self] in + let modal: ConfirmationModal = ConfirmationModal( + targetView: self?.view, + info: ConfirmationModal.Info( + title: "attachmentsErrorLoad".localized(), + body: .text("attachmentsErrorNotSupported".localized()), + cancelTitle: "okay".localized(), + cancelStyle: .alert_text + ) + ) + self?.present(modal, animated: true) + } + return + } + + let fileName: String = (urlResourceValues.name ?? "attachment".localized()) + guard let dataSource = DataSourcePath(fileUrl: url, sourceFilename: urlResourceValues.name, shouldDeleteOnDeinit: false, using: dependencies) else { + DispatchQueue.main.async { [weak self] in + self?.viewModel.showToast(text: "attachmentsErrorLoad".localized()) + } + return + } + dataSource.sourceFilename = fileName + + // Although we want to be able to send higher quality attachments through the document picker + // it's more imporant that we ensure the sent format is one all clients can accept (e.g. *not* quicktime .mov) + guard !SignalAttachment.isInvalidVideo(dataSource: dataSource, type: type) else { + self?.showAttachmentApprovalDialogAfterProcessingVideo(at: url, with: fileName) + return + } + + // "Document picker" attachments _SHOULD NOT_ be resized + let attachment = SignalAttachment.attachment(dataSource: dataSource, type: type, imageQuality: .original, using: dependencies) + self?.showAttachmentApprovalDialog(for: [ attachment ]) + }, + wasCancelled: { [weak self] _ in + self?.showInputAccessoryView() + self?.becomeFirstResponder() + self?.documentHandler = nil + } + ) + documentPickerVC.delegate = self.documentHandler + present(documentPickerVC, animated: true, completion: nil) } @@ -412,59 +487,6 @@ extension ConversationVC: showAttachmentApprovalDialog(for: [ attachment ]) } - // MARK: - UIDocumentPickerDelegate - - func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { - guard let url = urls.first else { return } // TODO: Handle multiple? - - let urlResourceValues: URLResourceValues - do { - urlResourceValues = try url.resourceValues(forKeys: [ .typeIdentifierKey, .isDirectoryKey, .nameKey ]) - } - catch { - DispatchQueue.main.async { [weak self] in - self?.viewModel.showToast(text: "attachmentsErrorLoad".localized()) - } - return - } - - let type: UTType = (urlResourceValues.typeIdentifier.map({ UTType($0) }) ?? .data) - guard urlResourceValues.isDirectory != true else { - DispatchQueue.main.async { [weak self] in - let modal: ConfirmationModal = ConfirmationModal( - targetView: self?.view, - info: ConfirmationModal.Info( - title: "attachmentsErrorLoad".localized(), - body: .text("attachmentsErrorNotSupported".localized()), - cancelTitle: "okay".localized(), - cancelStyle: .alert_text - ) - ) - self?.present(modal, animated: true) - } - return - } - - let fileName: String = (urlResourceValues.name ?? "attachment".localized()) - guard let dataSource = DataSourcePath(fileUrl: url, sourceFilename: urlResourceValues.name, shouldDeleteOnDeinit: false, using: viewModel.dependencies) else { - DispatchQueue.main.async { [weak self] in - self?.viewModel.showToast(text: "attachmentsErrorLoad".localized()) - } - return - } - dataSource.sourceFilename = fileName - - // Although we want to be able to send higher quality attachments through the document picker - // it's more imporant that we ensure the sent format is one all clients can accept (e.g. *not* quicktime .mov) - guard !SignalAttachment.isInvalidVideo(dataSource: dataSource, type: type) else { - return showAttachmentApprovalDialogAfterProcessingVideo(at: url, with: fileName) - } - - // "Document picker" attachments _SHOULD NOT_ be resized - let attachment = SignalAttachment.attachment(dataSource: dataSource, type: type, imageQuality: .original, using: viewModel.dependencies) - showAttachmentApprovalDialog(for: [ attachment ]) - } - func showAttachmentApprovalDialog(for attachments: [SignalAttachment]) { guard let navController = AttachmentApprovalViewController.wrappedInNavController( threadId: self.viewModel.threadData.threadId, @@ -514,13 +536,13 @@ extension ConversationVC: // MARK: - InputViewDelegate - func handleDisabledInputTapped() { + @MainActor func handleDisabledInputTapped() { guard viewModel.threadData.threadIsBlocked == true else { return } self.showBlockedModalIfNeeded() } - func handleCharacterLimitLabelTapped() { + @MainActor func handleCharacterLimitLabelTapped() { guard !showSessionProCTAIfNeeded() else { return } self.hideInputAccessoryView() @@ -560,7 +582,7 @@ extension ConversationVC: present(confirmationModal, animated: true, completion: nil) } - func handleDisabledAttachmentButtonTapped() { + @MainActor func handleDisabledAttachmentButtonTapped() { /// This logic was added because an Apple reviewer rejected an emergency update as they thought these buttons were /// unresponsive (even though there is copy on the screen communicating that they are intentionally disabled) - in order /// to prevent this happening in the future we've added this toast when pressing on the disabled button @@ -577,7 +599,7 @@ extension ConversationVC: ) } - func handleDisabledVoiceMessageButtonTapped() { + @MainActor func handleDisabledVoiceMessageButtonTapped() { /// This logic was added because an Apple reviewer rejected an emergency update as they thought these buttons were /// unresponsive (even though there is copy on the screen communicating that they are intentionally disabled) - in order /// to prevent this happening in the future we've added this toast when pressing on the disabled button @@ -596,7 +618,7 @@ extension ConversationVC: // MARK: --Message Sending - func handleSendButtonTapped() { + @MainActor func handleSendButtonTapped() { guard LibSession.numberOfCharactersLeft( for: snInputView.text.trimmingCharacters(in: .whitespacesAndNewlines), isSessionPro: viewModel.isSessionPro @@ -612,7 +634,7 @@ extension ConversationVC: ) } - func showModalForMessagesExceedingCharacterLimit(isSessionPro: Bool) { + @MainActor func showModalForMessagesExceedingCharacterLimit(isSessionPro: Bool) { guard !showSessionProCTAIfNeeded() else { return } self.hideInputAccessoryView() @@ -789,6 +811,7 @@ extension ConversationVC: // FIXME: Remove this once we don't generate unique Profile entries for the current users blinded ids if (try? SessionId.Prefix(from: optimisticData.interaction.authorId)) != .standard { let currentUserProfile: Profile = dependencies.mutate(cache: .libSession) { $0.profile } + let sentTimestamp: TimeInterval = (Double(optimisticData.interaction.timestampMs) / 1000) try? Profile.updateIfNeeded( db, @@ -799,7 +822,7 @@ extension ConversationVC: fallback: .none, using: dependencies ), - sentTimestamp: (Double(optimisticData.interaction.timestampMs) / 1000), + profileUpdateTimestamp: (currentUserProfile.profileLastUpdated ?? sentTimestamp), using: dependencies ) } @@ -848,7 +871,10 @@ extension ConversationVC: } } - func showLinkPreviewSuggestionModal() { + @MainActor func showLinkPreviewSuggestionModal() { + // Hides accessory view while link preview confirmation is presented + hideInputAccessoryView() + let linkPreviewModal: ConfirmationModal = ConfirmationModal( info: ConfirmationModal.Info( title: "linkPreviewsEnable".localized(), @@ -859,18 +885,23 @@ extension ConversationVC: ), confirmTitle: "enable".localized(), confirmStyle: .danger, - cancelStyle: .alert_text - ) { [weak self, dependencies = viewModel.dependencies] _ in - dependencies.setAsync(.areLinkPreviewsEnabled, true) { - self?.snInputView.autoGenerateLinkPreview() + cancelStyle: .alert_text, + onConfirm: { [weak self, dependencies = viewModel.dependencies] _ in + dependencies.setAsync(.areLinkPreviewsEnabled, true) { + self?.snInputView.autoGenerateLinkPreview() + } + }, + afterClosed: { [weak self] in + // Bring back accessory view after confirmation action + self?.showInputAccessoryView() } - } + ) ) present(linkPreviewModal, animated: true, completion: nil) } - func inputTextViewDidChangeContent(_ inputTextView: InputTextView) { + @MainActor func inputTextViewDidChangeContent(_ inputTextView: InputTextView) { // Note: If there is a 'draft' message then we don't want it to trigger the typing indicator to // appear (as that is not expected/correct behaviour) guard !viewIsAppearing else { return } @@ -896,7 +927,7 @@ extension ConversationVC: // MARK: --Attachments - func didPasteImageFromPasteboard(_ image: UIImage) { + @MainActor func didPasteImageFromPasteboard(_ image: UIImage) { guard let imageData = image.jpegData(compressionQuality: 1.0) else { return } let dataSource = DataSourceValue(data: imageData, dataType: .jpeg, using: viewModel.dependencies) @@ -917,7 +948,7 @@ extension ConversationVC: // MARK: --Mentions - func handleMentionSelected(_ mentionInfo: MentionInfo, from view: MentionSelectionView) { + @MainActor func handleMentionSelected(_ mentionInfo: MentionInfo, from view: MentionSelectionView) { guard let currentMentionStartIndex = currentMentionStartIndex else { return } mentions.append(mentionInfo) @@ -1044,7 +1075,7 @@ extension ConversationVC: } // MARK: MessageCellDelegate - + func handleItemLongPressed(_ cellViewModel: MessageViewModel) { // Show the unblock modal if needed guard self.viewModel.threadData.threadIsBlocked != true else { @@ -1169,7 +1200,7 @@ extension ConversationVC: let currentTimestampMs: Int64 = dependencies[cache: .snodeAPI].currentOffsetTimestampMs() let interactionId = try messageDisappearingConfig - .saved(db) + .upserted(db) .insertControlMessage( db, threadVariant: cellViewModel.threadVariant, @@ -1699,6 +1730,33 @@ extension ConversationVC: } func removeAllReactions(_ cellViewModel: MessageViewModel, for emoji: String) { + // Dismiss current reaction sheet to present alert dialog + currentReactionListSheet?.dismiss(animated: true) + currentReactionListSheet = nil + + let modal: ConfirmationModal = ConfirmationModal( + info: ConfirmationModal.Info( + title: "clearAll".localized(), + body: .attributedText( + "emojiReactsClearAll" + .put(key: "emoji", value: emoji) + .localizedFormatted(baseFont: ConfirmationModal.explanationFont) + ), + confirmTitle: "clear".localized(), + confirmStyle: .danger, + cancelStyle: .alert_text, + onConfirm: { [weak self] modal in + // Call clear reaction event + self?.clearAllReactions(cellViewModel, for: emoji) + modal.dismiss(animated: true) + } + ) + ) + + present(modal, animated: true, completion: nil) + } + + func clearAllReactions(_ cellViewModel: MessageViewModel, for emoji: String) { guard cellViewModel.threadVariant == .community, let roomToken: String = viewModel.threadData.openGroupRoomToken, @@ -1708,7 +1766,7 @@ extension ConversationVC: let openGroupServerMessageId: Int64 = cellViewModel.openGroupServerMessageId else { return } - let pendingChange: OpenGroupAPI.PendingChange = viewModel.dependencies[singleton: .openGroupManager] + let pendingChange: OpenGroupManager.PendingChange = viewModel.dependencies[singleton: .openGroupManager] .addPendingReaction( emoji: emoji, id: openGroupServerMessageId, @@ -1718,7 +1776,7 @@ extension ConversationVC: ) Result { - try OpenGroupAPI.preparedReactionDeleteAll( + try Network.SOGS.preparedReactionDeleteAll( emoji: emoji, id: openGroupServerMessageId, roomToken: roomToken, @@ -1793,14 +1851,14 @@ extension ConversationVC: typealias OpenGroupInfo = ( pendingReaction: Reaction?, - pendingChange: OpenGroupAPI.PendingChange, + pendingChange: OpenGroupManager.PendingChange, preparedRequest: Network.PreparedRequest ) /// Perform the sending logic, we generate the pending reaction first in a deferred future closure to prevent the OpenGroup /// cache from blocking either the main thread or the database write thread Deferred { [dependencies = viewModel.dependencies] in - Future { resolver in + Future { resolver in guard threadVariant == .community, let serverMessageId: Int64 = cellViewModel.openGroupServerMessageId, @@ -1821,7 +1879,7 @@ extension ConversationVC: } } .subscribe(on: DispatchQueue.global(qos: .userInitiated), using: viewModel.dependencies) - .flatMapStorageWritePublisher(using: viewModel.dependencies) { [weak self, dependencies = viewModel.dependencies] db, pendingChange -> (OpenGroupAPI.PendingChange?, Reaction?, Message.Destination, AuthenticationMethod) in + .flatMapStorageWritePublisher(using: viewModel.dependencies) { [weak self, dependencies = viewModel.dependencies] db, pendingChange -> (OpenGroupManager.PendingChange?, Reaction?, Message.Destination, AuthenticationMethod) in // Update the thread to be visible (if it isn't already) if self?.viewModel.threadData.threadShouldBeVisible == false { try SessionThread.updateVisibility( @@ -1899,12 +1957,12 @@ extension ConversationVC: let serverMessageId: Int64 = cellViewModel.openGroupServerMessageId, let openGroupServer: String = cellViewModel.threadOpenGroupServer, let openGroupRoom: String = openGroupRoom, - let pendingChange: OpenGroupAPI.PendingChange = pendingChange + let pendingChange: OpenGroupManager.PendingChange = pendingChange else { throw MessageSenderError.invalidMessage } let preparedRequest: Network.PreparedRequest = try { guard !remove else { - return try OpenGroupAPI + return try Network.SOGS .preparedReactionDelete( emoji: emoji, id: serverMessageId, @@ -1915,7 +1973,7 @@ extension ConversationVC: .map { _, response in response.seqNo } } - return try OpenGroupAPI + return try Network.SOGS .preparedReactionAdd( emoji: emoji, id: serverMessageId, @@ -1966,7 +2024,7 @@ extension ConversationVC: ) ), to: destination, - namespace: .default, + namespace: destination.defaultNamespace, interactionId: cellViewModel.id, attachments: nil, authMethod: authMethod, @@ -2211,8 +2269,27 @@ extension ConversationVC: model: quoteDraft, isOutgoing: (cellViewModel.variant == .standardOutgoing) ) - _ = snInputView.becomeFirstResponder() - completion?() + + // If the `MessageInfoViewController` is visible then we want to show the keyboard after + // the pop transition completes (and don't want to delay triggering the completion closure) + let messageInfoScreenVisible: Bool = (self.navigationController?.viewControllers.last is MessageInfoViewController) + + guard !messageInfoScreenVisible else { + if self.isShowingSearchUI == true { self.willManuallyCancelSearchUI() } + self.hasPendingInputKeyboardPresentationEvent = true + completion?() + return + } + + // Add delay before doing any ui updates + // Delay added to give time for long press actions to dismiss + let delay = completion == nil ? 0 : ContextMenuVC.dismissDuration + + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in + if self?.isShowingSearchUI == true { self?.willManuallyCancelSearchUI() } + _ = self?.snInputView.becomeFirstResponder() + completion?() + } } func copy(_ cellViewModel: MessageViewModel, completion: (() -> Void)?) { @@ -2375,12 +2452,12 @@ extension ConversationVC: } func save(_ cellViewModel: MessageViewModel, completion: (() -> Void)?) { - guard cellViewModel.cellType == .mediaMessage else { return } - - let mediaAttachments: [(Attachment, String)] = (cellViewModel.attachments ?? []) + let validAttachments: [(Attachment, String)] = (cellViewModel.attachments ?? []) .filter { attachment in - attachment.isValid && - attachment.isVisualMedia && ( + attachment.isValid && ( + cellViewModel.cellType != .mediaMessage || + attachment.isVisualMedia + ) && ( attachment.state == .downloaded || attachment.state == .uploaded ) @@ -2399,63 +2476,112 @@ extension ConversationVC: return (attachment, path) } - guard !mediaAttachments.isEmpty else { return } - - Permissions.requestLibraryPermissionIfNeeded( - isSavingMedia: true, - presentingViewController: self, - using: viewModel.dependencies - ) { [weak self, dependencies = viewModel.dependencies] in - PHPhotoLibrary.shared().performChanges( - { - mediaAttachments.forEach { attachment, path in - if attachment.isImage || attachment.isAnimated { - PHAssetChangeRequest.creationRequestForAssetFromImage( - atFileURL: URL(fileURLWithPath: path) - ) + guard !validAttachments.isEmpty else { return } + + switch cellViewModel.cellType { + case .audio, .genericAttachment: + let documentPicker = UIDocumentPickerViewController( + forExporting: validAttachments.map { _, path in URL(fileURLWithPath: path) }, + asCopy: true + ) + + self.documentHandler = DocumentPickerHandler( + didPickDocumentsAt: { [weak self, dependencies = viewModel.dependencies] _, _ in + validAttachments.forEach { attachment, path in + /// Sanity check to make sure we don't unintentionally remove a proper attachment file + guard path.hasPrefix(dependencies[singleton: .fileManager].temporaryDirectory) else { + return + } + + try? dependencies[singleton: .fileManager].removeItem(atPath: path) } - else if attachment.isVideo { - PHAssetChangeRequest.creationRequestForAssetFromVideo( - atFileURL: URL(fileURLWithPath: path) + + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(Int(ContextMenuVC.dismissDurationPartOne * 1000))) { [weak self] in + self?.viewModel.showToast( + text: "saved".localized(), + backgroundColor: .toast_background, + inset: Values.largeSpacing + (self?.inputAccessoryView?.frame.height ?? 0) ) - } - } - }, - completionHandler: { [dependencies] _, _ in - mediaAttachments.forEach { attachment, path in - /// Sanity check to make sure we don't unintentionally remove a proper attachment file - guard path.hasPrefix(dependencies[singleton: .fileManager].temporaryDirectory) else { - return + + // Send a 'media saved' notification if needed + guard self?.viewModel.threadData.threadVariant == .contact, cellViewModel.variant == .standardIncoming else { + return + } + + self?.sendDataExtraction(kind: .mediaSaved(timestamp: UInt64(cellViewModel.timestampMs))) } - try? dependencies[singleton: .fileManager].removeItem(atPath: path) - } - - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(Int(ContextMenuVC.dismissDurationPartOne * 1000))) { [weak self] in - self?.viewModel.showToast( - text: "saved".localized(), - backgroundColor: .toast_background, - inset: Values.largeSpacing + (self?.inputAccessoryView?.frame.height ?? 0) - ) + self?.showInputAccessoryView() + self?.becomeFirstResponder() + self?.documentHandler = nil + }, + wasCancelled: { [weak self] _ in + self?.showInputAccessoryView() + self?.becomeFirstResponder() + self?.documentHandler = nil } + ) + documentPicker.delegate = documentHandler + present(documentPicker, animated: true) + + case .mediaMessage: + Permissions.requestLibraryPermissionIfNeeded( + isSavingMedia: true, + presentingViewController: self, + using: viewModel.dependencies + ) { [weak self, dependencies = viewModel.dependencies] in + PHPhotoLibrary.shared().performChanges( + { + validAttachments.forEach { attachment, path in + if attachment.isImage || attachment.isAnimated { + PHAssetChangeRequest.creationRequestForAssetFromImage( + atFileURL: URL(fileURLWithPath: path) + ) + } + else if attachment.isVideo { + PHAssetChangeRequest.creationRequestForAssetFromVideo( + atFileURL: URL(fileURLWithPath: path) + ) + } + } + }, + completionHandler: { [weak self, dependencies] _, _ in + validAttachments.forEach { attachment, path in + /// Sanity check to make sure we don't unintentionally remove a proper attachment file + guard path.hasPrefix(dependencies[singleton: .fileManager].temporaryDirectory) else { + return + } + + try? dependencies[singleton: .fileManager].removeItem(atPath: path) + } + + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(Int(ContextMenuVC.dismissDurationPartOne * 1000))) { [weak self] in + self?.viewModel.showToast( + text: "saved".localized(), + backgroundColor: .toast_background, + inset: Values.largeSpacing + (self?.inputAccessoryView?.frame.height ?? 0) + ) + } + + // Send a 'media saved' notification if needed + guard self?.viewModel.threadData.threadVariant == .contact, cellViewModel.variant == .standardIncoming else { + return + } + + self?.sendDataExtraction(kind: .mediaSaved(timestamp: UInt64(cellViewModel.timestampMs))) + } + ) } - ) - - // Send a 'media saved' notification if needed - guard self?.viewModel.threadData.threadVariant == .contact, cellViewModel.variant == .standardIncoming else { - return - } - - self?.sendDataExtraction(kind: .mediaSaved(timestamp: UInt64(cellViewModel.timestampMs))) + + completion?() + + default: break } - - completion?() } func ban(_ cellViewModel: MessageViewModel, completion: (() -> Void)?) { guard cellViewModel.threadVariant == .community else { return } - let threadId: String = self.viewModel.threadData.threadId let modal: ConfirmationModal = ConfirmationModal( targetView: self.view, info: ConfirmationModal.Info( @@ -2489,7 +2615,7 @@ extension ConversationVC: } .publisher .tryFlatMap { (roomToken: String, authMethod: AuthenticationMethod) in - try OpenGroupAPI.preparedUserBan( + try Network.SOGS.preparedUserBan( sessionId: cellViewModel.authorId, from: [roomToken], authMethod: authMethod, @@ -2534,7 +2660,6 @@ extension ConversationVC: func banAndDeleteAllMessages(_ cellViewModel: MessageViewModel, completion: (() -> Void)?) { guard cellViewModel.threadVariant == .community else { return } - let threadId: String = self.viewModel.threadData.threadId let modal: ConfirmationModal = ConfirmationModal( targetView: self.view, info: ConfirmationModal.Info( @@ -2568,7 +2693,7 @@ extension ConversationVC: } .publisher .tryFlatMap { (roomToken: String, authMethod: AuthenticationMethod) in - try OpenGroupAPI.preparedUserBanAndDeleteAllMessages( + try Network.SOGS.preparedUserBanAndDeleteAllMessages( sessionId: cellViewModel.authorId, roomToken: roomToken, authMethod: authMethod, @@ -2929,10 +3054,11 @@ extension ConversationVC { .writePublisher { [dependencies = viewModel.dependencies] db in /// Remove any existing `infoGroupInfoInvited` interactions from the group (don't want to have a /// duplicate one from inside the group history) - _ = try Interaction - .filter(Interaction.Columns.threadId == group.id) + try Interaction.deleteWhere( + db, + .filter(Interaction.Columns.threadId == group.id), .filter(Interaction.Columns.variant == Interaction.Variant.infoGroupInfoInvited) - .deleteAll(db) + ) /// Optimistically insert a `standard` member for the current user in this group (it'll be update to the correct /// one once we receive the first `GROUP_MEMBERS` config message but adding it here means the `canWrite` diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index e3a7228511..671fbaa391 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -29,6 +29,10 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa /// never have disappeared before - this is only needed for value observers since they run asynchronously) private var hasReloadedThreadDataAfterDisappearance: Bool = true + /// This flag indicates that a need for inputview keyboard presentation is needed, this is in events + /// where a delegate action is trigger before poping back into `ConversationVC` + var hasPendingInputKeyboardPresentationEvent: Bool = false + var focusedInteractionInfo: Interaction.TimestampInfo? var focusBehaviour: ConversationViewModel.FocusBehaviour = .none @@ -43,6 +47,7 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa // Context menu var contextMenuWindow: ContextMenuWindow? var contextMenuVC: ContextMenuVC? + var documentHandler: DocumentPickerHandler? // Mentions var currentMentionStartIndex: String.Index? @@ -360,7 +365,6 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa .backgroundPrimary, .backgroundPrimary ] - result.set(.height, to: 92) return result }() @@ -381,6 +385,16 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa return result }() + + // Handle taps outside of tableview cell + private lazy var tableViewTapGesture: UITapGestureRecognizer = { + let result: UITapGestureRecognizer = UITapGestureRecognizer() + result.delegate = self + result.addTarget(self, action: #selector(dismissKeyboardOnTap)) + result.cancelsTouchesInView = false + + return result + }() // MARK: - Settings @@ -532,6 +546,9 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa object: nil ) } + + // Gesture + view.addGestureRecognizer(tableViewTapGesture) self.viewModel.navigatableState.setupBindings(viewController: self, disposables: &self.viewModel.disposables) @@ -580,6 +597,15 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa self?.didFinishInitialLayout = true self?.viewIsAppearing = false self?.lastPresentedViewController = nil + + // Show inputview keyboard + if self?.hasPendingInputKeyboardPresentationEvent == true { + // Added 0.1 delay to remove inputview stutter animation glitch while keyboard is animating up + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in + _ = self?.snInputView.becomeFirstResponder() + } + self?.hasPendingInputKeyboardPresentationEvent = false + } } } @@ -1566,7 +1592,8 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa // value will break things) let tableViewBottom: CGFloat = (tableView.contentSize.height - tableView.bounds.height + tableView.contentInset.bottom) - if tableView.contentOffset.y < (tableViewBottom - 5) { + // Added `insetDifference > 0` to remove sudden table collapse and overscroll + if tableView.contentOffset.y < (tableViewBottom - 5) && insetDifference > 0 { tableView.contentOffset.y += insetDifference } @@ -1692,6 +1719,7 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa shouldExpanded: viewModel.messageExpandedInteractionIds .contains(cellViewModel.id), lastSearchText: viewModel.lastSearchedText, + tableSize: tableView.bounds.size, using: viewModel.dependencies ) cell.delegate = self @@ -1708,7 +1736,7 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa switch section.model { case .loadOlder, .loadNewer: let loadingIndicator: UIActivityIndicatorView = UIActivityIndicatorView(style: .medium) - loadingIndicator.themeTintColor = .textPrimary + loadingIndicator.themeColor = .textPrimary loadingIndicator.alpha = 0.5 loadingIndicator.startAnimating() @@ -1978,6 +2006,12 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa becomeFirstResponder() reloadInputViews() } + + // Manually cancel the search and clear the text to remove hightlights + func willManuallyCancelSearchUI() { + searchController.uiSearchController.isActive = false + searchController.uiSearchController.searchBar.text = "" + } func didDismissSearchController(_ searchController: UISearchController) { hideSearchUI() @@ -2097,6 +2131,14 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa self.highlightCellIfNeeded(interactionId: interactionInfo.id, behaviour: focusBehaviour) self.focusedInteractionInfo = nil self.focusBehaviour = .none + + // Check if the last known keyboard frame exists, + // if it does not intersect with the target rectangle (the cell to be scrolled to), + if let keyboardFrame = lastKnownKeyboardFrame, !keyboardFrame.intersects(targetRect) { + // If all conditions are met, scroll the table view to make the target rectangle visible. + // This is to ensure a cell is not covered by the keyboard. + self.tableView.scrollRectToVisible(targetRect, animated: true) + } return } diff --git a/Session/Conversations/ConversationViewModel.swift b/Session/Conversations/ConversationViewModel.swift index 3c8ede79ed..804521bd4d 100644 --- a/Session/Conversations/ConversationViewModel.swift +++ b/Session/Conversations/ConversationViewModel.swift @@ -6,7 +6,7 @@ import UniformTypeIdentifiers import Lucide import GRDB import DifferenceKit -import SessionSnodeKit +import SessionNetworkingKit import SessionMessagingKit import SessionUtilitiesKit import SessionUIKit @@ -741,9 +741,6 @@ public class ConversationViewModel: OWSAudioPlayerDelegate, NavigatableStateHold body: text ), expiresInSeconds: threadData.disappearingMessagesConfiguration?.expiresInSeconds(), - expiresStartedAtMs: threadData.disappearingMessagesConfiguration?.initialExpiresStartedAtMs( - sentTimestampMs: Double(sentTimestampMs) - ), linkPreviewUrl: linkPreviewDraft?.urlString, isProMessage: dependencies[cache: .libSession].isSessionPro, using: dependencies diff --git a/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift b/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift index b366b5e2a8..289a6bb8e2 100644 --- a/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift +++ b/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift @@ -322,7 +322,12 @@ private class EmojiSectionHeader: UICollectionReusableView { label.font = .systemFont(ofSize: Values.smallFontSize) label.themeTextColor = .textPrimary addSubview(label) - label.pin(to: self) + + label.pin(.top, to: .top, of: self) + label.pin(.leading, to: .leading, of: self, withInset: Values.mediumSpacing) + label.pin(.trailing, to: .trailing, of: self, withInset: -Values.mediumSpacing) + label.pin(.bottom, to: .bottom, of: self) + label.setCompressionResistance(to: .required) } diff --git a/Session/Conversations/Input View/InputView.swift b/Session/Conversations/Input View/InputView.swift index 74316f9525..175222f4a0 100644 --- a/Session/Conversations/Input View/InputView.swift +++ b/Session/Conversations/Input View/InputView.swift @@ -62,6 +62,15 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M return result }() + private lazy var swipeGestureRecognizer: UISwipeGestureRecognizer = { + let result: UISwipeGestureRecognizer = UISwipeGestureRecognizer() + result.direction = .down + result.addTarget(self, action: #selector(didSwipeDown)) + result.cancelsTouchesInView = false + + return result + }() + private var bottomStackView: UIStackView? private lazy var attachmentsButton: ExpandingAttachmentsButton = { let result = ExpandingAttachmentsButton(delegate: delegate) @@ -227,6 +236,7 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M autoresizingMask = .flexibleHeight addGestureRecognizer(tapGestureRecognizer) + addGestureRecognizer(swipeGestureRecognizer) // Background & blur let backgroundView = UIView() @@ -296,12 +306,12 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M // MARK: - Updating - func inputTextViewDidChangeSize(_ inputTextView: InputTextView) { + @MainActor func inputTextViewDidChangeSize(_ inputTextView: InputTextView) { invalidateIntrinsicContentSize() self.bottomStackView?.alignment = (inputTextView.contentSize.height > inputTextView.minHeight) ? .top : .center } - func inputTextViewDidChangeContent(_ inputTextView: InputTextView) { + @MainActor func inputTextViewDidChangeContent(_ inputTextView: InputTextView) { let hasText = !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty sendButton.isHidden = !hasText voiceMessageButtonContainer.isHidden = hasText @@ -310,7 +320,7 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M delegate?.inputTextViewDidChangeContent(inputTextView) } - func updateNumberOfCharactersLeft(_ text: String) { + @MainActor func updateNumberOfCharactersLeft(_ text: String) { let numberOfCharactersLeft: Int = LibSession.numberOfCharactersLeft( for: text.trimmingCharacters(in: .whitespacesAndNewlines), isSessionPro: dependencies[cache: .libSession].isSessionPro @@ -321,7 +331,7 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M characterLimitLabelTapGestureRecognizer.isEnabled = (numberOfCharactersLeft < Self.thresholdForCharacterLimit) } - func didPasteImageFromPasteboard(_ inputTextView: InputTextView, image: UIImage) { + @MainActor func didPasteImageFromPasteboard(_ inputTextView: InputTextView, image: UIImage) { delegate?.didPasteImageFromPasteboard(image) } @@ -454,6 +464,7 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M self.accessibilityIdentifier = updatedInputState.accessibility?.identifier self.accessibilityLabel = updatedInputState.accessibility?.label tapGestureRecognizer.isEnabled = (updatedInputState.allowedInputTypes == .none) + inputState = updatedInputState disabledInputLabel.text = (updatedInputState.message ?? "") disabledInputLabel.accessibilityIdentifier = updatedInputState.messageAccessibility?.identifier @@ -503,14 +514,14 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M return super.point(inside: point, with: event) } - func handleInputViewButtonTapped(_ inputViewButton: InputViewButton) { + @MainActor func handleInputViewButtonTapped(_ inputViewButton: InputViewButton) { if inputViewButton == sendButton { delegate?.handleSendButtonTapped() } if inputViewButton == voiceMessageButton && inputState.allowedInputTypes != .all { delegate?.handleDisabledVoiceMessageButtonTapped() } } - func handleInputViewButtonLongPressBegan(_ inputViewButton: InputViewButton?) { + @MainActor func handleInputViewButtonLongPressBegan(_ inputViewButton: InputViewButton?) { guard inputViewButton == voiceMessageButton else { return } guard inputState.allowedInputTypes == .all else { return } @@ -521,7 +532,7 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M delegate?.startVoiceMessageRecording() } - func handleInputViewButtonLongPressMoved(_ inputViewButton: InputViewButton, with touch: UITouch?) { + @MainActor func handleInputViewButtonLongPressMoved(_ inputViewButton: InputViewButton, with touch: UITouch?) { guard let voiceMessageRecordingView: VoiceMessageRecordingView = voiceMessageRecordingView, inputViewButton == voiceMessageButton, @@ -531,7 +542,7 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M voiceMessageRecordingView.handleLongPressMoved(to: location) } - func handleInputViewButtonLongPressEnded(_ inputViewButton: InputViewButton, with touch: UITouch?) { + @MainActor func handleInputViewButtonLongPressEnded(_ inputViewButton: InputViewButton, with touch: UITouch?) { guard let voiceMessageRecordingView: VoiceMessageRecordingView = voiceMessageRecordingView, inputViewButton == voiceMessageButton, @@ -615,7 +626,7 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M } } - func handleMentionSelected(_ mentionInfo: MentionInfo, from view: MentionSelectionView) { + @MainActor func handleMentionSelected(_ mentionInfo: MentionInfo, from view: MentionSelectionView) { delegate?.handleMentionSelected(mentionInfo, from: view) } @@ -630,6 +641,10 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M @objc private func characterLimitLabelTapped() { delegate?.handleCharacterLimitLabelTapped() } + + @objc private func didSwipeDown() { + inputTextView.resignFirstResponder() + } // MARK: - Convenience @@ -647,12 +662,12 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M // MARK: - Delegate protocol InputViewDelegate: ExpandingAttachmentsButtonDelegate, VoiceMessageRecordingViewDelegate { - func showLinkPreviewSuggestionModal() - func handleSendButtonTapped() - func handleDisabledInputTapped() - func handleDisabledVoiceMessageButtonTapped() - func handleCharacterLimitLabelTapped() - func inputTextViewDidChangeContent(_ inputTextView: InputTextView) - func handleMentionSelected(_ mentionInfo: MentionInfo, from view: MentionSelectionView) - func didPasteImageFromPasteboard(_ image: UIImage) + @MainActor func showLinkPreviewSuggestionModal() + @MainActor func handleSendButtonTapped() + @MainActor func handleDisabledInputTapped() + @MainActor func handleDisabledVoiceMessageButtonTapped() + @MainActor func handleCharacterLimitLabelTapped() + @MainActor func inputTextViewDidChangeContent(_ inputTextView: InputTextView) + @MainActor func handleMentionSelected(_ mentionInfo: MentionInfo, from view: MentionSelectionView) + @MainActor func didPasteImageFromPasteboard(_ image: UIImage) } diff --git a/Session/Conversations/Input View/MentionSelectionView.swift b/Session/Conversations/Input View/MentionSelectionView.swift index 2f1703d3a8..5abdeebb4b 100644 --- a/Session/Conversations/Input View/MentionSelectionView.swift +++ b/Session/Conversations/Input View/MentionSelectionView.swift @@ -218,5 +218,5 @@ private extension MentionSelectionView { // MARK: - Delegate protocol MentionSelectionViewDelegate: AnyObject { - func handleMentionSelected(_ mention: MentionInfo, from view: MentionSelectionView) + @MainActor func handleMentionSelected(_ mention: MentionInfo, from view: MentionSelectionView) } diff --git a/Session/Conversations/Message Cells/CallMessageCell.swift b/Session/Conversations/Message Cells/CallMessageCell.swift index 305ea3a29c..75fbbfa946 100644 --- a/Session/Conversations/Message Cells/CallMessageCell.swift +++ b/Session/Conversations/Message Cells/CallMessageCell.swift @@ -10,27 +10,45 @@ final class CallMessageCell: MessageCell { private static let iconSize: CGFloat = 16 private static let timerViewSize: CGFloat = 16 private static let inset = Values.mediumSpacing + private static let verticalInset = Values.smallSpacing + private static let horizontalInset = Values.mediumSmallSpacing private static let margin = UIScreen.main.bounds.width * 0.1 private var isHandlingLongPress: Bool = false override var contextSnapshotView: UIView? { return container } + override var allowedGestureRecognizers: Set { + return [ + .longPress, + .tap + ] + } + // MARK: - UI private lazy var topConstraint: NSLayoutConstraint = mainStackView.pin(.top, to: .top, of: self, withInset: CallMessageCell.inset) - private lazy var iconImageViewWidthConstraint: NSLayoutConstraint = iconImageView.set(.width, to: 0) - private lazy var iconImageViewHeightConstraint: NSLayoutConstraint = iconImageView.set(.height, to: 0) - private lazy var infoImageViewWidthConstraint: NSLayoutConstraint = infoImageView.set(.width, to: 0) - private lazy var infoImageViewHeightConstraint: NSLayoutConstraint = infoImageView.set(.height, to: 0) - private lazy var iconImageView: UIImageView = UIImageView() + private lazy var iconImageView: UIImageView = { + let result: UIImageView = UIImageView() + result.themeTintColor = .textPrimary + result.set(.width, to: CallMessageCell.iconSize) + result.set(.height, to: CallMessageCell.iconSize) + result.setContentHugging(.horizontal, to: .required) + result.setCompressionResistance(.horizontal, to: .required) + + return result + }() private lazy var infoImageView: UIImageView = { let result: UIImageView = UIImageView( image: UIImage(named: "ic_info")? .withRenderingMode(.alwaysTemplate) ) result.themeTintColor = .textPrimary + result.set(.width, to: CallMessageCell.iconSize) + result.set(.height, to: CallMessageCell.iconSize) + result.setContentHugging(.horizontal, to: .required) + result.setCompressionResistance(.horizontal, to: .required) return result }() @@ -48,43 +66,30 @@ final class CallMessageCell: MessageCell { private lazy var label: UILabel = { let result: UILabel = UILabel() - result.font = .boldSystemFont(ofSize: Values.smallFontSize) + result.font = .systemFont(ofSize: Values.mediumFontSize) result.themeTextColor = .textPrimary result.textAlignment = .center result.lineBreakMode = .byWordWrapping result.numberOfLines = 0 + result.setContentHugging(.horizontal, to: .defaultLow) + result.setCompressionResistance(.horizontal, to: .defaultLow) return result }() - private lazy var container: UIView = { - let result: UIView = UIView() + private lazy var contentStackView: UIStackView = { + let result: UIStackView = UIStackView(arrangedSubviews: [iconImageView, label, infoImageView]) + result.axis = .horizontal + result.alignment = .center + result.spacing = CallMessageCell.horizontalInset + + return result + }() + + private lazy var container: UIStackView = { + let result: UIStackView = UIStackView() result.themeBackgroundColor = .backgroundSecondary result.layer.cornerRadius = 18 - result.addSubview(label) - - label.pin(.top, to: .top, of: result, withInset: CallMessageCell.inset) - label.pin( - .left, - to: .left, - of: result, - withInset: ((CallMessageCell.inset * 2) + infoImageView.bounds.size.width) - ) - label.pin( - .right, - to: .right, - of: result, - withInset: -((CallMessageCell.inset * 2) + infoImageView.bounds.size.width) - ) - label.pin(.bottom, to: .bottom, of: result, withInset: -CallMessageCell.inset) - - result.addSubview(iconImageView) - iconImageView.center(.vertical, in: result) - iconImageView.pin(.left, to: .left, of: result, withInset: CallMessageCell.inset) - - result.addSubview(infoImageView) - infoImageView.center(.vertical, in: result) - infoImageView.pin(.right, to: .right, of: result, withInset: -CallMessageCell.inset) return result }() @@ -103,25 +108,20 @@ final class CallMessageCell: MessageCell { override func setUpViewHierarchy() { super.setUpViewHierarchy() - iconImageViewWidthConstraint.isActive = true - iconImageViewHeightConstraint.isActive = true + container.addSubview(contentStackView) addSubview(mainStackView) + contentStackView.pin(.top, to: .top, of: container, withInset: CallMessageCell.verticalInset) + contentStackView.pin(.leading, to: .leading, of: container, withInset: CallMessageCell.horizontalInset) + contentStackView.pin(.trailing, to: .trailing, of: container, withInset: -CallMessageCell.horizontalInset) + contentStackView.pin(.bottom, to: .bottom, of: container, withInset: -CallMessageCell.verticalInset) + topConstraint.isActive = true mainStackView.pin(.left, to: .left, of: self, withInset: CallMessageCell.margin) mainStackView.pin(.right, to: .right, of: self, withInset: -CallMessageCell.margin) mainStackView.pin(.bottom, to: .bottom, of: self, withInset: -CallMessageCell.inset) } - override func setUpGestureRecognizers() { - let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress)) - addGestureRecognizer(longPressRecognizer) - - let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap)) - tapGestureRecognizer.numberOfTapsRequired = 1 - addGestureRecognizer(tapGestureRecognizer) - } - // MARK: - Updating override func update( @@ -130,6 +130,7 @@ final class CallMessageCell: MessageCell { showExpandedReactions: Bool, shouldExpanded: Bool, lastSearchText: String?, + tableSize: CGSize, using dependencies: Dependencies ) { guard @@ -163,8 +164,7 @@ final class CallMessageCell: MessageCell { default: return nil } }() - iconImageViewWidthConstraint.constant = (iconImageView.image != nil ? CallMessageCell.iconSize : 0) - iconImageViewHeightConstraint.constant = (iconImageView.image != nil ? CallMessageCell.iconSize : 0) + iconImageView.isHidden = (iconImageView.image == nil) let shouldShowInfoIcon: Bool = ( ( @@ -175,8 +175,7 @@ final class CallMessageCell: MessageCell { Permissions.microphone != .granted ) ) - infoImageViewWidthConstraint.constant = (shouldShowInfoIcon ? CallMessageCell.iconSize : 0) - infoImageViewHeightConstraint.constant = (shouldShowInfoIcon ? CallMessageCell.iconSize : 0) + infoImageView.isHidden = !shouldShowInfoIcon label.text = cellViewModel.body @@ -205,7 +204,7 @@ final class CallMessageCell: MessageCell { // MARK: - Interaction - @objc func handleLongPress(_ gestureRecognizer: UITapGestureRecognizer) { + override func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { if [ .ended, .cancelled, .failed ].contains(gestureRecognizer.state) { isHandlingLongPress = false return @@ -216,7 +215,7 @@ final class CallMessageCell: MessageCell { isHandlingLongPress = true } - @objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { + override func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { guard let dependencies: Dependencies = self.dependencies, let cellViewModel: MessageViewModel = self.viewModel, diff --git a/Session/Conversations/Message Cells/Content Views/DeletedMessageView.swift b/Session/Conversations/Message Cells/Content Views/DeletedMessageView.swift index 566ddceca1..58ed68ab64 100644 --- a/Session/Conversations/Message Cells/Content Views/DeletedMessageView.swift +++ b/Session/Conversations/Message Cells/Content Views/DeletedMessageView.swift @@ -10,6 +10,8 @@ import SessionUtilitiesKit final class DeletedMessageView: UIView { private static let iconSize: CGFloat = 18 private static let iconImageViewSize: CGFloat = 30 + private static let horizontalInset = Values.mediumSmallSpacing + private static let verticalInset = Values.smallSpacing // MARK: - Lifecycle @@ -29,24 +31,26 @@ final class DeletedMessageView: UIView { } private func setUpViewHierarchy(textColor: ThemeValue, variant: Interaction.Variant, maxWidth: CGFloat) { - // Image view - let imageContainerView: UIView = UIView() - imageContainerView.set(.width, to: DeletedMessageView.iconImageViewSize) - imageContainerView.set(.height, to: DeletedMessageView.iconImageViewSize) + let trashIcon = Lucide.image(icon: .trash2, size: DeletedMessageView.iconSize)? + .withRenderingMode(.alwaysTemplate) - let imageView = UIImageView(image: Lucide.image(icon: .trash2, size: DeletedMessageView.iconSize)?.withRenderingMode(.alwaysTemplate)) + let imageView = UIImageView(image: trashIcon) imageView.themeTintColor = textColor + imageView.alpha = Values.highOpacity imageView.contentMode = .scaleAspectFit imageView.set(.width, to: DeletedMessageView.iconSize) imageView.set(.height, to: DeletedMessageView.iconSize) - imageContainerView.addSubview(imageView) - imageView.center(in: imageContainerView) + + let imageViewContainer: UIView = UIView() + imageViewContainer.addSubview(imageView) + imageView.center(.vertical, in: imageViewContainer) + imageView.pin(.leading, to: .leading, of: imageViewContainer) + imageView.pin(.trailing, to: .trailing, of: imageViewContainer) // Body label let titleLabel = UILabel() titleLabel.setContentHuggingPriority(.required, for: .vertical) - titleLabel.preferredMaxLayoutWidth = maxWidth - 6 // `6` for the `stackView.layoutMargins` - titleLabel.font = .systemFont(ofSize: Values.smallFontSize) + titleLabel.font = .italicSystemFont(ofSize: Values.mediumFontSize) titleLabel.text = { switch variant { case .standardIncomingDeletedLocally, .standardOutgoingDeletedLocally: @@ -56,19 +60,29 @@ final class DeletedMessageView: UIView { } }() titleLabel.themeTextColor = textColor + titleLabel.alpha = Values.highOpacity titleLabel.lineBreakMode = .byTruncatingTail titleLabel.numberOfLines = 2 + titleLabel.setContentHugging(.vertical, to: .required) + titleLabel.setCompressionResistance(.vertical, to: .required) // Stack view - let stackView = UIStackView(arrangedSubviews: [ imageContainerView, titleLabel ]) + let stackView = UIStackView(arrangedSubviews: [ + imageViewContainer, + titleLabel + ]) stackView.axis = .horizontal - stackView.alignment = .center + stackView.alignment = .fill + stackView.spacing = Values.smallSpacing stackView.isLayoutMarginsRelativeArrangement = true stackView.layoutMargins = UIEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 6) addSubview(stackView) - let calculatedSize: CGSize = stackView.systemLayoutSizeFitting(CGSize(width: maxWidth, height: 999)) - stackView.pin(to: self, withInset: Values.smallSpacing) - stackView.set(.height, to: calculatedSize.height) + stackView.pin(.top, to: .top, of: self, withInset: Self.verticalInset) + stackView.pin(.leading, to: .leading, of: self, withInset: Self.horizontalInset) + stackView.pin(.trailing, to: .trailing, of: self, withInset: -Self.horizontalInset) + stackView.pin(.bottom, to: .bottom, of: self, withInset: -Self.verticalInset) + stackView.setContentHugging(.vertical, to: .required) + stackView.setCompressionResistance(.vertical, to: .required) } } diff --git a/Session/Conversations/Message Cells/Content Views/DisappearingMessageTimerView.swift b/Session/Conversations/Message Cells/Content Views/DisappearingMessageTimerView.swift index b0f17cc31e..b835fe5e86 100644 --- a/Session/Conversations/Message Cells/Content Views/DisappearingMessageTimerView.swift +++ b/Session/Conversations/Message Cells/Content Views/DisappearingMessageTimerView.swift @@ -1,7 +1,7 @@ // Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. import UIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit class DisappearingMessageTimerView: UIView { diff --git a/Session/Conversations/Message Cells/Content Views/DocumentView.swift b/Session/Conversations/Message Cells/Content Views/DocumentView.swift index de017bffbf..aef1f49e9b 100644 --- a/Session/Conversations/Message Cells/Content Views/DocumentView.swift +++ b/Session/Conversations/Message Cells/Content Views/DocumentView.swift @@ -74,7 +74,7 @@ final class DocumentView: UIView { rightContainerView.set(.height, to: 24) let activityIndicator = UIActivityIndicatorView(style: .medium) - activityIndicator.themeTintColor = .textPrimary + activityIndicator.themeColor = textColor activityIndicator.startAnimating() activityIndicator.hidesWhenStopped = true activityIndicator.isHidden = (attachment.state != .uploading && attachment.state != .downloading) diff --git a/Session/Conversations/Message Cells/Content Views/MediaLoaderView.swift b/Session/Conversations/Message Cells/Content Views/MediaLoaderView.swift index b8a56e1d1e..ba98b62b81 100644 --- a/Session/Conversations/Message Cells/Content Views/MediaLoaderView.swift +++ b/Session/Conversations/Message Cells/Content Views/MediaLoaderView.swift @@ -5,9 +5,12 @@ import SessionUIKit final class MediaLoaderView: UIView { private let bar = UIView() + private var cachedWidth: CGFloat = 0 private lazy var barLeftConstraint = bar.pin(.left, to: .left, of: self) - private lazy var barRightConstraint = bar.pin(.right, to: .right, of: self) + private lazy var barRightConstraint = bar + .pin(.right, to: .right, of: self) + .setting(priority: .defaultHigh) // MARK: - Lifecycle @@ -30,14 +33,22 @@ final class MediaLoaderView: UIView { barLeftConstraint.isActive = true bar.pin(.top, to: .top, of: self) barRightConstraint.isActive = true - bar.pin(.bottom, to: .bottom, of: self) + bar.pin(.bottom, to: .bottom, of: self).setting(priority: .defaultHigh) step1() } + override func layoutSubviews() { + super.layoutSubviews() + + if cachedWidth != bounds.width { + cachedWidth = bounds.width + } + } + // MARK: - Animation func step1() { - barRightConstraint.constant = -bounds.width + barRightConstraint.constant = -cachedWidth UIView.animate(withDuration: 0.5, animations: { [weak self] in guard let self = self else { return } self.barRightConstraint.constant = 0 @@ -51,7 +62,7 @@ final class MediaLoaderView: UIView { barLeftConstraint.constant = 0 UIView.animate(withDuration: 0.5, animations: { [weak self] in guard let self = self else { return } - self.barLeftConstraint.constant = self.bounds.width + self.barLeftConstraint.constant = cachedWidth self.layoutIfNeeded() }, completion: { [weak self] _ in Timer.scheduledTimer(withTimeInterval: 0.25, repeats: false) { _ in @@ -75,7 +86,7 @@ final class MediaLoaderView: UIView { barRightConstraint.constant = 0 UIView.animate(withDuration: 0.5, animations: { [weak self] in guard let self = self else { return } - self.barRightConstraint.constant = -self.bounds.width + self.barRightConstraint.constant = -cachedWidth self.layoutIfNeeded() }, completion: { [weak self] _ in Timer.scheduledTimer(withTimeInterval: 0.25, repeats: false) { _ in diff --git a/Session/Conversations/Message Cells/Content Views/MediaView.swift b/Session/Conversations/Message Cells/Content Views/MediaView.swift index 370c14ac1f..e1e942ee2d 100644 --- a/Session/Conversations/Message Cells/Content Views/MediaView.swift +++ b/Session/Conversations/Message Cells/Content Views/MediaView.swift @@ -117,7 +117,8 @@ public class MediaView: UIView { let result: GradientView = GradientView() result.themeBackgroundGradient = [ .value(.black, alpha: 0), - .value(.black, alpha: 0.4) + .value(.black, alpha: 0.75), + .value(.black, alpha: 0.75) ] result.isHidden = true @@ -129,7 +130,6 @@ public class MediaView: UIView { result.font = .systemFont(ofSize: Values.smallFontSize) result.text = attachment.duration.map { Format.duration($0) } result.themeTextColor = .white - result.isHidden = true return result }() @@ -162,14 +162,12 @@ public class MediaView: UIView { errorIconView.center(in: self) addSubview(durationBackgroundView) - durationBackgroundView.set(.height, to: 40) durationBackgroundView.pin(.leading, to: .leading, of: imageView) durationBackgroundView.pin(.trailing, to: .trailing, of: imageView) durationBackgroundView.pin(.bottom, to: .bottom, of: imageView) - addSubview(durationLabel) - durationLabel.pin(.trailing, to: .trailing, of: imageView, withInset: -Values.smallSpacing) - durationLabel.pin(.bottom, to: .bottom, of: imageView, withInset: -Values.smallSpacing) + durationBackgroundView.addSubview(durationLabel) + durationLabel.pin(to: durationBackgroundView, withInset: Values.smallSpacing) addSubview(playButtonIcon) playButtonIcon.set(.width, to: 72) @@ -194,11 +192,11 @@ public class MediaView: UIView { case (_, false, _), (_, _, false): return configure(forError: .invalid) case (_, true, true): - imageView.loadThumbnail(size: .medium, attachment: attachment, using: dependencies) { [weak self] success in - guard !success else { return } + imageView.loadThumbnail(size: .medium, attachment: attachment, using: dependencies) { [weak self] processedData in + guard processedData == nil else { return } Log.error("[MediaView] Could not load thumbnail") - Task { @MainActor [weak self] in self?.configure(forError: .invalid) } + self?.configure(forError: .invalid) } } @@ -215,13 +213,12 @@ public class MediaView: UIView { !loadingIndicator.isHidden || !attachment.isVideo ) - durationLabel.isHidden = ( + durationBackgroundView.isHidden = ( shouldSupressControls || attachment.duration == nil || !loadingIndicator.isHidden || !attachment.isVideo ) - durationBackgroundView.isHidden = durationLabel.isHidden } @MainActor diff --git a/Session/Conversations/Message Cells/Content Views/QuoteView.swift b/Session/Conversations/Message Cells/Content Views/QuoteView.swift index 452fd78e2e..ecc93aef65 100644 --- a/Session/Conversations/Message Cells/Content Views/QuoteView.swift +++ b/Session/Conversations/Message Cells/Content Views/QuoteView.swift @@ -90,6 +90,7 @@ final class QuoteView: UIView { mainStackView.isLayoutMarginsRelativeArrangement = true mainStackView.layoutMargins = UIEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: smallSpacing) mainStackView.alignment = .center + mainStackView.setCompressionResistance(.vertical, to: .required) // Content view let contentView = UIView() @@ -131,8 +132,8 @@ final class QuoteView: UIView { } // Generate the thumbnail if needed - imageView.loadThumbnail(size: .medium, attachment: attachment, using: dependencies) { [weak imageView] success in - guard success else { return } + imageView.loadThumbnail(size: .small, attachment: attachment, using: dependencies) { [weak imageView] processedData in + guard processedData != nil else { return } imageView?.contentMode = .scaleAspectFill } @@ -220,6 +221,7 @@ final class QuoteView: UIView { authorLabel.lineBreakMode = .byTruncatingTail authorLabel.isHidden = (authorLabel.text == nil) authorLabel.numberOfLines = 1 + authorLabel.setCompressionResistance(.vertical, to: .required) let labelStackView = UIStackView(arrangedSubviews: [ authorLabel, bodyLabel ]) labelStackView.axis = .vertical @@ -227,6 +229,7 @@ final class QuoteView: UIView { labelStackView.distribution = .equalCentering labelStackView.isLayoutMarginsRelativeArrangement = true labelStackView.layoutMargins = UIEdgeInsets(top: labelStackViewVMargin, left: 0, bottom: labelStackViewVMargin, right: 0) + labelStackView.setCompressionResistance(.vertical, to: .required) mainStackView.addArrangedSubview(labelStackView) // Constraints diff --git a/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift b/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift index 80f11e7122..35cf6343e3 100644 --- a/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift +++ b/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift @@ -107,7 +107,7 @@ final class ReactionContainerView: UIView { mainStackView.pin(.top, to: .top, of: self) mainStackView.pin(.leading, to: .leading, of: self) - mainStackView.pin(.trailing, to: .trailing, of: self) + mainStackView.pin(.trailing, to: .trailing, of: self).setting(priority: .defaultHigh) mainStackView.pin(.bottom, to: .bottom, of: self, withInset: -Values.verySmallSpacing) reactionContainerView.set(.width, to: .width, of: mainStackView) collapseButton.set(.width, to: .width, of: mainStackView) @@ -125,6 +125,8 @@ final class ReactionContainerView: UIView { self.reactionViews = [] self.reactionContainerView.arrangedSubviews.forEach { $0.removeFromSuperview() } + guard !reactions.isEmpty else { return } + let collapsedCount: Int = { // If there are already more than 'maxEmojiBeforeCollapse' then no need to calculate, just // always collapse diff --git a/Session/Conversations/Message Cells/Content Views/ReactionView.swift b/Session/Conversations/Message Cells/Content Views/ReactionView.swift index 6a24fcc001..2250290531 100644 --- a/Session/Conversations/Message Cells/Content Views/ReactionView.swift +++ b/Session/Conversations/Message Cells/Content Views/ReactionView.swift @@ -152,6 +152,6 @@ final class ExpandingReactionButton: UIView { rightMargin += margin } - set(.width, to: rightMargin - margin + size) + set(.width, to: rightMargin - margin + size).setting(priority: .defaultHigh) } } diff --git a/Session/Conversations/Message Cells/Content Views/SwiftUI/QuoteView_SwiftUI.swift b/Session/Conversations/Message Cells/Content Views/SwiftUI/QuoteView_SwiftUI.swift index ac22e57bdb..9f92551551 100644 --- a/Session/Conversations/Message Cells/Content Views/SwiftUI/QuoteView_SwiftUI.swift +++ b/Session/Conversations/Message Cells/Content Views/SwiftUI/QuoteView_SwiftUI.swift @@ -83,7 +83,7 @@ struct QuoteView_SwiftUI: View { SessionAsyncImage( attachment: attachment, - thumbnailSize: .medium, + thumbnailSize: .small, using: dependencies ) { image in image diff --git a/Session/Conversations/Message Cells/Content Views/VoiceMessageView.swift b/Session/Conversations/Message Cells/Content Views/VoiceMessageView.swift index 71bfeb1790..d2b65b96d6 100644 --- a/Session/Conversations/Message Cells/Content Views/VoiceMessageView.swift +++ b/Session/Conversations/Message Cells/Content Views/VoiceMessageView.swift @@ -168,7 +168,6 @@ public final class VoiceMessageView: UIView { } // MARK: - Updating - public func update( with attachment: Attachment, isPlaying: Bool, @@ -176,42 +175,49 @@ public final class VoiceMessageView: UIView { playbackRate: Double, oldPlaybackRate: Double ) { - switch attachment.state { - case .downloaded, .uploaded: - loader.isHidden = true - loader.stopAnimating() - - toggleImageView.image = (isPlaying ? UIImage(named: "Pause") : UIImage(named: "Play"))? - .withRenderingMode(.alwaysTemplate) - countdownLabel.text = max(0, (floor(attachment.duration.defaulting(to: 0) - progress))) - .formatted(format: .hoursMinutesSeconds) - - guard let duration: TimeInterval = attachment.duration, duration > 0, progress > 0 else { - return progressViewRightConstraint.constant = -VoiceMessageView.width - } - - let fraction: Double = (progress / duration) - progressViewRightConstraint.constant = -(VoiceMessageView.width * (1 - fraction)) - - // If the playback rate changed then show the 'speedUpLabel' briefly - guard playbackRate > oldPlaybackRate else { return } - - UIView.animate(withDuration: 0.25) { [weak self] in - self?.countdownLabel.alpha = 0 - self?.speedUpLabel.alpha = 1 - } - - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1250)) { - UIView.animate(withDuration: 0.25) { [weak self] in - self?.countdownLabel.alpha = 1 - self?.speedUpLabel.alpha = 0 - } - } - - default: - if !loader.isAnimating { - loader.startAnimating() - } + + // Updates countdown label to attachments duration + // Should be set regardless of attachment state + let remainingTime = max(0, floor(attachment.duration.defaulting(to: 0) - progress)) + + countdownLabel.text = remainingTime.formatted(format: .hoursMinutesSeconds) + + guard attachment.state == .downloaded || attachment.state == .uploaded else { + if !loader.isAnimating { + loader.startAnimating() + } + return + } + + loader.isHidden = true + loader.stopAnimating() + + toggleImageView.image = (isPlaying ? UIImage(named: "Pause") : UIImage(named: "Play"))? + .withRenderingMode(.alwaysTemplate) + + guard + let duration: TimeInterval = attachment.duration, + duration > 0, progress > 0 + else { + return progressViewRightConstraint.constant = -VoiceMessageView.width + } + + let fraction: Double = (progress / duration) + progressViewRightConstraint.constant = -(VoiceMessageView.width * (1 - fraction)) + + // If the playback rate changed then show the 'speedUpLabel' briefly + guard playbackRate > oldPlaybackRate else { return } + + UIView.animate(withDuration: 0.25) { [weak self] in + self?.countdownLabel.alpha = 0 + self?.speedUpLabel.alpha = 1 + } + + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1250)) { + UIView.animate(withDuration: 0.25) { [weak self] in + self?.countdownLabel.alpha = 1 + self?.speedUpLabel.alpha = 0 + } } } } diff --git a/Session/Conversations/Message Cells/DateHeaderCell.swift b/Session/Conversations/Message Cells/DateHeaderCell.swift index 352f49359e..f06c123333 100644 --- a/Session/Conversations/Message Cells/DateHeaderCell.swift +++ b/Session/Conversations/Message Cells/DateHeaderCell.swift @@ -45,6 +45,7 @@ final class DateHeaderCell: MessageCell { showExpandedReactions: Bool, shouldExpanded: Bool, lastSearchText: String?, + tableSize: CGSize, using dependencies: Dependencies ) { guard cellViewModel.cellType == .dateHeader else { return } diff --git a/Session/Conversations/Message Cells/InfoMessageCell.swift b/Session/Conversations/Message Cells/InfoMessageCell.swift index 9c626a90c5..e21578ad1d 100644 --- a/Session/Conversations/Message Cells/InfoMessageCell.swift +++ b/Session/Conversations/Message Cells/InfoMessageCell.swift @@ -13,6 +13,13 @@ final class InfoMessageCell: MessageCell { override var contextSnapshotView: UIView? { return label } + override var allowedGestureRecognizers: Set { + return [ + .longPress, + .tap + ] + } + // MARK: - UI private lazy var iconContainerViewWidthConstraint = iconContainerView.set(.width, to: InfoMessageCell.iconSize) @@ -77,15 +84,6 @@ final class InfoMessageCell: MessageCell { stackView.pin(.right, to: .right, of: self, withInset: -Values.massiveSpacing) stackView.pin(.bottom, to: .bottom, of: self, withInset: -InfoMessageCell.inset) } - - override func setUpGestureRecognizers() { - let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress)) - addGestureRecognizer(longPressRecognizer) - - let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap)) - tapGestureRecognizer.numberOfTapsRequired = 1 - addGestureRecognizer(tapGestureRecognizer) - } // MARK: - Updating @@ -95,6 +93,7 @@ final class InfoMessageCell: MessageCell { showExpandedReactions: Bool, shouldExpanded: Bool, lastSearchText: String?, + tableSize: CGSize, using dependencies: Dependencies ) { guard cellViewModel.variant.isInfoMessage else { return } @@ -169,7 +168,7 @@ final class InfoMessageCell: MessageCell { // MARK: - Interaction - @objc func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { + override func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { if [ .ended, .cancelled, .failed ].contains(gestureRecognizer.state) { isHandlingLongPress = false return @@ -180,9 +179,9 @@ final class InfoMessageCell: MessageCell { isHandlingLongPress = true } - @objc func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { + override func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { guard let cellViewModel: MessageViewModel = self.viewModel else { return } - + if cellViewModel.variant == .infoDisappearingMessagesUpdate && cellViewModel.canDoFollowingSetting() { delegate?.handleItemTapped(cellViewModel, cell: self, cellLocation: gestureRecognizer.location(in: self)) } diff --git a/Session/Conversations/Message Cells/MessageCell.swift b/Session/Conversations/Message Cells/MessageCell.swift index 39730a9d6c..cbb7caa696 100644 --- a/Session/Conversations/Message Cells/MessageCell.swift +++ b/Session/Conversations/Message Cells/MessageCell.swift @@ -10,11 +10,16 @@ public enum SwipeState { case cancelled } +public enum GestureRecognizerType { + case tap, longPress, doubleTap +} + public class MessageCell: UITableViewCell { var dependencies: Dependencies? var viewModel: MessageViewModel? weak var delegate: MessageCellDelegate? open var contextSnapshotView: UIView? { return nil } + open var allowedGestureRecognizers: Set { return [] } // Override to have gestures // MARK: - Lifecycle @@ -41,7 +46,32 @@ public class MessageCell: UITableViewCell { } func setUpGestureRecognizers() { - // To be overridden by subclasses + var tapGestureRecognizer: UITapGestureRecognizer? + var doubleTapGestureRecognizer: UITapGestureRecognizer? + + if allowedGestureRecognizers.contains(.tap) { + let tapGesture: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap)) + tapGesture.numberOfTapsRequired = 1 + addGestureRecognizer(tapGesture) + tapGestureRecognizer = tapGesture + } + + if allowedGestureRecognizers.contains(.doubleTap) { + let doubleTapGesture: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTap)) + doubleTapGesture.numberOfTapsRequired = 2 + addGestureRecognizer(doubleTapGesture) + doubleTapGestureRecognizer = doubleTapGesture + } + + if allowedGestureRecognizers.contains(.longPress) { + let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress)) + addGestureRecognizer(longPressGesture) + } + + // If we have both tap and double tap gestures then the single tap should fail if a double tap occurs + if let tapGesture: UITapGestureRecognizer = tapGestureRecognizer, let doubleTapGesture: UITapGestureRecognizer = doubleTapGestureRecognizer { + tapGesture.require(toFail: doubleTapGesture) + } } // MARK: - Updating @@ -59,6 +89,7 @@ public class MessageCell: UITableViewCell { showExpandedReactions: Bool, shouldExpanded: Bool, lastSearchText: String?, + tableSize: CGSize, using dependencies: Dependencies ) { preconditionFailure("Must be overridden by subclasses.") @@ -93,6 +124,16 @@ public class MessageCell: UITableViewCell { return CallMessageCell.self } } + + // MARK: - Gesture events + @objc + func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) {} + + @objc + func handleTap(_ gestureRecognizer: UITapGestureRecognizer) {} + + @objc + func handleDoubleTap() {} } // MARK: - MessageCellDelegate diff --git a/Session/Conversations/Message Cells/TypingIndicatorCell.swift b/Session/Conversations/Message Cells/TypingIndicatorCell.swift index 6600b22898..42b530ffc3 100644 --- a/Session/Conversations/Message Cells/TypingIndicatorCell.swift +++ b/Session/Conversations/Message Cells/TypingIndicatorCell.swift @@ -46,6 +46,7 @@ final class TypingIndicatorCell: MessageCell { showExpandedReactions: Bool, shouldExpanded: Bool, lastSearchText: String?, + tableSize: CGSize, using dependencies: Dependencies ) { guard cellViewModel.cellType == .typingIndicator else { return } diff --git a/Session/Conversations/Message Cells/UnreadMarkerCell.swift b/Session/Conversations/Message Cells/UnreadMarkerCell.swift index 4d33afe6b6..12313e50d5 100644 --- a/Session/Conversations/Message Cells/UnreadMarkerCell.swift +++ b/Session/Conversations/Message Cells/UnreadMarkerCell.swift @@ -66,6 +66,7 @@ final class UnreadMarkerCell: MessageCell { showExpandedReactions: Bool, shouldExpanded: Bool, lastSearchText: String?, + tableSize: CGSize, using dependencies: Dependencies ) { guard cellViewModel.cellType == .unreadMarker else { return } diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index 58dbf649b5..20d64ebed9 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -25,29 +25,14 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { override var contextSnapshotView: UIView? { return snContentView } - // Constraints - internal lazy var authorLabelTopConstraint = authorLabel.pin(.top, to: .top, of: self) - private lazy var authorLabelHeightConstraint = authorLabel.set(.height, to: 0) - private lazy var profilePictureViewLeadingConstraint = profilePictureView.pin(.leading, to: .leading, of: self, withInset: VisibleMessageCell.groupThreadHSpacing) - internal lazy var contentViewLeadingConstraint1 = snContentView.pin(.leading, to: .trailing, of: profilePictureView, withInset: VisibleMessageCell.groupThreadHSpacing) - private lazy var contentViewLeadingConstraint2 = snContentView.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor, constant: VisibleMessageCell.gutterSize) - private lazy var contentViewTopConstraint = snContentView.pin(.top, to: .bottom, of: authorLabel, withInset: VisibleMessageCell.authorLabelBottomSpacing) - internal lazy var contentViewTrailingConstraint1 = snContentView.pin(.trailing, to: .trailing, of: self, withInset: -VisibleMessageCell.contactThreadHSpacing) - private lazy var contentViewTrailingConstraint2 = snContentView.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor, constant: -VisibleMessageCell.gutterSize) - private lazy var contentBottomConstraint = snContentView.bottomAnchor - .constraint(lessThanOrEqualTo: self.bottomAnchor, constant: -1) - - private lazy var underBubbleStackViewIncomingLeadingConstraint: NSLayoutConstraint = underBubbleStackView.pin(.leading, to: .leading, of: snContentView) - private lazy var underBubbleStackViewIncomingTrailingConstraint: NSLayoutConstraint = underBubbleStackView.pin(.trailing, to: .trailing, of: self, withInset: -VisibleMessageCell.contactThreadHSpacing) - private lazy var underBubbleStackViewOutgoingLeadingConstraint: NSLayoutConstraint = underBubbleStackView.pin(.leading, to: .leading, of: self, withInset: VisibleMessageCell.contactThreadHSpacing) - private lazy var underBubbleStackViewOutgoingTrailingConstraint: NSLayoutConstraint = underBubbleStackView.pin(.trailing, to: .trailing, of: snContentView) - private lazy var underBubbleStackViewNoHeightConstraint: NSLayoutConstraint = underBubbleStackView.set(.height, to: 0) + override var allowedGestureRecognizers: Set { + return [ + .tap, + .longPress, + .doubleTap + ] + } - private lazy var timerViewOutgoingMessageConstraint = timerView.pin(.trailing, to: .trailing, of: messageStatusContainerView) - private lazy var timerViewIncomingMessageConstraint = timerView.pin(.leading, to: .leading, of: messageStatusContainerView) - private lazy var messageStatusLabelOutgoingMessageConstraint = messageStatusLabel.pin(.trailing, to: .leading, of: timerView, withInset: -2) - private lazy var messageStatusLabelIncomingMessageConstraint = messageStatusLabel.pin(.leading, to: .trailing, of: timerView, withInset: 2) - private lazy var panGestureRecognizer: UIPanGestureRecognizer = { let result = UIPanGestureRecognizer(target: self, action: #selector(handlePan)) result.delegate = self @@ -56,23 +41,62 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { // MARK: - UI Components + private lazy var contentHStackTopConstraint: NSLayoutConstraint = + contentHStack.pin(.top, to: .top, of: contentView) + private lazy var viewsToMoveForReply: [UIView] = [ snContentView, profilePictureView, replyButton, timerView, - messageStatusContainerView, + messageStatusStackView, reactionContainerView ] + private lazy var leadingSpacer: UIView = { + let result: UIView = UIView() + result.setContentHugging(.horizontal, to: .defaultLow) + result.setCompressionResistance(.horizontal, to: .defaultLow) + + return result + }() + + private lazy var trailingSpacer: UIView = { + let result: UIView = UIView() + result.setContentHugging(.horizontal, to: .defaultLow) + result.setCompressionResistance(.horizontal, to: .defaultLow) + + return result + }() + + private lazy var profilePictureViewContainer: UIView = { + let result: UIView = UIView() + + return result + }() + private lazy var profilePictureView: ProfilePictureView = ProfilePictureView( size: .message, dataManager: nil ) + public lazy var contentHStack: UIStackView = { + let result: UIStackView = UIStackView( + arrangedSubviews: [leadingSpacer, profilePictureViewContainer, mainVStack, trailingSpacer] + ) + result.axis = .horizontal + result.alignment = .fill + result.spacing = VisibleMessageCell.groupThreadHSpacing + + return result + }() + lazy var bubbleBackgroundView: UIView = { let result = UIView() result.layer.cornerRadius = VisibleMessageCell.largeCornerRadius + result.setContentHugging(.vertical, to: .required) + result.setCompressionResistance(.vertical, to: .required) + return result }() @@ -81,12 +105,32 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { result.clipsToBounds = true result.layer.cornerRadius = VisibleMessageCell.largeCornerRadius result.set(.width, greaterThanOrEqualTo: VisibleMessageCell.largeCornerRadius * 2) + result.setContentHugging(.vertical, to: .required) + result.setCompressionResistance(.vertical, to: .required) + + return result + }() + + private lazy var mainVStack: UIStackView = { + let result: UIStackView = UIStackView( + arrangedSubviews: [authorLabel, snContentView, underBubbleStackView] + ) + result.axis = .vertical + result.alignment = .fill + result.setCustomSpacing(VisibleMessageCell.authorLabelBottomSpacing, after: authorLabel) + result.setCustomSpacing(Values.verySmallSpacing, after: snContentView) + result.setContentHugging(.vertical, to: .required) + result.setCompressionResistance(.vertical, to: .required) + return result }() private lazy var authorLabel: UILabel = { - let result = UILabel() + let result: UILabel = UILabel() result.font = .boldSystemFont(ofSize: Values.smallFontSize) + result.setContentHugging(.vertical, to: .required) + result.setCompressionResistance(.vertical, to: .required) + return result }() @@ -95,6 +139,11 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { result.axis = .vertical result.spacing = Values.verySmallSpacing result.alignment = .leading + result.setContentHugging(.horizontal, to: .defaultHigh) + result.setContentHugging(.vertical, to: .required) + result.setCompressionResistance(.horizontal, to: .required) + result.setCompressionResistance(.vertical, to: .required) + return result }() @@ -132,24 +181,39 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { return result }() - - private lazy var timerView: DisappearingMessageTimerView = DisappearingMessageTimerView() lazy var underBubbleStackView: UIStackView = { - let result = UIStackView(arrangedSubviews: []) + let result = UIStackView( + arrangedSubviews: [reactionContainerView, messageStatusStackView] + ) result.setContentHuggingPriority(.required, for: .vertical) result.setContentCompressionResistancePriority(.required, for: .vertical) result.axis = .vertical result.spacing = Values.verySmallSpacing result.alignment = .trailing + result.setContentHugging(.vertical, to: .required) + result.setCompressionResistance(.vertical, to: .required) return result }() private lazy var reactionContainerView = ReactionContainerView() - internal lazy var messageStatusContainerView: UIView = { - let result = UIView() + internal lazy var messageStatusStackView: UIStackView = { + let result: UIStackView = UIStackView( + arrangedSubviews: [messageStatusLabel, messageStatusImageView, timerView] + ) + result.axis = .horizontal + result.alignment = .center + result.spacing = 2 + + return result + }() + + private lazy var timerView: DisappearingMessageTimerView = { + let result: DisappearingMessageTimerView = DisappearingMessageTimerView() + result.set(.width, to: VisibleMessageCell.messageStatusImageViewSize) + result.set(.height, to: VisibleMessageCell.messageStatusImageViewSize) return result }() @@ -170,11 +234,11 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { result.accessibilityLabel = "Message sent status tick" result.contentMode = .scaleAspectFit result.themeTintColor = .messageBubble_deliveryStatus + result.set(.width, to: VisibleMessageCell.messageStatusImageViewSize) + result.set(.height, to: VisibleMessageCell.messageStatusImageViewSize) return result }() - - internal lazy var messageStatusLabelPaddingView: UIView = UIView() // MARK: - Settings @@ -210,81 +274,48 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { override func setUpViewHierarchy() { super.setUpViewHierarchy() - // Author label - addSubview(authorLabel) - authorLabelTopConstraint.isActive = true - authorLabelHeightConstraint.isActive = true + contentView.addSubview(contentHStack) + contentView.addSubview(replyButton) - // Profile picture view - addSubview(profilePictureView) - profilePictureViewLeadingConstraint.isActive = true + profilePictureViewContainer.addSubview(profilePictureView) + bubbleBackgroundView.addSubview(bubbleView) - // Content view - addSubview(snContentView) - contentViewLeadingConstraint1.isActive = true - contentViewTopConstraint.isActive = true - contentViewTrailingConstraint1.isActive = true - snContentView.pin(.bottom, to: .bottom, of: profilePictureView) + replyButton.addSubview(replyIconImageView) + + contentHStackTopConstraint.isActive = true + contentHStack.pin( + .leading, + to: .leading, + of: contentView, + withInset: VisibleMessageCell.contactThreadHSpacing + ) + contentHStack.pin( + .trailing, + to: .trailing, + of: contentView, + withInset: -VisibleMessageCell.contactThreadHSpacing + ) + contentHStack + .pin(.bottom, to: .bottom, of: contentView, withInset: -Values.verySmallSpacing) + .setting(priority: .defaultHigh) /// Avoid breaking encapsulated height + + // Profile picture view + profilePictureView.pin(.bottom, to: .bottom, of: profilePictureViewContainer) + profilePictureView.pin(.leading, to: .leading, of: profilePictureViewContainer) + profilePictureView.pin(.trailing, to: .trailing, of: profilePictureViewContainer) // Bubble background view - bubbleBackgroundView.addSubview(bubbleView) bubbleView.pin(to: bubbleBackgroundView) // Reply button - addSubview(replyButton) - replyButton.addSubview(replyIconImageView) replyIconImageView.center(in: replyButton) replyButton.pin(.leading, to: .trailing, of: snContentView, withInset: Values.smallSpacing) replyButton.center(.vertical, in: snContentView) - // Remaining constraints - authorLabel.pin(.leading, to: .leading, of: snContentView, withInset: VisibleMessageCell.authorLabelInset) - authorLabel.pin(.trailing, to: .trailing, of: self, withInset: -Values.mediumSpacing) - - // Under bubble content - addSubview(underBubbleStackView) - underBubbleStackView.pin(.top, to: .bottom, of: snContentView, withInset: Values.verySmallSpacing) - underBubbleStackView.pin(.bottom, to: .bottom, of: self) - - underBubbleStackView.addArrangedSubview(reactionContainerView) - underBubbleStackView.addArrangedSubview(messageStatusContainerView) - underBubbleStackView.addArrangedSubview(messageStatusLabelPaddingView) - - messageStatusContainerView.addSubview(messageStatusLabel) - messageStatusContainerView.addSubview(messageStatusImageView) - messageStatusContainerView.addSubview(timerView) - - reactionContainerView.widthAnchor - .constraint(lessThanOrEqualTo: underBubbleStackView.widthAnchor) - .isActive = true - messageStatusImageView.pin(.top, to: .top, of: messageStatusContainerView) - messageStatusImageView.pin(.bottom, to: .bottom, of: messageStatusContainerView) - messageStatusImageView.pin(.trailing, to: .trailing, of: messageStatusContainerView) - messageStatusImageView.set(.width, to: VisibleMessageCell.messageStatusImageViewSize) - messageStatusImageView.set(.height, to: VisibleMessageCell.messageStatusImageViewSize) - timerView.pin(.top, to: .top, of: messageStatusContainerView) - timerView.pin(.bottom, to: .bottom, of: messageStatusContainerView) - timerView.set(.width, to: VisibleMessageCell.messageStatusImageViewSize) - timerView.set(.height, to: VisibleMessageCell.messageStatusImageViewSize) - messageStatusLabel.center(.vertical, in: messageStatusContainerView) - messageStatusLabelPaddingView.pin(.leading, to: .leading, of: messageStatusContainerView) - messageStatusLabelPaddingView.pin(.trailing, to: .trailing, of: messageStatusContainerView) + // Reactions container + reactionContainerView.set(.width, lessThanOrEqualTo: .width, of: underBubbleStackView) } - - override func setUpGestureRecognizers() { - let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress)) - addGestureRecognizer(longPressRecognizer) - - let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap)) - tapGestureRecognizer.numberOfTapsRequired = 1 - addGestureRecognizer(tapGestureRecognizer) - - let doubleTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTap)) - doubleTapGestureRecognizer.numberOfTapsRequired = 2 - addGestureRecognizer(doubleTapGestureRecognizer) - tapGestureRecognizer.require(toFail: doubleTapGestureRecognizer) - } - + // MARK: - Updating override func update( @@ -293,6 +324,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { showExpandedReactions: Bool, shouldExpanded: Bool, lastSearchText: String?, + tableSize: CGSize, using dependencies: Dependencies ) { self.dependencies = dependencies @@ -307,6 +339,13 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { cellViewModel.isOnlyMessageInCluster ) ) + contentHStackTopConstraint.constant = (shouldAddTopInset ? Values.mediumSpacing : 0) + + // Author label + authorLabel.isHidden = (cellViewModel.senderName == nil) + authorLabel.text = cellViewModel.senderName + authorLabel.themeTextColor = .textPrimary + let isGroupThread: Bool = ( cellViewModel.threadVariant == .community || cellViewModel.threadVariant == .legacyGroup || @@ -315,11 +354,11 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { // Profile picture view (should always be handled as a standard 'contact' profile picture) let profileShouldBeVisible: Bool = ( + isGroupThread && cellViewModel.canHaveProfile && cellViewModel.shouldShowProfile && cellViewModel.profile != nil ) - profilePictureViewLeadingConstraint.constant = (isGroupThread ? VisibleMessageCell.groupThreadHSpacing : 0) profilePictureView.isHidden = !cellViewModel.canHaveProfile profilePictureView.alpha = (profileShouldBeVisible ? 1 : 0) profilePictureView.setDataManager(dependencies[singleton: .imageDataManager]) @@ -333,13 +372,6 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { ) // Bubble view - contentViewLeadingConstraint1.isActive = cellViewModel.variant.isIncoming - contentViewLeadingConstraint1.constant = (isGroupThread ? VisibleMessageCell.groupThreadHSpacing : VisibleMessageCell.contactThreadHSpacing) - contentViewLeadingConstraint2.isActive = cellViewModel.variant.isOutgoing - contentViewTopConstraint.constant = (cellViewModel.senderName == nil ? 0 : VisibleMessageCell.authorLabelBottomSpacing) - contentViewTrailingConstraint1.isActive = cellViewModel.variant.isOutgoing - contentViewTrailingConstraint2.isActive = cellViewModel.variant.isIncoming - let bubbleBackgroundColor: ThemeValue = (cellViewModel.variant.isIncoming ? .messageBubble_incomingBackground : .messageBubble_outgoingBackground) bubbleView.themeBackgroundColor = bubbleBackgroundColor bubbleBackgroundView.themeBackgroundColor = bubbleBackgroundColor @@ -351,6 +383,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { playbackInfo: playbackInfo, shouldExpanded: shouldExpanded, lastSearchText: lastSearchText, + tableSize: tableSize, using: dependencies ) @@ -358,17 +391,6 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { bubbleView.accessibilityLabel = bodyTappableLabel?.attributedText?.string bubbleView.isAccessibilityElement = true - // Author label - authorLabelTopConstraint.constant = (shouldAddTopInset ? Values.mediumSpacing : 0) - authorLabel.isHidden = (cellViewModel.senderName == nil) - authorLabel.text = cellViewModel.senderName - authorLabel.themeTextColor = .textPrimary - - let authorLabelAvailableWidth: CGFloat = (VisibleMessageCell.getMaxWidth(for: cellViewModel) - 2 * VisibleMessageCell.authorLabelInset) - let authorLabelAvailableSpace = CGSize(width: authorLabelAvailableWidth, height: .greatestFiniteMagnitude) - let authorLabelSize = authorLabel.sizeThatFits(authorLabelAvailableSpace) - authorLabelHeightConstraint.constant = (cellViewModel.senderName != nil ? authorLabelSize.height : 0) - // Flip horizontally for RTL languages replyIconImageView.transform = CGAffineTransform.identity .scaledBy( @@ -384,19 +406,13 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { removeGestureRecognizer(panGestureRecognizer) } - // Under bubble content - underBubbleStackView.alignment = (cellViewModel.variant.isOutgoing ?.trailing : .leading) - underBubbleStackViewIncomingLeadingConstraint.isActive = !cellViewModel.variant.isOutgoing - underBubbleStackViewIncomingTrailingConstraint.isActive = !cellViewModel.variant.isOutgoing - underBubbleStackViewOutgoingLeadingConstraint.isActive = cellViewModel.variant.isOutgoing - underBubbleStackViewOutgoingTrailingConstraint.isActive = cellViewModel.variant.isOutgoing - // Reaction view reactionContainerView.isHidden = (cellViewModel.reactionInfo?.isEmpty != false) populateReaction( for: cellViewModel, maxWidth: VisibleMessageCell.getMaxWidth( for: cellViewModel, + cellWidth: tableSize.width, includingOppositeGutter: false ), showExpandedReactions: showExpandedReactions @@ -413,7 +429,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { messageStatusImageView.image = image messageStatusLabel.accessibilityIdentifier = "Message sent status: \(statusText ?? "invalid")" messageStatusImageView.themeTintColor = tintColor - messageStatusContainerView.isHidden = ( + messageStatusStackView.isHidden = ( (cellViewModel.expiresInSeconds ?? 0) == 0 && ( !cellViewModel.variant.isOutgoing || cellViewModel.variant.isDeletedMessage || @@ -424,10 +440,6 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { ) ) ) - messageStatusLabelPaddingView.isHidden = ( - messageStatusContainerView.isHidden || - cellViewModel.isLast - ) // Timer if @@ -450,16 +462,24 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { messageStatusImageView.isHidden = false } - timerViewOutgoingMessageConstraint.isActive = cellViewModel.variant.isOutgoing - timerViewIncomingMessageConstraint.isActive = cellViewModel.variant.isIncoming - messageStatusLabelOutgoingMessageConstraint.isActive = cellViewModel.variant.isOutgoing - messageStatusLabelIncomingMessageConstraint.isActive = cellViewModel.variant.isIncoming + // Hide the underBubbleStackView if all of it's content is hidden + underBubbleStackView.isHidden = !underBubbleStackView.arrangedSubviews.contains { !$0.isHidden } - // Set the height of the underBubbleStackView to 0 if it has no content (need to do this - // otherwise it can randomly stretch) - underBubbleStackViewNoHeightConstraint.isActive = underBubbleStackView.arrangedSubviews - .filter { !$0.isHidden } - .isEmpty + if cellViewModel.variant.isOutgoing { + leadingSpacer.isHidden = false + trailingSpacer.isHidden = true + + snContentView.alignment = .trailing + underBubbleStackView.alignment = .trailing + } + else { + leadingSpacer.isHidden = true + trailingSpacer.isHidden = false + contentHStack.spacing = (cellViewModel.canHaveProfile ? VisibleMessageCell.groupThreadHSpacing : 0) + + snContentView.alignment = .leading + underBubbleStackView.alignment = .leading + } } private func populateContentView( @@ -467,6 +487,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { playbackInfo: ConversationViewModel.PlaybackInfo?, shouldExpanded: Bool, lastSearchText: String?, + tableSize: CGSize, using dependencies: Dependencies ) { let bodyLabelTextColor: ThemeValue = (cellViewModel.variant.isOutgoing ? @@ -501,7 +522,12 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { let deletedMessageView: DeletedMessageView = DeletedMessageView( textColor: bodyLabelTextColor, variant: cellViewModel.variant, - maxWidth: (VisibleMessageCell.getMaxWidth(for: cellViewModel) - 2 * inset) + maxWidth: ( + VisibleMessageCell.getMaxWidth( + for: cellViewModel, + cellWidth: tableSize.width + ) - 2 * inset + ) ) bubbleView.addSubview(deletedMessageView) deletedMessageView.pin(to: bubbleView) @@ -514,7 +540,11 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { // FIXME: We should support rendering link previews alongside the other variants (bigger refactor) guard cellViewModel.cellType != .textOnlyMessage else { let inset: CGFloat = 12 - let maxWidth: CGFloat = (VisibleMessageCell.getMaxWidth(for: cellViewModel) - 2 * inset) + let maxWidth: CGFloat = ( + VisibleMessageCell.getMaxWidth( + for: cellViewModel, + cellWidth: tableSize.width + ) - 2 * inset) if let linkPreview: LinkPreview = cellViewModel.linkPreview { switch linkPreview.variant { @@ -559,6 +589,8 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { let stackView = UIStackView(arrangedSubviews: []) stackView.axis = .vertical stackView.spacing = 2 + stackView.setContentHugging(.vertical, to: .required) + stackView.setCompressionResistance(.vertical, to: .required) // Quote view if let quote: Quote = cellViewModel.quote { @@ -592,10 +624,8 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { stackView.addArrangedSubview(bodyTappableLabel) readMoreButton.themeTextColor = bodyLabelTextColor let maxHeight: CGFloat = VisibleMessageCell.getMaxHeightAfterTruncation(for: cellViewModel) - self.bodayTappableLabelHeightConstraint = bodyTappableLabel.set( - .height, - to: (shouldExpanded ? height : min(height, maxHeight)) - ) + bodyTappableLabel.numberOfLines = shouldExpanded ? 0 : VisibleMessageCell.maxNumberOfLinesAfterTruncation + if (height > maxHeight && !shouldExpanded) { stackView.addArrangedSubview(readMoreButton) readMoreButton.isHidden = false @@ -639,7 +669,12 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { /// Add any quote & body if present let inset: CGFloat = 12 - let maxWidth: CGFloat = (VisibleMessageCell.getMaxWidth(for: cellViewModel) - 2 * inset) + let maxWidth: CGFloat = ( + VisibleMessageCell.getMaxWidth( + for: cellViewModel, + cellWidth: tableSize.width + ) - 2 * inset + ) switch (cellViewModel.quote, cellViewModel.body) { /// Both quote and body @@ -679,10 +714,8 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { stackView.addArrangedSubview(bodyTappableLabel) readMoreButton.themeTextColor = bodyLabelTextColor let maxHeight: CGFloat = VisibleMessageCell.getMaxHeightAfterTruncation(for: cellViewModel) - self.bodayTappableLabelHeightConstraint = bodyTappableLabel.set( - .height, - to: (shouldExpanded ? height : min(height, maxHeight)) - ) + bodyTappableLabel.numberOfLines = shouldExpanded ? 0 : VisibleMessageCell.maxNumberOfLinesAfterTruncation + if (height > maxHeight && !shouldExpanded) { stackView.addArrangedSubview(readMoreButton) readMoreButton.isHidden = false @@ -716,10 +749,8 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { stackView.addArrangedSubview(bodyTappableLabel) readMoreButton.themeTextColor = bodyLabelTextColor let maxHeight: CGFloat = VisibleMessageCell.getMaxHeightAfterTruncation(for: cellViewModel) - self.bodayTappableLabelHeightConstraint = bodyTappableLabel.set( - .height, - to: (shouldExpanded ? height : min(height, maxHeight)) - ) + bodyTappableLabel.numberOfLines = shouldExpanded ? 0 : VisibleMessageCell.maxNumberOfLinesAfterTruncation + if (height > maxHeight && !shouldExpanded) { stackView.addArrangedSubview(readMoreButton) readMoreButton.isHidden = false @@ -766,7 +797,10 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { case .mediaMessage: // Album view - let maxMessageWidth: CGFloat = VisibleMessageCell.getMaxWidth(for: cellViewModel) + let maxMessageWidth: CGFloat = VisibleMessageCell.getMaxWidth( + for: cellViewModel, + cellWidth: tableSize.width + ) let albumView = MediaAlbumView( items: (cellViewModel.attachments? .filter { $0.isVisualMedia }) @@ -776,9 +810,10 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { using: dependencies ) self.albumView = albumView - let size = getSize(for: cellViewModel) + let size = getSize(for: cellViewModel, tableSize: tableSize) albumView.set(.width, to: size.width) albumView.set(.height, to: size.height) + albumView.isAccessibilityElement = true albumView.accessibilityLabel = "contentDescriptionMediaMessage".localized() snContentView.addArrangedSubview(albumView) @@ -912,6 +947,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { bodyTappableLabel = nil bodyTappableLabelHeight = 0 bodayTappableLabelHeightConstraint = nil + viewsToMoveForReply.forEach { $0.transform = .identity } replyButton.alpha = 0 timerView.prepareForReuse() @@ -968,7 +1004,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { } } - @objc func handleLongPress(_ gestureRecognizer: UITapGestureRecognizer) { + override func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { if [ .ended, .cancelled, .failed ].contains(gestureRecognizer.state) { isHandlingLongPress = false return @@ -994,9 +1030,9 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { isHandlingLongPress = true } - @objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { + override func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { guard let cellViewModel: MessageViewModel = self.viewModel else { return } - + let location = gestureRecognizer.location(in: self) if profilePictureView.bounds.contains(profilePictureView.convert(location, from: self)), cellViewModel.shouldShowProfile { @@ -1051,7 +1087,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { } else if snContentView.bounds.contains(snContentView.convert(location, from: self)) { if !readMoreButton.isHidden && readMoreButton.bounds.contains(readMoreButton.convert(location, from: self)) { - bodayTappableLabelHeightConstraint?.constant = self.bodyTappableLabelHeight + bodyTappableLabel?.numberOfLines = 0 bodyTappableLabel?.invalidateIntrinsicContentSize() readMoreButton.isHidden = true self.bodyContainerStackView?.removeArrangedSubview(readMoreButton) @@ -1062,7 +1098,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { } } - @objc private func handleDoubleTap() { + override func handleDoubleTap() { guard let cellViewModel: MessageViewModel = self.viewModel else { return } delegate?.handleItemDoubleTapped(cellViewModel) @@ -1165,75 +1201,77 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { return CGFloat(maxNumberOfLinesAfterTruncation) * UIFont.systemFont(ofSize: getFontSize(for: cellViewModel)).lineHeight } - private func getSize(for cellViewModel: MessageViewModel) -> CGSize { + private func getSize(for cellViewModel: MessageViewModel, tableSize: CGSize) -> CGSize { guard let mediaAttachments: [Attachment] = cellViewModel.attachments?.filter({ $0.isVisualMedia }) else { preconditionFailure() } - let maxMessageWidth = VisibleMessageCell.getMaxWidth(for: cellViewModel) + let maxMessageWidth = VisibleMessageCell.getMaxWidth( + for: cellViewModel, + cellWidth: tableSize.width + ) let defaultSize = MediaAlbumView.layoutSize(forMaxMessageWidth: maxMessageWidth, items: mediaAttachments) guard let firstAttachment: Attachment = mediaAttachments.first, - var width: CGFloat = firstAttachment.width.map({ CGFloat($0) }), - var height: CGFloat = firstAttachment.height.map({ CGFloat($0) }), + let originalWidth: CGFloat = firstAttachment.width.map({ CGFloat($0) }), + let originalHeight: CGFloat = firstAttachment.height.map({ CGFloat($0) }), mediaAttachments.count == 1, - width > 0, - height > 0 + originalWidth > 0, + originalHeight > 0 else { return defaultSize } // Honor the content aspect ratio for single media - let size: CGSize = CGSize(width: width, height: height) - var aspectRatio = (size.width / size.height) + let originalSize: CGSize = CGSize(width: originalWidth, height: originalHeight) + var aspectRatio = (originalSize.width / originalSize.height) + // Clamp the aspect ratio so that very thin/wide content still looks alright let minAspectRatio: CGFloat = 0.35 let maxAspectRatio = 1 / minAspectRatio - let maxSize = CGSize(width: maxMessageWidth, height: maxMessageWidth) aspectRatio = aspectRatio.clamp(minAspectRatio, maxAspectRatio) + // Constraint the image + let constraintWidth = min(maxMessageWidth, originalSize.width) + let constraintHeight = min(maxMessageWidth, originalSize.height) + + var finalWidth: CGFloat + var finalHeight: CGFloat + if aspectRatio > 1 { - width = maxSize.width - height = width / aspectRatio + finalWidth = constraintWidth + finalHeight = finalWidth / aspectRatio } else { - height = maxSize.height - width = height * aspectRatio + finalHeight = constraintHeight + finalWidth = finalHeight * aspectRatio } - // Don't blow up small images unnecessarily - let minSize: CGFloat = 150 - let shortSourceDimension = min(size.width, size.height) - let shortDestinationDimension = min(width, height) - - if shortDestinationDimension > minSize && shortDestinationDimension > shortSourceDimension { - let factor = minSize / shortDestinationDimension - width *= factor; height *= factor - } - - return CGSize(width: width, height: height) + return CGSize(width: finalWidth, height: finalHeight) } - static func getMaxWidth(for cellViewModel: MessageViewModel, includingOppositeGutter: Bool = true) -> CGFloat { - let screen: CGRect = UIScreen.main.bounds - let width: CGFloat = UIDevice.current.isIPad ? screen.width * 0.75 : screen.width + static func getMaxWidth( + for cellViewModel: MessageViewModel, + cellWidth: CGFloat, + includingOppositeGutter: Bool = true + ) -> CGFloat { + let horizontalPadding: CGFloat = (contactThreadHSpacing * 2) + let isGroupThread: Bool = ( + cellViewModel.threadVariant == .community || + cellViewModel.threadVariant == .legacyGroup || + cellViewModel.threadVariant == .group + ) + let profileSpace: CGFloat = { + guard + cellViewModel.variant.isIncoming, + isGroupThread, + cellViewModel.canHaveProfile + else { return 0 } + + return ProfilePictureView.Size.message.viewSize + groupThreadHSpacing + }() let oppositeEdgePadding: CGFloat = (includingOppositeGutter ? gutterSize : contactThreadHSpacing) - switch cellViewModel.variant { - case .standardOutgoing, .standardOutgoingDeleted, .standardOutgoingDeletedLocally: - return (width - contactThreadHSpacing - oppositeEdgePadding) - - case .standardIncoming, .standardIncomingDeleted, .standardIncomingDeletedLocally: - let isGroupThread = ( - cellViewModel.threadVariant == .community || - cellViewModel.threadVariant == .legacyGroup || - cellViewModel.threadVariant == .group - ) - let leftEdgeGutterSize = (isGroupThread ? leftGutterSize : contactThreadHSpacing) - - return (width - leftEdgeGutterSize - oppositeEdgePadding) - - default: preconditionFailure() - } + return (cellWidth - horizontalPadding - profileSpace - oppositeEdgePadding) } // stringlint:ignore_contents @@ -1372,7 +1410,8 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { using dependencies: Dependencies ) -> (label: TappableLabel, height: CGFloat) { let result: TappableLabel = TappableLabel() - result.setContentCompressionResistancePriority(.required, for: .vertical) + result.setContentHugging(.vertical, to: .required) + result.setCompressionResistance(.vertical, to: .required) result.themeAttributedText = VisibleMessageCell.getBodyAttributedText( for: cellViewModel, textColor: textColor, diff --git a/Session/Conversations/Settings/ThreadDisappearingMessagesSettingsViewModel.swift b/Session/Conversations/Settings/ThreadDisappearingMessagesSettingsViewModel.swift index b7df60ec3c..ff45656e18 100644 --- a/Session/Conversations/Settings/ThreadDisappearingMessagesSettingsViewModel.swift +++ b/Session/Conversations/Settings/ThreadDisappearingMessagesSettingsViewModel.swift @@ -7,7 +7,7 @@ import DifferenceKit import SessionUIKit import SessionMessagingKit import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTableSource { typealias TableItem = String @@ -359,7 +359,7 @@ class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel, Naviga let currentOffsetTimestampMs: Int64 = dependencies[cache: .snodeAPI].currentOffsetTimestampMs() let interactionId = try updatedConfig - .saved(db) + .upserted(db) .insertControlMessage( db, threadVariant: threadVariant, diff --git a/Session/Conversations/Settings/ThreadNotificationSettingsViewModel.swift b/Session/Conversations/Settings/ThreadNotificationSettingsViewModel.swift index e89a98303b..f5b51cf455 100644 --- a/Session/Conversations/Settings/ThreadNotificationSettingsViewModel.swift +++ b/Session/Conversations/Settings/ThreadNotificationSettingsViewModel.swift @@ -7,7 +7,7 @@ import DifferenceKit import SessionUIKit import SessionMessagingKit import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit class ThreadNotificationSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTableSource { public let dependencies: Dependencies diff --git a/Session/Conversations/Settings/ThreadSettingsViewModel.swift b/Session/Conversations/Settings/ThreadSettingsViewModel.swift index c2462e2193..742ff3c964 100644 --- a/Session/Conversations/Settings/ThreadSettingsViewModel.swift +++ b/Session/Conversations/Settings/ThreadSettingsViewModel.swift @@ -9,7 +9,7 @@ import SessionUIKit import SessionMessagingKit import SignalUtilitiesKit import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit class ThreadSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTableSource { public let dependencies: Dependencies @@ -1280,7 +1280,7 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, Ob public static func createMemberListViewController( threadId: String, - transitionToConversation: @escaping (String) -> Void, + transitionToConversation: @escaping @MainActor (String) -> Void, using dependencies: Dependencies ) -> UIViewController { return SessionTableViewController( @@ -1315,7 +1315,9 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, Ob ) }, completion: { _ in - transitionToConversation(memberInfo.profileId) + Task { @MainActor in + transitionToConversation(memberInfo.profileId) + } } ) }, @@ -1676,11 +1678,13 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, Ob }, icon: .rightPlus, style: .circular, + description: nil, accessibility: Accessibility( identifier: "Image picker", label: "Image picker" ), dataManager: dependencies[singleton: .imageDataManager], + onProBageTapped: nil, onClick: { [weak self] onDisplayPictureSelected in self?.onDisplayPictureSelected = onDisplayPictureSelected self?.showPhotoLibraryForAvatar() @@ -1689,7 +1693,7 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, Ob confirmTitle: "save".localized(), confirmEnabled: .afterChange { info in switch info.body { - case .image(let source, _, _, _, _, _, _): return (source?.imageData != nil) + case .image(let source, _, _, _, _, _, _, _, _): return (source?.imageData != nil) default: return false } }, @@ -1699,7 +1703,7 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, Ob dismissOnConfirm: false, onConfirm: { [weak self] modal in switch modal.info.body { - case .image(.some(let source), _, _, _, _, _, _): + case .image(.some(let source), _, _, _, _, _, _, _, _): guard let imageData: Data = source.imageData else { return } self?.updateGroupDisplayPicture( @@ -1764,9 +1768,9 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, Ob case .groupUploadImageData(let data): /// Show a blocking loading indicator while uploading but not while updating or syncing the group configs return dependencies[singleton: .displayPictureManager] - .prepareAndUploadDisplayPicture(imageData: data) + .prepareAndUploadDisplayPicture(imageData: data, compression: true) .showingBlockingLoading(in: self?.navigatableState) - .map { url, filePath, key -> DisplayPictureManager.Update in + .map { url, filePath, key, _ -> DisplayPictureManager.Update in .groupUpdateTo(url: url, key: key, filePath: filePath) } .mapError { $0 as Error } diff --git a/Session/Emoji/Emoji+Category.swift b/Session/Emoji/Emoji+Category.swift index 343d9a0e87..996f7d83a4 100644 --- a/Session/Emoji/Emoji+Category.swift +++ b/Session/Emoji/Emoji+Category.swift @@ -20,21 +20,29 @@ extension Emoji { var localizedName: String { switch self { case .smileysAndPeople: - return NSLocalizedString("emojiCategorySmileys", comment: "The name for the emoji category 'Smileys & People'") + // The name for the emoji category 'Smileys & People' + return "emojiCategorySmileys".localized() case .animals: - return NSLocalizedString("emojiCategoryAnimals", comment: "The name for the emoji category 'Animals & Nature'") + // The name for the emoji category 'Animals & Nature' + return "emojiCategoryAnimals".localized() case .food: - return NSLocalizedString("emojiCategoryFood", comment: "The name for the emoji category 'Food & Drink'") + // The name for the emoji category 'Food & Drink' + return "emojiCategoryFood".localized() case .activities: - return NSLocalizedString("emojiCategoryActivities", comment: "The name for the emoji category 'Activities'") + // The name for the emoji category 'Activities' + return "emojiCategoryActivities".localized() case .travel: - return NSLocalizedString("emojiCategoryTravel", comment: "The name for the emoji category 'Travel & Places'") + // The name for the emoji category 'Travel & Places' + return "emojiCategoryTravel".localized() case .objects: - return NSLocalizedString("emojiCategoryObjects", comment: "The name for the emoji category 'Objects'") + // The name for the emoji category 'Objects' + return "emojiCategoryObjects".localized() case .symbols: - return NSLocalizedString("emojiCategorySymbols", comment: "The name for the emoji category 'Symbols'") + // The name for the emoji category 'Symbols' + return "emojiCategorySymbols".localized() case .flags: - return NSLocalizedString("emojiCategoryFlags", comment: "The name for the emoji category 'Flags'") + // The name for the emoji category 'Flags' + return "emojiCategoryFlags".localized() } } @@ -92,6 +100,8 @@ extension Emoji { .faceExhaling, .lyingFace, .shakingFace, + .headShakingHorizontally, + .headShakingVertically, .relieved, .pensive, .sleepy, @@ -450,24 +460,42 @@ extension Emoji { .walking, .manWalking, .womanWalking, + .personWalkingFacingRight, + .womanWalkingFacingRight, + .manWalkingFacingRight, .standingPerson, .manStanding, .womanStanding, .kneelingPerson, .manKneeling, .womanKneeling, + .personKneelingFacingRight, + .womanKneelingFacingRight, + .manKneelingFacingRight, .personWithProbingCane, + .personWithWhiteCaneFacingRight, .manWithProbingCane, + .manWithWhiteCaneFacingRight, .womanWithProbingCane, + .womanWithWhiteCaneFacingRight, .personInMotorizedWheelchair, + .personInMotorizedWheelchairFacingRight, .manInMotorizedWheelchair, + .manInMotorizedWheelchairFacingRight, .womanInMotorizedWheelchair, + .womanInMotorizedWheelchairFacingRight, .personInManualWheelchair, + .personInManualWheelchairFacingRight, .manInManualWheelchair, + .manInManualWheelchairFacingRight, .womanInManualWheelchair, + .womanInManualWheelchairFacingRight, .runner, .manRunning, .womanRunning, + .personRunningFacingRight, + .womanRunningFacingRight, + .manRunningFacingRight, .dancer, .manDancing, .manInBusinessSuitLevitating, @@ -540,7 +568,6 @@ extension Emoji { .womanHeartMan, .manHeartMan, .womanHeartWoman, - .family, .manWomanBoy, .manWomanGirl, .manWomanGirlBoy, @@ -570,6 +597,11 @@ extension Emoji { .bustInSilhouette, .bustsInSilhouette, .peopleHugging, + .family, + .familyAdultAdultChild, + .familyAdultAdultChildChild, + .familyAdultChild, + .familyAdultChildChild, .footprints, ] case .animals: @@ -661,6 +693,7 @@ extension Emoji { .wing, .blackBird, .goose, + .phoenix, .frog, .crocodile, .turtle, @@ -734,6 +767,7 @@ extension Emoji { .watermelon, .tangerine, .lemon, + .lime, .banana, .pineapple, .mango, @@ -765,6 +799,7 @@ extension Emoji { .chestnut, .gingerRoot, .peaPod, + .brownMushroom, .bread, .croissant, .baguetteBread, @@ -1382,6 +1417,7 @@ extension Emoji { .scales, .probingCane, .link, + .brokenChain, .chains, .hook, .toolbox, @@ -1989,6 +2025,8 @@ extension Emoji { case .faceExhaling: return .smileysAndPeople case .lyingFace: return .smileysAndPeople case .shakingFace: return .smileysAndPeople + case .headShakingHorizontally: return .smileysAndPeople + case .headShakingVertically: return .smileysAndPeople case .relieved: return .smileysAndPeople case .pensive: return .smileysAndPeople case .sleepy: return .smileysAndPeople @@ -2347,24 +2385,42 @@ extension Emoji { case .walking: return .smileysAndPeople case .manWalking: return .smileysAndPeople case .womanWalking: return .smileysAndPeople + case .personWalkingFacingRight: return .smileysAndPeople + case .womanWalkingFacingRight: return .smileysAndPeople + case .manWalkingFacingRight: return .smileysAndPeople case .standingPerson: return .smileysAndPeople case .manStanding: return .smileysAndPeople case .womanStanding: return .smileysAndPeople case .kneelingPerson: return .smileysAndPeople case .manKneeling: return .smileysAndPeople case .womanKneeling: return .smileysAndPeople + case .personKneelingFacingRight: return .smileysAndPeople + case .womanKneelingFacingRight: return .smileysAndPeople + case .manKneelingFacingRight: return .smileysAndPeople case .personWithProbingCane: return .smileysAndPeople + case .personWithWhiteCaneFacingRight: return .smileysAndPeople case .manWithProbingCane: return .smileysAndPeople + case .manWithWhiteCaneFacingRight: return .smileysAndPeople case .womanWithProbingCane: return .smileysAndPeople + case .womanWithWhiteCaneFacingRight: return .smileysAndPeople case .personInMotorizedWheelchair: return .smileysAndPeople + case .personInMotorizedWheelchairFacingRight: return .smileysAndPeople case .manInMotorizedWheelchair: return .smileysAndPeople + case .manInMotorizedWheelchairFacingRight: return .smileysAndPeople case .womanInMotorizedWheelchair: return .smileysAndPeople + case .womanInMotorizedWheelchairFacingRight: return .smileysAndPeople case .personInManualWheelchair: return .smileysAndPeople + case .personInManualWheelchairFacingRight: return .smileysAndPeople case .manInManualWheelchair: return .smileysAndPeople + case .manInManualWheelchairFacingRight: return .smileysAndPeople case .womanInManualWheelchair: return .smileysAndPeople + case .womanInManualWheelchairFacingRight: return .smileysAndPeople case .runner: return .smileysAndPeople case .manRunning: return .smileysAndPeople case .womanRunning: return .smileysAndPeople + case .personRunningFacingRight: return .smileysAndPeople + case .womanRunningFacingRight: return .smileysAndPeople + case .manRunningFacingRight: return .smileysAndPeople case .dancer: return .smileysAndPeople case .manDancing: return .smileysAndPeople case .manInBusinessSuitLevitating: return .smileysAndPeople @@ -2437,7 +2493,6 @@ extension Emoji { case .womanHeartMan: return .smileysAndPeople case .manHeartMan: return .smileysAndPeople case .womanHeartWoman: return .smileysAndPeople - case .family: return .smileysAndPeople case .manWomanBoy: return .smileysAndPeople case .manWomanGirl: return .smileysAndPeople case .manWomanGirlBoy: return .smileysAndPeople @@ -2467,6 +2522,11 @@ extension Emoji { case .bustInSilhouette: return .smileysAndPeople case .bustsInSilhouette: return .smileysAndPeople case .peopleHugging: return .smileysAndPeople + case .family: return .smileysAndPeople + case .familyAdultAdultChild: return .smileysAndPeople + case .familyAdultAdultChildChild: return .smileysAndPeople + case .familyAdultChild: return .smileysAndPeople + case .familyAdultChildChild: return .smileysAndPeople case .footprints: return .smileysAndPeople case .monkeyFace: return .animals case .monkey: return .animals @@ -2555,6 +2615,7 @@ extension Emoji { case .wing: return .animals case .blackBird: return .animals case .goose: return .animals + case .phoenix: return .animals case .frog: return .animals case .crocodile: return .animals case .turtle: return .animals @@ -2625,6 +2686,7 @@ extension Emoji { case .watermelon: return .food case .tangerine: return .food case .lemon: return .food + case .lime: return .food case .banana: return .food case .pineapple: return .food case .mango: return .food @@ -2656,6 +2718,7 @@ extension Emoji { case .chestnut: return .food case .gingerRoot: return .food case .peaPod: return .food + case .brownMushroom: return .food case .bread: return .food case .croissant: return .food case .baguetteBread: return .food @@ -3264,6 +3327,7 @@ extension Emoji { case .scales: return .objects case .probingCane: return .objects case .link: return .objects + case .brokenChain: return .objects case .chains: return .objects case .hook: return .objects case .toolbox: return .objects @@ -3821,4 +3885,3 @@ extension Emoji { } } } -// swiftlint:disable all diff --git a/Session/Emoji/Emoji+Name.swift b/Session/Emoji/Emoji+Name.swift index 2ddb050adb..2b6abe71f9 100644 --- a/Session/Emoji/Emoji+Name.swift +++ b/Session/Emoji/Emoji+Name.swift @@ -1,9 +1,11 @@ // This file is generated by EmojiGenerator.swift, do not manually edit it. - +// // swiftlint:disable all // stringlint:disable +import Foundation + extension Emoji { var name: String { switch self { @@ -57,6 +59,8 @@ extension Emoji { case .faceExhaling: return "face exhaling, face_exhaling, faceexhaling" case .lyingFace: return "lying face, lying_face, lyingface" case .shakingFace: return "shaking face, shaking_face, shakingface" + case .headShakingHorizontally: return "head shaking horizontally, head_shaking_horizontally, headshakinghorizontally" + case .headShakingVertically: return "head shaking vertically, head_shaking_vertically, headshakingvertically" case .relieved: return "relieved, relieved face" case .pensive: return "pensive, pensive face" case .sleepy: return "sleepy, sleepy face" @@ -415,24 +419,42 @@ extension Emoji { case .walking: return "pedestrian, walking" case .manWalking: return "man walking, man-walking, manwalking" case .womanWalking: return "woman walking, woman-walking, womanwalking" + case .personWalkingFacingRight: return "person walking facing right, person_walking_facing_right, personwalkingfacingright" + case .womanWalkingFacingRight: return "woman walking facing right, woman_walking_facing_right, womanwalkingfacingright" + case .manWalkingFacingRight: return "man walking facing right, man_walking_facing_right, manwalkingfacingright" case .standingPerson: return "standing person, standing_person, standingperson" case .manStanding: return "man standing, man_standing, manstanding" case .womanStanding: return "woman standing, woman_standing, womanstanding" case .kneelingPerson: return "kneeling person, kneeling_person, kneelingperson" case .manKneeling: return "man kneeling, man_kneeling, mankneeling" case .womanKneeling: return "woman kneeling, woman_kneeling, womankneeling" + case .personKneelingFacingRight: return "person kneeling facing right, person_kneeling_facing_right, personkneelingfacingright" + case .womanKneelingFacingRight: return "woman kneeling facing right, woman_kneeling_facing_right, womankneelingfacingright" + case .manKneelingFacingRight: return "man kneeling facing right, man_kneeling_facing_right, mankneelingfacingright" case .personWithProbingCane: return "person with white cane, person_with_probing_cane, personwithprobingcane" + case .personWithWhiteCaneFacingRight: return "person with white cane facing right, person_with_white_cane_facing_right, personwithwhitecanefacingright" case .manWithProbingCane: return "man with white cane, man_with_probing_cane, manwithprobingcane" + case .manWithWhiteCaneFacingRight: return "man with white cane facing right, man_with_white_cane_facing_right, manwithwhitecanefacingright" case .womanWithProbingCane: return "woman with white cane, woman_with_probing_cane, womanwithprobingcane" + case .womanWithWhiteCaneFacingRight: return "woman with white cane facing right, woman_with_white_cane_facing_right, womanwithwhitecanefacingright" case .personInMotorizedWheelchair: return "person in motorized wheelchair, person_in_motorized_wheelchair, personinmotorizedwheelchair" + case .personInMotorizedWheelchairFacingRight: return "person in motorized wheelchair facing right, person_in_motorized_wheelchair_facing_right, personinmotorizedwheelchairfacingright" case .manInMotorizedWheelchair: return "man in motorized wheelchair, man_in_motorized_wheelchair, maninmotorizedwheelchair" + case .manInMotorizedWheelchairFacingRight: return "man in motorized wheelchair facing right, man_in_motorized_wheelchair_facing_right, maninmotorizedwheelchairfacingright" case .womanInMotorizedWheelchair: return "woman in motorized wheelchair, woman_in_motorized_wheelchair, womaninmotorizedwheelchair" + case .womanInMotorizedWheelchairFacingRight: return "woman in motorized wheelchair facing right, woman_in_motorized_wheelchair_facing_right, womaninmotorizedwheelchairfacingright" case .personInManualWheelchair: return "person in manual wheelchair, person_in_manual_wheelchair, personinmanualwheelchair" + case .personInManualWheelchairFacingRight: return "person in manual wheelchair facing right, person_in_manual_wheelchair_facing_right, personinmanualwheelchairfacingright" case .manInManualWheelchair: return "man in manual wheelchair, man_in_manual_wheelchair, maninmanualwheelchair" + case .manInManualWheelchairFacingRight: return "man in manual wheelchair facing right, man_in_manual_wheelchair_facing_right, maninmanualwheelchairfacingright" case .womanInManualWheelchair: return "woman in manual wheelchair, woman_in_manual_wheelchair, womaninmanualwheelchair" + case .womanInManualWheelchairFacingRight: return "woman in manual wheelchair facing right, woman_in_manual_wheelchair_facing_right, womaninmanualwheelchairfacingright" case .runner: return "runner, running" case .manRunning: return "man running, man-running, manrunning" case .womanRunning: return "woman running, woman-running, womanrunning" + case .personRunningFacingRight: return "person running facing right, person_running_facing_right, personrunningfacingright" + case .womanRunningFacingRight: return "woman running facing right, woman_running_facing_right, womanrunningfacingright" + case .manRunningFacingRight: return "man running facing right, man_running_facing_right, manrunningfacingright" case .dancer: return "dancer" case .manDancing: return "man dancing, man_dancing, mandancing" case .manInBusinessSuitLevitating: return "man_in_business_suit_levitating, maninbusinesssuitlevitating, person in suit levitating" @@ -505,7 +527,6 @@ extension Emoji { case .womanHeartMan: return "couple with heart: woman, man, woman-heart-man, womanheartman" case .manHeartMan: return "couple with heart: man, man, man-heart-man, manheartman" case .womanHeartWoman: return "couple with heart: woman, woman, woman-heart-woman, womanheartwoman" - case .family: return "family" case .manWomanBoy: return "family: man, woman, boy, man-woman-boy, manwomanboy" case .manWomanGirl: return "family: man, woman, girl, man-woman-girl, manwomangirl" case .manWomanGirlBoy: return "family: man, woman, girl, boy, man-woman-girl-boy, manwomangirlboy" @@ -535,6 +556,11 @@ extension Emoji { case .bustInSilhouette: return "bust in silhouette, bust_in_silhouette, bustinsilhouette" case .bustsInSilhouette: return "busts in silhouette, busts_in_silhouette, bustsinsilhouette" case .peopleHugging: return "people hugging, people_hugging, peoplehugging" + case .family: return "family" + case .familyAdultAdultChild: return "family: adult, adult, child, family_adult_adult_child, familyadultadultchild" + case .familyAdultAdultChildChild: return "family: adult, adult, child, child, family_adult_adult_child_child, familyadultadultchildchild" + case .familyAdultChild: return "family: adult, child, family_adult_child, familyadultchild" + case .familyAdultChildChild: return "family: adult, child, child, family_adult_child_child, familyadultchildchild" case .footprints: return "footprints" case .skinTone2: return "emoji modifier fitzpatrick type-1-2, skin-tone-2, skintone2" case .skinTone3: return "emoji modifier fitzpatrick type-3, skin-tone-3, skintone3" @@ -628,6 +654,7 @@ extension Emoji { case .wing: return "wing" case .blackBird: return "black bird, black_bird, blackbird" case .goose: return "goose" + case .phoenix: return "phoenix" case .frog: return "frog, frog face" case .crocodile: return "crocodile" case .turtle: return "turtle" @@ -698,6 +725,7 @@ extension Emoji { case .watermelon: return "watermelon" case .tangerine: return "tangerine" case .lemon: return "lemon" + case .lime: return "lime" case .banana: return "banana" case .pineapple: return "pineapple" case .mango: return "mango" @@ -729,6 +757,7 @@ extension Emoji { case .chestnut: return "chestnut" case .gingerRoot: return "ginger root, ginger_root, gingerroot" case .peaPod: return "pea pod, pea_pod, peapod" + case .brownMushroom: return "brown mushroom, brown_mushroom, brownmushroom" case .bread: return "bread" case .croissant: return "croissant" case .baguetteBread: return "baguette bread, baguette_bread, baguettebread" @@ -1337,6 +1366,7 @@ extension Emoji { case .scales: return "balance scale, scales" case .probingCane: return "probing cane, probing_cane, probingcane" case .link: return "link, link symbol" + case .brokenChain: return "broken chain, broken_chain, brokenchain" case .chains: return "chains" case .hook: return "hook" case .toolbox: return "toolbox" @@ -1852,7 +1882,7 @@ extension Emoji { case .flagTm: return "flag-tm, flagtm, turkmenistan flag" case .flagTn: return "flag-tn, flagtn, tunisia flag" case .flagTo: return "flag-to, flagto, tonga flag" - case .flagTr: return "flag-tr, flagtr, turkey flag" + case .flagTr: return "flag-tr, flagtr, türkiye flag" case .flagTt: return "flag-tt, flagtt, trinidad & tobago flag" case .flagTv: return "flag-tv, flagtv, tuvalu flag" case .flagTw: return "flag-tw, flagtw, taiwan flag" @@ -1885,4 +1915,3 @@ extension Emoji { } } } -// swiftlint:disable all diff --git a/Session/Emoji/Emoji+SkinTones.swift b/Session/Emoji/Emoji+SkinTones.swift index f1fb18434f..9f34de6151 100644 --- a/Session/Emoji/Emoji+SkinTones.swift +++ b/Session/Emoji/Emoji+SkinTones.swift @@ -1,9 +1,11 @@ // This file is generated by EmojiGenerator.swift, do not manually edit it. - +// // swiftlint:disable all // stringlint:disable +import Foundation + extension Emoji { enum SkinTone: String, CaseIterable, Equatable { case light = "🏻" @@ -1841,6 +1843,30 @@ extension Emoji { [.mediumDark]: "🚶🏾‍♀️", [.dark]: "🚶🏿‍♀️", ] + case .personWalkingFacingRight: + return [ + [.light]: "🚶🏻‍➡️", + [.mediumLight]: "🚶🏼‍➡️", + [.medium]: "🚶🏽‍➡️", + [.mediumDark]: "🚶🏾‍➡️", + [.dark]: "🚶🏿‍➡️", + ] + case .womanWalkingFacingRight: + return [ + [.light]: "🚶🏻‍♀️‍➡️", + [.mediumLight]: "🚶🏼‍♀️‍➡️", + [.medium]: "🚶🏽‍♀️‍➡️", + [.mediumDark]: "🚶🏾‍♀️‍➡️", + [.dark]: "🚶🏿‍♀️‍➡️", + ] + case .manWalkingFacingRight: + return [ + [.light]: "🚶🏻‍♂️‍➡️", + [.mediumLight]: "🚶🏼‍♂️‍➡️", + [.medium]: "🚶🏽‍♂️‍➡️", + [.mediumDark]: "🚶🏾‍♂️‍➡️", + [.dark]: "🚶🏿‍♂️‍➡️", + ] case .standingPerson: return [ [.light]: "🧍🏻", @@ -1889,6 +1915,30 @@ extension Emoji { [.mediumDark]: "🧎🏾‍♀️", [.dark]: "🧎🏿‍♀️", ] + case .personKneelingFacingRight: + return [ + [.light]: "🧎🏻‍➡️", + [.mediumLight]: "🧎🏼‍➡️", + [.medium]: "🧎🏽‍➡️", + [.mediumDark]: "🧎🏾‍➡️", + [.dark]: "🧎🏿‍➡️", + ] + case .womanKneelingFacingRight: + return [ + [.light]: "🧎🏻‍♀️‍➡️", + [.mediumLight]: "🧎🏼‍♀️‍➡️", + [.medium]: "🧎🏽‍♀️‍➡️", + [.mediumDark]: "🧎🏾‍♀️‍➡️", + [.dark]: "🧎🏿‍♀️‍➡️", + ] + case .manKneelingFacingRight: + return [ + [.light]: "🧎🏻‍♂️‍➡️", + [.mediumLight]: "🧎🏼‍♂️‍➡️", + [.medium]: "🧎🏽‍♂️‍➡️", + [.mediumDark]: "🧎🏾‍♂️‍➡️", + [.dark]: "🧎🏿‍♂️‍➡️", + ] case .personWithProbingCane: return [ [.light]: "🧑🏻‍🦯", @@ -1897,6 +1947,14 @@ extension Emoji { [.mediumDark]: "🧑🏾‍🦯", [.dark]: "🧑🏿‍🦯", ] + case .personWithWhiteCaneFacingRight: + return [ + [.light]: "🧑🏻‍🦯‍➡️", + [.mediumLight]: "🧑🏼‍🦯‍➡️", + [.medium]: "🧑🏽‍🦯‍➡️", + [.mediumDark]: "🧑🏾‍🦯‍➡️", + [.dark]: "🧑🏿‍🦯‍➡️", + ] case .manWithProbingCane: return [ [.light]: "👨🏻‍🦯", @@ -1905,6 +1963,14 @@ extension Emoji { [.mediumDark]: "👨🏾‍🦯", [.dark]: "👨🏿‍🦯", ] + case .manWithWhiteCaneFacingRight: + return [ + [.light]: "👨🏻‍🦯‍➡️", + [.mediumLight]: "👨🏼‍🦯‍➡️", + [.medium]: "👨🏽‍🦯‍➡️", + [.mediumDark]: "👨🏾‍🦯‍➡️", + [.dark]: "👨🏿‍🦯‍➡️", + ] case .womanWithProbingCane: return [ [.light]: "👩🏻‍🦯", @@ -1913,6 +1979,14 @@ extension Emoji { [.mediumDark]: "👩🏾‍🦯", [.dark]: "👩🏿‍🦯", ] + case .womanWithWhiteCaneFacingRight: + return [ + [.light]: "👩🏻‍🦯‍➡️", + [.mediumLight]: "👩🏼‍🦯‍➡️", + [.medium]: "👩🏽‍🦯‍➡️", + [.mediumDark]: "👩🏾‍🦯‍➡️", + [.dark]: "👩🏿‍🦯‍➡️", + ] case .personInMotorizedWheelchair: return [ [.light]: "🧑🏻‍🦼", @@ -1921,6 +1995,14 @@ extension Emoji { [.mediumDark]: "🧑🏾‍🦼", [.dark]: "🧑🏿‍🦼", ] + case .personInMotorizedWheelchairFacingRight: + return [ + [.light]: "🧑🏻‍🦼‍➡️", + [.mediumLight]: "🧑🏼‍🦼‍➡️", + [.medium]: "🧑🏽‍🦼‍➡️", + [.mediumDark]: "🧑🏾‍🦼‍➡️", + [.dark]: "🧑🏿‍🦼‍➡️", + ] case .manInMotorizedWheelchair: return [ [.light]: "👨🏻‍🦼", @@ -1929,6 +2011,14 @@ extension Emoji { [.mediumDark]: "👨🏾‍🦼", [.dark]: "👨🏿‍🦼", ] + case .manInMotorizedWheelchairFacingRight: + return [ + [.light]: "👨🏻‍🦼‍➡️", + [.mediumLight]: "👨🏼‍🦼‍➡️", + [.medium]: "👨🏽‍🦼‍➡️", + [.mediumDark]: "👨🏾‍🦼‍➡️", + [.dark]: "👨🏿‍🦼‍➡️", + ] case .womanInMotorizedWheelchair: return [ [.light]: "👩🏻‍🦼", @@ -1937,6 +2027,14 @@ extension Emoji { [.mediumDark]: "👩🏾‍🦼", [.dark]: "👩🏿‍🦼", ] + case .womanInMotorizedWheelchairFacingRight: + return [ + [.light]: "👩🏻‍🦼‍➡️", + [.mediumLight]: "👩🏼‍🦼‍➡️", + [.medium]: "👩🏽‍🦼‍➡️", + [.mediumDark]: "👩🏾‍🦼‍➡️", + [.dark]: "👩🏿‍🦼‍➡️", + ] case .personInManualWheelchair: return [ [.light]: "🧑🏻‍🦽", @@ -1945,6 +2043,14 @@ extension Emoji { [.mediumDark]: "🧑🏾‍🦽", [.dark]: "🧑🏿‍🦽", ] + case .personInManualWheelchairFacingRight: + return [ + [.light]: "🧑🏻‍🦽‍➡️", + [.mediumLight]: "🧑🏼‍🦽‍➡️", + [.medium]: "🧑🏽‍🦽‍➡️", + [.mediumDark]: "🧑🏾‍🦽‍➡️", + [.dark]: "🧑🏿‍🦽‍➡️", + ] case .manInManualWheelchair: return [ [.light]: "👨🏻‍🦽", @@ -1953,6 +2059,14 @@ extension Emoji { [.mediumDark]: "👨🏾‍🦽", [.dark]: "👨🏿‍🦽", ] + case .manInManualWheelchairFacingRight: + return [ + [.light]: "👨🏻‍🦽‍➡️", + [.mediumLight]: "👨🏼‍🦽‍➡️", + [.medium]: "👨🏽‍🦽‍➡️", + [.mediumDark]: "👨🏾‍🦽‍➡️", + [.dark]: "👨🏿‍🦽‍➡️", + ] case .womanInManualWheelchair: return [ [.light]: "👩🏻‍🦽", @@ -1961,6 +2075,14 @@ extension Emoji { [.mediumDark]: "👩🏾‍🦽", [.dark]: "👩🏿‍🦽", ] + case .womanInManualWheelchairFacingRight: + return [ + [.light]: "👩🏻‍🦽‍➡️", + [.mediumLight]: "👩🏼‍🦽‍➡️", + [.medium]: "👩🏽‍🦽‍➡️", + [.mediumDark]: "👩🏾‍🦽‍➡️", + [.dark]: "👩🏿‍🦽‍➡️", + ] case .runner: return [ [.light]: "🏃🏻", @@ -1985,6 +2107,30 @@ extension Emoji { [.mediumDark]: "🏃🏾‍♀️", [.dark]: "🏃🏿‍♀️", ] + case .personRunningFacingRight: + return [ + [.light]: "🏃🏻‍➡️", + [.mediumLight]: "🏃🏼‍➡️", + [.medium]: "🏃🏽‍➡️", + [.mediumDark]: "🏃🏾‍➡️", + [.dark]: "🏃🏿‍➡️", + ] + case .womanRunningFacingRight: + return [ + [.light]: "🏃🏻‍♀️‍➡️", + [.mediumLight]: "🏃🏼‍♀️‍➡️", + [.medium]: "🏃🏽‍♀️‍➡️", + [.mediumDark]: "🏃🏾‍♀️‍➡️", + [.dark]: "🏃🏿‍♀️‍➡️", + ] + case .manRunningFacingRight: + return [ + [.light]: "🏃🏻‍♂️‍➡️", + [.mediumLight]: "🏃🏼‍♂️‍➡️", + [.medium]: "🏃🏽‍♂️‍➡️", + [.mediumDark]: "🏃🏾‍♂️‍➡️", + [.dark]: "🏃🏿‍♂️‍➡️", + ] case .dancer: return [ [.light]: "💃🏻", @@ -2741,4 +2887,3 @@ extension Emoji { } } } -// swiftlint:disable all diff --git a/Session/Emoji/Emoji.swift b/Session/Emoji/Emoji.swift index aa8352ca24..a315b53273 100644 --- a/Session/Emoji/Emoji.swift +++ b/Session/Emoji/Emoji.swift @@ -1,9 +1,11 @@ // This file is generated by EmojiGenerator.swift, do not manually edit it. - +// // swiftlint:disable all // stringlint:disable +import Foundation + /// A sorted representation of all available emoji enum Emoji: String, CaseIterable, Equatable { case grinning = "😀" @@ -56,6 +58,8 @@ enum Emoji: String, CaseIterable, Equatable { case faceExhaling = "😮‍💨" case lyingFace = "🤥" case shakingFace = "🫨" + case headShakingHorizontally = "🙂‍↔️" + case headShakingVertically = "🙂‍↕️" case relieved = "😌" case pensive = "😔" case sleepy = "😪" @@ -414,24 +418,42 @@ enum Emoji: String, CaseIterable, Equatable { case walking = "🚶" case manWalking = "🚶‍♂️" case womanWalking = "🚶‍♀️" + case personWalkingFacingRight = "🚶‍➡️" + case womanWalkingFacingRight = "🚶‍♀️‍➡️" + case manWalkingFacingRight = "🚶‍♂️‍➡️" case standingPerson = "🧍" case manStanding = "🧍‍♂️" case womanStanding = "🧍‍♀️" case kneelingPerson = "🧎" case manKneeling = "🧎‍♂️" case womanKneeling = "🧎‍♀️" + case personKneelingFacingRight = "🧎‍➡️" + case womanKneelingFacingRight = "🧎‍♀️‍➡️" + case manKneelingFacingRight = "🧎‍♂️‍➡️" case personWithProbingCane = "🧑‍🦯" + case personWithWhiteCaneFacingRight = "🧑‍🦯‍➡️" case manWithProbingCane = "👨‍🦯" + case manWithWhiteCaneFacingRight = "👨‍🦯‍➡️" case womanWithProbingCane = "👩‍🦯" + case womanWithWhiteCaneFacingRight = "👩‍🦯‍➡️" case personInMotorizedWheelchair = "🧑‍🦼" + case personInMotorizedWheelchairFacingRight = "🧑‍🦼‍➡️" case manInMotorizedWheelchair = "👨‍🦼" + case manInMotorizedWheelchairFacingRight = "👨‍🦼‍➡️" case womanInMotorizedWheelchair = "👩‍🦼" + case womanInMotorizedWheelchairFacingRight = "👩‍🦼‍➡️" case personInManualWheelchair = "🧑‍🦽" + case personInManualWheelchairFacingRight = "🧑‍🦽‍➡️" case manInManualWheelchair = "👨‍🦽" + case manInManualWheelchairFacingRight = "👨‍🦽‍➡️" case womanInManualWheelchair = "👩‍🦽" + case womanInManualWheelchairFacingRight = "👩‍🦽‍➡️" case runner = "🏃" case manRunning = "🏃‍♂️" case womanRunning = "🏃‍♀️" + case personRunningFacingRight = "🏃‍➡️" + case womanRunningFacingRight = "🏃‍♀️‍➡️" + case manRunningFacingRight = "🏃‍♂️‍➡️" case dancer = "💃" case manDancing = "🕺" case manInBusinessSuitLevitating = "🕴️" @@ -504,7 +526,6 @@ enum Emoji: String, CaseIterable, Equatable { case womanHeartMan = "👩‍❤️‍👨" case manHeartMan = "👨‍❤️‍👨" case womanHeartWoman = "👩‍❤️‍👩" - case family = "👪" case manWomanBoy = "👨‍👩‍👦" case manWomanGirl = "👨‍👩‍👧" case manWomanGirlBoy = "👨‍👩‍👧‍👦" @@ -534,6 +555,11 @@ enum Emoji: String, CaseIterable, Equatable { case bustInSilhouette = "👤" case bustsInSilhouette = "👥" case peopleHugging = "🫂" + case family = "👪" + case familyAdultAdultChild = "🧑‍🧑‍🧒" + case familyAdultAdultChildChild = "🧑‍🧑‍🧒‍🧒" + case familyAdultChild = "🧑‍🧒" + case familyAdultChildChild = "🧑‍🧒‍🧒" case footprints = "👣" case skinTone2 = "🏻" case skinTone3 = "🏼" @@ -627,6 +653,7 @@ enum Emoji: String, CaseIterable, Equatable { case wing = "🪽" case blackBird = "🐦‍⬛" case goose = "🪿" + case phoenix = "🐦‍🔥" case frog = "🐸" case crocodile = "🐊" case turtle = "🐢" @@ -697,6 +724,7 @@ enum Emoji: String, CaseIterable, Equatable { case watermelon = "🍉" case tangerine = "🍊" case lemon = "🍋" + case lime = "🍋‍🟩" case banana = "🍌" case pineapple = "🍍" case mango = "🥭" @@ -728,6 +756,7 @@ enum Emoji: String, CaseIterable, Equatable { case chestnut = "🌰" case gingerRoot = "🫚" case peaPod = "🫛" + case brownMushroom = "🍄‍🟫" case bread = "🍞" case croissant = "🥐" case baguetteBread = "🥖" @@ -1336,6 +1365,7 @@ enum Emoji: String, CaseIterable, Equatable { case scales = "⚖️" case probingCane = "🦯" case link = "🔗" + case brokenChain = "⛓️‍💥" case chains = "⛓️" case hook = "🪝" case toolbox = "🧰" @@ -1882,4 +1912,3 @@ enum Emoji: String, CaseIterable, Equatable { case flagScotland = "🏴󠁧󠁢󠁳󠁣󠁴󠁿" case flagWales = "🏴󠁧󠁢󠁷󠁬󠁳󠁿" } -// swiftlint:disable all diff --git a/Session/Emoji/EmojiWithSkinTones+String.swift b/Session/Emoji/EmojiWithSkinTones+String.swift index e1b1c84daa..3572ff1249 100644 --- a/Session/Emoji/EmojiWithSkinTones+String.swift +++ b/Session/Emoji/EmojiWithSkinTones+String.swift @@ -1,9 +1,11 @@ // This file is generated by EmojiGenerator.swift, do not manually edit it. - +// // swiftlint:disable all // stringlint:disable +import Foundation + extension EmojiWithSkinTones { init?(rawValue: String) { guard rawValue.isSingleEmoji else { return nil } @@ -75,16 +77,19 @@ extension EmojiWithSkinTones { case 1934: self = EmojiWithSkinTones.emojiFrom1934(rawValue) case 1935: self = EmojiWithSkinTones.emojiFrom1935(rawValue) case 1937: self = EmojiWithSkinTones.emojiFrom1937(rawValue) + case 2104: self = EmojiWithSkinTones.emojiFrom2104(rawValue) case 2109: self = EmojiWithSkinTones.emojiFrom2109(rawValue) case 2111: self = EmojiWithSkinTones.emojiFrom2111(rawValue) case 2112: self = EmojiWithSkinTones.emojiFrom2112(rawValue) case 2113: self = EmojiWithSkinTones.emojiFrom2113(rawValue) case 2116: self = EmojiWithSkinTones.emojiFrom2116(rawValue) case 2117: self = EmojiWithSkinTones.emojiFrom2117(rawValue) + case 2120: self = EmojiWithSkinTones.emojiFrom2120(rawValue) case 2123: self = EmojiWithSkinTones.emojiFrom2123(rawValue) case 2125: self = EmojiWithSkinTones.emojiFrom2125(rawValue) case 2126: self = EmojiWithSkinTones.emojiFrom2126(rawValue) case 2127: self = EmojiWithSkinTones.emojiFrom2127(rawValue) + case 2128: self = EmojiWithSkinTones.emojiFrom2128(rawValue) case 2129: self = EmojiWithSkinTones.emojiFrom2129(rawValue) case 2210: self = EmojiWithSkinTones.emojiFrom2210(rawValue) case 2549: self = EmojiWithSkinTones.emojiFrom2549(rawValue) @@ -105,8 +110,10 @@ extension EmojiWithSkinTones { case 2641: self = EmojiWithSkinTones.emojiFrom2641(rawValue) case 2642: self = EmojiWithSkinTones.emojiFrom2642(rawValue) case 2644: self = EmojiWithSkinTones.emojiFrom2644(rawValue) + case 2645: self = EmojiWithSkinTones.emojiFrom2645(rawValue) case 2646: self = EmojiWithSkinTones.emojiFrom2646(rawValue) case 2649: self = EmojiWithSkinTones.emojiFrom2649(rawValue) + case 2650: self = EmojiWithSkinTones.emojiFrom2650(rawValue) case 2655: self = EmojiWithSkinTones.emojiFrom2655(rawValue) case 2656: self = EmojiWithSkinTones.emojiFrom2656(rawValue) case 2657: self = EmojiWithSkinTones.emojiFrom2657(rawValue) @@ -117,6 +124,9 @@ extension EmojiWithSkinTones { case 2760: self = EmojiWithSkinTones.emojiFrom2760(rawValue) case 2761: self = EmojiWithSkinTones.emojiFrom2761(rawValue) case 2764: self = EmojiWithSkinTones.emojiFrom2764(rawValue) + case 2943: self = EmojiWithSkinTones.emojiFrom2943(rawValue) + case 2951: self = EmojiWithSkinTones.emojiFrom2951(rawValue) + case 2959: self = EmojiWithSkinTones.emojiFrom2959(rawValue) case 3289: self = EmojiWithSkinTones.emojiFrom3289(rawValue) case 3295: self = EmojiWithSkinTones.emojiFrom3295(rawValue) case 3389: self = EmojiWithSkinTones.emojiFrom3389(rawValue) @@ -126,12 +136,16 @@ extension EmojiWithSkinTones { case 3394: self = EmojiWithSkinTones.emojiFrom3394(rawValue) case 3396: self = EmojiWithSkinTones.emojiFrom3396(rawValue) case 3397: self = EmojiWithSkinTones.emojiFrom3397(rawValue) + case 3400: self = EmojiWithSkinTones.emojiFrom3400(rawValue) case 3403: self = EmojiWithSkinTones.emojiFrom3403(rawValue) case 3404: self = EmojiWithSkinTones.emojiFrom3404(rawValue) case 3405: self = EmojiWithSkinTones.emojiFrom3405(rawValue) case 3406: self = EmojiWithSkinTones.emojiFrom3406(rawValue) case 3407: self = EmojiWithSkinTones.emojiFrom3407(rawValue) + case 3408: self = EmojiWithSkinTones.emojiFrom3408(rawValue) case 3477: self = EmojiWithSkinTones.emojiFrom3477(rawValue) + case 3491: self = EmojiWithSkinTones.emojiFrom3491(rawValue) + case 3505: self = EmojiWithSkinTones.emojiFrom3505(rawValue) case 3921: self = EmojiWithSkinTones.emojiFrom3921(rawValue) case 3922: self = EmojiWithSkinTones.emojiFrom3922(rawValue) case 3924: self = EmojiWithSkinTones.emojiFrom3924(rawValue) @@ -149,9 +163,16 @@ extension EmojiWithSkinTones { case 3951: self = EmojiWithSkinTones.emojiFrom3951(rawValue) case 4007: self = EmojiWithSkinTones.emojiFrom4007(rawValue) case 4046: self = EmojiWithSkinTones.emojiFrom4046(rawValue) + case 4048: self = EmojiWithSkinTones.emojiFrom4048(rawValue) + case 4223: self = EmojiWithSkinTones.emojiFrom4223(rawValue) + case 4231: self = EmojiWithSkinTones.emojiFrom4231(rawValue) + case 4239: self = EmojiWithSkinTones.emojiFrom4239(rawValue) + case 4771: self = EmojiWithSkinTones.emojiFrom4771(rawValue) + case 4785: self = EmojiWithSkinTones.emojiFrom4785(rawValue) case 4840: self = EmojiWithSkinTones.emojiFrom4840(rawValue) case 5237: self = EmojiWithSkinTones.emojiFrom5237(rawValue) case 5370: self = EmojiWithSkinTones.emojiFrom5370(rawValue) + case 5425: self = EmojiWithSkinTones.emojiFrom5425(rawValue) case 6037: self = EmojiWithSkinTones.emojiFrom6037(rawValue) case 6065: self = EmojiWithSkinTones.emojiFrom6065(rawValue) case 6579: self = EmojiWithSkinTones.emojiFrom6579(rawValue) @@ -961,9 +982,9 @@ extension EmojiWithSkinTones { "👬": EmojiWithSkinTones(baseEmoji: .twoMenHoldingHands, skinTones: nil), "💏": EmojiWithSkinTones(baseEmoji: .personKissPerson, skinTones: nil), "💑": EmojiWithSkinTones(baseEmoji: .personHeartPerson, skinTones: nil), - "👪": EmojiWithSkinTones(baseEmoji: .family, skinTones: nil), "👤": EmojiWithSkinTones(baseEmoji: .bustInSilhouette, skinTones: nil), "👥": EmojiWithSkinTones(baseEmoji: .bustsInSilhouette, skinTones: nil), + "👪": EmojiWithSkinTones(baseEmoji: .family, skinTones: nil), "💐": EmojiWithSkinTones(baseEmoji: .bouquet, skinTones: nil), "💮": EmojiWithSkinTones(baseEmoji: .whiteFlower, skinTones: nil), "💒": EmojiWithSkinTones(baseEmoji: .wedding, skinTones: nil), @@ -1993,6 +2014,14 @@ extension EmojiWithSkinTones { return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } + private static func emojiFrom2104(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🙂‍↔️": EmojiWithSkinTones(baseEmoji: .headShakingHorizontally, skinTones: nil), + "🙂‍↕️": EmojiWithSkinTones(baseEmoji: .headShakingVertically, skinTones: nil) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + private static func emojiFrom2109(_ rawValue: String) -> EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "🏃‍♂️": EmojiWithSkinTones(baseEmoji: .manRunning, skinTones: nil), @@ -2046,7 +2075,9 @@ extension EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "👨‍✈️": EmojiWithSkinTones(baseEmoji: .malePilot, skinTones: nil), "👩‍✈️": EmojiWithSkinTones(baseEmoji: .femalePilot, skinTones: nil), - "🐻‍❄️": EmojiWithSkinTones(baseEmoji: .polarBear, skinTones: nil) + "🏃‍➡️": EmojiWithSkinTones(baseEmoji: .personRunningFacingRight, skinTones: nil), + "🐻‍❄️": EmojiWithSkinTones(baseEmoji: .polarBear, skinTones: nil), + "⛓️‍💥": EmojiWithSkinTones(baseEmoji: .brokenChain, skinTones: nil) ] return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } @@ -2084,6 +2115,13 @@ extension EmojiWithSkinTones { return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } + private static func emojiFrom2120(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🚶‍➡️": EmojiWithSkinTones(baseEmoji: .personWalkingFacingRight, skinTones: nil) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + private static func emojiFrom2123(_ rawValue: String) -> EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "🤦‍♂️": EmojiWithSkinTones(baseEmoji: .manFacepalming, skinTones: nil), @@ -2159,6 +2197,13 @@ extension EmojiWithSkinTones { return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } + private static func emojiFrom2128(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🧎‍➡️": EmojiWithSkinTones(baseEmoji: .personKneelingFacingRight, skinTones: nil) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + private static func emojiFrom2129(_ rawValue: String) -> EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "❤️‍🩹": EmojiWithSkinTones(baseEmoji: .mendingHeart, skinTones: nil) @@ -3197,6 +3242,13 @@ extension EmojiWithSkinTones { return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } + private static func emojiFrom2645(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🐦‍🔥": EmojiWithSkinTones(baseEmoji: .phoenix, skinTones: nil) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + private static func emojiFrom2646(_ rawValue: String) -> EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "👨‍🔧": EmojiWithSkinTones(baseEmoji: .maleMechanic, skinTones: nil), @@ -3219,6 +3271,14 @@ extension EmojiWithSkinTones { return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } + private static func emojiFrom2650(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🍋‍🟩": EmojiWithSkinTones(baseEmoji: .lime, skinTones: nil), + "🍄‍🟫": EmojiWithSkinTones(baseEmoji: .brownMushroom, skinTones: nil) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + private static func emojiFrom2655(_ rawValue: String) -> EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "🧑‍🎓": EmojiWithSkinTones(baseEmoji: .student, skinTones: nil), @@ -3293,7 +3353,8 @@ extension EmojiWithSkinTones { "🧑‍🦲": EmojiWithSkinTones(baseEmoji: .baldPerson, skinTones: nil), "🧑‍🦯": EmojiWithSkinTones(baseEmoji: .personWithProbingCane, skinTones: nil), "🧑‍🦼": EmojiWithSkinTones(baseEmoji: .personInMotorizedWheelchair, skinTones: nil), - "🧑‍🦽": EmojiWithSkinTones(baseEmoji: .personInManualWheelchair, skinTones: nil) + "🧑‍🦽": EmojiWithSkinTones(baseEmoji: .personInManualWheelchair, skinTones: nil), + "🧑‍🧒": EmojiWithSkinTones(baseEmoji: .familyAdultChild, skinTones: nil) ] return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } @@ -3323,6 +3384,30 @@ extension EmojiWithSkinTones { return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } + private static func emojiFrom2943(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🏃‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanRunningFacingRight, skinTones: nil), + "🏃‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manRunningFacingRight, skinTones: nil) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + + private static func emojiFrom2951(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🚶‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanWalkingFacingRight, skinTones: nil), + "🚶‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manWalkingFacingRight, skinTones: nil) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + + private static func emojiFrom2959(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🧎‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanKneelingFacingRight, skinTones: nil), + "🧎‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manKneelingFacingRight, skinTones: nil) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + private static func emojiFrom3289(_ rawValue: String) -> EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "🏳️‍🌈": EmojiWithSkinTones(baseEmoji: .rainbowFlag, skinTones: nil) @@ -3519,14 +3604,19 @@ extension EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "👨🏻‍✈️": EmojiWithSkinTones(baseEmoji: .malePilot, skinTones: [.light]), "👩🏻‍✈️": EmojiWithSkinTones(baseEmoji: .femalePilot, skinTones: [.light]), + "🏃🏻‍➡️": EmojiWithSkinTones(baseEmoji: .personRunningFacingRight, skinTones: [.light]), "👨🏼‍✈️": EmojiWithSkinTones(baseEmoji: .malePilot, skinTones: [.mediumLight]), "👩🏼‍✈️": EmojiWithSkinTones(baseEmoji: .femalePilot, skinTones: [.mediumLight]), + "🏃🏼‍➡️": EmojiWithSkinTones(baseEmoji: .personRunningFacingRight, skinTones: [.mediumLight]), "👨🏽‍✈️": EmojiWithSkinTones(baseEmoji: .malePilot, skinTones: [.medium]), "👩🏽‍✈️": EmojiWithSkinTones(baseEmoji: .femalePilot, skinTones: [.medium]), + "🏃🏽‍➡️": EmojiWithSkinTones(baseEmoji: .personRunningFacingRight, skinTones: [.medium]), "👨🏾‍✈️": EmojiWithSkinTones(baseEmoji: .malePilot, skinTones: [.mediumDark]), "👩🏾‍✈️": EmojiWithSkinTones(baseEmoji: .femalePilot, skinTones: [.mediumDark]), + "🏃🏾‍➡️": EmojiWithSkinTones(baseEmoji: .personRunningFacingRight, skinTones: [.mediumDark]), "👨🏿‍✈️": EmojiWithSkinTones(baseEmoji: .malePilot, skinTones: [.dark]), - "👩🏿‍✈️": EmojiWithSkinTones(baseEmoji: .femalePilot, skinTones: [.dark]) + "👩🏿‍✈️": EmojiWithSkinTones(baseEmoji: .femalePilot, skinTones: [.dark]), + "🏃🏿‍➡️": EmojiWithSkinTones(baseEmoji: .personRunningFacingRight, skinTones: [.dark]) ] return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } @@ -3659,6 +3749,17 @@ extension EmojiWithSkinTones { return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } + private static func emojiFrom3400(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🚶🏻‍➡️": EmojiWithSkinTones(baseEmoji: .personWalkingFacingRight, skinTones: [.light]), + "🚶🏼‍➡️": EmojiWithSkinTones(baseEmoji: .personWalkingFacingRight, skinTones: [.mediumLight]), + "🚶🏽‍➡️": EmojiWithSkinTones(baseEmoji: .personWalkingFacingRight, skinTones: [.medium]), + "🚶🏾‍➡️": EmojiWithSkinTones(baseEmoji: .personWalkingFacingRight, skinTones: [.mediumDark]), + "🚶🏿‍➡️": EmojiWithSkinTones(baseEmoji: .personWalkingFacingRight, skinTones: [.dark]) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + private static func emojiFrom3403(_ rawValue: String) -> EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "🤦🏻‍♂️": EmojiWithSkinTones(baseEmoji: .manFacepalming, skinTones: [.light]), @@ -3914,6 +4015,17 @@ extension EmojiWithSkinTones { return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } + private static func emojiFrom3408(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🧎🏻‍➡️": EmojiWithSkinTones(baseEmoji: .personKneelingFacingRight, skinTones: [.light]), + "🧎🏼‍➡️": EmojiWithSkinTones(baseEmoji: .personKneelingFacingRight, skinTones: [.mediumLight]), + "🧎🏽‍➡️": EmojiWithSkinTones(baseEmoji: .personKneelingFacingRight, skinTones: [.medium]), + "🧎🏾‍➡️": EmojiWithSkinTones(baseEmoji: .personKneelingFacingRight, skinTones: [.mediumDark]), + "🧎🏿‍➡️": EmojiWithSkinTones(baseEmoji: .personKneelingFacingRight, skinTones: [.dark]) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + private static func emojiFrom3477(_ rawValue: String) -> EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "👩‍❤️‍👨": EmojiWithSkinTones(baseEmoji: .womanHeartMan, skinTones: nil), @@ -3923,6 +4035,27 @@ extension EmojiWithSkinTones { return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } + private static func emojiFrom3491(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "👨‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .manWithWhiteCaneFacingRight, skinTones: nil), + "👩‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .womanWithWhiteCaneFacingRight, skinTones: nil), + "👨‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .manInMotorizedWheelchairFacingRight, skinTones: nil), + "👩‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .womanInMotorizedWheelchairFacingRight, skinTones: nil), + "👨‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .manInManualWheelchairFacingRight, skinTones: nil), + "👩‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .womanInManualWheelchairFacingRight, skinTones: nil) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + + private static func emojiFrom3505(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🧑‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .personWithWhiteCaneFacingRight, skinTones: nil), + "🧑‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .personInMotorizedWheelchairFacingRight, skinTones: nil), + "🧑‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .personInManualWheelchairFacingRight, skinTones: nil) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + private static func emojiFrom3921(_ rawValue: String) -> EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "👨🏻‍🎓": EmojiWithSkinTones(baseEmoji: .maleStudent, skinTones: [.light]), @@ -4359,6 +4492,119 @@ extension EmojiWithSkinTones { return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } + private static func emojiFrom4048(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🧑‍🧑‍🧒": EmojiWithSkinTones(baseEmoji: .familyAdultAdultChild, skinTones: nil), + "🧑‍🧒‍🧒": EmojiWithSkinTones(baseEmoji: .familyAdultChildChild, skinTones: nil) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + + private static func emojiFrom4223(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🏃🏻‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanRunningFacingRight, skinTones: [.light]), + "🏃🏻‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manRunningFacingRight, skinTones: [.light]), + "🏃🏼‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanRunningFacingRight, skinTones: [.mediumLight]), + "🏃🏼‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manRunningFacingRight, skinTones: [.mediumLight]), + "🏃🏽‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanRunningFacingRight, skinTones: [.medium]), + "🏃🏽‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manRunningFacingRight, skinTones: [.medium]), + "🏃🏾‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanRunningFacingRight, skinTones: [.mediumDark]), + "🏃🏾‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manRunningFacingRight, skinTones: [.mediumDark]), + "🏃🏿‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanRunningFacingRight, skinTones: [.dark]), + "🏃🏿‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manRunningFacingRight, skinTones: [.dark]) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + + private static func emojiFrom4231(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🚶🏻‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanWalkingFacingRight, skinTones: [.light]), + "🚶🏻‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manWalkingFacingRight, skinTones: [.light]), + "🚶🏼‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanWalkingFacingRight, skinTones: [.mediumLight]), + "🚶🏼‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manWalkingFacingRight, skinTones: [.mediumLight]), + "🚶🏽‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanWalkingFacingRight, skinTones: [.medium]), + "🚶🏽‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manWalkingFacingRight, skinTones: [.medium]), + "🚶🏾‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanWalkingFacingRight, skinTones: [.mediumDark]), + "🚶🏾‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manWalkingFacingRight, skinTones: [.mediumDark]), + "🚶🏿‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanWalkingFacingRight, skinTones: [.dark]), + "🚶🏿‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manWalkingFacingRight, skinTones: [.dark]) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + + private static func emojiFrom4239(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🧎🏻‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanKneelingFacingRight, skinTones: [.light]), + "🧎🏻‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manKneelingFacingRight, skinTones: [.light]), + "🧎🏼‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanKneelingFacingRight, skinTones: [.mediumLight]), + "🧎🏼‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manKneelingFacingRight, skinTones: [.mediumLight]), + "🧎🏽‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanKneelingFacingRight, skinTones: [.medium]), + "🧎🏽‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manKneelingFacingRight, skinTones: [.medium]), + "🧎🏾‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanKneelingFacingRight, skinTones: [.mediumDark]), + "🧎🏾‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manKneelingFacingRight, skinTones: [.mediumDark]), + "🧎🏿‍♀️‍➡️": EmojiWithSkinTones(baseEmoji: .womanKneelingFacingRight, skinTones: [.dark]), + "🧎🏿‍♂️‍➡️": EmojiWithSkinTones(baseEmoji: .manKneelingFacingRight, skinTones: [.dark]) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + + private static func emojiFrom4771(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "👨🏻‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .manWithWhiteCaneFacingRight, skinTones: [.light]), + "👩🏻‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .womanWithWhiteCaneFacingRight, skinTones: [.light]), + "👨🏻‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .manInMotorizedWheelchairFacingRight, skinTones: [.light]), + "👩🏻‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .womanInMotorizedWheelchairFacingRight, skinTones: [.light]), + "👨🏻‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .manInManualWheelchairFacingRight, skinTones: [.light]), + "👩🏻‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .womanInManualWheelchairFacingRight, skinTones: [.light]), + "👨🏼‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .manWithWhiteCaneFacingRight, skinTones: [.mediumLight]), + "👩🏼‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .womanWithWhiteCaneFacingRight, skinTones: [.mediumLight]), + "👨🏼‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .manInMotorizedWheelchairFacingRight, skinTones: [.mediumLight]), + "👩🏼‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .womanInMotorizedWheelchairFacingRight, skinTones: [.mediumLight]), + "👨🏼‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .manInManualWheelchairFacingRight, skinTones: [.mediumLight]), + "👩🏼‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .womanInManualWheelchairFacingRight, skinTones: [.mediumLight]), + "👨🏽‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .manWithWhiteCaneFacingRight, skinTones: [.medium]), + "👩🏽‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .womanWithWhiteCaneFacingRight, skinTones: [.medium]), + "👨🏽‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .manInMotorizedWheelchairFacingRight, skinTones: [.medium]), + "👩🏽‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .womanInMotorizedWheelchairFacingRight, skinTones: [.medium]), + "👨🏽‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .manInManualWheelchairFacingRight, skinTones: [.medium]), + "👩🏽‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .womanInManualWheelchairFacingRight, skinTones: [.medium]), + "👨🏾‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .manWithWhiteCaneFacingRight, skinTones: [.mediumDark]), + "👩🏾‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .womanWithWhiteCaneFacingRight, skinTones: [.mediumDark]), + "👨🏾‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .manInMotorizedWheelchairFacingRight, skinTones: [.mediumDark]), + "👩🏾‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .womanInMotorizedWheelchairFacingRight, skinTones: [.mediumDark]), + "👨🏾‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .manInManualWheelchairFacingRight, skinTones: [.mediumDark]), + "👩🏾‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .womanInManualWheelchairFacingRight, skinTones: [.mediumDark]), + "👨🏿‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .manWithWhiteCaneFacingRight, skinTones: [.dark]), + "👩🏿‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .womanWithWhiteCaneFacingRight, skinTones: [.dark]), + "👨🏿‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .manInMotorizedWheelchairFacingRight, skinTones: [.dark]), + "👩🏿‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .womanInMotorizedWheelchairFacingRight, skinTones: [.dark]), + "👨🏿‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .manInManualWheelchairFacingRight, skinTones: [.dark]), + "👩🏿‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .womanInManualWheelchairFacingRight, skinTones: [.dark]) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + + private static func emojiFrom4785(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🧑🏻‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .personWithWhiteCaneFacingRight, skinTones: [.light]), + "🧑🏻‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .personInMotorizedWheelchairFacingRight, skinTones: [.light]), + "🧑🏻‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .personInManualWheelchairFacingRight, skinTones: [.light]), + "🧑🏼‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .personWithWhiteCaneFacingRight, skinTones: [.mediumLight]), + "🧑🏼‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .personInMotorizedWheelchairFacingRight, skinTones: [.mediumLight]), + "🧑🏼‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .personInManualWheelchairFacingRight, skinTones: [.mediumLight]), + "🧑🏽‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .personWithWhiteCaneFacingRight, skinTones: [.medium]), + "🧑🏽‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .personInMotorizedWheelchairFacingRight, skinTones: [.medium]), + "🧑🏽‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .personInManualWheelchairFacingRight, skinTones: [.medium]), + "🧑🏾‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .personWithWhiteCaneFacingRight, skinTones: [.mediumDark]), + "🧑🏾‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .personInMotorizedWheelchairFacingRight, skinTones: [.mediumDark]), + "🧑🏾‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .personInManualWheelchairFacingRight, skinTones: [.mediumDark]), + "🧑🏿‍🦯‍➡️": EmojiWithSkinTones(baseEmoji: .personWithWhiteCaneFacingRight, skinTones: [.dark]), + "🧑🏿‍🦼‍➡️": EmojiWithSkinTones(baseEmoji: .personInMotorizedWheelchairFacingRight, skinTones: [.dark]), + "🧑🏿‍🦽‍➡️": EmojiWithSkinTones(baseEmoji: .personInManualWheelchairFacingRight, skinTones: [.dark]) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + private static func emojiFrom4840(_ rawValue: String) -> EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "👩‍❤️‍💋‍👨": EmojiWithSkinTones(baseEmoji: .womanKissMan, skinTones: nil), @@ -4409,6 +4655,13 @@ extension EmojiWithSkinTones { return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } + private static func emojiFrom5425(_ rawValue: String) -> EmojiWithSkinTones { + let lookup: [String: EmojiWithSkinTones] = [ + "🧑‍🧑‍🧒‍🧒": EmojiWithSkinTones(baseEmoji: .familyAdultAdultChildChild, skinTones: nil) + ] + return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) + } + private static func emojiFrom6037(_ rawValue: String) -> EmojiWithSkinTones { let lookup: [String: EmojiWithSkinTones] = [ "👩🏻‍❤️‍👨🏻": EmojiWithSkinTones(baseEmoji: .womanHeartMan, skinTones: [.light]), @@ -4729,4 +4982,3 @@ extension EmojiWithSkinTones { return (lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue)) } } -// swiftlint:disable all diff --git a/Session/Home/App Review/AppReviewPromptModel.swift b/Session/Home/App Review/AppReviewPromptModel.swift new file mode 100644 index 0000000000..d7c766e66c --- /dev/null +++ b/Session/Home/App Review/AppReviewPromptModel.swift @@ -0,0 +1,133 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import SessionUIKit +import SessionUtilitiesKit + +struct AppReviewPromptModel { + let title: String + let message: String + + var primaryButtonTitle: String? + var primaryButtonAccessibilityIdentifier: String? + + var secondaryButtonTitle: String? + var secondaryButtonAccessibilityIdentifier: String? +} + +extension AppReviewPromptModel { + // Base version where app review prompt became available + static private let reviewPromptAvailabilityVersion = "2.14.2" // stringlint:ignore + + /// Determines the initial state of the app review prompt. + static func loadInitialAppReviewPromptState(using dependencies: Dependencies) -> AppReviewPromptState? { + var promptState: AppReviewPromptState? + + if checkAndRefreshAppReviewState(using: dependencies) { + promptState = .rateSession + + } else if dependencies[defaults: .standard, key: .didShowAppReviewPrompt] && !dependencies[defaults: .standard, key: .didActionAppReviewPrompt] { + dependencies[defaults: .standard, key: .didShowAppReviewPrompt] = false + + promptState = .enjoyingSession + } + + return promptState + } + + static func checkAndRefreshAppReviewState(using dependencies: Dependencies) -> Bool { + /// Check if incomplete app review can be shown again to user on next app launch + let retryCount = dependencies[defaults: .standard, key: .rateAppRetryAttemptCount] + + // A buffer of 1 hour + let buffer: TimeInterval = 60 * 60 + + if retryCount == 0, let retryDate = dependencies[defaults: .standard, key: .rateAppRetryDate], dependencies.dateNow.timeIntervalSince(retryDate) >= -buffer { + // This block will execute if the current time is within 1 hour of the retryDate + // or if the current time is past the retryDate. + dependencies[defaults: .standard, key: .rateAppRetryDate] = nil + dependencies[defaults: .standard, key: .rateAppRetryAttemptCount] = 1 + dependencies[defaults: .standard, key: .didShowAppReviewPrompt] = false + + return true + } + + return false + } + + /// Checks if version was from install or from update + static func checkIfAppWasInstalledPriorToAppReviewRelease(using dependencies: Dependencies) -> Bool { + guard let firstAppVersion = dependencies[cache: .appVersion].firstAppVersion else { + return false + } + + let comparisonResult = firstAppVersion.compare( + reviewPromptAvailabilityVersion, + options: .numeric + ) + + // App was updated to the latest version with app review prompt + return comparisonResult == .orderedAscending + } +} + +enum AppReviewPromptState { + case enjoyingSession + case rateSession + case feedback + case rateLimit + + var promptContent: AppReviewPromptModel { + switch self { + case .enjoyingSession: + return .init( + title: "enjoyingSession" + .put(key: "app_name", value: Constants.app_name) + .localized(), + message: "enjoyingSessionDescription" + .put(key: "app_name", value: Constants.app_name) + .localized(), + primaryButtonTitle: "enjoyingSessionButtonPositive" + .put(key: "emoji", value: "❤️") + .localized(), + primaryButtonAccessibilityIdentifier: "enjoy-session-positive-button", + secondaryButtonTitle: "enjoyingSessionButtonNegative" + .put(key: "emoji", value: "😕") + .localized(), + secondaryButtonAccessibilityIdentifier: "enjoy-session-negative-button" + ) + case .rateSession: + return .init( + title: "rateSession" + .put(key: "app_name", value: Constants.app_name) + .localized(), + message: "rateSessionModalDescription" + .put(key: "app_name", value: Constants.app_name) + .put(key: "storevariant", value: Constants.store_name) + .localized(), + primaryButtonTitle: "rateSessionApp".localized(), + primaryButtonAccessibilityIdentifier: "rate-app-button", + secondaryButtonTitle: "notNow".localized(), + secondaryButtonAccessibilityIdentifier: "not-now-button" + ) + case .feedback: + return .init( + title: "giveFeedback".localized(), + message: "giveFeedbackDescription" + .put(key: "app_name", value: Constants.app_name) + .localized(), + primaryButtonTitle: "openSurvey".localized(), + primaryButtonAccessibilityIdentifier: "open-survey-button", + secondaryButtonTitle: "notNow".localized(), + secondaryButtonAccessibilityIdentifier: "not-now-button" + ) + case .rateLimit: + return .init( + title: "reviewLimit".localized(), + message: "reviewLimitDescription" + .put(key: "app_name", value: Constants.app_name) + .localized() + ) + } + } +} diff --git a/Session/Home/App Review/View/AppReviewPromptDialog.swift b/Session/Home/App Review/View/AppReviewPromptDialog.swift new file mode 100644 index 0000000000..f1878753ea --- /dev/null +++ b/Session/Home/App Review/View/AppReviewPromptDialog.swift @@ -0,0 +1,187 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import UIKit +import SessionUIKit + +class AppReviewPromptDialog: UIView { + var onCloseTapped: (() -> Void)? + var onPrimaryTapped: ((AppReviewPromptState) -> Void)? + var onSecondaryTapped: ((AppReviewPromptState) -> Void)? + + private static let closeSize: CGFloat = 24 + + private lazy var closeButton: UIButton = UIButton(primaryAction: UIAction { [weak self] _ in self?.close() }) + .withConfiguration( + UIButton.Configuration + .plain() + .withImage(UIImage(named: "X")?.withRenderingMode(.alwaysTemplate)) + .withContentInsets(NSDirectionalEdgeInsets(top: 6, leading: 6, bottom: 6, trailing: 6)) + ) + .withConfigurationUpdateHandler { button in + switch button.state { + case .highlighted: button.imageView?.tintAdjustmentMode = .dimmed + default: button.imageView?.tintAdjustmentMode = .normal + } + } + .withImageViewContentMode(.scaleAspectFit) + .withThemeTintColor(.textPrimary) + .withAccessibility( + identifier: "Close button" + ) + .with(.width, of: AppReviewPromptDialog.closeSize) + .with(.height, of: AppReviewPromptDialog.closeSize) + + private lazy var titleLabel: UILabel = { + let result = UILabel() + result.textAlignment = .center + result.numberOfLines = 0 + result.themeTextColor = .alert_text + result.font = .boldSystemFont(ofSize: Values.mediumFontSize) + + return result + }() + + private lazy var messageLabel: UILabel = { + let result = UILabel() + result.textAlignment = .center + result.numberOfLines = 0 + result.themeTextColor = .alert_text + result.font = ConfirmationModal.explanationFont + + return result + }() + + private lazy var primaryButton: UIButton = { + let result = UIButton(type: .custom) + result.setThemeTitleColor(.sessionButton_text, for: .normal) + result.setThemeTitleColor(.sessionButton_highlight, for: .highlighted) + result.titleLabel?.font = .boldSystemFont(ofSize: Values.mediumFontSize) + result.titleLabel?.numberOfLines = 0 + result.titleLabel?.textAlignment = .center + result.addTarget(self, action: #selector(primaryEvent), for: .touchUpInside) + + return result + }() + + private lazy var secondaryButton: UIButton = { + let result = UIButton(type: .custom) + result.setThemeTitleColor(.textPrimary, for: .normal) + result.setThemeTitleColor(.textSecondary, for: .highlighted) + result.titleLabel?.font = .boldSystemFont(ofSize: Values.mediumFontSize) + result.titleLabel?.numberOfLines = 0 + result.titleLabel?.textAlignment = .center + result.addTarget(self, action: #selector(secondaryEvent), for: .touchUpInside) + + return result + }() + + private lazy var buttonStack: UIStackView = { + let result = UIStackView(arrangedSubviews: [ + primaryButton, + secondaryButton + ]) + result.axis = .horizontal + result.distribution = .fillEqually + result.alignment = .fill + result.isLayoutMarginsRelativeArrangement = true + + return result + }() + + private lazy var contentStack: UIStackView = { + let result = UIStackView(arrangedSubviews: [ + titleLabel, + messageLabel, + buttonStack + ]) + result.axis = .vertical + result.distribution = .fill + result.spacing = 8 + result.isLayoutMarginsRelativeArrangement = true + result.layoutMargins = UIEdgeInsets( + top: Values.largeSpacing, + left: Values.veryLargeSpacing, + bottom: Values.verySmallSpacing, + right: Values.veryLargeSpacing + ) + + return result + }() + + private var prompt: AppReviewPromptState? + + override init(frame: CGRect) { + super.init(frame: frame) + + setupHierarchy() + setupLayout() + + setReviewPrompt(nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setReviewPrompt(_ prompt: AppReviewPromptState?) { + self.prompt = prompt + + isHidden = prompt == nil + + titleLabel.text = prompt?.promptContent.title + titleLabel.accessibilityIdentifier = "Modal heading" + titleLabel.accessibilityLabel = titleLabel.text + + messageLabel.text = prompt?.promptContent.message + messageLabel.accessibilityIdentifier = "Modal description" + messageLabel.accessibilityLabel = messageLabel.text + + primaryButton.isHidden = prompt?.promptContent.primaryButtonTitle == nil + primaryButton.setTitle(prompt?.promptContent.primaryButtonTitle, for: .normal) + primaryButton.accessibilityIdentifier = prompt?.promptContent.primaryButtonAccessibilityIdentifier + + secondaryButton.isHidden = prompt?.promptContent.secondaryButtonTitle == nil + secondaryButton.setTitle(prompt?.promptContent.secondaryButtonTitle, for: .normal) + secondaryButton.accessibilityIdentifier = prompt?.promptContent.secondaryButtonAccessibilityIdentifier + + let isButtonsHidden = primaryButton.isHidden && secondaryButton.isHidden + + buttonStack.layoutMargins = .init( + top: isButtonsHidden ? 0: Values.mediumSpacing, + left: 0, + bottom: Values.mediumSpacing, + right: 0 + ) + } + + @objc + func close() { + onCloseTapped?() + } + + @objc + func primaryEvent() { + let current = prompt ?? .enjoyingSession + onPrimaryTapped?(current) + } + + @objc + func secondaryEvent() { + let current = prompt ?? .enjoyingSession + onSecondaryTapped?(current) + } +} + +private extension AppReviewPromptDialog { + func setupHierarchy() { + addSubview(contentStack) + addSubview(closeButton) + } + + func setupLayout() { + closeButton.pin(.top, to: .top, of: self, withInset: Values.smallSpacing) + closeButton.pin(.right, to: .right, of: self, withInset: -Values.smallSpacing) + + contentStack.pin(to: self) + } +} diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index a313630232..cf9e8ed989 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -33,7 +33,7 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi super.init(nibName: nil, bundle: nil) } - + required init?(coder: NSCoder) { preconditionFailure("Use init() instead.") } @@ -67,29 +67,34 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi return result }() - + private lazy var tableView: UITableView = { let result = UITableView() result.separatorStyle = .none result.themeBackgroundColor = .clear - result.contentInset = UIEdgeInsets( - top: 0, - left: 0, - bottom: ( - Values.largeSpacing + - HomeVC.newConversationButtonSize + - Values.smallSpacing + - (UIApplication.shared.keyWindow?.safeAreaInsets.bottom ?? 0) - ), - right: 0 - ) result.showsVerticalScrollIndicator = false result.register(view: MessageRequestsCell.self) result.register(view: FullConversationCell.self) result.dataSource = self result.delegate = self result.sectionHeaderTopPadding = 0 - + + return result + }() + + private lazy var appReviewPrompt: AppReviewPromptDialog = { + let result = AppReviewPromptDialog() + + // Layers + result.layer.borderWidth = 1 + result.layer.cornerRadius = 12 + result.themeBorderColor = .borderSeparator + result.themeBackgroundColor = .alert_background + result.themeShadowColor = .black + result.onPrimaryTapped = { [viewModel = self.viewModel] state in viewModel.handlePrimaryTappedForState(state) } + result.onSecondaryTapped = { [viewModel = self.viewModel] in viewModel.handleSecondayTappedForState($0) } + result.onCloseTapped = { [viewModel = self.viewModel] in viewModel.handlePromptChangeState(nil) } + return result }() @@ -160,7 +165,7 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi innerShadowLayer.shadowOffset = .zero innerShadowLayer.shadowOpacity = 0.4 innerShadowLayer.shadowRadius = 2 - + let cutout: UIBezierPath = UIBezierPath( roundedRect: innerShadowLayer.bounds .insetBy(dx: innerShadowLayer.shadowRadius, dy: innerShadowLayer.shadowRadius), @@ -173,7 +178,7 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi path.append(cutout) innerShadowLayer.shadowPath = path.cgPath result.layer.addSublayer(innerShadowLayer) - + return result }() @@ -192,7 +197,7 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi instructionLabel.lineBreakMode = .byWordWrapping instructionLabel.numberOfLines = 0 - let result = UIStackView(arrangedSubviews: [ + let result = UIStackView(arrangedSubviews: [ emptyConvoLabel, UIView.vSpacer(Values.smallSpacing), instructionLabel @@ -250,7 +255,7 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi .localized() welcomeLabel.themeTextColor = .sessionButton_text welcomeLabel.textAlignment = .center - + let result = UIStackView(arrangedSubviews: [ image, accountCreatedLabel, @@ -334,6 +339,12 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi newConversationButton.center(.horizontal, in: view) newConversationButton.pin(.bottom, to: .bottom, of: view.safeAreaLayoutGuide, withInset: -Values.smallSpacing) + // Preview prompt + view.addSubview(appReviewPrompt) + appReviewPrompt.pin(.left, to: .left, of: view, withInset: 12) + appReviewPrompt.pin(.right, to: .right, of: view, withInset: -12) + appReviewPrompt.pin(.bottom, to: .top, of: newConversationButton, withInset: -10) + // Start polling if needed (i.e. if the user just created or restored their Session ID) if viewModel.dependencies[cache: .general].userExists, @@ -348,12 +359,28 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi // Bind the UI to the view model bindViewModel() + + viewModel.navigatableState.setupBindings(viewController: self, disposables: &disposables) + + // Notification + NotificationCenter.default.addObserver( + self, + selector: #selector(applicationDidBecomeActive(_:)), + name: UIApplication.didBecomeActiveNotification, + object: nil + ) } public override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) viewModel.dependencies[singleton: .notificationsManager].scheduleSessionNetworkPageLocalNotifcation(force: false) + + viewModel.viewDidAppear() + } + + deinit { + NotificationCenter.default.removeObserver(self) } // MARK: - Updating @@ -391,7 +418,7 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi tableViewTopConstraint?.isActive = false loadingConversationsLabelTopConstraint?.isActive = false seedReminderView.isHidden = !state.showViewedSeedBanner - + if state.showViewedSeedBanner { loadingConversationsLabelTopConstraint = loadingConversationsLabel.pin(.top, to: .bottom, of: seedReminderView, withInset: Values.mediumSpacing) tableViewTopConstraint = tableView.pin(.top, to: .bottom, of: seedReminderView) @@ -425,6 +452,23 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi // don't want to trigger the callbacks until a successful load) guard state.viewState != .loading else { return } + // App reivew, check if `state.appReviewPromptState` has value + // `state.appReviewPromptState` will only have value if triggered via viewDidAppear or review prompt events + appReviewPrompt.setReviewPrompt(state.appReviewPromptState) + + tableView.contentInset = UIEdgeInsets( + top: 0, + left: 0, + bottom: ( + Values.largeSpacing + + HomeVC.newConversationButtonSize + + Values.smallSpacing + + (UIApplication.shared.keyWindow?.safeAreaInsets.bottom ?? 0) + + (state.appReviewPromptState != nil ? (appReviewPrompt.frame.size.height + 24) : 0) + ), + right: 0 + ) + // Reload the table content (update without animations on the first render) guard initialConversationLoadComplete else { sections = state.sections(viewModel: viewModel) @@ -452,7 +496,7 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi } } - private func updateNavBarButtons( + @MainActor private func updateNavBarButtons( userProfile: Profile, serviceNetwork: ServiceNetwork, forceOffline: Bool @@ -549,7 +593,7 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi switch section.model { case .loadMore: let loadingIndicator: UIActivityIndicatorView = UIActivityIndicatorView(style: .medium) - loadingIndicator.themeTintColor = .textPrimary + loadingIndicator.themeColor = .textPrimary loadingIndicator.alpha = 0.5 loadingIndicator.startAnimating() @@ -580,7 +624,7 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi default: break } } - + public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) @@ -602,7 +646,7 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi using: viewModel.dependencies ) self.navigationController?.pushViewController(viewController, animated: true) - + default: break } } @@ -630,10 +674,10 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi // provide it there either guard threadViewModel.threadVariant != .legacyGroup && - threadViewModel.threadId != threadViewModel.currentUserSessionId && ( - threadViewModel.threadVariant != .contact || - (try? SessionId(from: section.elements[indexPath.row].threadId))?.prefix == .standard - ) + threadViewModel.threadId != threadViewModel.currentUserSessionId && ( + threadViewModel.threadVariant != .contact || + (try? SessionId(from: section.elements[indexPath.row].threadId))?.prefix == .standard + ) else { return nil } return UIContextualAction.configuration( @@ -648,7 +692,7 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi using: viewModel.dependencies ) ) - + default: return nil } } @@ -678,9 +722,9 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi // Cannot properly sync outgoing blinded message requests so only provide valid options let shouldHavePinAction: Bool = { switch threadViewModel.threadVariant { - // Only allow unpin for legacy groups + // Only allow unpin for legacy groups case .legacyGroup: return threadViewModel.threadPinnedPriority > 0 - + default: return ( sessionIdPrefix != .blinded15 && @@ -695,7 +739,7 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi sessionIdPrefix != .blinded15 && sessionIdPrefix != .blinded25 ) - + case .group: return (threadViewModel.currentUserIsClosedGroupMember == true) case .legacyGroup: return false @@ -746,7 +790,7 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi present(targetViewController, animated: true, completion: nil) return } - + let viewController: SessionHostingViewController = SessionHostingViewController(rootView: recoveryPasswordView) viewController.setNavBarTitle("sessionRecoveryPassword".localized()) self.navigationController?.pushViewController(viewController, animated: true) @@ -773,6 +817,12 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi viewModel.dependencies[singleton: .app].createNewConversation() } + @objc func applicationDidBecomeActive(_ notification: Notification) { + DispatchQueue.main.async { [weak self] in + self?.viewModel.didReturnFromBackground() + } + } + func createNewDMFromDeepLink(sessionId: String) { let viewController: SessionHostingViewController = SessionHostingViewController( rootView: NewMessageScreen(accountId: sessionId, using: viewModel.dependencies) diff --git a/Session/Home/HomeViewModel.swift b/Session/Home/HomeViewModel.swift index ade9c3f15e..98d29d990e 100644 --- a/Session/Home/HomeViewModel.swift +++ b/Session/Home/HomeViewModel.swift @@ -7,6 +7,8 @@ import DifferenceKit import SignalUtilitiesKit import SessionMessagingKit import SessionUtilitiesKit +import StoreKit +import SessionUIKit // MARK: - Log.Category @@ -35,17 +37,24 @@ public class HomeViewModel: NavigatableStateHolder { public let dependencies: Dependencies private let userSessionId: SessionId + private var didPresentAppReviewPrompt: Bool = false /// This value is the current state of the view @MainActor @Published private(set) var state: State private var observationTask: Task? // MARK: - Initialization - @MainActor init(using dependencies: Dependencies) { self.dependencies = dependencies self.userSessionId = dependencies[cache: .general].sessionId - self.state = State.initialState(using: dependencies) + + self.state = State.initialState( + using: dependencies, + appReviewPromptState: AppReviewPromptModel + .loadInitialAppReviewPromptState(using: dependencies), + appWasInstalledPriorToAppReviewRelease: AppReviewPromptModel + .checkIfAppWasInstalledPriorToAppReviewRelease(using: dependencies) + ) /// Bind the state self.observationTask = ObservationBuilder @@ -56,6 +65,15 @@ public class HomeViewModel: NavigatableStateHolder { .assign { [weak self] updatedState in self?.state = updatedState } } + deinit { + NotificationCenter.default.removeObserver(self) + } + + public struct HomeViewModelEvent: Hashable { + let pendingAppReviewPromptState: AppReviewPromptState? + let appReviewPromptState: AppReviewPromptState? + } + // MARK: - State public struct State: ObservableKeyProvider { @@ -76,6 +94,9 @@ public class HomeViewModel: NavigatableStateHolder { let unreadMessageRequestThreadCount: Int let loadedPageInfo: PagedData.LoadedInfo let itemCache: [String: SessionThreadViewModel] + let appReviewPromptState: AppReviewPromptState? + let pendingAppReviewPromptState: AppReviewPromptState? + let appWasInstalledPriorToAppReviewRelease: Bool @MainActor public func sections(viewModel: HomeViewModel) -> [SectionModel] { HomeViewModel.sections(state: self, viewModel: viewModel) @@ -83,6 +104,8 @@ public class HomeViewModel: NavigatableStateHolder { public var observedKeys: Set { var result: Set = [ + .appLifecycle(.willEnterForeground), + .databaseLifecycle(.resumed), .loadPage(HomeViewModel.self), .messageRequestAccepted, .messageRequestDeleted, @@ -97,7 +120,11 @@ public class HomeViewModel: NavigatableStateHolder { .setting(.hasHiddenMessageRequests), .conversationCreated, .anyMessageCreatedInAnyConversation, - .anyContactBlockedStatusChanged + .anyContactBlockedStatusChanged, + .userDefault(.hasVisitedPathScreen), + .userDefault(.hasPressedDonateButton), + .userDefault(.hasChangedTheme), + .updateScreen(HomeViewModel.self) ] itemCache.values.forEach { threadViewModel in @@ -124,7 +151,7 @@ public class HomeViewModel: NavigatableStateHolder { return result } - static func initialState(using dependencies: Dependencies) -> State { + static func initialState(using dependencies: Dependencies, appReviewPromptState: AppReviewPromptState?, appWasInstalledPriorToAppReviewRelease: Bool) -> State { return State( viewState: .loading, userProfile: Profile(id: dependencies[cache: .general].sessionId.hexString, name: ""), @@ -148,7 +175,10 @@ public class HomeViewModel: NavigatableStateHolder { groupSQL: SessionThreadViewModel.groupSQL, orderSQL: SessionThreadViewModel.homeOrderSQL ), - itemCache: [:] + itemCache: [:], + appReviewPromptState: nil, + pendingAppReviewPromptState: appReviewPromptState, + appWasInstalledPriorToAppReviewRelease: appWasInstalledPriorToAppReviewRelease ) } } @@ -170,6 +200,9 @@ public class HomeViewModel: NavigatableStateHolder { var unreadMessageRequestThreadCount: Int = previousState.unreadMessageRequestThreadCount var loadResult: PagedData.LoadResult = previousState.loadedPageInfo.asResult var itemCache: [String: SessionThreadViewModel] = previousState.itemCache + var appReviewPromptState: AppReviewPromptState? = previousState.appReviewPromptState + var pendingAppReviewPromptState: AppReviewPromptState? = previousState.pendingAppReviewPromptState + let appWasInstalledPriorToAppReviewRelease: Bool = previousState.appWasInstalledPriorToAppReviewRelease /// Store a local copy of the events so we can manipulate it based on the state changes var eventsToProcess: [ObservedEvent] = events @@ -214,7 +247,7 @@ public class HomeViewModel: NavigatableStateHolder { } /// Handle database events first - if let databaseEvents: Set = splitEvents[.databaseQuery], !databaseEvents.isEmpty { + if !dependencies[singleton: .storage].isSuspended, let databaseEvents: Set = splitEvents[.databaseQuery], !databaseEvents.isEmpty { do { var fetchedConversations: [SessionThreadViewModel] = [] let idsNeedingRequery: Set = self.extractIdsNeedingRequery( @@ -323,6 +356,9 @@ public class HomeViewModel: NavigatableStateHolder { Log.critical(.homeViewModel, "Failed to fetch state for events [\(eventList)], due to error: \(error)") } } + else if let databaseEvents: Set = splitEvents[.databaseQuery], !databaseEvents.isEmpty { + Log.warn(.homeViewModel, "Ignored \(databaseEvents.count) database event(s) sent while storage was suspended.") + } /// Then handle non-database events let groupedOtherEvents: [GenericObservableKey: Set]? = splitEvents[.other]? @@ -361,9 +397,36 @@ public class HomeViewModel: NavigatableStateHolder { } } + /// Next trigger should be ignored if `didShowAppReviewPrompt` is true + if dependencies[defaults: .standard, key: .didShowAppReviewPrompt] == true { + pendingAppReviewPromptState = nil + } else { + groupedOtherEvents?[.userDefault]?.forEach { event in + guard let value: Bool = event.value as? Bool else { return } + + switch (event.key, value, appWasInstalledPriorToAppReviewRelease) { + case (.userDefault(.hasVisitedPathScreen), true, false): + pendingAppReviewPromptState = .enjoyingSession + + case (.userDefault(.hasPressedDonateButton), true, _): + pendingAppReviewPromptState = .enjoyingSession + + case (.userDefault(.hasChangedTheme), true, false): + pendingAppReviewPromptState = .enjoyingSession + + default: break + } + } + } + + if let event: HomeViewModelEvent = events.first?.value as? HomeViewModelEvent { + pendingAppReviewPromptState = event.pendingAppReviewPromptState + appReviewPromptState = event.appReviewPromptState + } + /// Generate the new state return State( - viewState: (loadResult.info.totalCount == 0 ? + viewState: (loadResult.info.totalCount == 0 && unreadMessageRequestThreadCount == 0 ? .empty(isNewUser: (startedAsNewUser && !hasSavedThread && !hasSavedMessage)) : .loaded ), @@ -376,7 +439,10 @@ public class HomeViewModel: NavigatableStateHolder { hasHiddenMessageRequests: hasHiddenMessageRequests, unreadMessageRequestThreadCount: unreadMessageRequestThreadCount, loadedPageInfo: loadResult.info, - itemCache: itemCache + itemCache: itemCache, + appReviewPromptState: appReviewPromptState, + pendingAppReviewPromptState: pendingAppReviewPromptState, + appWasInstalledPriorToAppReviewRelease: appWasInstalledPriorToAppReviewRelease ) } @@ -384,6 +450,15 @@ public class HomeViewModel: NavigatableStateHolder { events: Set, cache: [String: SessionThreadViewModel] ) -> Set { + let requireFullRefresh: Bool = events.contains(where: { event in + event.key == .appLifecycle(.willEnterForeground) || + event.key == .databaseLifecycle(.resumed) + }) + + guard !requireFullRefresh else { + return Set(cache.keys) + } + return events.reduce(into: []) { result, event in switch (event.key.generic, event.value) { case (.conversationUpdated, let event as ConversationEvent): result.insert(event.id) @@ -475,6 +550,176 @@ public class HomeViewModel: NavigatableStateHolder { ].flatMap { $0 } } + // MARK: - Handle App review + @MainActor + func viewDidAppear() { + guard state.pendingAppReviewPromptState != nil else { return } + + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { [weak self, dependencies] in + guard let updatedState: AppReviewPromptState = self?.state.pendingAppReviewPromptState else { return } + + dependencies[defaults: .standard, key: .didActionAppReviewPrompt] = false + + self?.handlePromptChangeState(updatedState) + } + } + + func scheduleAppReviewRetry() { + /// Wait 2 weeks before trying again + dependencies[defaults: .standard, key: .rateAppRetryDate] = dependencies.dateNow + .addingTimeInterval(2 * 7 * 24 * 60 * 60) + } + + func handlePromptChangeState(_ state: AppReviewPromptState?) { + // Set`didActionAppReviewPrompt` to true when closed from `x` button of prompt + // or in show rate limit prompt so it does not show again on relaunch + if state == nil || state == .rateLimit { dependencies[defaults: .standard, key: .didActionAppReviewPrompt] = true } + + // Set `didShowAppReviewPrompt` when a new state is presented + if state != nil { dependencies[defaults: .standard, key: .didShowAppReviewPrompt] = true } + + dependencies.notifyAsync( + priority: .immediate, + key: .updateScreen(HomeViewModel.self), + value: HomeViewModelEvent( + pendingAppReviewPromptState: nil, + appReviewPromptState: state + ) + ) + } + + @MainActor + func submitAppStoreReview() { + dependencies[defaults: .standard, key: .rateAppRetryDate] = nil + dependencies[defaults: .standard, key: .rateAppRetryAttemptCount] = 0 + + NotificationCenter.default.addObserver( + self, + selector: #selector(self.windowBecameVisibleAfterTriggeringAppStoreReview(notification:)), + name: UIWindow.didBecomeVisibleNotification, object: nil + ) + + if !dependencies[feature: .simulateAppReviewLimit] { + requestAppReview() + } + + // Added 2 sec delay to give time for requet review to proc + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) { [weak self] in + guard let this = self else { return } + + NotificationCenter.default.removeObserver(this, name: UIWindow.didBecomeVisibleNotification, object: nil) + + guard this.didPresentAppReviewPrompt else { + // Show rate limit prompt + this.handlePromptChangeState(.rateLimit) + return + } + + // Reset flag just in case it will be triggered again + this.didPresentAppReviewPrompt = false + } + } + + @MainActor + private func requestAppReview() { + guard let scene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene else { + return + } + + if #available(iOS 16.0, *) { + AppStore.requestReview(in: scene) + } else { + SKStoreReviewController.requestReview(in: scene) + } + } + + @objc + private func windowBecameVisibleAfterTriggeringAppStoreReview(notification: Notification) { + didPresentAppReviewPrompt = true + } + + @MainActor + func submitFeedbackSurvery() { + guard let url: URL = URL(string: Constants.session_feedback_url) else { return } + + // stringlint:disable + let surveyUrl: URL = url.appending(queryItems: [ + .init(name: "platform", value: Constants.platform_name), + .init(name: "version", value: dependencies[cache: .appVersion].appVersion) + ]) + + let modal: ConfirmationModal = ConfirmationModal( + info: ConfirmationModal.Info( + title: "urlOpen".localized(), + body: .attributedText( + "urlOpenDescription" + .put(key: "url", value: surveyUrl.absoluteString) + .localizedFormatted(baseFont: .systemFont(ofSize: Values.smallFontSize)) + ), + confirmTitle: "open".localized(), + confirmStyle: .danger, + cancelTitle: "urlCopy".localized(), + cancelStyle: .alert_text, + hasCloseButton: true, + onConfirm: { modal in + UIApplication.shared.open(surveyUrl, options: [:], completionHandler: nil) + modal.dismiss(animated: true) + }, + onCancel: { modal in + UIPasteboard.general.string = surveyUrl.absoluteString + + modal.dismiss(animated: true) + } + ) + ) + + self.transitionToScreen(modal, transitionType: .present) + } + + @MainActor + func handlePrimaryTappedForState(_ state: AppReviewPromptState) { + dependencies[defaults: .standard, key: .didActionAppReviewPrompt] = true + + switch state { + case .enjoyingSession: + handlePromptChangeState(.rateSession) + scheduleAppReviewRetry() + case .feedback: + // Close prompt before showing survery + handlePromptChangeState(nil) + submitFeedbackSurvery() + case .rateSession: + // Close prompt before showing app review + handlePromptChangeState(nil) + submitAppStoreReview() + default: break + } + } + + func handleSecondayTappedForState(_ state: AppReviewPromptState) { + dependencies[defaults: .standard, key: .didActionAppReviewPrompt] = true + + switch state { + case .feedback, .rateSession: handlePromptChangeState(nil) + case .enjoyingSession: handlePromptChangeState(.feedback) + default: break + } + } + + @MainActor + @objc func didReturnFromBackground() { + // Observe changes to app state retry and flags when app goes to bg to fg + if AppReviewPromptModel.checkAndRefreshAppReviewState(using: dependencies) { + // state.appReviewPromptState check so it does not replace existing prompt if there is any + let updatedState = state.appReviewPromptState ?? .rateSession + + // Handles scenario where app is in background -> foreground when the retry date is hit + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { [weak self] in + self?.handlePromptChangeState(updatedState) + } + } + } + // MARK: - Functions @MainActor func loadPageBefore() { @@ -510,6 +755,7 @@ private extension ObservedEvent { case (.feature(.forceOffline), _): return .other case (.setting(.hasViewedSeed), _): return .other + case (.appLifecycle(.willEnterForeground), _): return .databaseQuery case (.messageRequestUnreadMessageReceived, _), (.messageRequestAccepted, _), (.messageRequestDeleted, _), (.messageRequestMessageRead, _): return .databaseQuery @@ -534,3 +780,18 @@ private extension ObservedEvent { } } } + +private extension URL { + @available(iOS, introduced: 13.0, obsoleted: 16.0) + func appending(queryItems: [URLQueryItem]) -> URL { + guard var components = URLComponents(url: self, resolvingAgainstBaseURL: false) else { + return self + } + + var existingItems = components.queryItems ?? [] + existingItems.append(contentsOf: queryItems) + components.queryItems = existingItems + + return components.url ?? self + } +} diff --git a/Session/Home/New Conversation/NewMessageScreen.swift b/Session/Home/New Conversation/NewMessageScreen.swift index c478eb8331..4eb0dbfa9f 100644 --- a/Session/Home/New Conversation/NewMessageScreen.swift +++ b/Session/Home/New Conversation/NewMessageScreen.swift @@ -5,7 +5,7 @@ import SessionUIKit import SessionMessagingKit import SessionUtilitiesKit import SignalUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit struct NewMessageScreen: View { @EnvironmentObject var host: HostWrapper @@ -87,7 +87,7 @@ struct NewMessageScreen: View { // This could be an ONS name ModalActivityIndicatorViewController .present(fromViewController: self.host.controller?.navigationController!, canCancel: false) { modalActivityIndicator in - SnodeAPI + Network.SnodeAPI .getSessionID(for: accountIdOrONS, using: dependencies) .subscribe(on: DispatchQueue.global(qos: .userInitiated)) .receive(on: DispatchQueue.main) diff --git a/Session/Media Viewing & Editing/GIFs/GifPickerCell.swift b/Session/Media Viewing & Editing/GIFs/GifPickerCell.swift index bf01d591f7..81bf19c1c7 100644 --- a/Session/Media Viewing & Editing/GIFs/GifPickerCell.swift +++ b/Session/Media Viewing & Editing/GIFs/GifPickerCell.swift @@ -4,7 +4,7 @@ import UIKit import Combine import UniformTypeIdentifiers import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SignalUtilitiesKit import SessionUtilitiesKit @@ -201,7 +201,7 @@ class GifPickerCell: UICollectionViewCell { clearViewState() return } - guard let dependencies: Dependencies = dependencies, Data.isValidImage(at: asset.filePath, type: .gif, using: dependencies) else { + guard let dependencies: Dependencies = dependencies, MediaUtils.isValidImage(at: asset.filePath, type: .gif, using: dependencies) else { Log.error(.giphy, "Cell received invalid asset.") clearViewState() return diff --git a/Session/Media Viewing & Editing/GIFs/GifPickerViewController.swift b/Session/Media Viewing & Editing/GIFs/GifPickerViewController.swift index b1557c2166..b741b3d384 100644 --- a/Session/Media Viewing & Editing/GIFs/GifPickerViewController.swift +++ b/Session/Media Viewing & Editing/GIFs/GifPickerViewController.swift @@ -19,7 +19,7 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect didSet { Log.debug(.giphy, "ViewController viewMode: \(viewMode)") - updateContents() + Task { @MainActor [weak self] in self?.updateContents() } } } @@ -220,7 +220,7 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect return label } - private func updateContents() { + @MainActor private func updateContents() { guard let noResultsView = self.noResultsView else { Log.error(.giphy, "ViewController missing noResultsView") return diff --git a/Session/Media Viewing & Editing/GIFs/GiphyAPI.swift b/Session/Media Viewing & Editing/GIFs/GiphyAPI.swift index 2e9b72936b..eb2cd33430 100644 --- a/Session/Media Viewing & Editing/GIFs/GiphyAPI.swift +++ b/Session/Media Viewing & Editing/GIFs/GiphyAPI.swift @@ -5,7 +5,7 @@ import Combine import CoreServices import UniformTypeIdentifiers import SignalUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Singleton diff --git a/Session/Media Viewing & Editing/ImagePickerController.swift b/Session/Media Viewing & Editing/ImagePickerController.swift index a7ea443621..ae89e63281 100644 --- a/Session/Media Viewing & Editing/ImagePickerController.swift +++ b/Session/Media Viewing & Editing/ImagePickerController.swift @@ -558,6 +558,29 @@ extension ImagePickerGridController: UIGestureRecognizerDelegate { return true } + + func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + if let pan = gestureRecognizer as? UIPanGestureRecognizer { + let velocity = pan.velocity(in: collectionView) + + // Threshold for what's considered "significant" movement in either direction. + let minVelocity: CGFloat = 30.0 + + guard abs(velocity.x) > minVelocity || abs(velocity.y) > minVelocity else { + // Not enough movement to make a decision, let other gestures handle it or ignore. + return false + } + + // We only want to activate the "drag to select" within a ~30 degree angle in either + // direction so approximate if the velocity is within this angle + let ratio = abs(velocity.y / velocity.x) + let tangentOf30DegreeBuffer: CGFloat = 0.577 // This is about `tan(30)` + + return (ratio < tangentOf30DegreeBuffer) + } + + return true + } } protocol TitleViewDelegate: AnyObject { diff --git a/Session/Media Viewing & Editing/MediaPageViewController.swift b/Session/Media Viewing & Editing/MediaPageViewController.swift index 51523207fb..28ab4d2426 100644 --- a/Session/Media Viewing & Editing/MediaPageViewController.swift +++ b/Session/Media Viewing & Editing/MediaPageViewController.swift @@ -6,7 +6,7 @@ import SessionUIKit import SessionMessagingKit import SignalUtilitiesKit import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate, MediaDetailViewControllerDelegate, InteractivelyDismissableViewController { class DynamicallySizedView: UIView { @@ -592,10 +592,11 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou ) // Delete any interactions which had all of their attachments removed - _ = try Interaction - .filter(id: itemToDelete.interactionId) - .having(Interaction.interactionAttachments.isEmpty) - .deleteAll(db) + try Interaction.deleteWhere( + db, + .filter(Interaction.Columns.id == itemToDelete.interactionId), + .hasAttachments(false) + ) } } actionSheet.addAction(UIAlertAction(title: "cancel".localized(), style: .cancel)) diff --git a/Session/Media Viewing & Editing/MediaTileViewController.swift b/Session/Media Viewing & Editing/MediaTileViewController.swift index de5111095b..a5cc997b8c 100644 --- a/Session/Media Viewing & Editing/MediaTileViewController.swift +++ b/Session/Media Viewing & Editing/MediaTileViewController.swift @@ -733,16 +733,12 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour ) // Delete any interactions which had all of their attachments removed - try items.forEach { item in - let remainingAttachmentCount: Int = try InteractionAttachment - .filter(InteractionAttachment.Columns.interactionId == item.interactionId) - .fetchCount(db) - - if remainingAttachmentCount == 0 { - _ = try Interaction.deleteOne(db, id: item.interactionId) - db.addMessageEvent(id: item.interactionId, threadId: threadId, type: .deleted) - } - } + try Interaction.deleteWhere( + db, + .filter(items.map { $0.interactionId }.contains(Interaction.Columns.id)), + .filter(Interaction.Columns.threadId == threadId), + .hasAttachments(false) + ) } self?.endSelectMode() @@ -878,22 +874,11 @@ class GalleryGridCellItem: PhotoGridItem { self.galleryItem = galleryItem } - var type: PhotoGridItemType { - if galleryItem.isVideo { - return .video - } - - if galleryItem.isAnimated { - return .animated - } - - return .photo - } - + var isVideo: Bool { galleryItem.isVideo } var source: ImageDataManager.DataSource { ImageDataManager.DataSource.thumbnailFrom( attachment: galleryItem.attachment, - size: .medium, + size: .small, using: dependencies ) ?? .image("", nil) } diff --git a/Session/Media Viewing & Editing/MessageInfoScreen.swift b/Session/Media Viewing & Editing/MessageInfoScreen.swift index e614b81b5f..6c42a3751c 100644 --- a/Session/Media Viewing & Editing/MessageInfoScreen.swift +++ b/Session/Media Viewing & Editing/MessageInfoScreen.swift @@ -2,7 +2,7 @@ import SwiftUI import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit import SessionMessagingKit @@ -183,7 +183,7 @@ struct MessageInfoScreen: View { spacing: Values.mediumSpacing ) { InfoBlock(title: "attachmentsFileId".localized()) { - Text(attachment.downloadUrl.map { Attachment.fileId(for: $0) } ?? "") + Text(attachment.downloadUrl.map { Network.FileServer.fileId(for: $0) } ?? "") .font(.system(size: Values.mediumFontSize)) .foregroundColor(themeColor: .textPrimary) } @@ -456,7 +456,12 @@ struct MessageBubble: View { var body: some View { ZStack { - let maxWidth: CGFloat = (VisibleMessageCell.getMaxWidth(for: messageViewModel) - 2 * Self.inset) + let maxWidth: CGFloat = ( + VisibleMessageCell.getMaxWidth( + for: messageViewModel, + cellWidth: UIScreen.main.bounds.width + ) - 2 * Self.inset + ) VStack( alignment: .leading, diff --git a/Session/Media Viewing & Editing/PhotoCapture.swift b/Session/Media Viewing & Editing/PhotoCapture.swift index d4e1bc8d2d..30b38d6a73 100644 --- a/Session/Media Viewing & Editing/PhotoCapture.swift +++ b/Session/Media Viewing & Editing/PhotoCapture.swift @@ -6,7 +6,7 @@ import Foundation import Combine import AVFoundation import CoreServices -import SessionSnodeKit +import SessionNetworkingKit import SessionMessagingKit import SessionUtilitiesKit diff --git a/Session/Media Viewing & Editing/PhotoGridViewCell.swift b/Session/Media Viewing & Editing/PhotoGridViewCell.swift index f32643c210..b687238049 100644 --- a/Session/Media Viewing & Editing/PhotoGridViewCell.swift +++ b/Session/Media Viewing & Editing/PhotoGridViewCell.swift @@ -6,28 +6,60 @@ import UIKit import SessionUIKit import SessionUtilitiesKit -public enum PhotoGridItemType { - case photo, animated, video -} - public protocol PhotoGridItem: AnyObject { - var type: PhotoGridItemType { get } + var isVideo: Bool { get } var source: ImageDataManager.DataSource { get } } public class PhotoGridViewCell: UICollectionViewCell { - public let imageView: SessionImageView - - private let contentTypeBadgeView: UIImageView - private let selectedBadgeView: UIImageView - - private let highlightedView: UIView - private let selectedView: UIView + private static let badgeSize: CGSize = CGSize(width: 32, height: 32) + public let imageView: SessionImageView = { + let result: SessionImageView = SessionImageView() + result.contentMode = .scaleAspectFill + + return result + }() + + private let contentTypeBadgeView: UIImageView = { + let result: UIImageView = UIImageView() + result.image = UIImage(named: "ic_gallery_badge_video") + result.isHidden = true + + return result + }() + + private let selectedBadgeView: UIImageView = { + let result: UIImageView = UIImageView() + result.image = UIImage(systemName: "checkmark.circle.fill")?.withRenderingMode(.alwaysTemplate) + result.themeTintColor = .primary + result.themeBorderColor = .textPrimary + result.themeBackgroundColor = .textPrimary + result.isHidden = true + result.layer.cornerRadius = (PhotoGridViewCell.badgeSize.width / 2) + + return result + }() + + private let highlightedView: UIView = { + let result: UIView = UIView() + result.alpha = 0.2 + result.themeBackgroundColor = .black + result.isHidden = true + + return result + }() + + private let selectedView: UIView = { + let result: UIView = UIView() + result.alpha = 0.3 + result.themeBackgroundColor = .black + result.isHidden = true + + return result + }() var item: PhotoGridItem? - private static let videoBadgeImage = #imageLiteral(resourceName: "ic_gallery_badge_video") - private static let animatedBadgeImage = #imageLiteral(resourceName: "ic_gallery_badge_gif") private static let selectedBadgeImage = UIImage(systemName: "checkmark.circle.fill") override public var isSelected: Bool { @@ -44,31 +76,6 @@ public class PhotoGridViewCell: UICollectionViewCell { } override init(frame: CGRect) { - self.imageView = SessionImageView() - imageView.contentMode = .scaleAspectFill - - self.contentTypeBadgeView = UIImageView() - contentTypeBadgeView.isHidden = true - - let kSelectedBadgeSize = CGSize(width: 32, height: 32) - self.selectedBadgeView = UIImageView() - selectedBadgeView.image = PhotoGridViewCell.selectedBadgeImage?.withRenderingMode(.alwaysTemplate) - selectedBadgeView.themeTintColor = .primary - selectedBadgeView.themeBorderColor = .textPrimary - selectedBadgeView.themeBackgroundColor = .textPrimary - selectedBadgeView.isHidden = true - selectedBadgeView.layer.cornerRadius = (kSelectedBadgeSize.width / 2) - - self.highlightedView = UIView() - highlightedView.alpha = 0.2 - highlightedView.themeBackgroundColor = .black - highlightedView.isHidden = true - - self.selectedView = UIView() - selectedView.alpha = 0.3 - selectedView.themeBackgroundColor = .black - selectedView.isHidden = true - super.init(frame: frame) self.clipsToBounds = true @@ -92,8 +99,8 @@ public class PhotoGridViewCell: UICollectionViewCell { selectedBadgeView.pin(.trailing, to: .trailing, of: contentView, withInset: -Values.verySmallSpacing) selectedBadgeView.pin(.bottom, to: .bottom, of: contentView, withInset: -Values.verySmallSpacing) - selectedBadgeView.set(.width, to: kSelectedBadgeSize.width) - selectedBadgeView.set(.height, to: kSelectedBadgeSize.height) + selectedBadgeView.set(.width, to: PhotoGridViewCell.badgeSize.width) + selectedBadgeView.set(.height, to: PhotoGridViewCell.badgeSize.height) } @available(*, unavailable, message: "Unimplemented") @@ -105,30 +112,17 @@ public class PhotoGridViewCell: UICollectionViewCell { self.item = item imageView.setDataManager(dependencies[singleton: .imageDataManager]) imageView.themeBackgroundColor = .textSecondary - imageView.loadImage(item.source) { [weak imageView] success in - imageView?.themeBackgroundColor = (success ? .clear : .textSecondary) - } - - switch item.type { - case .video: - contentTypeBadgeView.image = PhotoGridViewCell.videoBadgeImage - contentTypeBadgeView.isHidden = false - - case .animated: - contentTypeBadgeView.image = PhotoGridViewCell.animatedBadgeImage - contentTypeBadgeView.isHidden = false - - case .photo: - contentTypeBadgeView.image = nil - contentTypeBadgeView.isHidden = true + imageView.loadImage(item.source) { [weak imageView] processedData in + imageView?.themeBackgroundColor = (processedData != nil ? .clear : .textSecondary) } + + contentTypeBadgeView.isHidden = !item.isVideo } override public func prepareForReuse() { super.prepareForReuse() self.item = nil - self.imageView.image = nil self.contentTypeBadgeView.isHidden = true self.highlightedView.isHidden = true self.selectedView.isHidden = true diff --git a/Session/Media Viewing & Editing/PhotoLibrary.swift b/Session/Media Viewing & Editing/PhotoLibrary.swift index 4523cd8532..16f3c4e715 100644 --- a/Session/Media Viewing & Editing/PhotoLibrary.swift +++ b/Session/Media Viewing & Editing/PhotoLibrary.swift @@ -47,18 +47,9 @@ class PhotoPickerAssetItem: PhotoGridItem { // MARK: PhotoGridItem - var type: PhotoGridItemType { - if asset.mediaType == .video { - return .video - } - - // TODO show GIF badge? - - return .photo - } - + var isVideo: Bool { asset.mediaType == .video } var source: ImageDataManager.DataSource { - return .closureThumbnail(self.asset.localIdentifier, size) { [photoCollectionContents, asset, size, pixelDimension] in + return .asyncSource(self.asset.localIdentifier) { [photoCollectionContents, asset, size, pixelDimension] in await photoCollectionContents.requestThumbnail( for: asset, size: size, @@ -148,44 +139,70 @@ class PhotoCollectionContents { // MARK: ImageManager - func requestThumbnail(for asset: PHAsset, size: ImageDataManager.ThumbnailSize, thumbnailSize: CGSize) async -> UIImage? { + func requestThumbnail(for asset: PHAsset, size: ImageDataManager.ThumbnailSize, thumbnailSize: CGSize) async -> ImageDataManager.DataSource? { var hasResumed: Bool = false - return await withCheckedContinuation { [imageManager] continuation in - let options = PHImageRequestOptions() - - switch size { - case .small: options.deliveryMode = .opportunistic - case .medium, .large: options.deliveryMode = .highQualityFormat - } - - imageManager.requestImage( - for: asset, - targetSize: thumbnailSize, - contentMode: .aspectFill, - options: options - ) { image, info in - guard !hasResumed else { return } - guard - info?[PHImageErrorKey] == nil, - (info?[PHImageCancelledKey] as? Bool) != true - else { - hasResumed = true - return continuation.resume(returning: nil) + /// The `requestImage` function will always return a static thumbnail so if it's an animated image then we need custom + /// handling (the default PhotoKit resizing can't resize animated images so we need to return the original file) + switch asset.utType?.isAnimated { + case .some(true): + return await withCheckedContinuation { [imageManager] continuation in + let options = PHImageRequestOptions() + options.deliveryMode = .highQualityFormat + options.isNetworkAccessAllowed = true + + imageManager.requestImageDataAndOrientation(for: asset, options: options) { data, uti, orientation, info in + guard !hasResumed else { return } + + guard let data = data, info?[PHImageErrorKey] == nil else { + hasResumed = true + continuation.resume(returning: nil) + return + } + + // Successfully fetched the data, resume with the animated result + hasResumed = true + continuation.resume(returning: .data(asset.localIdentifier, data)) + } } - switch size { - case .small: break // We want the first image, whether it is degraded or not - case .medium, .large: - // For medium and large thumbnails we want the full image so ignore any - // degraded images - guard (info?[PHImageResultIsDegradedKey] as? Bool) != true else { return } - + default: + return await withCheckedContinuation { [imageManager] continuation in + let options = PHImageRequestOptions() + + switch size { + case .small: options.deliveryMode = .opportunistic + case .medium, .large: options.deliveryMode = .highQualityFormat + } + + imageManager.requestImage( + for: asset, + targetSize: thumbnailSize, + contentMode: .aspectFill, + options: options + ) { image, info in + guard !hasResumed else { return } + guard + info?[PHImageErrorKey] == nil, + (info?[PHImageCancelledKey] as? Bool) != true + else { + hasResumed = true + return continuation.resume(returning: nil) + } + + switch size { + case .small: break // We want the first image, whether it is degraded or not + case .medium, .large: + // For medium and large thumbnails we want the full image so ignore any + // degraded images + guard (info?[PHImageResultIsDegradedKey] as? Bool) != true else { return } + + } + + continuation.resume(returning: .image("\(asset.localIdentifier)-\(size)", image)) + hasResumed = true + } } - - continuation.resume(returning: image) - hasResumed = true - } } } @@ -238,14 +255,34 @@ class PhotoCollectionContents { Future { [weak self] resolver in let options: PHVideoRequestOptions = PHVideoRequestOptions() options.isNetworkAccessAllowed = true + options.deliveryMode = .highQualityFormat - _ = self?.imageManager.requestExportSession(forVideo: asset, options: options, exportPreset: AVAssetExportPresetMediumQuality) { exportSession, info in + self?.imageManager.requestAVAsset(forVideo: asset, options: options) { avAsset, _, info in if let error: Error = info?[PHImageErrorKey] as? Error { return resolver(.failure(error)) } - guard let exportSession = exportSession else { + guard let avAsset: AVAsset = avAsset else { + return resolver(Result.failure(PhotoLibraryError.assertionError(description: "avAsset was unexpectedly nil"))) + } + + let compatiblePresets = AVAssetExportSession.exportPresets(compatibleWith: avAsset) + var bestExportPreset: String + + if compatiblePresets.contains(AVAssetExportPresetPassthrough) { + bestExportPreset = AVAssetExportPresetPassthrough + Log.debug("[PhotoLibrary] Using Passthrough export preset.") + } else { + bestExportPreset = AVAssetExportPresetHighestQuality + Log.debug("[PhotoLibrary] Passthrough not available. Falling back to HighestQuality export preset.") + } + + if (info?[PHImageCancelledKey] as? Bool) == true { + return resolver(.failure(PhotoLibraryError.assertionError(description: "Video request cancelled"))) + } + + guard let exportSession: AVAssetExportSession = AVAssetExportSession(asset: avAsset, presetName: bestExportPreset) else { resolver(Result.failure(PhotoLibraryError.assertionError(description: "exportSession was unexpectedly nil"))) return } @@ -462,3 +499,10 @@ class PhotoLibrary: NSObject, PHPhotoLibraryChangeObserver { return collections } } + +private extension PHAsset { + var utType: UTType? { + return (value(forKey: "uniformTypeIdentifier") as? String) // stringlint:ignore + .map { UTType($0) } + } +} diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index 46cafadd53..e70361c687 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -8,7 +8,7 @@ import SessionUIKit import SessionMessagingKit import SessionUtilitiesKit import SignalUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit // MARK: - Log.Category @@ -70,13 +70,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD self.loadingViewController = LoadingViewController() AppSetup.setupEnvironment( - additionalMigrationTargets: [DeprecatedUIKitMigrationTarget.self], appSpecificBlock: { [dependencies] in Log.setup(with: Logger(primaryPrefix: "Session", using: dependencies)) Log.info(.cat, "Setting up environment.") - /// Create a proper `NotificationPresenter` and `SessionCallManager` for the main app (defaults to no-op versions) - dependencies.set(singleton: .notificationsManager, to: NotificationPresenter(using: dependencies)) + /// Create a proper `SessionCallManager` for the main app (defaults to a no-op version) dependencies.set(singleton: .callManager, to: SessionCallManager(using: dependencies)) // Setup LibSession @@ -86,6 +84,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // Configure the different targets SNUtilitiesKit.configure( networkMaxFileSize: Network.maxFileSize, + maxValidImageDimention: ImageDataManager.DataSource.maxValidDimension, using: dependencies ) SNMessagingKit.configure(using: dependencies) @@ -168,10 +167,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD mainWindow.rootViewController = self.loadingViewController mainWindow.makeKeyAndVisible() - // This must happen in appDidFinishLaunching or earlier to ensure we don't - // miss notifications. - // Setting the delegate also seems to prevent us from getting the legacy notification - // notification callbacks upon launch e.g. 'didReceiveLocalNotification' + /// Create a proper `NotificationPresenter` for the main app (defaults to a no-op version) + /// + /// **Note:** This must happen in `appDidFinishLaunching` to ensure we don't miss notifications. Setting the delegate + /// also seems to prevent us from getting the legacy notification notification callbacks upon launch e.g. `didReceiveLocalNotification` + dependencies.set(singleton: .notificationsManager, to: NotificationPresenter(using: dependencies)) dependencies[singleton: .notificationsManager].setDelegate(self) NotificationCenter.default.addObserver( @@ -221,7 +221,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // Dispatch async so things can continue to be progressed if a migration does need to run DispatchQueue.global(qos: .userInitiated).async { [weak self, dependencies] in AppSetup.runPostSetupMigrations( - additionalMigrationTargets: [DeprecatedUIKitMigrationTarget.self], migrationProgressChanged: { progress, minEstimatedTotalTime in self?.loadingViewController?.updateProgress( progress: progress, @@ -307,11 +306,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } } - // On every activation, clear old temp directories. + /// On every activation, clear old temp directories. dependencies[singleton: .fileManager].clearOldTemporaryDirectories() - if dependencies.mutate(cache: .libSession, { $0.get(.areCallsEnabled) }) && dependencies[defaults: .standard, key: .hasRequestedLocalNetworkPermission] { - Permissions.checkLocalNetworkPermission(using: dependencies) + /// It's likely that on a fresh launch that the `libSession` cache won't have been initialised by this point, so detatch a task to + /// wait for it before checking the local network permission + Task.detached { [dependencies] in + try? await dependencies.waitUntilInitialised(cache: .libSession) + + if dependencies.mutate(cache: .libSession, { $0.get(.areCallsEnabled) }) && dependencies[defaults: .standard, key: .hasRequestedLocalNetworkPermission] { + Permissions.checkLocalNetworkPermission(using: dependencies) + } } } @@ -456,7 +461,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD /// We need to do a clean up for disappear after send messages that are received by push notifications before /// the app set up the main screen and load initial data to prevent a case when the PagedDatabaseObserver /// hasn't been setup yet then the conversation screen can show stale (ie. deleted) interactions incorrectly - DisappearingMessagesJob.cleanExpiredMessagesOnLaunch(using: dependencies) + DisappearingMessagesJob.cleanExpiredMessagesOnResume(using: dependencies) /// Now that the database is setup we can load in any messages which were processed by the extensions (flag that we will load /// them in this thread and create a task to _actually_ load them asynchronously @@ -599,7 +604,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // The re-run the migration (should succeed since there is no data) AppSetup.runPostSetupMigrations( - additionalMigrationTargets: [DeprecatedUIKitMigrationTarget.self], migrationProgressChanged: { [weak self] progress, minEstimatedTotalTime in self?.loadingViewController?.updateProgress( progress: progress, @@ -700,7 +704,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD self?.startPollersIfNeeded() - SessionNetworkAPI.client.initialize(using: dependencies) + Network.SessionNetwork.client.initialize(using: dependencies) if dependencies[singleton: .appContext].isMainApp { DispatchQueue.main.async { diff --git a/Session/Meta/Images.xcassets/ic_gallery_badge_gif.imageset/Contents.json b/Session/Meta/Images.xcassets/ic_gallery_badge_gif.imageset/Contents.json deleted file mode 100644 index be7b71198d..0000000000 --- a/Session/Meta/Images.xcassets/ic_gallery_badge_gif.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "icon_GIF@1x.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "icon_GIF@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "icon_GIF@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Session/Meta/Images.xcassets/ic_gallery_badge_gif.imageset/icon_GIF@1x.png b/Session/Meta/Images.xcassets/ic_gallery_badge_gif.imageset/icon_GIF@1x.png deleted file mode 100644 index 58592c8463..0000000000 Binary files a/Session/Meta/Images.xcassets/ic_gallery_badge_gif.imageset/icon_GIF@1x.png and /dev/null differ diff --git a/Session/Meta/Images.xcassets/ic_gallery_badge_gif.imageset/icon_GIF@2x.png b/Session/Meta/Images.xcassets/ic_gallery_badge_gif.imageset/icon_GIF@2x.png deleted file mode 100644 index 529bb11dae..0000000000 Binary files a/Session/Meta/Images.xcassets/ic_gallery_badge_gif.imageset/icon_GIF@2x.png and /dev/null differ diff --git a/Session/Meta/Images.xcassets/ic_gallery_badge_gif.imageset/icon_GIF@3x.png b/Session/Meta/Images.xcassets/ic_gallery_badge_gif.imageset/icon_GIF@3x.png deleted file mode 100644 index df5f374ad4..0000000000 Binary files a/Session/Meta/Images.xcassets/ic_gallery_badge_gif.imageset/icon_GIF@3x.png and /dev/null differ diff --git a/Session/Meta/Session - Anonymous Messenger.storekit b/Session/Meta/Session - Anonymous Messenger.storekit new file mode 100644 index 0000000000..901eabe896 --- /dev/null +++ b/Session/Meta/Session - Anonymous Messenger.storekit @@ -0,0 +1,117 @@ +{ + "appPolicies" : { + "eula" : "", + "policies" : [ + { + "locale" : "en_US", + "policyText" : "", + "policyURL" : "" + } + ] + }, + "identifier" : "E83EE03B", + "nonRenewingSubscriptions" : [ + + ], + "products" : [ + + ], + "settings" : { + "_applicationInternalID" : "1470168868", + "_developerTeamID" : "SUQ8J2PCT7", + "_failTransactionsEnabled" : false, + "_lastSynchronizedDate" : 779753823.36554396, + "_locale" : "en_US", + "_storefront" : "USA", + "_storeKitErrors" : [ + { + "current" : null, + "enabled" : false, + "name" : "Load Products" + }, + { + "current" : null, + "enabled" : false, + "name" : "Purchase" + }, + { + "current" : null, + "enabled" : false, + "name" : "Verification" + }, + { + "current" : null, + "enabled" : false, + "name" : "App Store Sync" + }, + { + "current" : null, + "enabled" : false, + "name" : "Subscription Status" + }, + { + "current" : null, + "enabled" : false, + "name" : "App Transaction" + }, + { + "current" : null, + "enabled" : false, + "name" : "Manage Subscriptions Sheet" + }, + { + "current" : null, + "enabled" : false, + "name" : "Refund Request Sheet" + }, + { + "current" : null, + "enabled" : false, + "name" : "Offer Code Redeem Sheet" + } + ] + }, + "subscriptionGroups" : [ + { + "id" : "21752814", + "localizations" : [ + + ], + "name" : "Session Pro", + "subscriptions" : [ + { + "adHocOffers" : [ + + ], + "codeOffers" : [ + + ], + "displayPrice" : "0.99", + "familyShareable" : false, + "groupNumber" : 1, + "internalID" : "6749836944", + "introductoryOffer" : null, + "localizations" : [ + { + "description" : "Test 1 Week Session Pro Subscription", + "displayName" : "Test Session Pro", + "locale" : "en_US" + } + ], + "productID" : "com.getsession.org.pro_sub", + "recurringSubscriptionPeriod" : "P1W", + "referenceName" : "Session Pro Subscription", + "subscriptionGroupID" : "21752814", + "type" : "RecurringSubscription", + "winbackOffers" : [ + + ] + } + ] + } + ], + "version" : { + "major" : 4, + "minor" : 0 + } +} diff --git a/Session/Meta/Session+SNUIKit.swift b/Session/Meta/Session+SNUIKit.swift index 862f7a9eb3..2c7a2aedfe 100644 --- a/Session/Meta/Session+SNUIKit.swift +++ b/Session/Meta/Session+SNUIKit.swift @@ -3,7 +3,7 @@ import UIKit import AVFoundation import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - SessionSNUIKitConfig diff --git a/Session/Meta/SessionApp.swift b/Session/Meta/SessionApp.swift index 412d4be4b3..e311b2ed71 100644 --- a/Session/Meta/SessionApp.swift +++ b/Session/Meta/SessionApp.swift @@ -158,7 +158,7 @@ public class SessionApp: SessionAppType { /// Show Session Network Page for this release. We'll be able to extend this fuction to show other screens that is new /// or we want to promote in the future. - public func showPromotedScreen() { + @MainActor public func showPromotedScreen() { guard let homeViewController: HomeVC = self.homeViewController else { return } let viewController: SessionHostingViewController = SessionHostingViewController( @@ -243,7 +243,7 @@ public protocol SessionAppType { @MainActor var homePresentedViewController: UIViewController? { get } func setHomeViewController(_ homeViewController: HomeVC) - func showHomeView() + @MainActor func showHomeView() @MainActor func presentConversationCreatingIfNeeded( for threadId: String, variant: SessionThread.Variant, @@ -253,7 +253,7 @@ public protocol SessionAppType { ) func createNewConversation() func resetData(onReset: (() -> ())) - func showPromotedScreen() + @MainActor func showPromotedScreen() } public extension SessionAppType { diff --git a/Session/Meta/Translations/InfoPlist.xcstrings b/Session/Meta/Translations/InfoPlist.xcstrings index 8199912edc..3d97c3284c 100644 --- a/Session/Meta/Translations/InfoPlist.xcstrings +++ b/Session/Meta/Translations/InfoPlist.xcstrings @@ -1507,7 +1507,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Session, səsli və görüntülü zənglər edə bilmək üçün daxili şəbəkəyə müraciət etməlidir." + "value" : "Session, səsli və görüntülü zənglər edə bilmək üçün lokal şəbəkəyə erişməlidir." } }, "ca" : { @@ -1546,6 +1546,18 @@ "value" : "Session bezonas aliron al loka reto por fari voĉajn kaj video vokojn." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session necesita acceso a la red local para realizar llamadas de voz y video." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session necesita acceso a la red local para realizar llamadas de voz y video." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -1564,6 +1576,18 @@ "value" : "A(z) Session alkalmazásnak hozzáférésre van szüksége a helyi hálózathoz a hang- és videohívások indításához." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session necessita dell'accesso alla rete locale per effettuare chiamate vocali e video." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session は音声・ビデオ通話を行うためにローカルネットワークへのアクセスが必要です。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -1582,6 +1606,18 @@ "value" : "Session potrzebuje dostępu do sieci lokalnej, aby wykonywać połączenia głosowe i wideo." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session precisa de acesso à rede local para efetuar chamadas de voz e vídeo." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session are nevoie de acces la rețeaua locală pentru a efectua apeluri vocale și video." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -1594,6 +1630,12 @@ "value" : "Session behöver åtkomst till det lokala nätverket för att kunna ringa röst och videosamtal." } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session uygulamasının sesli ve görüntülü arama yapabilmesi için yerel ağa erişmesi gerekiyor." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -1611,6 +1653,12 @@ "state" : "translated", "value" : "Session需要访问本地网络才能进行语音和视频通话。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "Session 需要存取本地網路以進行語音與視訊通話。" + } } } }, @@ -2117,7 +2165,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Session qoşmaları və medianı saxlamaq üçün anbara müraciət etməlidir." + "value" : "Session qoşmaları və medianı saxlamaq üçün anbara erişməlidir." } }, "bal" : { diff --git a/Session/Meta/Translations/Localizable.xcstrings b/Session/Meta/Translations/Localizable.xcstrings index 0a4a817c36..929defed6a 100644 --- a/Session/Meta/Translations/Localizable.xcstrings +++ b/Session/Meta/Translations/Localizable.xcstrings @@ -980,6 +980,12 @@ "value" : "معرف الحساب" } }, + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hesab ID-si" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -992,6 +998,12 @@ "value" : "ID účtu" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Account-ID" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -1004,12 +1016,30 @@ "value" : "Konta ID" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "ID de cuenta" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "ID de cuenta" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "ID du client" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "खाता ID" + } + }, "hu" : { "stringUnit" : { "state" : "translated", @@ -1022,6 +1052,18 @@ "value" : "ID Akun" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "ID account" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アカウント ID" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -1040,11 +1082,53 @@ "value" : "Identyfikator konta" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "ID da Conta" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "ID cont" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "ID аккаунта" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Account ID" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hesap kimliği" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Ідентифікатор облікового запису" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "账户 ID" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "帳號 ID" + } } } }, @@ -6859,6 +6943,12 @@ "value" : "Tilføj administratorer" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Administratoren hinzufügen" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -6871,12 +6961,30 @@ "value" : "Aldoni administrantojn" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Añadir Administradores" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Añadir Administradores" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Ajouter des administrateurs" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "एडमिन जोड़ें" + } + }, "hu" : { "stringUnit" : { "state" : "translated", @@ -6889,6 +6997,18 @@ "value" : "Tambah Admin" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aggiungi amministratori" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "管理者を追加する" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -6907,6 +7027,36 @@ "value" : "Dodaj administratorów" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Adicionar administradores" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Adaugă administratori" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Добавить администраторов" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lägg till administratörer" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yönetici Ekle" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -6918,12 +7068,24 @@ "state" : "translated", "value" : "添加管理员" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "新增管理員" + } } } }, "addAdminsDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Admin edəcəyiniz istifadəçinin Hesab ID-sini daxil edin.

Birdən çox istifadəçi əlavə etmək üçün vergüllə ayrılmış hər Hesab ID-sini daxil edin. Bir dəfəyə 20-yə qədər Hesab ID-si daxil edilə bilər." + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -6936,24 +7098,60 @@ "value" : "Zadejte ID účtu uživatele, kterého povyšujete na správce.

Chcete-li přidat více uživatelů, zadejte každé ID účtu oddělené čárkou. Najednou lze zadat až 20 ID účtů." } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gib die Account-ID des Nutzers ein, den du zum Administrator ernennst.

Um mehrere Nutzer hinzuzufügen, gib jede Account-ID durch ein Komma getrennt ein. Es können bis zu 20 Account-IDs gleichzeitig angegeben werden." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Enter the Account ID of the user you are promoting to admin.

To add multiple users, enter each Account ID separated by a comma. Up to 20 Account IDs can be specified at a time." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ingrese el Account ID del usuario que desea promover a administrador.

Para agregar varios usuarios, ingrese cada Account ID separado por una coma. Puede especificar hasta 20 Account ID a la vez." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ingrese el Account ID del usuario que desea promover a administrador.

Para agregar varios usuarios, ingrese cada Account ID separado por una coma. Puede especificar hasta 20 Account ID a la vez." + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Entrez l'identifiant du compte de l'utilisateur que vous souhaitez promouvoir en administrateur.

Pour ajouter plusieurs utilisateurs, saisissez chaque identifiant de compte séparé par une virgule. Vous pouvez spécifier jusqu'à 20 identifiants à la fois." } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "उस उपयोगकर्ता का Account ID दर्ज करें जिसे आप एडमिन बना रहे हैं।

एक से अधिक उपयोगकर्ताओं को जोड़ने के लिए, प्रत्येक Account ID को कॉमा से अलग करके दर्ज करें। एक बार में अधिकतम 20 Account ID दर्ज किए जा सकते हैं।" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Adja meg a felhasználó fiókazonosítóját, akit adminisztrátorrá kíván kinevezni.

Egyszerre több felhasználó hozzáadásához adja meg az egyes fiókazonosítókat vesszővel elválasztva. Egyszerre legfeljebb 20 fiókazonosító adható meg." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inserisci l'Account ID dell'utente che vuoi promuovere ad amministratore.

Per aggiungere più utenti, inserisci ogni Account ID separato da una virgola. È possibile specificare fino a 20 Account ID alla volta." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "管理者に昇格させるユーザーのAccount IDを入力してください。

複数のユーザーを追加するには、各Account IDをカンマで区切って入力してください。一度に最大20件まで指定できます。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -6972,6 +7170,36 @@ "value" : "Wprowadź identyfikator konta użytkownika, którego chcesz awansować na administratora.

Aby dodać wielu użytkowników, wpisz każdy identyfikator konta oddzielone przecinkiem. Można jednocześnie podać maksymalnie 20 identyfikatorów kont." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Introduza o ID de Conta do utilizador que está a promover a administrador.

Para adicionar vários utilizadores, introduza cada ID de Conta separado por vírgulas. Podem ser especificados até 20 IDs de Conta de cada vez." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Introdu ID-ul contului utilizatorului pe care îl promovezi ca administrator.

Pentru a adăuga mai mulți utilizatori, introduceți fiecare ID al contului separat prin virgulă. Pot fi specificate până la 20 de ID-uri de cont o dată." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Введите ID аккаунта пользователя, которого вы повышаете до администратора.

Чтобы добавить нескольких пользователей, введите ID каждого аккаунта через запятую. Одновременно можно указать до 20 идентификаторов учётных записей." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ange Account ID för användaren du gör till administratör.

För att lägga till flera användare, ange varje Account ID separerat med ett kommatecken. Upp till 20 Account ID:er kan anges åt gången." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yönetici olarak atadığınız kullanıcının Hesap Kimliğini girin.

Birden fazla kullanıcı eklemek için her Hesap Kimliğini virgülle ayırarak girin. Tek seferde en fazla 20 Hesap Kimliği belirtilebilir." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -6983,6 +7211,12 @@ "state" : "translated", "value" : "请输入您正在授权为管理员的用户的帐户 ID。

要添加多个用户,请输入用逗号分隔的每个帐户 ID。一次最多可以指定20个帐户 ID。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "請輸入您要晉升為管理員的使用者的 Account ID。

若要新增多位使用者,請輸入以逗號分隔的每個 Account ID。一次最多可指定 20 個 Account ID。" + } } } }, @@ -12887,6 +13121,12 @@ "value" : "Promozione non inviata" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "昇進が送信されていません" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -12905,6 +13145,18 @@ "value" : "Promocja niewysłana" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promoção não enviada" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promovarea nu a fost trimisă" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -12940,6 +13192,12 @@ "state" : "translated", "value" : "授权未发送" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "晉升未傳送" + } } } }, @@ -13521,6 +13779,12 @@ "value" : "Stato promozione sconosciuto" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "昇進のステータスが不明です" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -13539,6 +13803,18 @@ "value" : "Status promocji nieznany" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Estado da promoção desconhecido" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Statusul promovării necunoscut" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -13574,6 +13850,12 @@ "state" : "translated", "value" : "授权状态未知" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "晉升狀態未知" + } } } }, @@ -18053,6 +18335,34 @@ } } }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Στέλνεται προαγωγή διαχειριστή" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Στέλνονται προαγωγές διαχειριστή" + } + } + } + } + } + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -18165,6 +18475,34 @@ } } }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Administraatori edutamine" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Administraatori edutamine on saatmisel" + } + } + } + } + } + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -18299,6 +18637,28 @@ } } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "アドミンへの昇進を送信中" + } + } + } + } + } + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -18321,6 +18681,34 @@ } } }, + "ku" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "ناردنی پرۆمۆشنی ئەدمین" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "ناردنی پرۆمۆشنی ئەدمین" + } + } + } + } + } + } + }, "ku-TR" : { "stringUnit" : { "state" : "translated", @@ -18349,6 +18737,34 @@ } } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sender adminforfremmelse" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sender adminforfremmelser" + } + } + } + } + } + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -18417,6 +18833,68 @@ } } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "A enviar promoção de administrador" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "A enviar promoções de administrador" + } + } + } + } + } + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se trimit promovările la nivel de administrator" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se trimite promovarea la nivel de administrator" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se trimit promovările la nivel de administrator" + } + } + } + } + } + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -18596,6 +19074,28 @@ } } } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "晉升中" + } + } + } + } + } + } } } }, @@ -20536,52 +21036,10 @@ "appearanceAutoDarkMode" : { "extractionState" : "manual", "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Auto donker-modus" - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "الوضع المظلم التلقائي" - } - }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "Avto qaranlıq rejim" - } - }, - "bal" : { - "stringUnit" : { - "state" : "translated", - "value" : "آٹو ڈارک موڈ." - } - }, - "be" : { - "stringUnit" : { - "state" : "translated", - "value" : "Аўтаматычны цёмная тэма" - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Автоматичен тъмен режим" - } - }, - "bn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Auto dark-mode" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mode fosc automàtic" + "value" : "Avto-qaranlıq rejimi" } }, "cs" : { @@ -20590,250 +21048,22 @@ "value" : "Automatický tmavý režim" } }, - "cy" : { - "stringUnit" : { - "state" : "translated", - "value" : "Auto ddull tywyll" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Auto mørk tilstand" - } - }, "de" : { "stringUnit" : { "state" : "translated", "value" : "Automatischer Dunkler Modus" } }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Αυτόματη σκοτεινή λειτουργία" - } - }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Auto dark-mode" - } - }, - "eo" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aŭtomata malhela reĝimo" - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Modo oscuro automático" - } - }, - "es-ES" : { - "stringUnit" : { - "state" : "translated", - "value" : "Modo oscuro automático" - } - }, - "et" : { - "stringUnit" : { - "state" : "translated", - "value" : "Autom. tumerežiim" - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Auto dark-mode" - } - }, - "fa" : { - "stringUnit" : { - "state" : "translated", - "value" : "حالت تیرهٔ خودکار" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Automaattinen tumma tila" - } - }, - "fil" : { - "stringUnit" : { - "state" : "translated", - "value" : "Auto dark-mode" + "value" : "Auto Dark Mode" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Thème sombre automatique" - } - }, - "gl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Modo escuro automático" - } - }, - "ha" : { - "stringUnit" : { - "state" : "translated", - "value" : "Yanayin duhu-atomatik" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "מצב כהה אוטומטי" - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "स्वचालित डार्क-मोड" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Automatski tamni način" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Automatikus sötét mód" - } - }, - "hy-AM" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ավտոմատ մութ ռեժիմ" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mode gelap otomatis" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Modalità scura automatica" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "オートダークモード" - } - }, - "ka" : { - "stringUnit" : { - "state" : "translated", - "value" : "ავტომატიკური დაბნელების რეჟიმი" - } - }, - "km" : { - "stringUnit" : { - "state" : "translated", - "value" : "ម៉ូដងងឹតដោយស្វ័យប្រវត្តិ" - } - }, - "kn" : { - "stringUnit" : { - "state" : "translated", - "value" : "ಸ್ವಯಂ ಡಾರ್ಕ್ ಮೋಡ್" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "자동 다크 모드" - } - }, - "ku" : { - "stringUnit" : { - "state" : "translated", - "value" : "ئۆتۆ مود - تەختە" - } - }, - "ku-TR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Moda tarî bi otomatîk" - } - }, - "lg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Auto dark-mode" - } - }, - "lo" : { - "stringUnit" : { - "state" : "translated", - "value" : "ໂໂມດມືດອັດຕະໂນມັດ" - } - }, - "lt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Automatinis tamsus režimas" - } - }, - "lv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Automātiska tumša tēma" - } - }, - "mk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Автоматски темен режим" - } - }, - "mn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Автомат бараан горим" - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "Auto mode-gelap" - } - }, - "my" : { - "stringUnit" : { - "state" : "translated", - "value" : "အလိုအလျောက် အမှောင်-mode" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Automatisk mørk-modus" - } - }, - "nb-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Automatisk mørkmodus" - } - }, - "ne-NP" : { - "stringUnit" : { - "state" : "translated", - "value" : "स्वतः अँध्यारो मोड" + "value" : "Mode sombre automatique" } }, "nl" : { @@ -20842,48 +21072,12 @@ "value" : "Automatische nachtmodus" } }, - "nn-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Automatisk mørkmodus" - } - }, - "ny" : { - "stringUnit" : { - "state" : "translated", - "value" : "Self mode yowala yowala" - } - }, - "pa-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "ਆਟੋ ਡਾਰਕ-ਮੋਡ" - } - }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Automatyczny tryb ciemny" } }, - "ps" : { - "stringUnit" : { - "state" : "translated", - "value" : "Auto dark-mode" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Modo escuro automático" - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Modo escuro automático" - } - }, "ro" : { "stringUnit" : { "state" : "translated", @@ -20893,85 +21087,19 @@ "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Автоматический темный режим" - } - }, - "sh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Automatski tamni mod" - } - }, - "si-LK" : { - "stringUnit" : { - "state" : "translated", - "value" : "ස්වයං අඳුරු ප්‍රකාරය" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Automatický tmavý režim" - } - }, - "sl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Samodejni temni način" - } - }, - "sq" : { - "stringUnit" : { - "state" : "translated", - "value" : "Modaliteti automatik i errësirës" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Аутоматски тамни режим" - } - }, - "sr-Latn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Automatski režim tamne teme" + "value" : "Автоматический тёмный режим" } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Automatiskt mörkläge" - } - }, - "sw" : { - "stringUnit" : { - "state" : "translated", - "value" : "Njia ya giza ya kiotomatiki" - } - }, - "ta" : { - "stringUnit" : { - "state" : "translated", - "value" : "தானியங்கு இருண்ட பயன்முறை" - } - }, - "te" : { - "stringUnit" : { - "state" : "translated", - "value" : "ఆటో డార్క్-మోడ్" - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "โหมดมืดอัตโนมัติ" + "value" : "Automatisk mörkt läge" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Otomatik karanlık-mod" + "value" : "Otomatik karanlık tema" } }, "uk" : { @@ -20979,42 +21107,6 @@ "state" : "translated", "value" : "Автоматичний темний режим" } - }, - "ur-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "آٹو ڈارک موڈ" - } - }, - "uz" : { - "stringUnit" : { - "state" : "translated", - "value" : "Avtomatik qorong'u rejim" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Chế độ tối tự động" - } - }, - "xh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Imowudi emnyama ngokuzenzekelayo" - } - }, - "zh-CN" : { - "stringUnit" : { - "state" : "translated", - "value" : "自动开启深色模式" - } - }, - "zh-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "自動深色模式" - } } } }, @@ -28272,6 +28364,18 @@ "value" : "Piktogramo de aplikaĵo" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ícono de la Aplicación" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ícono de la app" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -28296,6 +28400,12 @@ "value" : "Ikon Aplikasi" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Icona dell'app" + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -28314,6 +28424,18 @@ "value" : "앱 아이콘" } }, + "ku" : { + "stringUnit" : { + "state" : "translated", + "value" : "ئایکۆنی ئەپ" + } + }, + "ku-TR" : { + "stringUnit" : { + "state" : "translated", + "value" : "ئایکۆنی ئەپ" + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -28326,6 +28448,18 @@ "value" : "Ikona aplikacji" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ícone da aplicação" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pictogramă" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -28338,6 +28472,12 @@ "value" : "App-ikon" } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uygulama ikonu" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -28355,6 +28495,12 @@ "state" : "translated", "value" : "应用图标" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "圖示" + } } } }, @@ -28367,6 +28513,12 @@ "value" : "تغيير اسم و أيقونة التطبيق" } }, + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tətbiq ikonunu və adını dəyişdir" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -28379,6 +28531,12 @@ "value" : "Změnit ikonu a název aplikace" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "App-Symbol und Name ändern" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -28391,18 +28549,48 @@ "value" : "Ŝanĝi piktogramon de aplikaĵo kaj nomon" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cambiar icono y nombre de la aplicación" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cambiar icono y nombre de la aplicación" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Changer l'icône et le nom de l'application" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "ऐप आइकन और नाम बदलें" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Az alkalmazás ikonjának és nevének módosítása" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cambia nome e icona dell'app" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アプリのアイコンと名前を変更" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -28421,11 +28609,53 @@ "value" : "Zmień ikonę i nazwę aplikacji" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alterar Ícone e Nome da Aplicação" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Schimbă pictograma și numele aplicației" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Изменить имя и иконку приложения" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Byt appikon och namn" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uygulama Simgesini ve Adını Değiştir" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Змінити значок і назву застосунку" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "更改应用图标和名称" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "變更應用程式圖示與名稱" + } } } }, @@ -28438,6 +28668,12 @@ "value" : "يتطلب تغيير أيقونة التطبيق واسمه إغلاق {app_name}. سوف تستمر الإشعارات في استخدام أيقونة واسم {app_name} الافتراضي." } }, + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tətbiq ikonunu və adını dəyişdirərkən {app_name} bağlanılacaq. Bildirişlər, ilkin {app_name} ikonunu və adını istifadə etməyə davam edəcək." + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -28450,24 +28686,60 @@ "value" : "Změna ikony a názvu aplikace vyžaduje, aby byla aplikace {app_name} zavřena. Oznámení budou nadále používat výchozí ikonu a název {app_name}." } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Das Ändern des App-Symbols und -Namens erfordert, dass {app_name} beendet wird. Benachrichtigungen verwenden weiterhin das Standardsymbol und den Standardnamen von {app_name}." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Changing the app icon and name requires {app_name} to be closed. Notifications will continue to use the default {app_name} icon and name." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cambiar el icono y el nombre de la aplicación requiere cerrar {app_name}. Las notificaciones seguirán utilizando el icono y nombre predeterminados de {app_name}." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cambiar el icono y el nombre de la aplicación requiere cerrar {app_name}. Las notificaciones seguirán utilizando el icono y nombre predeterminados de {app_name}." + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Changer l'icône et le nom de l'application nécessite la fermeture de {app_name}. Les notifications continueront d'utiliser l'icône et le nom par défaut de {app_name}." } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "ऐप आइकन और नाम बदलने के लिए {app_name} को बंद करना आवश्यक है। सूचनाएं डिफ़ॉल्ट {app_name} आइकन और नाम का उपयोग करना जारी रखेंगी।" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Az alkalmazás ikonjának és nevének módosításához be kell zárni a Session alkalmazást. Az értesítések továbbra is az alapértelmezett Session ikont és nevet fogják használni." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "La modifica dell'icona e del nome dell'app richiede la chiusura di {app_name}. Le notifiche continueranno a mostrare l'icona e il nome predefiniti di {app_name}." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アプリのアイコンと名前を変更するには {app_name} を終了する必要があります。通知では引き続きデフォルトの {app_name} のアイコンと名前が使用されます。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -28486,11 +28758,53 @@ "value" : "Zmiana ikony i nazwy aplikacji wymaga zamknięcia aplikacji {app_name}. Powiadomienia nadal będą używać domyślnej ikony i nazwy {app_name}." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alterar o ícone e nome da aplicação requer o encerramento do {app_name}. As notificações continuarão a usar o ícone e nome predefinidos de {app_name}." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Schimbarea pictogramei și a numelui aplicației necesită închiderea {app_name}. Notificările vor continua să folosească pictograma și numele implicit {app_name}." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Для изменения значка и названия приложения необходимо закрыть приложение {app_name}. Уведомления продолжат использовать стандартный значок и название приложения {app_name}." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "För att byta appikon och namn måste {app_name} stängas. Aviseringar kommer fortsätta använda standardikonen och namnet för {app_name}." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uygulama simgesini ve adını değiştirmek, {app_name} uygulamasının kapatılmasını gerektirir. Bildirimler, varsayılan {app_name} simgesini ve adını kullanmaya devam edecektir." + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Зміна значка та назви застосунку потребує закриття {app_name}. Сповіщення надходитимуть зі стандартною назвою та значком {app_name}." } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "更改应用图标和名称需要关闭 {app_name}。通知仍将使用默认的 {app_name} 图标和名称。" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "變更應用程式圖示與名稱需要關閉 {app_name}。通知將繼續使用預設的 {app_name} 圖示與名稱。" + } } } }, @@ -28533,6 +28847,18 @@ "value" : "Alternate app icon and name is displayed on home screen and app drawer." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ícono y nombre alternativos para la aplicación se muestran en la pantalla de inicio y en el menú de aplicaciones." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "El ícono y nombre alternativos de la app se muestran en la pantalla principal y el cajón de aplicaciones." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -28551,6 +28877,18 @@ "value" : "Az alternatív alkalmazásikon és név megjelenik a kezdőképernyőn és az alkalmazásfiókban." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "L'icona e il nome alternativi dell'app sono visualizzati nella schermata principale e nel cassetto delle app." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "代替のアプリアイコンと名前は、ホーム画面およびアプリドロワーに表示されます。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -28569,6 +28907,18 @@ "value" : "Alternatywna ikona i nazwa aplikacji są wyświetlane na ekranie głównym i w szufladzie aplikacji." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "O ícone e nome alternativos da aplicação são exibidos no ecrã principal e na gaveta de aplicações." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pictograma și numele alternative ale aplicației sunt afișate pe ecranul principal și în sertarul aplicației." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -28581,6 +28931,12 @@ "value" : "Alternativ app-ikon och namn visas på hem skärmen och app." } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alternatif uygulama ikonu ve ismi, ana ekran ve uygulama çekmecesinde gözükür." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -28598,6 +28954,12 @@ "state" : "translated", "value" : "替代的应用图标与应用名会显示在主页和应用抽屉中。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "替代的應用程式圖示與名稱會顯示於主畫面與應用程式抽屜中。" + } } } }, @@ -28610,6 +28972,12 @@ "value" : "يتم عرض أيقونة التطبيق المحدد والاسم على الشاشة الرئيسة و درج التطبيقات." } }, + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seçilmiş tətbiq ikonu və adı, əsas ekranda və tətbiq siyirməsində nümayiş olunur." + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -28622,24 +28990,60 @@ "value" : "Na domovské obrazovce a v seznamu aplikací se zobrazí vybraná ikona a název aplikace." } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Das ausgewählte App-Symbol und der Name werden auf dem Startbildschirm und in der App-Übersicht angezeigt." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "The selected app icon and name is displayed on the home screen and app drawer." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "El icono y el nombre seleccionados de la aplicación se muestran en la pantalla de inicio y en el cajón de aplicaciones." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "El icono y el nombre seleccionados de la aplicación se muestran en la pantalla de inicio y en el cajón de aplicaciones." + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "L'icône et le nom de l'application sélectionnés sont affichés sur l'écran d'accueil et dans le tiroir d'applications" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "चयनित ऐप आइकन और नाम होम स्क्रीन और ऐप ड्रॉअर में प्रदर्शित होता है।" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "A kiválasztott alkalmazás ikonja és neve megjelenik a kezdőképernyőn és az alkalmazásfiókban." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "L'icona e il nome dell'app selezionati vengono mostrati nella schermata principale e nel drawer delle app." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "選択したアプリアイコンと名前は、ホーム画面とアプリドロワーに表示されます。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -28658,11 +29062,53 @@ "value" : "Wybrana ikona i nazwa aplikacji będą wyświetlane na ekranie głównym oraz w szufladzie aplikacji." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "O ícone e o nome da aplicação selecionados são apresentados no ecrã principal e na gaveta de aplicações." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pictograma și numele aplicației selectate sunt afișate pe ecranul principal și în sertarul de aplicații." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Выбранный значок и название приложения отображаются на главном экране и в панели приложений" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vald appikon och namn visas på hemskärmen och i applådan." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seçilen uygulama simgesi ve adı, ana ekranda ve uygulama çekmecesinde görüntülenir." + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Альтернативний значок та назва використовуються на домашньому екрані та у переліку застосунків." } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "所选应用图标与名称将显示在主屏幕和应用抽屉中。" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "所選擇的應用程式圖示與名稱將顯示於主畫面與應用程式清單中。" + } } } }, @@ -28717,6 +29163,18 @@ "value" : "Piktogramo kaj nomo" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ícono y nombre" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ícono y nombre" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -28741,6 +29199,18 @@ "value" : "Ikon dan nama" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Icona e nome" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アイコンと名前" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -28759,6 +29229,18 @@ "value" : "Ikona i nazwa" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ícone e nome" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pictogramă și nume" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -28771,6 +29253,12 @@ "value" : "Ikon och namn" } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İkon ve isim" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -28788,6 +29276,12 @@ "state" : "translated", "value" : "图标与应用名" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "圖示與名稱" + } } } }, @@ -28830,6 +29324,18 @@ "value" : "Alternate app icon is displayed on home screen and app library. App name will still appear as '{app_name}'." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "El ícono alternativo para la aplicación se muestra en la pantalla de inicio y en el menú de aplicaciones. El nombre de la aplicación seguirá apareciendo como '{app_name}'." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "El ícono alternativo de la app se muestra en la pantalla principal y en la biblioteca de apps. El nombre de la app seguirá apareciendo como \"{app_name}\"." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -28848,6 +29354,18 @@ "value" : "Az alternatív alkalmazásikon megjelenik a kezdőképernyőn és az alkalmazáskönyvtárban. Az alkalmazás neve továbbra is „{app_name}” néven jelenik meg." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "L'icona alternativa dell'app è visualizzata nella schermata principale e nella libreria delle app. Il nome dell'app sarà comunque visualizzato come \"{app_name}\"." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "代替のアプリアイコンはホーム画面およびアプリライブラリに表示されます。アプリ名は「{app_name}」として表示されます。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -28866,6 +29384,18 @@ "value" : "Alternatywna ikona aplikacji jest wyświetlana na ekranie głównym i w bibliotece aplikacji. Nazwa aplikacji będzie nadal wyświetlana jako „{app_name}”." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "O ícone alternativo da aplicação é exibido no ecrã principal e na biblioteca de aplicações. O nome da aplicação continuará a aparecer como \"{app_name}\"." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pictograma alternativă a aplicației este afișată pe ecranul principal și în biblioteca de aplicații. Numele aplicației va apărea în continuare ca „{app_name}”." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -28878,6 +29408,12 @@ "value" : "Alternativ app-ikon visas på hem skärmen och app biblioteket. App namn kommer fortsätta visas som '{app_name}'." } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alternatif uygulama simgesi, ana ekranda ve uygulama arşivinde görüntülenir. Uygulama adı yine {app_name} olarak görünmeye devam edecektir." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -28889,6 +29425,12 @@ "state" : "translated", "value" : "替代的应用图标会显示在主页和应用列表中。应用名仍会显示为“{app_name}”。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "替代的應用程式圖示會顯示於主畫面與應用程式資料庫中。應用程式名稱仍會顯示為「{app_name}」。" + } } } }, @@ -28943,6 +29485,18 @@ "value" : "Uzi alternativan piktogramon de aplikaĵo" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Usar un ícono alternativo para la aplicación" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Usar ícono alternativo de la app" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -28967,6 +29521,18 @@ "value" : "Gunakan ikon aplikasi alternatif" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Usa un'icona alternativa" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "代替アプリアイコンを使用" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -28985,6 +29551,18 @@ "value" : "Użyj alternatywnej ikony aplikacji" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Usar ícone alternativo da aplicação" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Folosește pictograma alternativă pentru aplicație" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -28997,6 +29575,12 @@ "value" : "Använda alternativ app-ikon" } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alternatif uygulama ikonu kullan" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -29014,6 +29598,12 @@ "state" : "translated", "value" : "使用替代的应用图标" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "使用替代的應用程式圖示" + } } } }, @@ -29068,6 +29658,18 @@ "value" : "Uzi alternativan piktogramon de aplikaĵo kaj nomon" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Usar un ícono y nombre alternativos para la aplicación" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Usar ícono y nombre alternativos de la app" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -29092,6 +29694,18 @@ "value" : "Gunakan ikon aplikasi alternatif dan nama" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Usa icona e nome alternativi" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "代替のアプリアイコンと名前を使用" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -29110,6 +29724,18 @@ "value" : "Użyj alternatywnej ikony i nazwy aplikacji" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Usar ícone e nome alternativos da aplicação" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Folosește pictograma și numele alternative pentru aplicație" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -29122,6 +29748,12 @@ "value" : "Använd alternativ app-ikon och namn" } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alternatif uygulama ikonu ve ismi kullan" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -29139,6 +29771,12 @@ "state" : "translated", "value" : "使用替代的应用图标与应用名" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "使用替代的應用程式圖示與名稱" + } } } }, @@ -29193,6 +29831,18 @@ "value" : "Elektu alternativan piktogramon de aplikaĵo" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seleccione un ícono alternativo para la aplicación" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seleccionar ícono alternativo de la app" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -29217,12 +29867,36 @@ "value" : "Pilih ikon aplikasi alternatif" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seleziona un'icona alternativa" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "代替アプリアイコンを選択" + } + }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "대체 앱 아이콘을 선택" } }, + "ku" : { + "stringUnit" : { + "state" : "translated", + "value" : "ئایکۆن ئەپی جێگرەوە هەڵبژێرە" + } + }, + "ku-TR" : { + "stringUnit" : { + "state" : "translated", + "value" : "ئایکۆن ئەپی جێگرەوە هەڵبژێرە" + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -29235,6 +29909,18 @@ "value" : "Wybierz alternatywną ikonę aplikacji" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Selecionar ícone alternativo da aplicação" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Selectează o pictogramă alternativă pentru aplicație" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -29247,6 +29933,12 @@ "value" : "Välj en alternativ app-ikon" } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alternatif uygulama ikonu seç" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -29264,6 +29956,12 @@ "state" : "translated", "value" : "选择替代的应用图标" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "選擇替代的應用程式圖示" + } } } }, @@ -29318,6 +30016,18 @@ "value" : "Piktogramo" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ícono" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ícono" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -29342,6 +30052,18 @@ "value" : "Ikon" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Icona" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アイコン" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -29360,6 +30082,18 @@ "value" : "Ikona" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ícone" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pictogramă" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -29372,6 +30106,12 @@ "value" : "Ikon" } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İkon" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -29389,6 +30129,12 @@ "state" : "translated", "value" : "图标" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "圖示" + } } } }, @@ -29443,6 +30189,18 @@ "value" : "Kalkulilo" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Calculadora" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Calculadora" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -29467,6 +30225,18 @@ "value" : "Kalkulator" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Calcolatrice" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "電卓" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -29485,6 +30255,18 @@ "value" : "Kalkulator" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Calculadora" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Calculator" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -29497,6 +30279,12 @@ "value" : "Miniräknare" } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hesap makinesi" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -29514,6 +30302,12 @@ "state" : "translated", "value" : "计算器" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "計算機" + } } } }, @@ -29562,6 +30356,18 @@ "value" : "KunvenoSE" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eventos" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "MeetingSE" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -29580,6 +30386,18 @@ "value" : "Találkozók" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "MeetingSE" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "MeetingSE" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -29598,6 +30416,18 @@ "value" : "Spotkania" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "ReuniãoSE" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "MeetingSE" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -29610,6 +30440,12 @@ "value" : "MötenSE" } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "MeetingSE" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -29621,6 +30457,12 @@ "state" : "translated", "value" : "SE云会议" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "MeetingSE" + } } } }, @@ -29675,6 +30517,18 @@ "value" : "Novaĵoj" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Noticias" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Noticias" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -29699,6 +30553,18 @@ "value" : "Berita" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Notizie" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ニュース" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -29717,6 +30583,18 @@ "value" : "Aktualności" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Notícias" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Știri" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -29729,6 +30607,12 @@ "value" : "Nyheter" } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Haberler" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -29746,6 +30630,12 @@ "state" : "translated", "value" : "新闻" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "新聞" + } } } }, @@ -29800,6 +30690,18 @@ "value" : "Notoj" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Notas" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Notas" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -29824,6 +30726,18 @@ "value" : "Catatan" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Note" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メモ" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -29842,6 +30756,18 @@ "value" : "Notatki" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Notas" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Note" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -29854,6 +30780,12 @@ "value" : "Anteckningar" } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Notlar" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -29871,6 +30803,12 @@ "state" : "translated", "value" : "笔记" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "筆記" + } } } }, @@ -29925,6 +30863,18 @@ "value" : "Akcioj" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Acciones" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Acciones" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -29949,6 +30899,18 @@ "value" : "Bursa Saham" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Borsa" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "株式" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -29967,6 +30929,18 @@ "value" : "Akcje" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ações" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Acțiuni" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -29979,6 +30953,12 @@ "value" : "Aktier" } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Borsa" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -29996,6 +30976,12 @@ "state" : "translated", "value" : "股票" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "股票" + } } } }, @@ -30050,6 +31036,18 @@ "value" : "Vetero" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Clima" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Clima" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -30074,6 +31072,18 @@ "value" : "Cuaca" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meteo" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "天気" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -30092,6 +31102,18 @@ "value" : "Pogoda" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meteorologia" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vremea" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -30104,6 +31126,12 @@ "value" : "Väder" } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hava Durumu" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -30121,6 +31149,65 @@ "state" : "translated", "value" : "天气" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "天氣" + } + } + } + }, + "appProBadge" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro} nişanı" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odznak {app_pro}" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro} Abzeichen" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro} Badge" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Badge {app_pro}" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro} Badge" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odznaka {app_pro}" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Значок {app_pro}" + } } } }, @@ -30636,6 +31723,12 @@ "value" : "Vedhæftninger" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Anhänge" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -30648,12 +31741,30 @@ "value" : "Alfiksitaĵoj" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Archivos adjuntos" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Archivos adjuntos" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Pièces jointes" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "अटैचमेंट्स" + } + }, "hu" : { "stringUnit" : { "state" : "translated", @@ -30666,6 +31777,18 @@ "value" : "Lampiran" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Allegati" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "添付ファイル" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -30684,6 +31807,36 @@ "value" : "Załączniki" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Anexos" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Atașamente" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вложения" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bilagor" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ekler" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -30695,6 +31848,12 @@ "state" : "translated", "value" : "附件" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "附件" + } } } }, @@ -32201,7 +33360,7 @@ "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Automaticky stahovat média a soubory z tohoto chatu." + "value" : "Automaticky stahovat média a soubory této konverzace." } }, "cy" : { @@ -44212,7 +45371,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Metadata, fayldan silinə bilmir." + "value" : "Meta veri, fayldan silinə bilmir." } }, "bal" : { @@ -52840,7 +53999,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Saxladığınız qoşmalara cihazınızdakı digər tətbiqlər müraciət edə bilər." + "value" : "Saxladığınız qoşmalara cihazınızdakı digər tətbiqlər erişə bilər." } }, "bal" : { @@ -56690,7 +57849,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Kimlik doğrulamaya müraciət edilə bilmədi." + "value" : "Kimlik doğrulamaya erişilə bilmədi." } }, "bal" : { @@ -60082,6 +61241,12 @@ "value" : "Indtast konto-ID'et for den bruger, du vil fjerne blokering for" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gib die Account-ID des Nutzers ein, den du entsperrst" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -60094,18 +61259,48 @@ "value" : "Enigu ID de la konto de la uzanto, kiun vi malblokas" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ingrese el Account ID del usuario al que va a quitar la prohibición." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ingrese el Account ID del usuario al que va a quitar la prohibición." + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Entrez l'identifiant du compte de l’utilisateur que vous souhaitez débannir" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "उस उपयोगकर्ता का Account ID दर्ज करें जिसे आप अनबैन कर रहे हैं" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Adja meg annak a felhasználónak a fiókazonosítóját, amelyiknek a kitiltását fel akarja oldani" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inserisci l'Account ID dell'utente a cui desideri revocare il blocco" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ブロック解除するユーザーのAccount IDを入力してください" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -60124,6 +61319,36 @@ "value" : "Wprowadź identyfikator konta użytkownika, który odbanowałeś" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Introduza o ID de Conta do utilizador que pretende desbloquear" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Introduce ID-ul contului utilizatorului căruia îi ridici interdicția" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Введите ID аккаунта пользователя, которого вы разблокируете" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ange Account ID för användaren du tar bort blockeringen för" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yasağını kaldırdığınız kullanıcının Hesap Kimliğini girin" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -60135,6 +61360,12 @@ "state" : "translated", "value" : "输入您想取消封禁的用户的帐户 ID" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "請輸入您要解除封鎖的使用者的 Account ID" + } } } }, @@ -61608,6 +62839,12 @@ "value" : "Indtast konto-ID'et for den bruger, du vil blokere" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gib die Account-ID des Nutzers ein, den du sperrst" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -61620,18 +62857,48 @@ "value" : "Enigu ID de la konto de la uzanto, kiun vi blokas" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ingrese el Account ID del usuario al que va a prohibir." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ingrese el Account ID del usuario al que va a prohibir." + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Entrez l'identifiant du compte de l'utilisateur que vous souhaitez débannir" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "उस उपयोगकर्ता का Account ID दर्ज करें जिसे आप बैन कर रहे हैं" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Adja meg annak a felhasználónak a fiókazonosítóját, amelyiket ki akarja tiltani" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inserisci l'Account ID dell'utente che desideri bloccare" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ブロックするユーザーのAccount IDを入力してください" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -61650,6 +62917,36 @@ "value" : "Wprowadź identyfikator konta użytkownika, którego chcesz zablokować" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Introduza o ID de Conta do utilizador que pretende bloquear" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Introduce ID-ul contului utilizatorului pe care îl blochezi" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Введите ID аккаунта пользователя, которого вы блокируете" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ange Account ID för användaren du blockerar" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yasakladığınız kullanıcının Hesap Kimliğini girin" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -61661,17 +62958,143 @@ "state" : "translated", "value" : "输入您想封禁的用户的帐户 ID" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "請輸入您要封鎖的使用者的 Account ID" + } } } }, "blindedId" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kor ID" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "ID cega" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Maskované ID" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verschleierte ID" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Blinded ID" } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "ID cegado" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "ID cegado" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "ID aveuglé" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "ब्लाइंडेड ID" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "ID offuscato" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ブラインドID" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Afgeschermde ID" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ukryty identyfikator" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "ID Oculto" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "ID cenzurat" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Скрытый ID" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Maskerat ID" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Körleştirilmiş Kimlik" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Знеособлений ID" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "盲化 ID" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "隱藏 ID" + } } } }, @@ -62163,6 +63586,48 @@ "blockBlockedDescription" : { "extractionState" : "manual", "localizations" : { + "af" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deblokkeer hierdie kontak om 'n boodskap te stuur." + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "إلغاء حظر جهة الإتصال لإرسال رسالة" + } + }, + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesaj göndərmək üçün bu kontaktı əngəldən çıxardın." + } + }, + "bal" : { + "stringUnit" : { + "state" : "translated", + "value" : "پیغام بھیجنے کے لئے اس رابطہ کو غیر بلاک کریں۔" + } + }, + "be" : { + "stringUnit" : { + "state" : "translated", + "value" : "Разблакуйце гэты кантакт, каб адправіць паведамленне" + } + }, + "bg" : { + "stringUnit" : { + "state" : "translated", + "value" : "Отблокирай този контакт за да изпратиш съобщение" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "মেসেজ পাঠাতে এই কন্টাক্টটি আনব্লক করুন।" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -62175,23 +63640,413 @@ "value" : "Pro odeslání zprávy tento kontakt odblokujte." } }, + "cy" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dadrwystro'r cyswllt hwn i anfon neges" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fjern blokering af denne kontakt for at sende en besked" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gib die Blockierung dieses Kontakts frei, um eine Nachricht zu senden." + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Καταργήστε τη φραγή αυτής τη επαφής για να στείλετε ένα μήνυμα" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Unblock this contact to send a message" } }, + "eo" : { + "stringUnit" : { + "state" : "translated", + "value" : "Malbloki tiun kontakton por sendi mesaĝon" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Desbloquea este contacto para enviarle mensajes" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Desbloquea este contacto para enviar mensajes." + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sõnumi saatmiseks eemalda selle kontakti blokeering" + } + }, + "eu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kontaktu hau desblokeatu mezu bat bidaltzeko" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "برای ارسال پیام،‌ ابتدا این مخاطب را از مسدود بودن درآورید!" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lähettääksesi viestin tälle yhteystiedolle sinun tulee ensin poistaa asettamasi esto." + } + }, + "fil" : { + "stringUnit" : { + "state" : "translated", + "value" : "I-unblock ang contact na ito para magpadala ng mensahe" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Débloquez ce contact pour envoyer un message" + } + }, + "gl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Desbloquea este contacto para enviar unha mensaxe" + } + }, + "ha" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cire katanga wannan saduwa don aika saƙo" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "בטל חסימה של איש קשר זה כדי לשלוח הודעה" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "कोई संदेश भेजने के लिए इस संपर्क को अनवरोधित करें" + } + }, + "hr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deblokiraj ovaj kontakt za slanje poruke" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Üzenet küldéséhez oldd fel a kontakt letiltását." + } + }, + "hy-AM" : { + "stringUnit" : { + "state" : "translated", + "value" : "Արգելաբացել այս կոնտակտը հաղորդագրություն ուղարկելու համար" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lepaskan blokir kontak ini untuk mengirim pesan." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Per inviare un messaggio sblocca questo contatto." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "この連絡先にメッセージを送るためにブロックを解除する" + } + }, + "ka" : { + "stringUnit" : { + "state" : "translated", + "value" : "შეტყობინების გაგზავნისთვის ბლოკი მოხსენით" + } + }, + "km" : { + "stringUnit" : { + "state" : "translated", + "value" : "ដោះការហាមឃាត់លេខទំនាក់ទំនងនេះ ដើម្បីផ្ញើសារ" + } + }, + "kn" : { + "stringUnit" : { + "state" : "translated", + "value" : "ಸಂದೇಶವೊಂದನ್ನು ಕಳುಹಿಸಲು ಈ ಸಂಪರ್ಕವನ್ನು ಬ್ಲಾಕ್ ಮಾಡಿ" + } + }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이 사용자에게 메시지를 보내려면 먼저 차단을 해제하세요" } }, + "ku" : { + "stringUnit" : { + "state" : "translated", + "value" : "ئەم پەیوەندە لابردن بۆ بریتیە لە ناردنی پەیامێک." + } + }, + "ku-TR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ji bo şandina peyamê vê bloka vî kontaktê rake" + } + }, + "lg" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sazaamu omukozesa kuno okusindika obubaka" + } + }, + "lt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Atblokuokite šį kontaktą, kad išsiųstumėte žinutę" + } + }, + "lv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Atbloķējiet šo kontaktu, lai nosūtītu ziņojumu" + } + }, + "mk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Одблокирај го овој контакт за да испратиш порака" + } + }, + "mn" : { + "stringUnit" : { + "state" : "translated", + "value" : "Түгжээг арилгаж, мессеж илгээх боломжтой болно" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nyahsekat kontak ini untuk menghantar mesej" + } + }, + "my" : { + "stringUnit" : { + "state" : "translated", + "value" : "မက်ဆေ့ချ် ပို့ရန်အတွက်ဤဆက်သွယ်မှုသို့ ဘလော့ကိုဖြေပါ။" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avblokker denne kontakten for å sende en beskjed." + } + }, + "nb-NO" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opphev blokkeringen på denne kontakten for å sende en melding." + } + }, + "ne-NP" : { + "stringUnit" : { + "state" : "translated", + "value" : "सन्देश पठाउन यो सम्पर्क अनब्लक गर्नुहोस्।" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deblokkeer dit contact om een bericht te verzenden." + } + }, + "nn-NO" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opphev blokkeringen på denne kontakten for å sende en melding" + } + }, + "ny" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pokankha Lamulo Llitsa lemba uthenga" + } + }, + "pa-IN" : { + "stringUnit" : { + "state" : "translated", + "value" : "ਸੁਨੇਹਾ ਭੇਜਣ ਲਈ ਇਸ ਸੰਪਰਕ ਨੂੰ ਅਨਬਲੌਕ ਕਰੋ।" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odblokuj ten kontakt, aby wysłać wiadomość" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "د پیغام استولو لپاره له دې اړیکې بې بندیز وکړئ" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Desbloquear este contato para enviar uma mensagem" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Desbloqueie este contacto para enviar uma mensagem." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deblochează acest contact pentru a putea trimite mesaje" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Разблокируйте этот контакт, чтобы отправить сообщение" + } + }, + "sh" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odblokirajte ovog kontakta da biste poslali poruku" + } + }, + "si-LK" : { + "stringUnit" : { + "state" : "translated", + "value" : "පණිවිඩය යැවීමට මෙම සබඳතාවය අනවහිර කරන්න" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pre odoslanie správy kontakt odblokujte" + } + }, + "sl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Za pošiljanje sporočila morate najprej odblokirati ta stik" + } + }, + "sq" : { + "stringUnit" : { + "state" : "translated", + "value" : "Që t’i dërgohet një mesazh, zhbllokojeni këtë kontakt" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Одблокирајте дописника да би послали поруку" + } + }, + "sr-Latn" : { + "stringUnit" : { + "state" : "translated", + "value" : "Одблокирајте дописника да би послали поруку" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avblockera denna kontakt för att skicka meddelanden." + } + }, + "sw" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ondolea kizuizi kwa mawasiliano haya kutuma ujumbe" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "ஒரு செய்தியை அனுப்ப இந்த தொடர்பை விடுவிக்கவும்." + } + }, + "te" : { + "stringUnit" : { + "state" : "translated", + "value" : "సందేశాన్ని పంపడానికి ఈ పరిచయాన్ని అనుమతించు" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İleti göndermek için bu kişinin engellenmesini kaldırın" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Розблокувати контакт для надсилання повідомлення." + } + }, + "ur-IN" : { + "stringUnit" : { + "state" : "translated", + "value" : "پیغام بھیجنے کے لیے اس رابطے کو ان بلاک کریں" + } + }, + "uz" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xabar yuborish uchun ushbu kontaktni blokdan chiqaring" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mở khóa người (liên lạc) này để gởi thông báo" + } + }, "zh-CN" : { "stringUnit" : { "state" : "translated", "value" : "取消屏蔽此联系人以发送消息" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "解除封鎖聯絡人以傳送訊息。" + } } } }, @@ -63644,6 +65499,71 @@ } } }, + "blockedContactsManageDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Əngəllənmiş kontaktları görün və idarə edin." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zobrazit a spravovat blokované kontakty." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Blockierte Kontakte anzeigen und verwalten." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "View and manage blocked contacts." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Afficher et gérer les contacts bloqués." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bekijk en beheer geblokkeerde contacten." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Przeglądaj i zarządzaj zablokowanymi kontaktami." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Просматривайте и управляйте списком заблокированных контактов." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Visa och hantera blockerade kontakter." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Переглядайте та керуйте заблокованими контактами." + } + } + } + }, "blockUnblock" : { "extractionState" : "manual", "localizations" : { @@ -71344,7 +73264,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Mikrofon müraciətinə icazə vermədiyiniz üçün {name} edən zəngi buraxdınız." + "value" : "Mikrofon erişiminə icazə vermədiyiniz üçün {name} edən zəngi buraxdınız." } }, "ca" : { @@ -74119,6 +76039,18 @@ "value" : "Uprawnienie „Połączenia głosowe i wideo” można włączyć w Ustawieniach uprawnień." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pode ativar a permissão de \"Chamadas de voz e vídeo\" nas Definições de Permissões." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Puteți activa permisiunea „Apeluri Vocale și Video” în Setările de Confidențialitate." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -74154,6 +76086,12 @@ "state" : "translated", "value" : "您可以在权限设置中启用 “语音和视频通话 ”权限。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "您可以在隱私權設定中啟用「語音和視訊通話」權限。" + } } } }, @@ -77034,478 +78972,64 @@ "callsVoiceAndVideoModalDescription" : { "extractionState" : "manual", "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jou IP is sigbaar vir jou oproepmaat en 'n Oxen Foundation-bediener terwyl beta-oproepe gebruik word." - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "عنوان IP الخاص بك مرئي لشريك الاتصال وخادم Oxen Foundation أثناء استخدام المكالمات التجريبية." - } - }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "Beta zənglərini istifadə edərkən IP ünvanınız zəng tərəfdaşınıza və Oxen Foundation serverinə görünür." - } - }, - "bal" : { - "stringUnit" : { - "state" : "translated", - "value" : "ما گپ درخواست قبول کردی آپ کہ آئیں طرفه IP پدنی Oxen Foundation کہ سرورے ہ مغامیگیا." - } - }, - "be" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ваш IP будзе бачныя вашаму партнёру па званках і серверу Oxen Foundation падчас выкарыстання бэта-званкоў." - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Вашият IP е видим за вашият партньор по време нослужване на beta обаждания и Oxen Foundation сървър." - } - }, - "bn" : { - "stringUnit" : { - "state" : "translated", - "value" : "বেটা কলগুলি ব্যবহার করার সময় আপনার আইপি আপনার কল পার্টনার এবং একটি Oxen Foundation সার্ভারের কাছে দৃশ্যমান হবে।" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "La vostra IP és visible a la vostra parella de trucada i a un servidor de Oxen Foundation mentre utilitzeu trucades beta." + "value" : "Beta zənglərini istifadə edərkən IP-niz zəng tərəfdaşınıza və {session_foundation} serverinə görünür." } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Vaše IP adresa je při používání beta hovorů viditelná pro vašeho volacího partnera a server Oxen Foundation." - } - }, - "cy" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mae eich GPS yn weladwy i'ch partner galwad ac i Wasanaeth Node Foundation Oxen wrth ddefnyddio galwadau beta." - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Din IP synlig for din opkaldspartner og en Oxen Foundation-server, mens du bruger betaopkald." + "value" : "Funkce hlasových hovorů, která je nyní ve vývojové fázi (beta), odhalí vaši IP adresu těm, se kterými si voláte a také {session_foundation} serveru." } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Deine IP ist für deinen Gesprächspartner und einen Oxen Foundation Server sichtbar, während du Beta-Anrufe nutzt." - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Η διεύθυνση IP σας είναι ορατή στον συνομιλητή σας και σε έναν διακομιστή του Oxen Foundation κατά τη χρήση κλήσεων beta." + "value" : "Deine IP ist deinem Anrufpartner und einem {session_foundation} Server sichtbar während Beta Anrufe getätigt werden." } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Your IP is visible to your call partner and a Session Technology Foundation server while using beta calls." - } - }, - "eo" : { - "stringUnit" : { - "state" : "translated", - "value" : "Via IP-adreso estas videbla al via vokopartnero kaj al servilo de Oxen Foundation dum vi uzas betajn vokojn." - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tu IP es visible para tu compañero de llamada y un servidor de la Oxen Foundation mientras usas llamadas beta." - } - }, - "es-ES" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tu IP es visible para tu socio de llamada y un servidor de Oxen Foundation mientras usas las llamadas beta." - } - }, - "et" : { - "stringUnit" : { - "state" : "translated", - "value" : "Teie IP on nähtav teie kõnepartnerile ja Oxen Foundation serverile beetakõnede ajal." - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zure IPa ikusgai egongo da zure dei-bikotekidearentzako eta Oxen Foundation zerbitzari batentzako, beta-deiak egiten ari bazara." - } - }, - "fa" : { - "stringUnit" : { - "state" : "translated", - "value" : "آدرس IP شما برای مخاطب تماس شما و همچنین سرور Oxen Foundation در هنگام استفاده از تماس آزمایشی مشخص خواهد بود." - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "IP-osoitteesi on näkyvissä puhelun aikana vastaanottajalle ja Oxen Foundation palvelimelle, kun käytät beta-puheluita." - } - }, - "fil" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ang iyong IP ay nakikita ng iyong kasamahan sa tawag at isang server ng Oxen Foundation habang gumagamit ng beta calls." + "value" : "Your IP is visible to your call partner and a {session_foundation} server while using beta calls." } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Votre adresse IP est visible par votre interlocuteur et un serveur d'Oxen Foundation pendant que vous utilisez des appels beta." - } - }, - "gl" : { - "stringUnit" : { - "state" : "translated", - "value" : "O teu IP é visible para o teu compañeire de chamada e un servidor da Oxen Foundation mentres utilizas chamadas beta." - } - }, - "ha" : { - "stringUnit" : { - "state" : "translated", - "value" : "IP ɗinku yana bayyane ga abokin kiran ku da sabar Oxen Foundation yayin amfani da ƙiran beta." - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "כתובת ה-IP שלך גלויה לשותפת השיחה שלך ולשרת קרן Oxen בעת שימוש בשיחות בטא." - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "अपने खाते में एन्क्रिप्टेड संदेश भेजते समय आपका IP आपके कॉल पार्टनर और Oxen Foundation सर्वर को दिखाई देगा।" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vaš IP je vidljiv vašem sugovorniku i poslužitelju Oxen Foundation dok koristite beta pozive." - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Az IP címed látható a hívópartner és egy Oxen Foundation szerver számára a béta hívások használata közben." - } - }, - "hy-AM" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ձեր IP հասցեն տեսանելի է ձեր զանգի գործընկերոջը և մի 'Oxen Foundation' սերվերին ծիծլած օգտագործման ժամանակ։" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "IP Anda terlihat oleh mitra panggilan Anda dan server Oxen Foundation saat menggunakan panggilan beta." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Il tuo IP è visibile all'utente che stai chiamando e a un server di Oxen Foundation durante l'utilizzo delle chiamate beta." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "音声通話とビデオ通話の使用中、あなたのIPはあなたの通話相手とOxen Foundationサーバーに表示されます。" - } - }, - "ka" : { - "stringUnit" : { - "state" : "translated", - "value" : "თქვენი IP გააშკარავდება თქვენი ზარის პარტნიორს და Oxen Foundation-ის სერვერს, ბეტა ზარების გამოყენებისას." - } - }, - "km" : { - "stringUnit" : { - "state" : "translated", - "value" : "IP អ្នក​ត្រូវបាន​លេចឮ​ច្បាស់នៅពេល​អ្នក​ប្រើកិច្ចប្រជុំ Beta ក្នុង Oxen Foundation។" - } - }, - "kn" : { - "stringUnit" : { - "state" : "translated", - "value" : "ಬೇಟ ಕಾಲ್ಲನ್ನು ಬಳಸಿದಾಗ ನಿಮ್ಮ IP ಕರೆ ಸಹವಾಸಿಗೂ ಮತ್ತು Oxen Foundation ಸರ್ವರ್‌ಗೆ ಗೋಚರುತ್ತದೆ." - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "베타 통화를 사용하는 동안 IP가 호출 파트너와 Oxen Foundation 서버에 보입니다." - } - }, - "ku" : { - "stringUnit" : { - "state" : "translated", - "value" : "پته‌ی IP ی تۆ بۆ شەریکەکەی تیپە و سێروێری Oxen Foundation پشت بە پشت ڕەنگە بونی بوو بێت کاتێک بەکارهێنانی بەتاکوڵ بوون بوزیەکان." - } - }, - "ku-TR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Adresa IP̧ya te bi dirising ti paşinspectek an hîn ya Fundaceya Oxen bêyê dîtin." - } - }, - "lg" : { - "stringUnit" : { - "state" : "translated", - "value" : "IP yo efulugettaka mu mateeka ne server ya Oxen Foundation nga okozeza omitting ekikugya ekiriotto." - } - }, - "lt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Naudojant beta skambučius, jūsų IP adresas matomas jūsų pašnekovui ir Oxen Foundation serveriui." - } - }, - "lv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Izmantojot beta zvanus, jūsu IP ir redzams jūsu zvana partnerim un Oxen Foundation serverim." - } - }, - "mk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Вашето IP е видливо за вашиот партнер за повик и серверот на Oxen Foundation додека користите бета повици." - } - }, - "mn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Таны IP хаягийг ярианы хамтрагч болон Oxen Foundation сервер харагдана." - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "Alamat IP anda boleh dilihat oleh rakan panggilan anda dan pelayan Oxen Foundation semasa menggunakan panggilan beta." - } - }, - "my" : { - "stringUnit" : { - "state" : "translated", - "value" : "သင့် IP ကို ဖုန်းခေါ်စဉ်တွင် သင်၏ ချိန်းညွှန်းပါတနာများနှင့် Oxen Foundation ဆာဗာကို မြင်ရပါသည်။" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "IP-adressen din er synlig for samtalepartneren din og en Oxen Foundation-server mens du bruker betasamtaler." - } - }, - "nb-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Din IP er synlig for samtalepartneren din og en Oxen Foundation-server mens du bruker beta-samtaler." - } - }, - "ne-NP" : { - "stringUnit" : { - "state" : "translated", - "value" : "तपाईंको आइपी तपाईको कल पार्टनर र ओक्सन फाउन्डेसन सर्भरलाई बेटा कलहरू प्रयोग गर्दा देखिनेछ।" + "value" : "Votre adresse IP est visible par votre interlocuteur et un serveur {session_foundation} pendant que vous utilisez des appels bêta." } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Uw IP is zichtbaar voor uw oproep partner en een Oxen Foundation server tijdens het gebruik van bètagesprekken." - } - }, - "nn-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "IP-adressa di er synlig for samtalepartnaren din og ein Oxen Foundation-server mens du bruker beta-samtaler." - } - }, - "ny" : { - "stringUnit" : { - "state" : "translated", - "value" : "IP yanu ikuwoneka kwa mnzake wanu ndi seva ya Oxen Foundation mukamagwiritsa ntchito mayitanidwe ozungulira." - } - }, - "pa-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "ਤੁਹਾਡੀ ਕਾਲ ਦੇ ਦੌਰਾਨ ਤੁਹਾਡਾ IP ਸਹਿਯੋਗੀ ਅਤੇ ਇਕ Oxen Foundation ਸਰਵਰ ਨੂੰ ਦੇਖਾਈ ਦੇਵੇਗਾ।" + "value" : "Uw IP is zichtbaar voor uw oproep partner en een {session_foundation} server tijdens het gebruik van bètagesprekken." } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Podczas korzystania z połączeń w wersji beta Twój adres IP jest widoczny dla partnera rozmowy i serwera Oxen Foundation." - } - }, - "ps" : { - "stringUnit" : { - "state" : "translated", - "value" : "ستاسو IP ستاسو د زنګ ملګري او یو Oxen Foundation سرور ته ښکاره کیږي کله چې بیتا زنګونه کاروئ." - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Seu IP estará visível para seu parceiro de chamada e para um servidor da Oxen Foundation enquanto usa chamadas beta." - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "O seu IP está visível para seu parceiro de chamada e para um servidor Oxen Foundation enquanto usa chamadas beta." - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "IP-ul dumneavoastră este vizibil către partenerul de apel și către un server al Oxen Foundation atunci când utilizați apeluri beta." + "value" : "Twój adres IP jest widoczny dla twojego rozmówcy i serwera {session_foundation}, kiedy używasz rozmów beta." } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Ваш IP виден вашему собеседнику и серверу Oxen Foundation при использовании бета-вызовов." - } - }, - "sh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tvoj IP je vidljiv tvom partneru na pozivu i serveru Oxen Foundation dok koristiš beta pozive." - } - }, - "si-LK" : { - "stringUnit" : { - "state" : "translated", - "value" : "ඔබගේ IP වැටීමට වයිස් සහ Oxen Foundation සේවාදායකයකු වෙත දැක්වේදීද පරීක්ෂණ ඇමතුම් භාවිතා කිරීමේදී." - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vaša IP adresa je viditeľná vášmu volajúcemu partnerovi a serveru Oxen Foundation pri používaní beta hovory." - } - }, - "sl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vaš IP je viden vašemu partnerskemu klicatelju in strežniku Oxen Foundation med uporabo beta klicev." - } - }, - "sq" : { - "stringUnit" : { - "state" : "translated", - "value" : "IP-ja juaj është e dukshme për partnerin tuaj të thirrjes dhe një server të Fondacionit Oxen gjatë përdorimit të thirrjeve beta." - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ваша IP адреса је видљива вашем сајговорнику и серверу Oxen Foundation док користите бета позиве." - } - }, - "sr-Latn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vaš IP je vidljiv vašem partneru za poziv i serveru Oxen Foundation dok koristite beta pozive." + "value" : "Your IP is visible to your call partner and a {session_foundation} server while using beta calls." } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Din IP är synlig för din samtalspartner och en Oxen Foundation server när du använder beta-samtal." - } - }, - "sw" : { - "stringUnit" : { - "state" : "translated", - "value" : "IP yako inaonekana kwa mshirika wako wa simu na seva ya Oxen Foundation wakati unatumia simu za beta." - } - }, - "ta" : { - "stringUnit" : { - "state" : "translated", - "value" : "அழைப்பு பிறையாளர் மற்றும் Oxen Foundation சர்வரில் உங்கள் ஐபி காட்டப்படவும் செய்தி சேவையின்போது." - } - }, - "te" : { - "stringUnit" : { - "state" : "translated", - "value" : "బీటా కాల్‌లను ఉపయోగిస్తున్నప్పుడు మీ ఐపి మీ కాల్ భాగస్వామికి మరియు ఒక Oxen Foundation సర్వర్‌కు కనిపిస్తుంది." - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "IP ของคุณจะสามารถมองเห็นได้โดยคู่สายของคุณและเซิร์ฟเวอร์ของ Oxen Foundation ในขณะที่ใช้เบต้าการโทร" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Deneme aramaları sırasında IP adresiniz arama ortağınıza ve Oxen Foundation sunucusuna görünür olacaktır." + "value" : "Din IP är synlig för din samtalspartner och en {session_foundation}-server när du använder beta-samtal." } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Ваша IP-адреса видима вашому партнеру по дзвінку та серверу Oxen Foundation при використанні бета-дзвінків." - } - }, - "ur-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "بیٹا کالز استعمال کرتے وقت آپ کا آئی پی آپ کے کال پارٹنر اور ایک اوکسن فاؤنڈیشن سرور کو نظر آئے گا۔" - } - }, - "uz" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hozirgi parolingiz noto'g'ri." - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Địa chỉ IP của bạn hiển thị với đối tác cuộc gọi và máy chủ Oxen Foundation trong khi sử dụng cuộc gọi beta." - } - }, - "xh" : { - "stringUnit" : { - "state" : "translated", - "value" : "I-IP yakho iyabonakala komnxibelele wakho nakwiOxen Foundation Server ngelixa usebenzisa i-beta calls." - } - }, - "zh-CN" : { - "stringUnit" : { - "state" : "translated", - "value" : "在使用测试版通话时,您的IP会暴露给您的通话对象和Oxen Foundation服务器。" - } - }, - "zh-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "您在使用測試版通話時,您的 IP 將會被通話夥伴和 Oxen Foundation 伺服器看到。" + "value" : "Під час здійснення бета-викликів Ваш IP може побачити співрозмовник та сервер {session_foundation}." } } } @@ -79923,7 +81447,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Kamera müraciətinə icazə ver" + "value" : "Kamera erişiminə icazə ver" } }, "bal" : { @@ -80402,7 +81926,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "{app_name} foto və video çəkmək üçün kameraya müraciət etməlidir, ancaq bu icazəyə həmişəlik rədd cavabı verilib. Lütfən tətbiq ayarlarına gedib \"İcazələr\"i seçin və \"Kamera\"nı fəallaşdırın." + "value" : "{app_name} foto və video çəkmək üçün kameraya erişməlidir, ancaq bu erişimə həmişəlik rədd cavabı verilib. Lütfən tətbiq ayarlarına gedib \"İcazələr\"i seçin və \"Kamera\"nı fəallaşdırın." } }, "bal" : { @@ -82324,6 +83848,146 @@ } } }, + "cancelPlan" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Planı ləğv et" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zrušit tarif" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancel Plan" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Annuler l’abonnement" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abonnement annuleren" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Anuluj plan" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Скасувати тарифний план" + } + } + } + }, + "cancelProPlatform" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancel your plan on the {platform} website, using the {platform_account} you signed up for Pro with." + } + } + } + }, + "cancelProPlatformStore" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancel your plan on the {platform_store} website, using the {platform_account} you signed up for Pro with." + } + } + } + }, + "change" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dəyişdir" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Změnit" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ändern" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Change" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Modifier" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wijzigen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zmień" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Schimba" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Изменить" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ändra" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Змінити" + } + } + } + }, "changePasswordFail" : { "extractionState" : "manual", "localizations" : { @@ -82803,6 +84467,140 @@ } } }, + "changePasswordModalDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} üçün parolunuzu dəyişdirin. Daxili olaraq saxlanılmış verilər, yeni parolunuzla təkrar şifrələnəcək." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Změňte své heslo pro {app_name}. Lokálně uložená data budou znovu zašifrována pomocí vašeho nového hesla." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ändere dein Passwort für {app_name}. Lokal gespeicherte Dateien werden mit deinem neuen Passwort entschlüsselt." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Change your password for {app_name}. Locally stored data will be re-encrypted with your new password." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Changez votre mot de passe pour {app_name}. Les données stockées localement seront re-chiffrées avec votre nouveau mot de passe." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wijzig je wachtwoord voor {app_name}. Lokaal opgeslagen gegevens worden opnieuw versleuteld met je nieuwe wachtwoord." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zmień hasło dla {app_name}. Dane przechowywane lokalnie zostaną ponownie zaszyfrowane nowym hasłem." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Измените пароль для {app_name}. Локально сохранённые данные будут повторно зашифрованы с использованием нового пароля." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ändra ditt lösenord för {app_name}. Lokalt lagrad data kommer att krypteras om med ditt nya lösenord." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Змінити ваш пароль для {app_name}. Локально збережені дані будуть наново шифровані з застосуванням нового паролю." + } + } + } + }, + "checkingProStatus" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} statusu yoxlanılır" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kontrola stavu {pro}" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Checking {pro} Status" + } + } + } + }, + "checkingProStatusDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} məlumatlarınız yoxlanılır. Bu səhifədəki bəzi məlumatlar, yoxlama tamamlanana qədər qeyri-dəqiq ola bilər." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kontrolují se vaše údaje {pro}. Některé informace na této stránce mohou být nepřesné, dokud kontrola nebude dokončena." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Checking your {pro} details. Some information on this page may be inaccurate until this check is complete." + } + } + } + }, + "checkingProStatusUpgradeDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} statusunuz yoxlanılır. Bu yoxlama tamamlandıqdan sonra {pro} ya yüksəldə biləcəksiniz." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kontroluje se váš stav {pro}. Jakmile kontrola skončí, budete moci navýšit na {pro}." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Checking your {pro} status. You'll be able to upgrade to {pro} once this check is complete." + } + } + } + }, "clear" : { "extractionState" : "manual", "localizations" : { @@ -83791,7 +85589,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Bütün datanı təmizlə" + "value" : "Bütün veriləri təmizlə" } }, "bal" : { @@ -84276,7 +86074,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Bu, mesajlarınızı və kontaktlarınızı həmişəlik siləcək. Yalnız bu cihazı təmizləmək istəyirsiniz, yoxsa datanızı bütün şəbəkədən də silmək istəyirsiniz?" + "value" : "Bu, mesajlarınızı və kontaktlarınızı həmişəlik siləcək. Yalnız bu cihazı təmizləmək istəyirsiniz, yoxsa verilərinizi bütün şəbəkədən də silmək istəyirsiniz?" } }, "bal" : { @@ -84755,7 +86553,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Data silinmədi" + "value" : "Verilər silinmədi" } }, "bal" : { @@ -85291,13 +87089,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Data, %lld Xidmət Düyünü tərəfindən silinmədi. Xidmət Düyünü kimliyi: {service_node_id}." + "value" : "Veri, %lld Xidmət Düyünü tərəfindən silinmədi. Xidmət Düyünü kimliyi: {service_node_id}." } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Data, %lld Xidmət Düyünü tərəfindən silinmədi. Xidmət Düyünü kimliyi: {service_node_id}." + "value" : "Veri, %lld Xidmət Düyünü tərəfindən silinmədi. Xidmət Düyünü kimliyi: {service_node_id}." } } } @@ -86805,7 +88603,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Bilinməyən bir xəta baş verdi və datanız silinmədi. Bunun əvəzinə datanızı yalnız bu cihazdan silmək istəyirsiniz?" + "value" : "Bilinməyən bir xəta baş verdi və veriləriniz silinmədi. Bunun əvəzinə verilərinizi yalnız bu cihazdan silmək istəyirsiniz?" } }, "bal" : { @@ -88260,7 +90058,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Datanızı şəbəkədən silmək istədiyinizə əminsiniz? Davam etsəniz, mesajlarınızı və kontaktlarınızı bərpa edə bilməyəcəksiniz." + "value" : "Verilərinizi şəbəkədən silmək istədiyinizə əminsiniz? Davam etsəniz, mesajlarınızı və kontaktlarınızı bərpa edə bilməyəcəksiniz." } }, "bal" : { @@ -89724,18 +91522,42 @@ "value" : "Slet enhed og genstart" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gerät löschen und neu starten" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Clear Device and Restart" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Borrar dispositivo y reiniciar" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Borrar dispositivo y reiniciar" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Effacer l’appareil et redémarrer" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "डिवाइस साफ़ करें और पुनः आरंभ करें" + } + }, "hu" : { "stringUnit" : { "state" : "translated", @@ -89748,6 +91570,18 @@ "value" : "Hapus Perangkat dan Hidupkan Ulang" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancella dispositivo e riavvia" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "端末の消去と再起動" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -89766,6 +91600,36 @@ "value" : "Wyczyść urządzenie i uruchom ponownie" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Limpar Dispositivo e Reiniciar" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Curăță dispozitivul și repornește" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Очистить устройство и перезапустить" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rensa enhet och starta om" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cihazı temizle ve Yeniden başlat" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -89777,6 +91641,18 @@ "state" : "translated", "value" : "Xóa thiết bị và khởi động lại" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "清除设备并重新启动应用" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "清除裝置並重新啟動" + } } } }, @@ -89807,18 +91683,42 @@ "value" : "Slet enhed og gendan" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gerät löschen und wiederherstellen" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Clear Device and Restore" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Borrar dispositivo y restaurar" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Borrar dispositivo y restaurar" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Effacer l’appareil et restaurer" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "डिवाइस साफ़ करें और पुनः प्राप्त करें" + } + }, "hu" : { "stringUnit" : { "state" : "translated", @@ -89831,6 +91731,18 @@ "value" : "Hapus Perangkat dan Pulihkan" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancella dispositivo e ripristina" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "端末の消去と復元" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -89849,6 +91761,36 @@ "value" : "Wyczyść urządzenie i przywróć" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Limpar Dispositivo e Restaurar" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Curăță dispozitivul și restaurează" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Очистить устройство и восстановить" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rensa enhet och återställ" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cihazı temizle ve geri yükle" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -89860,6 +91802,18 @@ "state" : "translated", "value" : "Xóa thiết bị và khôi phục" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "清除设备并恢复" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "清除裝置並還原" + } } } }, @@ -90860,24 +92814,60 @@ "value" : "Er du sikker på, at du vil slette alle beskeder fra din samtale med {name} på denne enhed?" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bist du sicher, dass du alle Nachrichten von deinem Gespräch mit {name} auf diesem Gerät löschen möchtest?" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Are you sure you want to clear all messages from your conversation with {name} on this device?" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Estás seguro de que quieres borrar todos los mensajes de tu conversación con {name} en este dispositivo?" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Estás seguro de que quieres borrar todos los mensajes de tu conversación con {name} en este dispositivo?" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Êtes-vous sûr de vouloir effacer tous les messages de votre conversation avec {name} sur cet appareil ?" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "क्या आप वाकई इस डिवाइस से {name} के साथ बातचीत से सभी संदेश साफ़ करना चाहते हैं?" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Biztosan törli az összes üzenetet a(z) {name} nevű partnerével való beszélgetésből ezen az eszközön?" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sei sicuro di voler cancellare tutti i messaggi della tua chat con {name} da questo dispositivo?" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このデバイス上の{name}との会話のすべてのメッセージを本当に削除しますか?" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -90896,11 +92886,53 @@ "value" : "Czy na pewno chcesz wyczyścić wszystkie wiadomości z konwersacji z {name} na tym urządzeniu?" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tem a certeza de que pretende limpar todas as mensagens da sua conversa com {name} neste dispositivo?" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ești sigur/ă că vrei să ștergi toate mesajele din conversația ta cu {name} de pe acest dispozitiv?" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вы уверены, что хотите удалить все сообщения из вашего чата с {name} на этом устройстве?" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Är du säker på att du vill rensa alla meddelanden från din konversation med {name} på denna enhet?" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "{name} ile olan sohbetinizdeki tüm mesajları bu cihazdan temizlemek istediğinizden emin misiniz?" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Ви впевнені, що хочете видалити всі повідомлення з вашої розмови з {name} на цьому пристрої?" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "您确定要清除设备上与 {name} 的所有对话消息吗?" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "您確定要清除此裝置上與 {name} 的所有對話訊息嗎?" + } } } }, @@ -91416,24 +93448,60 @@ "value" : "Er du sikker på, at du vil slette alle beskeder fra {community_name} på denne enhed?" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bist du sicher, dass du alle Nachrichten von {community_name} auf diesem Gerät löschen möchtest?" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Are you sure you want to clear all messages from {community_name} on this device?" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Estás seguro de que quieres borrar todos los mensajes de {community_name} en este dispositivo?" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Estás seguro de que quieres borrar todos los mensajes de {community_name} en este dispositivo?" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Êtes-vous sûr de vouloir effacer tous les messages de {community_name} sur cet appareil ?" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "क्या आप वाकई इस डिवाइस से {community_name} के सभी संदेश साफ़ करना चाहते हैं?" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Biztosan törli a(z) {community_name} összes üzenetét ezen az eszközön?" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sei sicuro di voler cancellare tutti i messaggi di {community_name} da questo dispositivo?" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このデバイス上の{community_name}のすべてのメッセージを本当に削除しますか?" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -91452,11 +93520,53 @@ "value" : "Czy na pewno chcesz wyczyścić wszystkie wiadomości z {community_name} na tym urządzeniu?" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tem a certeza de que pretende limpar todas as mensagens de {community_name} deste dispositivo?" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ești sigur/ă că vrei să ștergi toate mesajele de la {community_name} de pe acest dispozitiv?" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вы уверены, что хотите удалить все сообщения из {community_name} на этом устройстве?" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Är du säker på att du vill rensa alla meddelanden från {community_name} på denna enhet?" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "{community_name} topluluğundaki tüm mesajları bu cihazdan temizlemek istediğinizden emin misiniz?" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Ви впевнені, що хочете видалити всі повідомлення від {community_name} на цьому пристрої?" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "您确定要清除设备上 {community_name}的所有消息吗?" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "您確定要清除本裝置上來自 {community_name} 的所有訊息嗎?" + } } } }, @@ -92942,24 +95052,60 @@ "value" : "Er du sikker på, at du vil slette alle beskeder fra {group_name}?" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bist du sicher, dass du alle Nachrichten von {group_name} löschen möchtest?" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Are you sure you want to clear all messages from {group_name}?" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Estás seguro de que quieres borrar todos los mensajes de {group_name}?" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Estás seguro de que quieres borrar todos los mensajes de {group_name}?" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Êtes-vous sûr de vouloir effacer tous les messages de {group_name} ?" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "क्या आप वाकई {group_name} से सभी संदेश साफ़ करना चाहते हैं?" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Biztosan törli a(z) {group_name} összes üzenetét?" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sei sicuro di voler cancellare tutti i messaggi di {group_name}?" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "{group_name}のすべてのメッセージを本当に削除しますか?" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -92978,11 +95124,53 @@ "value" : "Czy na pewno chcesz wyczyścić wszystkie wiadomości z {group_name}?" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tem a certeza de que pretende limpar todas as mensagens de {group_name}?" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ești sigur/ă că dorești să ștergi toate mesajele de la {group_name}?" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вы уверены, что хотите удалить все сообщения из {group_name}?" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Är du säker på att du vill rensa alla meddelanden från {group_name}?" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "{group_name} grubundaki tüm mesajları temizlemek istediğinizden emin misiniz?" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Ви впевнені, що хочете видалити всі повідомлення з {group_name}?" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "您确定要清除设备上 {group_name} 的所有消息吗?" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "您確定要清除 {group_name} 中的所有訊息嗎?" + } } } }, @@ -93498,24 +95686,60 @@ "value" : "Er du sikker på, at du vil slette alle beskeder fra {group_name} på denne enhed?" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bist du sicher, dass du alle Nachrichten von {group_name} auf diesem Gerät löschen möchtest?" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Are you sure you want to clear all messages from {group_name} on this device?" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Estás seguro de que quieres borrar todos los mensajes de {group_name} en este dispositivo?" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Estás seguro de que quieres borrar todos los mensajes de {group_name} en este dispositivo?" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Êtes-vous sûr de vouloir effacer tous les messages de {group_name} sur cet appareil ?" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "क्या आप वाकई इस डिवाइस से {group_name} के सभी संदेश साफ़ करना चाहते हैं?" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Biztosan törli a(z) {group_name} összes üzenetét ezen az eszközön?" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sei sicuro di voler cancellare tutti i messaggi di {group_name} da questo dispositivo?" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このデバイス上の{group_name}のすべてのメッセージを本当に削除しますか?" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -93534,11 +95758,53 @@ "value" : "Czy na pewno chcesz wyczyścić wszystkie wiadomości z {group_name} na tym urządzeniu?" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tem a certeza de que pretende limpar todas as mensagens de {group_name} deste dispositivo?" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ești sigur/ă că vrei să ștergi toate mesajele de la {group_name} de pe acest dispozitiv?" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вы уверены, что хотите удалить все сообщения из {group_name} на этом устройстве?" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Är du säker på att du vill rensa alla meddelanden från {group_name} på denna enhet?" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "{group_name} grubundaki tüm mesajları bu cihazdan temizlemek istediğinizden emin misiniz?" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Ви впевнені, що хочете видалити всі повідомлення з {group_name} на цьому пристрої?" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "您确定要清除设备上 {group_name} 的所有消息吗?" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "您確定要清除本裝置上來自 {group_name} 的所有訊息嗎?" + } } } }, @@ -94054,24 +96320,60 @@ "value" : "Er du sikker på, at du vil slette alle Egen note-beskeder fra din enhed?" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bist du sicher, dass du alle Nachrichten in »Notiz an mich« auf diesem Gerät löschen möchtest?" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Are you sure you want to clear all Note to Self messages on this device?" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Estás seguro de que deseas eliminar todos los mensajes de Nota Personal en este dispositivo?" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Estás seguro de que deseas eliminar todos los mensajes de Nota Personal en este dispositivo?" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Êtes-vous sûr de vouloir effacer tous les messages Note pour soi-même sur cet appareil ?" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "क्या आप वाकई इस डिवाइस से सभी अपने लिए नोट संदेश साफ़ करना चाहते हैं?" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Biztosan törli az összes Jegyzet magamnak üzenetet ezen az eszközön?" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sei sicuro di voler cancellare tutti i messaggi di Note to Self da questo dispositivo?" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このデバイス上の自分用メモのすべてのメッセージを本当に削除しますか?" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -94090,11 +96392,53 @@ "value" : "Czy na pewno chcesz usunąć z urządzenia wszystkie Moje notatki?" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tem a certeza de que pretende limpar todas as mensagens da Nota Pessoal deste dispositivo?" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ești sigur că vrei să ștergi toate mesajele Notă personală de pe acest dispozitiv?" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вы уверены, что хотите удалить все сообщения из Заметки для себя на этом устройстве?" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Är du säker på att du vill rensa alla meddelanden i Notera till mig själv på denna enhet?" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu cihazdaki tüm Kendime Not mesajlarını temizlemek istediğinizden emin misiniz?" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Ви впевнені, що хочете видалити всі повідомлення в Нотатці для себе на цьому пристрої?" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "您确定要清除设备上所有 Note to Self 消息吗?" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "您確定要清除此裝置上的所有 小筆記訊息嗎?" + } } } }, @@ -94125,6 +96469,12 @@ "value" : "Slet på denne enhed" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Auf diesem Gerät löschen" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -94137,12 +96487,30 @@ "value" : "Forigi sur ĉi tiu aparato" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Borrar en este dispositivo" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Borrar en este dispositivo" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Effacer sur cet appareil" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "इस डिवाइस पर साफ़ करें" + } + }, "hu" : { "stringUnit" : { "state" : "translated", @@ -94155,6 +96523,18 @@ "value" : "Hapus dalam perangkat ini" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancella da questo dispositivo" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このデバイス上で削除" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -94173,17 +96553,53 @@ "value" : "Wyczyść na tym urządzeniu" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Limpar neste dispositivo" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Curăță pe acest dispozitiv" + } + }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удалить на этом устройстве" } }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rensa på denna enhet" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu cihazı temizle" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Очистити на цьому пристрої" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "在此设备上清除" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "在此裝置上清除" + } } } }, @@ -94681,6 +97097,12 @@ "value" : "إغلاق التطبيق" } }, + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tətbiqi bağla" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -94693,6 +97115,12 @@ "value" : "Zavřít aplikaci" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "App schließen" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -94705,12 +97133,30 @@ "value" : "Fermi aplikaĵon" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cerrar aplicación" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cerrar aplicación" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Fermer l'application" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "ऐप बंद करें" + } + }, "hu" : { "stringUnit" : { "state" : "translated", @@ -94723,6 +97169,18 @@ "value" : "Tutup Aplikasi" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chiudi app" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アプリを終了" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -94741,11 +97199,53 @@ "value" : "Zamknij aplikację" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fechar Aplicação" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Închide aplicația" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Закрыть приложение" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Stäng appen" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uygulamayı kapat" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Закрити застосунок" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "关闭应用程序" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "關閉應用程式" + } } } }, @@ -96635,6 +99135,131 @@ } } }, + "communityDescriptionEnter" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "İcma aaçıqlamasını daxil edin" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Introduïu una descripció de la comunitat" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zadejte popis komunity" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Community-Beschreibung eingeben" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enter a community description" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Introduce una descripción de la comunidad" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Introduce una descripción de la comunidad" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Entrez une description de la communauté" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "एक सामुदायिक विवरण दर्ज करें" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inserisci una descrizione della Comunità" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "コミュニティの説明を入力してください" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Voer een communitybeschrijving in" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wprowadź opis społeczności" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Digite uma descrição da Comunidade" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Introdu o descriere a comunității" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Введите описание сообщества" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ange en communitybeskrivning" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Введіть опис спільноти" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "输入社群描述" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "輸入社群描述" + } + } + } + }, "communityEnterUrl" : { "extractionState" : "manual", "localizations" : { @@ -103365,6 +105990,256 @@ } } }, + "communityNameEnter" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "İcma adını daxil edin" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Introduïu un nom de comunitat" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zadejte název komunity" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Community-Namen eingeben" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enter a community name" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Introduce un nombre de comunidad" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Introduce un nombre de comunidad" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Entrez un nom de communauté" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "एक सामुदायिक नाम दर्ज करें" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inserisci un nome della Comunità" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "コミュニティ名を入力してください" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Voer een communitynaam in" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wprowadź nazwę społeczności" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Digite o nome da Comunidade" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Introdu numele comunității" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Введите название сообщества" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ange ett communitynamn" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Введіть назву спільноти" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "输入社群名称" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "輸入社群名稱" + } + } + } + }, + "communityNameEnterPlease" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lütfən, icma adını daxil edin" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Introduïu un nom de comunitat" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Prosím zadejte název komunity" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bitte gib einen Community-Namen ein." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Please enter a community name" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Por favor, introduce un nombre de comunidad" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Por favor, introduce un nombre de comunidad" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Veuillez entrer un nom de communauté" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "कृपया एक सामुदायिक नाम दर्ज करें" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inserisci un nome della Comunità" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "コミュニティ名を入力してください" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Voer een communitynaam in" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Proszę wprowadzić nazwę społeczności" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Por favor, insira um nome de Comunidade" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Te rugăm să introduci un nume al comunității" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пожалуйста, введите название сообщества" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vänligen ange ett communitynamn" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Будь ласка, введіть назву спільноти" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "请输入社群名称" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "請輸入社群名稱" + } + } + } + }, "communityUnknown" : { "extractionState" : "manual", "localizations" : { @@ -111077,6 +113952,65 @@ } } }, + "contentNotificationDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yeni bir mesaj alındıqda daxili bildirişlərdə nümayiş olunacaq məzmunu seçin." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vyberte obsah, který se zobrazí v místních upozorněních při přijetí zprávy." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Choose the content displayed in local notifications when an incoming message is received." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Choisissez le contenu affiché dans les notifications locales lorsqu'un message entrant est reçu." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kies de inhoud die wordt weergegeven in lokale meldingen wanneer een inkomend bericht wordt ontvangen." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wybierz treść wyświetlaną w lokalnych powiadomieniach, kiedy pojawia się nowa wiadomość." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Выберите содержимое, отображаемое в локальных уведомлениях при получении входящего сообщения." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Välj vilket innehåll som ska visas i lokala aviseringar när ett inkommande meddelande tas emot." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Обирайте, який вміст показуватиметься у сповіщеннях після отримання повідомлення." + } + } + } + }, "conversationsAddedToHome" : { "extractionState" : "manual", "localizations" : { @@ -116892,19769 +119826,19585 @@ } }, "conversationsEnterDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enter və Shift+Enter düymələrinin danışıqlarda necə işləyəcəyini təyin edin." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Definujte, jak budou fungovat klávesy Enter a Shift+Enter v konverzacích." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Definiere, wie Eingabe- und Umschalttaste in Konversationen funktionieren." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Define how the Enter and Shift+Enter keys function in conversations." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Définissez le fonctionnement des touches Entrée et Maj+Entrée dans les conversations." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Stel in hoe de toetsen Enter en Shift+Enter functioneren in gesprekken." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zdefiniuj jak klawisz Enter i kombinacja Shift+Enter działają w konwersacjach." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Определите, как будут работать клавиши Enter и Shift+Enter в переписке." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Definiera hur Enter- och Skift+Enter-tangenterna fungerar i konversationer." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Налаштування дій клавіш Enter та Shift+Enter у розмовах." + } + } + } + }, + "conversationsEnterNewLine" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "SHIFT + ENTER mesajı göndərir, ENTER yeni sətrə keçir." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "SHIFT + ENTER odešle zprávu, ENTER začne nový řádek." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "SHIFT + ENTER sends a message, ENTER starts a new line." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "MAJUSCULE + ENTRÉE envoie un message, ENTRÉE commence une nouvelle ligne." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "SHIFT + ENTER verzendt een bericht, ENTER begint een nieuwe regel." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "SHIFT + ENTER wysyła wiadomość, ENTER zaczyna nowy wiersz." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "SHIFT + ENTER отправляет сообщение, ENTER начинает новую строку." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "SHIFT + ENTER skickar ett meddelande, ENTER startar en ny rad." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "SHIFT + ENTER надсилає повідомлення, ENTER починає новий рядок." + } + } + } + }, + "conversationsEnterSends" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "ENTER mesajı göndərir, SHIFT + ENTER yeni sətrə keçir." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zmáčknutím ENTER se zpráva odešle, SHIFT + ENTER vytvoří nový řádek." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "ENTER sends a message, SHIFT + ENTER starts a new line." + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "ENTER envía un mensaje, SHIFT + ENTER inicia una nueva línea." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "ENTER envía un mensaje, SHIFT + ENTER inicia una nueva línea." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "ENTRÉE envoie un message, MAJ + ENTRÉE commence une nouvelle ligne." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "ENTER verzendt een bericht, SHIFT + ENTER begint een nieuwe regel." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "ENTER wysyła wiadomość, SHIFT + ENTER zaczyna nowy wiersz." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "ENTER для отправки, SHIFT + ENTER для новой строки." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "ENTER skickar ett meddelande, SHIFT + ENTER påbörjar en ny rad." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "ENTER надсилає повідомлення, SHIFT + ENTER починає новий рядок." + } + } + } + }, + "conversationsGroups" : { "extractionState" : "manual", "localizations" : { "af" : { "stringUnit" : { "state" : "translated", - "value" : "Funksie van die Enter-sleutel wanneer u 'n gesprek tik." + "value" : "Groepe" } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "وظيفة مفتاح الإدخال عند الكتابة في محادثة." + "value" : "مجموعات" } }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "Bir danışıqda yazarkən Enter düyməsinin funksiyası." + "value" : "Qruplar" } }, "bal" : { "stringUnit" : { "state" : "translated", - "value" : "واژئے کُنجیء کارکردگی ہدباتءَ نیںّ گفتگوئے شتگ." + "value" : "گروپاں" } }, "be" : { "stringUnit" : { "state" : "translated", - "value" : "Функцыя клавішы Enter пры наборы тэксту ў гутарцы." + "value" : "Групы" } }, "bg" : { "stringUnit" : { "state" : "translated", - "value" : "Функция на клавиша Enter, когато пишете в разговор." + "value" : "Групи" } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "কথোপকথনে টাইপ করার সময় এন্টার কী এর ফাংশন।" + "value" : "গ্রুপস" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Funció de la tecla d'introducció durant l'escriptura en una conversa." + "value" : "Grups" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Funkce klávesy Enter při psaní v konverzaci." + "value" : "Skupiny" } }, "cy" : { "stringUnit" : { "state" : "translated", - "value" : "Swyddogaeth allwedd enter wrth deipio mewn sgwrs." + "value" : "Grwpiau" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Funktion af enter-tasten, når du skriver i en samtale." + "value" : "Grupper" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Funktion der Eingabetaste beim Tippen in einer Unterhaltung." + "value" : "Gruppen" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Λειτουργία του πλήκτρου εισαγωγής κατά την πληκτρολόγηση σε συνομιλία." + "value" : "Ομάδες" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Function of the enter key when typing in a conversation." + "value" : "Groups" } }, "eo" : { "stringUnit" : { "state" : "translated", - "value" : "Funkcio de la eniga klavo dum tajpado en konversacio." + "value" : "Grupoj" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Función de la tecla enter al escribir en una conversación." + "value" : "Grupos" } }, "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Función de la tecla Enter al escribir en una conversación." + "value" : "Grupos" } }, "et" : { "stringUnit" : { "state" : "translated", - "value" : "Enter-klahvi funktsioon vestluse ajal tippimisel." + "value" : "Grupid" } }, "eu" : { "stringUnit" : { "state" : "translated", - "value" : "Sartu tekla funtzioa elkarrizketan idazten ari zarenean." + "value" : "Taldeak" } }, "fa" : { "stringUnit" : { "state" : "translated", - "value" : "ویژگی کلید Enter در هنگام تایپ در یک مکالمه." + "value" : "گروه‌ها" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Enter-näppäimen toiminto keskustelussa kirjoittaessa." + "value" : "Ryhmät" } }, "fil" : { "stringUnit" : { "state" : "translated", - "value" : "Gamit ng enter key kapag nagta-type sa isang usapan." + "value" : "Mga Grupo" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Fonction de la touche Entrée lors de la saisie dans une conversation." + "value" : "Groupes" } }, "gl" : { "stringUnit" : { "state" : "translated", - "value" : "Función da tecla Enter ao escribir nunha conversa." + "value" : "Grupos" } }, "ha" : { "stringUnit" : { "state" : "translated", - "value" : "Aikin maɓallin shigar yayin rubutu a cikin tattaunawa." + "value" : "Rukuni" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "פונקציה של מקש ה-Enter בעת הקלדה בשיחה." + "value" : "קבוצות" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "बातचीत में टाइप करते समय एंटर कुंजी का कार्य।" + "value" : "समूह" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Funkcija tipke enter pri pisanju u razgovoru." + "value" : "Grupe" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Az Enter billentyű funkciója beszélgetés közben." + "value" : "Csoportok" } }, "hy-AM" : { "stringUnit" : { "state" : "translated", - "value" : "Enter ստեղնի գործառույթը զրույցի ժամանակ:" + "value" : "Խմբեր" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Fungsi tombol enter saat mengetik dalam percakapan." + "value" : "Grup" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Funzione del tasto invio quando si digita in una chat." + "value" : "Gruppi" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "会話中のEnterキーの機能" + "value" : "グループ" } }, "ka" : { "stringUnit" : { "state" : "translated", - "value" : "ფუნქცია, როცა კლავიატურაზე იყენებთ Enter ღილაკს საუბრის დროს." + "value" : "ჯგუფები" } }, "km" : { "stringUnit" : { "state" : "translated", - "value" : "មុខងាររបស់ឃី enter នៅពេលវាយក្នុងការសន្ទនា។" + "value" : "ក្រុម" } }, "kn" : { "stringUnit" : { "state" : "translated", - "value" : "ಸಂಭಾಷಣೆಯಲ್ಲಿ ಟೈಪಿಂಗ್‌ನಲ್ಲಿ ಎಂಟರ್ ಕೀಯ ತಂತ್ರಜ್ಞಾನ." + "value" : "ಗುಂಪುಗಳು" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "대화에서 Enter 키 기능." + "value" : "그룹" } }, "ku" : { "stringUnit" : { "state" : "translated", - "value" : "فەرمیۆنی لەگەڵ داخستنی کلیلەکەرە" + "value" : "گروپەکان" } }, "ku-TR" : { "stringUnit" : { "state" : "translated", - "value" : "Karwilaşa tuşî zimanê gava niqaşe." + "value" : "Kom" } }, "lg" : { "stringUnit" : { "state" : "translated", - "value" : "Enkola ya ki Enter okunyiga kulwokola message nga wandikira mu kwogereza." + "value" : "Ebibinja" } }, "lt" : { "stringUnit" : { "state" : "translated", - "value" : "Įveskite pokalbio metu naudojamo enter klavišo funkciją." + "value" : "Grupės" } }, "lv" : { "stringUnit" : { "state" : "translated", - "value" : "Enter taustiņa funkcija, rakstot sarunā." + "value" : "Grupas" } }, "mk" : { "stringUnit" : { "state" : "translated", - "value" : "Функција на Enter копчето при пишување во разговор." + "value" : "Групи" } }, "mn" : { "stringUnit" : { "state" : "translated", - "value" : "Товчилсон түлхүүрийн үүрэг нь харилцан ярианы үед." + "value" : "Бүлгүүд" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Fungsi kekunci enter semasa menaip dalam perbualan." + "value" : "Kumpulan" } }, "my" : { "stringUnit" : { "state" : "translated", - "value" : "စကားပြောမှာ enter key ရဲ့လုပ်ဆောင်ချက်" + "value" : "အုပ်စုများ" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Funksjon til enter-tasten når man skriver i en samtale." + "value" : "Grupper" } }, "nb-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Funksjon av enter-tasten når du skriver i en samtale." + "value" : "Grupper" } }, "ne-NP" : { "stringUnit" : { "state" : "translated", - "value" : "समूह निमन्त्रणा सफल" + "value" : "समूहहरू" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Functie van de entertoets bij het typen in een gesprek." + "value" : "Groepen" } }, "nn-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Funksjon for enter-tasten ved skriving i samtale." + "value" : "Grupper" } }, "ny" : { "stringUnit" : { "state" : "translated", - "value" : "Ntchito ya kiyi ya enter mukalemba mu kukambirana." + "value" : "Magulu" } }, "pa-IN" : { "stringUnit" : { "state" : "translated", - "value" : "ਗੱਲਬਾਤ ਵਿੱਚ ਲਿਖਣ ਵੇਲੇ ਐਂਟਰ ਕੀ ਦਾ ਫੰਕਸ਼ਨ।" + "value" : "ਗਰੁੱਪ" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Funkcja klawisza Enter podczas pisania wiadomości w konwersacji." + "value" : "Grupy" } }, "ps" : { "stringUnit" : { "state" : "translated", - "value" : "د خبرو اترو په دوران کې د انټر کیلي دنده." + "value" : "ډلې" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Função da tecla Enter ao digitar em uma conversa." + "value" : "Grupos" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Função da tecla Enter numa conversa." + "value" : "Grupos" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Funcția tastei Enter când scrii într-o conversație." + "value" : "Grupuri" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Функция клавиши Enter при вводе текста в беседе." + "value" : "Группы" } }, "sh" : { "stringUnit" : { "state" : "translated", - "value" : "Funkcija tastera enter tokom tipkanja u razgovoru." + "value" : "Grupe" } }, "si-LK" : { "stringUnit" : { "state" : "translated", - "value" : "සංවාදයේ, Enter යතුරේ ක්‍රියාකාරිත්වය." + "value" : "සමූහ" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Funkcia klávesy Enter pri písaní v konverzácii." + "value" : "Skupiny" } }, "sl" : { "stringUnit" : { "state" : "translated", - "value" : "Funkcija tipke enter pri tipkanju v pogovoru." + "value" : "Skupine" } }, "sq" : { "stringUnit" : { "state" : "translated", - "value" : "Funksioni i tastit enter kur shkrini mesazhe." + "value" : "Grupe" } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : "Функција тастера ентер при куцању у разговору." + "value" : "Групе" } }, "sr-Latn" : { "stringUnit" : { "state" : "translated", - "value" : "Funkcija tastera enter prilikom kucanja poruke." + "value" : "Grupe" } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Funktionen av Enter-tangenten när du skriver i en konversation." + "value" : "Grupper" } }, "sw" : { "stringUnit" : { "state" : "translated", - "value" : "Kazi ya kitufe cha kuingiza wakati wa kuandika katika mazungumzo." + "value" : "Makundi" } }, "ta" : { "stringUnit" : { "state" : "translated", - "value" : "உரையாடலில் தட்டச்சு செய்வதற்கான எந்திரக் கை நுட்பம்." + "value" : "குழுக்கள்" } }, "te" : { "stringUnit" : { "state" : "translated", - "value" : "సంభాషణలో టైపింగ్ చేస్తున్నప్పుడు ఎంటర్ కీ యొక్క విధానం." + "value" : "సమూహాలు" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "ฟังก์ชันของปุ่ม Enter เมื่อพิมพ์ในบทสนทนา" + "value" : "กลุ่ม" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Sohbette yazarken enter tuşunun fonksiyonu." + "value" : "Gruplar" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Функція клавіші Enter під час набору повідомлення." + "value" : "Групи" } }, "ur-IN" : { "stringUnit" : { "state" : "translated", - "value" : "گفتگو میں ٹائپ کرتے وقت انٹر کی کلید کا فنکشن" + "value" : "گروپس" } }, "uz" : { "stringUnit" : { "state" : "translated", - "value" : "Suhbatda enter tugmasining vazifasi." + "value" : "Guruhlar" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Chức năng của phím enter khi nhập nội dung trong cuộc trò chuyện." + "value" : "Nhóm" } }, "xh" : { "stringUnit" : { "state" : "translated", - "value" : "Umsebenzi weqhosha loku ngenisa xa uchwetheza kwincoko." + "value" : "Amaqela" } }, "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "在对话中输入回车键时的功能。" + "value" : "群组" } }, "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "在對話中鍵入時回車鍵的功能。" + "value" : "群組" } } } }, - "conversationsEnterNewLine" : { + "conversationsMessageTrimming" : { "extractionState" : "manual", "localizations" : { "af" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER stuur 'n boodskap, ENTER begin 'n nuwe lyn" + "value" : "Boodskap Snoei" } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER يرسل الرسالة، ENTER يبدأ سطرًا جديدًا" + "value" : "تقليم الرسالة" } }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER mesajı göndərir, ENTER yeni sətrə keçir" + "value" : "Mesajları kəs" } }, "bal" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER پیام روانگی، ENTER نوکی خط شروع کـــــــن" + "value" : "Message Trimming" } }, "be" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER адпраўляе паведамленне, ENTER пачынае новы радок" + "value" : "Абрэзка паведамленняў" } }, "bg" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER изпраща съобщение, ENTER започва нов ред" + "value" : "Съкращаване на съобщенията" } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER একটি বার্তা পাঠাবে, ENTER একটি নতুন লাইন শুরু করবে" + "value" : "বার্তা সংক্ষিপ্তকরণ" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "MAJÚSCULA + ENTRA envia un missatge, ENTRA comença una línia nova" + "value" : "Escapçament de missatges" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER odešle zprávu, ENTER začne nový řádek" + "value" : "Pročištění zpráv" } }, "cy" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER yn anfon neges, ENTER yn dechrau llinell newydd" + "value" : "Tocio Negeseuon" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER sender en besked, ENTER starter en ny linje" + "value" : "Trimning af beskeder" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + Eingabe sendet eine Nachricht, Eingabe startet eine neue Zeile" + "value" : "Nachrichtenkürzung" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER στέλνει ένα μήνυμα, ENTER ξεκινά μια νέα γραμμή" + "value" : "Περικοπή μηνυμάτων" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER sends a message, ENTER starts a new line" + "value" : "Message Trimming" } }, "eo" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER sendas mesaĝon, ENTER komencas novan linion" + "value" : "Tondeto de mesaĝoj" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER envía un mensaje, ENTER inicia una nueva línea" + "value" : "Recorte de mensajes en chats" } }, "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER envía un mensaje, ENTER inicia una nueva línea" + "value" : "Recorte de mensajes en chats" } }, "et" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER saadab sõnumi, ENTER alustab uut rida" + "value" : "Sõnumi piiramine" } }, "eu" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER mezu bat bidaltzen du, ENTER lerro berri bat hasten" + "value" : "Mezuen ebaketa" } }, "fa" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER ارسال پیام، ENTER خط جدید آغاز می‌کند" + "value" : "پیرایش پیام" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER lähettää viestin, ENTER aloittaa uuden rivin" + "value" : "Keskustelujen tiivistys" } }, "fil" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER ang nagpapadala ng mensahe, ENTER ang nagsisimula ng bagong linya" + "value" : "Pagtatabas ng Mensahe" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "MAJUSCULE + ENTRÉE envoie un message, ENTRÉE commence une nouvelle ligne" + "value" : "Élagage des messages" } }, "gl" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER envía unha mensaxe, ENTER inicia unha nova liña" + "value" : "Recorte das mensaxes" } }, "ha" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER yana aikawa da saƙo, ENTER yana fara sabon layi" + "value" : "Rage Saƙo" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER שולח הודעה, ENTER מתחיל שורה חדשה" + "value" : "קיצוץ הודעה" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER एक संदेश भेजता है, ENTER एक नई पंक्ति शुरू करता है" + "value" : "संदेश ट्रिमिंग" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER šalje poruku, ENTER započinje novi redak" + "value" : "Skraćivanje poruka" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER elküldi az üzenetet, ENTER új sort kezd" + "value" : "Üzenetek rövidítése" } }, "hy-AM" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER-ը ուղարկում է հաղորդագրություն, ENTER-ը սկսում է նոր տող" + "value" : "Նամակների կարճեցում" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER mengirim pesan, ENTER mulai baris baru" + "value" : "Pemangkasan Pesan" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + INVIO invia un messaggio, INVIO inizia una nuova riga" + "value" : "Sfoltitura dei messaggi" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "シフト + エンター 送信、エンター 改行" + "value" : "メッセージの削減" } }, "ka" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER აგზავნის შეტყობინებას, ENTER იწყებს ახალ სტრიქონს" + "value" : "deleteAfterGroupPR1MessageSound" } }, "km" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER ផ្ញើសារ ENTER ចាប់ផ្តើមបន្ទាត់ថ្មី" + "value" : "តម្រឹមសារ" } }, "kn" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER ಒಂದು ಸಂದೇಶವನ್ನು ಕಳುಹಿಸುತ್ತದೆ, ENTER ಹೊಸ ಎರಡು ಸಾಲು ಪ್ರಾರಂಭಿಸುತ್ತದೆ" + "value" : "ಸಂದೇಶಗಳ ಕತ್ತರಿಸಿ ಹಾಕುವಿಕೆ" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER 메시지 전송, ENTER 새 줄 시작" + "value" : "대화 줄이기" } }, "ku" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER (وێنەی هاوبەش بە ناوی کانفرم ، ENTER (نوێ دەستەکی." + "value" : "بڕینی نامە" } }, "ku-TR" : { "stringUnit" : { "state" : "translated", - "value" : "BIKAR BIKI + ENTER peyamekê dişîne, ENTER xeta nû dest pê dibej." + "value" : "Kesixîna Peyamê" } }, "lg" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER esoose obubaka, ENTER entandiika olumuli" + "value" : "Tikka olubaka olwa teriiko ly’oteereza" } }, "lt" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER siunčia žinutę, ENTER pradeda naują eilutę" + "value" : "Žinučių išvalymas" } }, "lv" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER nosūta ziņu, ENTER sāk jaunu rindu" + "value" : "Ziņu apgriešana" } }, "mk" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER испраќа порака, ENTER започнува нов ред" + "value" : "Сечење на пораки" } }, "mn" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER мессеж илгээнэ, ENTER шинэ мөр эхлүүлнэ" + "value" : "Мессеж хасалт" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER untuk menghantar mesej, ENTER untuk memulakan baris baru" + "value" : "Pemangkasan Mesej" } }, "my" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER သည် စာ မက်ဆေ့ချ်ပေးပါ။ ENTER သည် စာကြောင်းအသစ်စ လား။" + "value" : "မဇ်ဆေ့ဂ ှ" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER sender en melding, ENTER starter en ny linje" + "value" : "Automatisk sletting" } }, "nb-NO" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER sender melding, ENTER starter en ny linje" + "value" : "Meldingsbeskjæring" } }, "ne-NP" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER सन्देश पठाउँछ, ENTER नयाँ लाइन सुरु गर्छ" + "value" : "सन्देश ट्रिमिंग" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER verzendt een bericht, ENTER begint een nieuwe regel" + "value" : "Beperk bewaartermijn" } }, "nn-NO" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER sender ei melding, ENTER startar ei ny linje" + "value" : "Meldingsbeskjæring" } }, "ny" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER sends a message, ENTER starts a new line" + "value" : "Kusandulika mauthenga" } }, "pa-IN" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER ਇੱਕ ਸੁਨੇਹਾ ਭੇਜਦਾ ਹੈ, ENTER ਇੱਕ ਨਵੀ ਲਾਈਨ ਸ਼ੁਰੂ ਕਰਦਾ ਹੈ" + "value" : "ਸੁਨੇਹਾ ਕਟਾਈ" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER wysyła wiadomość, ENTER zaczyna nową linijkę" + "value" : "Przycinanie wiadomości" } }, "ps" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER پیغام لیږي، ENTER نوی کرښه پیلوي" + "value" : "پیغام تراشې" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER envia uma mensagem, ENTER inicia uma nova linha" + "value" : "Corte de Mensagens" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER envia uma mensagem, ENTER começa uma nova linha" + "value" : "Redução do tamanho de mensagem" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER trimite un mesaj, ENTER începe o linie nouă" + "value" : "Scurtarea mesajelor" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER отправляет сообщение, ENTER начинает новую строку" + "value" : "Обрезка сообщений" } }, "sh" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER šalje poruku, ENTER započinje novi red" + "value" : "Skraćivanje poruke" } }, "si-LK" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER පණිවිඩය යවයි, ENTER නව මූලය පටන් ගනී" + "value" : "පණිවිඩ කැපීම" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER odošle správu, ENTER začne nový riadok" + "value" : "Prečistenie správ" } }, "sl" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER pošlje sporočilo, ENTER začne novo vrstico" + "value" : "Obrezovanje sporočil" } }, "sq" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER dërgon një mesazh, ENTER fillon një rresht të ri" + "value" : "Shkurtim mesazhesh" } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER шаље поруку, ENTER почиње нови ред" + "value" : "Скраћивање порука" } }, "sr-Latn" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER šalje poruku, ENTER započinje novi red" + "value" : "Skraćivanje poruka" } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER skickar ett meddelande, ENTER startar en ny rad" + "value" : "Trimma meddelanden" } }, "sw" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER kutuma ujumbe, ENTER kuanza mstari mpya" + "value" : "Ujumbe unapunguza" } }, "ta" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER செய்தி அனுப்பும், ENTER புதிய வரியைத் தொடங்குகிறது" + "value" : "செய்தி மேராசூக்கம்" } }, "te" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER సందేశం పంపుతుంది, ENTER కొత్త పంక్తిని ప్రారంభిస్తుంది" + "value" : "సందేశం కత్తిరించి సరి చేయుట" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER ส่งข้อความ, ENTER เริ่มบรรทัดใหม่" + "value" : "ตัดข้อความให้สั้นลง" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER ileti gönderir, ENTER yeni bir satır başlatır" + "value" : "İleti kırpma" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER надсилає повідомлення, ENTER починає новий рядок" + "value" : "Автовидалення повідомлень" } }, "ur-IN" : { "stringUnit" : { "state" : "translated", - "value" : "شفٹ + انٹر پیغام بھیجتا ہے، انٹر نئی لائن شروع کرتا ہے" + "value" : "پیغام کی کانٹ چھانٹ" } }, "uz" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER xabarni yuboradi, ENTER yangi qatordan boshlaydi" + "value" : "Xabarni chetlatish" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER gửi tin nhắn, ENTER bắt đầu dòng mới" + "value" : "Thu gọn tin nhắn" } }, "xh" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER sends a message, ENTER starts a new line" + "value" : "Ukuskrowa umyalezo" } }, "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT+回车键发送消息,回车键换行" + "value" : "消息整理" } }, "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "SHIFT + ENTER 傳送訊息, ENTER 起新行" + "value" : "訊息清理" } } } }, - "conversationsEnterSends" : { + "conversationsMessageTrimmingTrimCommunities" : { "extractionState" : "manual", "localizations" : { "af" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER stuur 'n boodskap, SHIFT + ENTER begin 'n nuwe lyn" + "value" : "Trim Gemeenskappe" } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "إدخال يرسل رسالة، SHIFT + ENTER يبدأ سطر جديد" + "value" : "تقليم المجتمعات" } }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER mesajı göndərir, SHIFT + ENTER yeni sətrə keçir" + "value" : "İcmalarda kəsmə" } }, "bal" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER ایک پیغام بھیجتا ہے، SHIFT + ENTER ایک نیا لائن شروع کرتا ہے" + "value" : "کمیونٹیز کو تراشیں" } }, "be" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER адпраўляе паведамленне, SHIFT + ENTER пачынае новы радок" + "value" : "Абрэжце супольніцтва" } }, "bg" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER изпраща съобщение, SHIFT + ENTER започва нов ред" + "value" : "Подрязване на Общности" } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER একটি বার্তা পাঠাবে, SHIFT + ENTER একটি নতুন লাইন শুরু করে" + "value" : "কমিউনিটিস ট্রিম করুন" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER envia un missatge, SHIFT + ENTER comença una nova línia" + "value" : "Limiteu comunitats" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Zmáčknutím ENTER se zpráva odešle, SHIFT + ENTER vytvoří nový řádek" + "value" : "Pročistit komunity" } }, "cy" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER yn anfon neges, SHIFT + ENTER yn dechrau llinell newydd" + "value" : "Trimio Cymunedau" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER sender en besked, SHIFT + ENTER starter ny linje" + "value" : "Trim Communities" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Die Eingabetaste sendet eine Nachricht, Shift-Taste + Eingabetaste startet eine neue Zeile" + "value" : "Communities kürzen" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER στέλνει ένα μήνυμα, SHIFT + ENTER ξεκινάει μια νέα γραμμή" + "value" : "Περικοπή Κοινοτήτων" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER sends a message, SHIFT + ENTER starts a new line" + "value" : "Trim Communities" } }, "eo" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER sendas mesaĝon, SHIFT + ENTER startas novan linion" + "value" : "Oblikvi Komunumojn" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER envía un mensaje, SHIFT + ENTER empieza una nueva línea" + "value" : "Recortar Comunidades" } }, "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER envía un mensaje, SHIFT + ENTER inicia una nueva línea" + "value" : "Recortar Comunidades" } }, "et" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER saadab sõnumi, SHIFT + ENTER alustab uut rida" + "value" : "Lähenda kogukonnad" } }, "eu" : { "stringUnit" : { "state" : "translated", - "value" : "SARRERA mezu bat bidaltzen du, SHIFT + SARRERA lerro berri bat hasten du" + "value" : "Komunitateen Ebaketak" } }, "fa" : { "stringUnit" : { "state" : "translated", - "value" : "کلید Enter پیام را ارسال می‌کند، SHIFT + ENTER یک خط جدید شروع می‌کند" + "value" : "کوتاه‌سازی انجمن‌ها" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER lähettää viestin, SHIFT + ENTER aloittaa uuden rivin" + "value" : "Tiivistä yhteisöt" } }, "fil" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER ay nagpapadala ng mensahe, SHIFT + ENTER ay nag-uumpisa ng bagong linya" + "value" : "Burahin ang Mga Mensaheng Nagtagal ng Higit sa 6 (na) Buwan" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "ENTRÉE envoie un message, MAJ + ENTRÉE commence une nouvelle ligne" + "value" : "Purger les groupes" } }, "gl" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER envía unha mensaxe, SHIFT + ENTER comeza unha nova liña" + "value" : "Recortar Comunidades" } }, "ha" : { "stringUnit" : { "state" : "translated", - "value" : "MENU tana aika saƙo, SHIFT + MENU yana farawa da layi na sabo" + "value" : "Datse Communities" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER שולח הודעה, SHIFT + ENTER מתחיל שורה חדשה" + "value" : "חתוך Communities" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER संदेश भेजता है, SHIFT + ENTER नई लाइन शुरू करता है" + "value" : "समुदायों को ट्रिम करें" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER šalje poruku, SHIFT + ENTER započinje novi red" + "value" : "Sažimanje zajednica" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER elküldi az üzenetet, SHIFT + ENTER új sort kezd" + "value" : "Közösségek rövidítése" } }, "hy-AM" : { "stringUnit" : { "state" : "translated", - "value" : "Enter ստեղնը հաղորդագրություն է ուղարկում, SHIFT + ENTER-ը նոր տող է սկսում" + "value" : "Կարճացրեք համայնքները" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER mengirim pesan, SHIFT + ENTER memulai baris baru" + "value" : "Pangkas Komunitas" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "INVIO invia un messaggio, MAIUSC + INVIO inizia una nuova riga" + "value" : "Sfoltisci Comunità" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "エンターでメッセージを送信、シフト + エンターで改行を開始" + "value" : "コミュニティをトリムする" } }, "ka" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER იღებს შეტყობინებას, SHIFT + ENTER იწყება ახალ ხაზზე" + "value" : "თემების შემცირება" } }, "km" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER ផ្ញើសារ, SHIFT + ENTER ចាប់ផ្តើមបន្ទាត់ថ្មី" + "value" : "បង្រួមសហគមន៍" } }, "kn" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER ಸಂದೇಶವನ್ನು ಕಳುಹಿಸುತ್ತದೆ, SHIFT + ENTER ಹೊಸ ಸಾಲನ್ನು ಪ್ರಾರಂಭಿಸುತ್ತದೆ" + "value" : "ಸಮುದಾಯಗಳನ್ನು ಟ್ರಿಮ್ ಮಾಡಿ" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "엔터(ENTER) 키로 메시지를 보내고, 시프트(Shift) + 엔터(ENTER) 키로 새 줄을 시작합니다." + "value" : "커뮤니티 정돈" } }, "ku" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER نامەیەک نێردە، SHIFT + ENTER هێڵەکی تازە دەستپێبکات" + "value" : "Trim Communities" } }, "ku-TR" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER peyamê dişîne, SHIFT + ENTER xeteke nû dide destpêkirin" + "value" : "Evya gelek di jorîn rereken di ke vestîn destke." } }, "lg" : { "stringUnit" : { "state" : "translated", - "value" : "ENU yerekawo, SHIFT + ENU evvumbula olunyiriri olupya" - } - }, - "lo" : { - "stringUnit" : { - "state" : "translated", - "value" : "ENTER ສົ່ງຂໍ້ຄວາມ, SHIFT + ENTER ເລີ່ມແຖວໃຫມ່" + "value" : "Fengereza Community" } }, "lt" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER išsiųs žinutę, SHIFT + ENTER pradės naują eilutę" + "value" : "Apkarpyti Communities" } }, "lv" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER nosūta ziņojumu, SHIFT + ENTER sāk jaunu rindu" + "value" : "Apgriezt kopienas" } }, "mk" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER испраќа порака, SHIFT + ENTER започнува нов ред" + "value" : "Тримување на заедници" } }, "mn" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER мессеж явуулна, SHIFT + ENTER шинэ мөр эхэлнэ" + "value" : "Community хасах" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER menghantar mesej, SHIFT + ENTER memulakan baris baru" + "value" : "Trim Komuniti" } }, "my" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER ဆို Message ပို့ပါ၊ SHIFT + ENTER ဆို စာကြောင်းအသစ်စတင်ပါ" + "value" : "ဖြတ်တောက်မှု အသိုင်းအဝိုင်းများ" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER sender en melding, SHIFT + ENTER starter en ny linje" + "value" : "Trim Communities" } }, "nb-NO" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER sender en melding, SHIFT + ENTER starter en ny linje" + "value" : "Trim Samfunn" } }, "ne-NP" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER सन्देश पठाउँछ, SHIFT + ENTER नयाँ लाइन सुरु गर्छ" + "value" : "समुदायहरू ट्रिम गर्नुहोस्" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER verzendt een bericht, SHIFT + ENTER begint een nieuwe regel" + "value" : "Communities opschonen" } }, "nn-NO" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER sender ei melding, SHIFT + ENTER startar ei ny linje" + "value" : "Trim Samfunn" } }, "ny" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER imatumiza uthenga, SHIFT + ENTER imayamba mzere watsopano" + "value" : "Chepetsani Community" } }, "pa-IN" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER ਸੰਦੇਸ਼ ਭੇਜਦਾ ਹੈ, SHIFT + ENTER ਇੱਕ ਨਵੀਂ ਲਾਈਨ ਤੋਂ ਸ਼ੁਰੂ ਹੁੰਦਾ ਹੈ" + "value" : "ਕਮਿਊਨਿਟੀਆਂ ਟਰਿਮ ਕਰੋ" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER wysyła wiadomość, SHIFT + ENTER rozpoczyna nową linijkę" + "value" : "Przytnij społeczności" } }, "ps" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER پیغام لیږي، SHIFT + ENTER نوی کرښه پیلوي" + "value" : "ټرم ټولنه" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER envia uma mensagem, SHIFT + ENTER inicia uma nova linha" + "value" : "Cortar Comunidades" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER envia a mensagem, SHIFT + ENTER inicia uma nova linha" + "value" : "Aparar Comunidades" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER trimite un mesaj, SHIFT + ENTER începe o linie nouă" + "value" : "Ajustare comunități" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER для отправки, SHIFT + ENTER для новой строки" + "value" : "Обрезать сообщения в сообществах" } }, "sh" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER šalje poruku, SHIFT + ENTER započinje novi red" + "value" : "Trim Zajednice" } }, "si-LK" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER පණිවිඩයක් යොමු කරයි, SHIFT + ENTER නව නිමාවක් ආරම්භ කරයි" + "value" : "ප්‍රජා ශිෂ්ටීකිරීම" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER odosiela správu, SHIFT + ENTER začína nový riadok" + "value" : "Prečistiť komunity" } }, "sl" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER pošlje sporočilo, SHIFT + ENTER začne novo vrstico" + "value" : "Obrezovanje skupnosti" } }, "sq" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER dërgon një mesazh, SHIFT + ENTER nis një rresht të ri" + "value" : "Pastro Communities" } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER шаље поруку, SHIFT + ENTER започиње нови ред" + "value" : "Скрати Заједницу" } }, "sr-Latn" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER šalje poruku, SHIFT + ENTER započinje novi red" + "value" : "Trim Communities" } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER sänder ett meddelande, SHIFT + ENTER påbörjar en ny rad" + "value" : "Beskär gemenskaper" } }, "sw" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER hutuma ujumbe, SHIFT + ENTER inaanza mstari mpya" + "value" : "Kata Community" } }, "ta" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER செய்தியை அனுப்பும், SHIFT + ENTER புதிய வரியை ஆரம்பிக்கும்" + "value" : "சமூகங்களை சுருக்கு" } }, "te" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER సందేశం పంపుతుంది, SHIFT + ENTER కొత్త పంక్తిని ప్రారంభిస్తుంది" + "value" : "కమ్యూనిటీలను ట్రిమ్ చేయండి" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER ส่งข้อความ, SHIFT + ENTER เริ่มบรรทัดใหม่" + "value" : "Trim Communities" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER iletiyi gönderir, SHIFT + ENTER yeni satır başlatır" + "value" : "Toplulukları Kırp" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER надсилає повідомлення, SHIFT + ENTER починає новий рядок" + "value" : "Автовидалення повідомлень спільнот" } }, "ur-IN" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER پیغام بھیجتا ہے، SHIFT + ENTER نئی لائن شروع کرتا ہے" + "value" : "کمیونٹیز ٹرم کریں" } }, "uz" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER xabarni yuboradi, SHIFT + ENTER yangi qatordan boshlaydi" + "value" : "Jamiyatlarni qisqartiring" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Enter gửi tin nhắn, SHIFT + ENTER bắt đầu một dòng mới" + "value" : "Cắt bớt Communities" } }, "xh" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER ithumela umyalezo, SHIFT + ENTER uqala ulayini omtsha" + "value" : "Coca i-Community" } }, "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "回车键发送消息,SHIFT+回车键换行" + "value" : "清理群组旧消息" } }, "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER 發送訊息,SHIFT + ENTER 創建新行" + "value" : "刪裁社群舊訊息" } } } }, - "conversationsGroups" : { + "conversationsMessageTrimmingTrimCommunitiesDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "2,000-dən çox mesajı olan icmalarda 6 aydan köhnə mesajları avtomatik sil." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Z komunit automaticky mazat zprávy starší než 6 měsíců, pokud jich je více než 2000." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nachrichten älter als 6 Monate in Communities mit mehr als 2000 Nachrichten automatisch löschen." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Auto-delete messages older than 6 months in communities with 2000+ messages." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Supprimer automatiquement les messages de plus de 6 mois dans les communautés ayant plus de 2000 messages." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Berichten ouder dan 6 maanden automatisch verwijderen in community's met meer dan 2000 berichten." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatycznie usuwaj wiadomości starsze niż 6 miesięcy w społecznościach powyżej 2000 wiadomości." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Удалять сообщения старше 6 месяцев в сообществах с более чем 2000 сообщений." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ta bort meddelanden som är äldre än 6 månader i gemenskaper med 2000+ meddelanden." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Автовидалення повідомлень старших за 6 місяців у спільнотах з 2000+ повідомлень." + } + } + } + }, + "conversationsNew" : { "extractionState" : "manual", "localizations" : { "af" : { "stringUnit" : { "state" : "translated", - "value" : "Groepe" + "value" : "Nuwe gesprek" } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "مجموعات" + "value" : "محادثة جديدة" } }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "Qruplar" + "value" : "Yeni danışıq" } }, "bal" : { "stringUnit" : { "state" : "translated", - "value" : "گروپاں" + "value" : "گلبگ ءِ کَتگ" } }, "be" : { "stringUnit" : { "state" : "translated", - "value" : "Групы" + "value" : "Новая размова" } }, "bg" : { "stringUnit" : { "state" : "translated", - "value" : "Групи" + "value" : "Нов разговор" } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "গ্রুপস" + "value" : "নতুন কথোপকথন" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Grups" + "value" : "Conversa nova" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Skupiny" + "value" : "Nová konverzace" } }, "cy" : { "stringUnit" : { "state" : "translated", - "value" : "Grwpiau" + "value" : "Sgwrs newydd" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Grupper" + "value" : "Ny samtale" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Gruppen" + "value" : "Neue Unterhaltung" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Ομάδες" + "value" : "Νέα Συνομιλία" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Groups" + "value" : "New Conversation" } }, "eo" : { "stringUnit" : { "state" : "translated", - "value" : "Grupoj" + "value" : "Nova Konversacio" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Grupos" + "value" : "Nueva conversación" } }, "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Grupos" + "value" : "Nueva conversación" } }, "et" : { "stringUnit" : { "state" : "translated", - "value" : "Grupid" + "value" : "Uus vestlus" } }, "eu" : { "stringUnit" : { "state" : "translated", - "value" : "Taldeak" + "value" : "Elkarrizketa Berria" } }, "fa" : { "stringUnit" : { "state" : "translated", - "value" : "گروه‌ها" + "value" : "مکالمه جدید" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Ryhmät" + "value" : "Uusi keskustelu" } }, "fil" : { "stringUnit" : { "state" : "translated", - "value" : "Mga Grupo" + "value" : "Bagong Usapan" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Groupes" + "value" : "Nouvelle conversation" } }, "gl" : { "stringUnit" : { "state" : "translated", - "value" : "Grupos" + "value" : "Nova conversa" } }, "ha" : { "stringUnit" : { "state" : "translated", - "value" : "Rukuni" + "value" : "Sabon Tattaunawa" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "קבוצות" + "value" : "שיחה חדשה" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "समूह" + "value" : "नई बातचीत" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Grupe" + "value" : "Novi razgovor" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Csoportok" + "value" : "Új beszélgetés" } }, "hy-AM" : { "stringUnit" : { "state" : "translated", - "value" : "Խմբեր" + "value" : "Նոր զրույց" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Grup" + "value" : "Percakapan Baru" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Gruppi" + "value" : "Nuova chat" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "グループ" + "value" : "新しい会話" } }, "ka" : { "stringUnit" : { "state" : "translated", - "value" : "ჯგუფები" + "value" : "ახალი საუბარი" } }, "km" : { "stringUnit" : { "state" : "translated", - "value" : "ក្រុម" + "value" : "ការសន្ទនាថ្មី" } }, "kn" : { "stringUnit" : { "state" : "translated", - "value" : "ಗುಂಪುಗಳು" + "value" : "ಹೊಸ ಸಂಭಾಷಣೆ" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "그룹" + "value" : "새 대화" } }, "ku" : { "stringUnit" : { "state" : "translated", - "value" : "گروپەکان" + "value" : "گفتوگۆی نوێ" } }, "ku-TR" : { "stringUnit" : { "state" : "translated", - "value" : "Kom" + "value" : "Sohbeteke nû" } }, "lg" : { "stringUnit" : { "state" : "translated", - "value" : "Ebibinja" + "value" : "Eddoboozi empya" } }, "lt" : { "stringUnit" : { "state" : "translated", - "value" : "Grupės" + "value" : "Naujas pokalbis" } }, "lv" : { "stringUnit" : { "state" : "translated", - "value" : "Grupas" + "value" : "Jauna sarakste" } }, "mk" : { "stringUnit" : { "state" : "translated", - "value" : "Групи" + "value" : "Нов разговор" } }, "mn" : { "stringUnit" : { "state" : "translated", - "value" : "Бүлгүүд" + "value" : "Шинэ харилцан яриа" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Kumpulan" + "value" : "Perbualan Baru" } }, "my" : { "stringUnit" : { "state" : "translated", - "value" : "အုပ်စုများ" + "value" : "New Conversation" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Grupper" + "value" : "Ny samtale" } }, "nb-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Grupper" + "value" : "Ny Samtale" } }, "ne-NP" : { "stringUnit" : { "state" : "translated", - "value" : "समूहहरू" + "value" : "नयाँ कुराकानी" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Groepen" + "value" : "Nieuw gesprek" } }, "nn-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Grupper" + "value" : "Ny samtale" } }, "ny" : { "stringUnit" : { "state" : "translated", - "value" : "Magulu" + "value" : "Mushuk rimariy" } }, "pa-IN" : { "stringUnit" : { "state" : "translated", - "value" : "ਗਰੁੱਪ" + "value" : "ਨਵੀਂ ਗੱਲਬਾਤ" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Grupy" + "value" : "Nowa rozmowa" } }, "ps" : { "stringUnit" : { "state" : "translated", - "value" : "ډلې" + "value" : "نوې خبرې اترې" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Grupos" + "value" : "Nova Conversa" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Grupos" + "value" : "Nova conversa" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Grupuri" + "value" : "Conversație nouă" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Группы" + "value" : "Новая беседа" } }, "sh" : { "stringUnit" : { "state" : "translated", - "value" : "Grupe" + "value" : "Novi razgovor" } }, "si-LK" : { "stringUnit" : { "state" : "translated", - "value" : "සමූහ" + "value" : "නව සංවාදය" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Skupiny" + "value" : "Nová konverzácia" } }, "sl" : { "stringUnit" : { "state" : "translated", - "value" : "Skupine" + "value" : "Nov pogovor" } }, "sq" : { "stringUnit" : { "state" : "translated", - "value" : "Grupe" + "value" : "Bisedë e re" } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : "Групе" + "value" : "Нова преписка" } }, "sr-Latn" : { "stringUnit" : { "state" : "translated", - "value" : "Grupe" + "value" : "Nova konverzacija" } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Grupper" + "value" : "Ny konversation" } }, "sw" : { "stringUnit" : { "state" : "translated", - "value" : "Makundi" + "value" : "Mazungumzo mapya" } }, "ta" : { "stringUnit" : { "state" : "translated", - "value" : "குழுக்கள்" + "value" : "புதிய உரையாடல்" } }, "te" : { "stringUnit" : { "state" : "translated", - "value" : "సమూహాలు" + "value" : "కొత్త సంభాషణ" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "กลุ่ม" + "value" : "การสนทนาใหม่" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Gruplar" + "value" : "Yeni Konuşma" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Групи" + "value" : "Нова бесіда" } }, "ur-IN" : { "stringUnit" : { "state" : "translated", - "value" : "گروپس" + "value" : "نئی گفتگو" } }, "uz" : { "stringUnit" : { "state" : "translated", - "value" : "Guruhlar" + "value" : "Yangi Suhbat" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Nhóm" + "value" : "Chuyện trò mới" } }, "xh" : { "stringUnit" : { "state" : "translated", - "value" : "Amaqela" + "value" : "Ingxoxo Entsha" } }, "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "群组" + "value" : "新建会话" } }, "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "群組" + "value" : "新對話" } } } }, - "conversationsMessageTrimming" : { + "conversationsNone" : { "extractionState" : "manual", "localizations" : { "af" : { "stringUnit" : { "state" : "translated", - "value" : "Boodskap Snoei" + "value" : "Jy het nog nie enige gesprekke nie" } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "تقليم الرسالة" + "value" : "لا تملك أي محادثات حتى الآن" } }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "Mesajları kəs" + "value" : "Hələ heç bir danışığınız yoxdur" } }, "bal" : { "stringUnit" : { "state" : "translated", - "value" : "Message Trimming" + "value" : "شما نیش کنورزیشن بیت۔" } }, "be" : { "stringUnit" : { "state" : "translated", - "value" : "Абрэзка паведамленняў" + "value" : "Вы пакуль не маеце размоў" } }, "bg" : { "stringUnit" : { "state" : "translated", - "value" : "Съкращаване на съобщенията" + "value" : "Вие нямате никакви разговори досега" } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "বার্তা সংক্ষিপ্তকরণ" + "value" : "আপনার কোনো কথোপকথন নেই এখনো" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Escapçament de missatges" + "value" : "Encara no tens cap conversa" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Pročištění zpráv" + "value" : "Zatím nemáte žádné konverzace" } }, "cy" : { "stringUnit" : { "state" : "translated", - "value" : "Tocio Negeseuon" + "value" : "Nid oes gennych unrhyw sgyrsiau eto" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Trimning af beskeder" + "value" : "Du har ingen samtaler endnu" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Nachrichtenkürzung" + "value" : "Du hast noch keine Unterhaltungen" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Περικοπή μηνυμάτων" + "value" : "Δεν έχετε συνομιλίες ακόμα" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Message Trimming" + "value" : "You don't have any conversations yet" } }, "eo" : { "stringUnit" : { "state" : "translated", - "value" : "Tondeto de mesaĝoj" + "value" : "Vi ankoraŭ ne havas konversaciojn" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Recorte de mensajes en chats" + "value" : "Aún no tienes conversaciones" } }, "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Recorte de mensajes en chats" + "value" : "Aún no tienes conversaciones" } }, "et" : { "stringUnit" : { "state" : "translated", - "value" : "Sõnumi piiramine" + "value" : "Teil pole veel ühtegi vestlust" } }, "eu" : { "stringUnit" : { "state" : "translated", - "value" : "Mezuen ebaketa" + "value" : "Ez daukazu elkarrizketarik oraindik" } }, "fa" : { "stringUnit" : { "state" : "translated", - "value" : "پیرایش پیام" + "value" : "شما هنوز هیچ مکالمه‌ای ندارید" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Keskustelujen tiivistys" + "value" : "Sinulla ei ole vielä yhtään keskustelua" } }, "fil" : { "stringUnit" : { "state" : "translated", - "value" : "Pagtatabas ng Mensahe" + "value" : "Wala ka pang mga pag-uusap" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Élagage des messages" + "value" : "Vous n'avez pas encore de conversations" } }, "gl" : { "stringUnit" : { "state" : "translated", - "value" : "Recorte das mensaxes" + "value" : "Aínda non tes ningunha conversa" } }, "ha" : { "stringUnit" : { "state" : "translated", - "value" : "Rage Saƙo" + "value" : "Ba ku da zantuka a halin yanzu" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "קיצוץ הודעה" + "value" : "עדיין אין לך שיחות" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "संदेश ट्रिमिंग" + "value" : "आपके पास अभी तक कोई वार्तालाप नहीं है" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Skraćivanje poruka" + "value" : "Još nemate razgovora" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Üzenetek rövidítése" + "value" : "Még nincsenek beszélgetéseid" } }, "hy-AM" : { "stringUnit" : { "state" : "translated", - "value" : "Նամակների կարճեցում" + "value" : "Դուք դեռևս ոչ մի զրույց ունեք" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Pemangkasan Pesan" + "value" : "Anda belum memiliki percakapan satu pun" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Sfoltitura dei messaggi" + "value" : "Non hai ancora nessuna chat" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "メッセージの削減" + "value" : "まだ通知はありません" } }, "ka" : { "stringUnit" : { "state" : "translated", - "value" : "deleteAfterGroupPR1MessageSound" + "value" : "თქვენ ჯერ არ გაქვთ საუბრები" } }, "km" : { "stringUnit" : { "state" : "translated", - "value" : "តម្រឹមសារ" + "value" : "អ្នក​មិន​ទាន់​មានការសន្ទនាណាមួយនៅឡើយទេ" } }, "kn" : { "stringUnit" : { "state" : "translated", - "value" : "ಸಂದೇಶಗಳ ಕತ್ತರಿಸಿ ಹಾಕುವಿಕೆ" + "value" : "ನೀವು ಇನ್ನೂ ಯಾವುದೇ ಸಂಭಾಷಣೆಗಳನ್ನು ಹೊಂದಿಲ್ಲ" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "대화 줄이기" + "value" : "아직 대화가 없습니다" } }, "ku" : { "stringUnit" : { "state" : "translated", - "value" : "بڕینی نامە" + "value" : "تۆ هیچ گفتگووەکت نییە یەکەوە" } }, "ku-TR" : { "stringUnit" : { "state" : "translated", - "value" : "Kesixîna Peyamê" + "value" : "Hêj tu diyalogekî nînin" } }, "lg" : { "stringUnit" : { "state" : "translated", - "value" : "Tikka olubaka olwa teriiko ly’oteereza" + "value" : "Tolina ngeri yenna ya kulagamu bukati." } }, "lt" : { "stringUnit" : { "state" : "translated", - "value" : "Žinučių išvalymas" + "value" : "Jūs dar neturite pokalbių" } }, "lv" : { "stringUnit" : { "state" : "translated", - "value" : "Ziņu apgriešana" + "value" : "Tev vēl nav nevienas sarunas" } }, "mk" : { "stringUnit" : { "state" : "translated", - "value" : "Сечење на пораки" + "value" : "Сè уште немате разговори" } }, "mn" : { "stringUnit" : { "state" : "translated", - "value" : "Мессеж хасалт" + "value" : "Танд одоогоор яриа байхгүй байна" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Pemangkasan Mesej" + "value" : "Anda belum mempunyai sebarang perbualan" } }, "my" : { "stringUnit" : { "state" : "translated", - "value" : "မဇ်ဆေ့ဂ ှ" + "value" : "သင့်တွင် စကားဝိုင်း မရှိသေးပါ" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Automatisk sletting" + "value" : "Du har ingen samtaler ennå" } }, "nb-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Meldingsbeskjæring" + "value" : "Du har ingen samtaler ennå" } }, "ne-NP" : { "stringUnit" : { "state" : "translated", - "value" : "सन्देश ट्रिमिंग" + "value" : "तपाईंसँग अहिलेसम्म कुनै कुराकानीहरू छैनन्" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Beperk bewaartermijn" + "value" : "U heeft nog geen gesprekken" } }, "nn-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Meldingsbeskjæring" + "value" : "Du har inga samtalar enno" } }, "ny" : { "stringUnit" : { "state" : "translated", - "value" : "Kusandulika mauthenga" + "value" : "Simunayambe kuyankhulana ndi aliyense" } }, "pa-IN" : { "stringUnit" : { "state" : "translated", - "value" : "ਸੁਨੇਹਾ ਕਟਾਈ" + "value" : "ਤੁਹਾਡੇ ਕੋਲ ਹਾਲੇ ਤੱਕ ਕੋਈ ਗੱਲਬਾਤ ਨਹੀਂ ਹੋਈ ਹੈ" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Przycinanie wiadomości" + "value" : "Nie masz jeszcze żadnych konwersacji" } }, "ps" : { "stringUnit" : { "state" : "translated", - "value" : "پیغام تراشې" + "value" : "تاسو لا تراوسه هېڅ خبرې ندي کړي" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Corte de Mensagens" + "value" : "Você ainda não tem nenhuma conversa" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Redução do tamanho de mensagem" + "value" : "Ainda não tem conversas" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Scurtarea mesajelor" + "value" : "Încă nu ai conversații" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Обрезка сообщений" + "value" : "У вас пока нет бесед" } }, "sh" : { "stringUnit" : { "state" : "translated", - "value" : "Skraćivanje poruke" + "value" : "Još nemaš nijednu poruku" } }, "si-LK" : { "stringUnit" : { "state" : "translated", - "value" : "පණිවිඩ කැපීම" + "value" : "ඔබට තවම සංවාදයක් නැත" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Prečistenie správ" + "value" : "Zatiaľ nemáte žiadne konverzácie" } }, "sl" : { "stringUnit" : { "state" : "translated", - "value" : "Obrezovanje sporočil" + "value" : "Nimate še nobenih pogovorov" } }, "sq" : { "stringUnit" : { "state" : "translated", - "value" : "Shkurtim mesazhesh" + "value" : "Ju nuk keni ende ndonjë bisedë" } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : "Скраћивање порука" + "value" : "Још увек немате ниједан разговор" } }, "sr-Latn" : { "stringUnit" : { "state" : "translated", - "value" : "Skraćivanje poruka" + "value" : "Još nemate nijednu konverzaciju" } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Trimma meddelanden" + "value" : "Du har inga konversationer än" } }, "sw" : { "stringUnit" : { "state" : "translated", - "value" : "Ujumbe unapunguza" + "value" : "Hauna mazungumzo yoyote bado" } }, "ta" : { "stringUnit" : { "state" : "translated", - "value" : "செய்தி மேராசூக்கம்" + "value" : "உங்களிடம் இதுவரை உரையாடல்கள் இல்லை" } }, "te" : { "stringUnit" : { "state" : "translated", - "value" : "సందేశం కత్తిరించి సరి చేయుట" + "value" : "మీకు ఇంకా ఏ టిందనో సంభాషణలు లేవు" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "ตัดข้อความให้สั้นลง" + "value" : "คุณยังไม่มีการสนทนา" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "İleti kırpma" + "value" : "Henüz herhangi bir konuşmanız yok" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Автовидалення повідомлень" + "value" : "У вас ще немає жодних розмов" } }, "ur-IN" : { "stringUnit" : { "state" : "translated", - "value" : "پیغام کی کانٹ چھانٹ" + "value" : "آپ کے پاس ابھی تک کوئی گفتگو نہیں ہے" } }, "uz" : { "stringUnit" : { "state" : "translated", - "value" : "Xabarni chetlatish" + "value" : "Sizda hali suhbatlar yo'q" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Thu gọn tin nhắn" + "value" : "Bạn chưa có cuộc trò chuyện nào" } }, "xh" : { "stringUnit" : { "state" : "translated", - "value" : "Ukuskrowa umyalezo" + "value" : "Awunazo naziphi na iincoko okwangoku" } }, "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "消息整理" + "value" : "您还没有任何会话" } }, "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "訊息清理" + "value" : "您還沒有任何對話" } } } }, - "conversationsMessageTrimmingTrimCommunities" : { + "conversationsSendWithEnterKey" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enter ilə göndər" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odeslat klávesou Enter" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mit Eingabetaste senden" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Send with Enter" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enter para enviar" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enter para enviar" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Envoyer avec Entrée" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verzenden met Enter" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wysyłaj przyciskiem Enter" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apasă Enter pentru a trimite" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Отправлять по Enter" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skicka med Enter" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Надіслати з Enter" + } + } + } + }, + "conversationsSendWithEnterKeyDescription" : { "extractionState" : "manual", "localizations" : { "af" : { "stringUnit" : { "state" : "translated", - "value" : "Trim Gemeenskappe" + "value" : "Tikking van die Enter Key sal boodskappe stuur in plaas van om ‘n nuwe lyn te begin." } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "تقليم المجتمعات" + "value" : "النقر على Enter سوف يرسل الرسالة بدلاَ من بدء سطر جديد." } }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "İcmalarda kəsmə" + "value" : "Enter düyməsinə toxunmaq, yeni bir sətir əlavə etmək əvəzinə mesajı göndərəcək." } }, "bal" : { "stringUnit" : { "state" : "translated", - "value" : "کمیونٹیز کو تراشیں" + "value" : "ENTER کی تپی ھتادیپی پیام بجھڈی ہے، SHIFT + ENTER نوکی کورتھ شروع کـــــــن" } }, "be" : { "stringUnit" : { "state" : "translated", - "value" : "Абрэжце супольніцтва" + "value" : "Націсканне Enter прывядзе да адпраўкі паведамлення замест пераходу на новы радок." } }, "bg" : { "stringUnit" : { "state" : "translated", - "value" : "Подрязване на Общности" + "value" : "Натискането на клавиша Enter ще изпрати съобщение вместо да започне нов ред." } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "কমিউনিটিস ট্রিম করুন" + "value" : "এন্টার কী ট্যাপ করলে নতুন লাইন শুরু করার পরিবর্তে মেসেজ পাঠাবে।" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Limiteu comunitats" + "value" : "Si piqueu la tecla Intro, s'enviarà un missatge en lloc d'iniciar una línia nova." } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Pročistit komunity" + "value" : "Klepnutím na klávesu Enter odešlete zprávu namísto zahájení nového řádku." } }, "cy" : { "stringUnit" : { "state" : "translated", - "value" : "Trimio Cymunedau" + "value" : "Tapio'r Allwedd Mynd i mewn bydd yn anfon neges yn lle dechrau llinell newydd." } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Trim Communities" + "value" : "Når du trykker på Enter-tasten, sendes beskeder i stedet for at starte en ny linje." } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Communities kürzen" + "value" : "Durch Tippen auf die Eingabetaste wird eine Nachricht gesendet, anstatt eine neue Zeile zu beginnen." } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Περικοπή Κοινοτήτων" + "value" : "Πατώντας το πλήκτρο Enter θα σταλεί μήνυμα αντί να ξεκινήσει μια νέα γραμμή." } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Trim Communities" + "value" : "Tapping the Enter Key will send message instead of starting a new line." } }, "eo" : { "stringUnit" : { "state" : "translated", - "value" : "Oblikvi Komunumojn" + "value" : "Frapante la Enigareton sendos mesaĝon anstataŭ komenci novan linion." } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Recortar Comunidades" + "value" : "Pulsar la tecla Intro enviará el mensaje en lugar de empezar una nueva línea." } }, "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Recortar Comunidades" + "value" : "Pulsar la tecla Intro enviará el mensaje en lugar de empezar una nueva línea." } }, "et" : { "stringUnit" : { "state" : "translated", - "value" : "Lähenda kogukonnad" + "value" : "Uuelt realt alustamise asemel saadab Enter-klahvi koputamine sõnumi." } }, "eu" : { "stringUnit" : { "state" : "translated", - "value" : "Komunitateen Ebaketak" + "value" : "Enter Tekla sakatzeak mezua bidaliko du, lerro berri bat hasi beharrean." } }, "fa" : { "stringUnit" : { "state" : "translated", - "value" : "کوتاه‌سازی انجمن‌ها" + "value" : "ضربه زدن روی کلید Enter به جای شروع یک خط جدید، پیام را ارسال خواهد کرد." } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Tiivistä yhteisöt" + "value" : "Rivinvaihdon sijaan Enter-näppäimen painallus lähettää viestin." } }, "fil" : { "stringUnit" : { "state" : "translated", - "value" : "Burahin ang Mga Mensaheng Nagtagal ng Higit sa 6 (na) Buwan" + "value" : "Kapag pinindot ang Enter Key ay magpapadala ito ng mensahe sa halip na magsimula ng bagong linya." } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Purger les groupes" + "value" : "Appuyer sur la touche Entrée enverra un message au lieu de commencer une nouvelle ligne." } }, "gl" : { "stringUnit" : { "state" : "translated", - "value" : "Recortar Comunidades" + "value" : "Tocar a tecla Enter enviará a mensaxe en vez de comezar unha nova liña." } }, "ha" : { "stringUnit" : { "state" : "translated", - "value" : "Datse Communities" + "value" : "Matsawa maɓallin Shigarwa zai aika saƙo maimakon fara sabon layi." } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "חתוך Communities" + "value" : "לחיצה על מקש Enter תשלח הודעה במקום לפתוח שורה חדשה." } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "समुदायों को ट्रिम करें" + "value" : "एन्टर कुंजी को दबाने से संदेश भेजा जाएगा नई लाइन शुरू करने से बजाय।" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Sažimanje zajednica" + "value" : "Pritiskom na tipku Enter poslati će se poruka umjesto započinjanja novog retka." } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Közösségek rövidítése" + "value" : "Az Enter billentyű lenyomása elküldi az üzenetet új sor kezdése helyett." } }, "hy-AM" : { "stringUnit" : { "state" : "translated", - "value" : "Կարճացրեք համայնքները" + "value" : "Enter ստեղնը սեղմելով՝ նոր տող սկսելու փոխարեն հաղորդագրություն կուղարկվի:" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Pangkas Komunitas" + "value" : "Mengetuk Tombol Enter akan mengirim pesan alih-alih memulai baris baru." } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Sfoltisci Comunità" + "value" : "Premere il tasto Invio invierà il messaggio invece d'iniziare una nuova riga." } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "コミュニティをトリムする" + "value" : "Enterキーをタップすると、改行ではなく、メッセージが送信されます。" } }, "ka" : { "stringUnit" : { "state" : "translated", - "value" : "თემების შემცირება" + "value" : "Enter ღილაკზე დაჭერა აერტგზავნის შეტყობინებას, არა ახალი ხაზის დაწყებას." } }, "km" : { "stringUnit" : { "state" : "translated", - "value" : "បង្រួមសហគមន៍" + "value" : "ការ​ចុចលើឃី Enter នឹងផ្ញើសារជំនួសឱ្យការចុះបន្ទាត់ថ្មី។" } }, "kn" : { "stringUnit" : { "state" : "translated", - "value" : "ಸಮುದಾಯಗಳನ್ನು ಟ್ರಿಮ್ ಮಾಡಿ" + "value" : "ಎಂಟರ್ ಕಿವಿಯನ್ನು ಟ್ಯಾಪ್ ಮಾಡುವುದರಿಂದ ಹೊಸ ಸಾಲು ಪ್ರಾರಂಭಿಸುವ ಬದಲು ಸಂದೇಶವನ್ನು ಕಳುಹಿಸಲಾಗುತ್ತದೆ." } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "커뮤니티 정돈" + "value" : "Enter를 누를 때 줄바꿈 대신 메시지를 전송합니다." } }, "ku" : { "stringUnit" : { "state" : "translated", - "value" : "Trim Communities" + "value" : "فەرمی پەیوەندیدانی کردنی کلیکی ئەنتەر پەیامیەک دەنێریت لە کاتی گۆڕینی هێڵی نوێ." } }, "ku-TR" : { "stringUnit" : { "state" : "translated", - "value" : "Evya gelek di jorîn rereken di ke vestîn destke." + "value" : "Bateya Li Keya Kêşkaran peyam ji bo şandina peyamê dike ser hûn kira xeta nûkirinê bide dest xetin." } }, "lg" : { "stringUnit" : { "state" : "translated", - "value" : "Fengereza Community" + "value" : "Okukonako ku Kiwandiiko kye Kikuta kiyinza okuweereza obubaka ne kuttandika olunyiriri olupya." } }, "lt" : { "stringUnit" : { "state" : "translated", - "value" : "Apkarpyti Communities" + "value" : "Enter klavišas siųs žinutę vietoje naujos eilutės pradžios." } }, "lv" : { "stringUnit" : { "state" : "translated", - "value" : "Apgriezt kopienas" + "value" : "Pieskaroties Enter taustiņam, tiks nosūtīta ziņa, nevis sākta jauna rinda." } }, "mk" : { "stringUnit" : { "state" : "translated", - "value" : "Тримување на заедници" + "value" : "Допирањето на копчето Enter ќе испрати порака наместо да започне нов ред." } }, "mn" : { "stringUnit" : { "state" : "translated", - "value" : "Community хасах" + "value" : "Enter товч дарахад шинэ мөр эхлүүлэхийн оронд мессеж илгээнэ." } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Trim Komuniti" + "value" : "Mengetuk Kekunci Masuk akan menghantar mesej daripada memulakan barisan baharu." } }, "my" : { "stringUnit" : { "state" : "translated", - "value" : "ဖြတ်တောက်မှု အသိုင်းအဝိုင်းများ" + "value" : "Enter key ကိုနှိပ်ခြင်းဖြင့် စာကြောင်းအသစ်ကို စမည့်အစား မက်ဆေ့ခ်ျကို ပေးပို့ပါမည်။" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Trim Communities" + "value" : "Ved å trykke på Enter nøkkel vil sende melding i stedet for å starte en ny linje." } }, "nb-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Trim Samfunn" + "value" : "Ved å trykke på Enter nøkkel vil sende melding i stedet for å starte en ny linje." } }, "ne-NP" : { "stringUnit" : { "state" : "translated", - "value" : "समुदायहरू ट्रिम गर्नुहोस्" + "value" : "ENTER कुञ्जीले सन्देश पठाउनेछ भनेको नयाँ लाइन सुरु गर्नुको सट्टा।" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Communities opschonen" + "value" : "Met de Enter Toets direct het bericht versturen in plaats van een nieuwe regel beginnen." } }, "nn-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Trim Samfunn" + "value" : "Ved å trykke på Enter nøkkel vil sende melding i stedet for å starte en ny linje." } }, "ny" : { "stringUnit" : { "state" : "translated", - "value" : "Chepetsani Community" + "value" : "Dinani Makiyi Olimba kuti mutumize uthenga m'malo moyamba mzere watsopano." } }, "pa-IN" : { "stringUnit" : { "state" : "translated", - "value" : "ਕਮਿਊਨਿਟੀਆਂ ਟਰਿਮ ਕਰੋ" + "value" : "ਇੰਟਰ ਕੀ ਨੂੰ ਟੈਪ ਕਰਨ ਨਾਲ ਸੁਨੇਹਾ ਭੇਜਿਆ ਜਾਵੇਗਾ ਤਦ ਇਹ ਇਕ ਨਵੀਂ ਲਾਈਨ ਸ਼ੁਰੂ ਕਰਨ ਦੇ ਬਜਾਏ।" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Przytnij społeczności" + "value" : "Naciśnięcie klawisza Enter spowoduje wysłanie wiadomości zamiast rozpoczęcia nowej linijki." } }, "ps" : { "stringUnit" : { "state" : "translated", - "value" : "ټرم ټولنه" + "value" : "د انټر کلیک کول به پیغام واستوي پرځای د نوي کرښې پیل کولو." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Cortar Comunidades" + "value" : "Tocar na tecla Enter enviará uma mensagem em vez de iniciar uma nova linha." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Aparar Comunidades" + "value" : "Tocar na tecla Enter enviará uma mensagem em vez de iniciar uma nova linha." } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Ajustare comunități" + "value" : "Atingerea tastei Enter va trimite un mesaj în loc de a iniția o nouă linie." } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Обрезать сообщения в сообществах" + "value" : "Нажатие Enter будет отправлять сообщение, а не начинать новую строку." } }, "sh" : { "stringUnit" : { "state" : "translated", - "value" : "Trim Zajednice" + "value" : "Pritiskom na tipku Enter poslat će se poruka umjesto da se započne novi redak." } }, "si-LK" : { "stringUnit" : { "state" : "translated", - "value" : "ප්‍රජා ශිෂ්ටීකිරීම" + "value" : "ඇතුළුවීමේ යතුර තට්ටු කිරීම මෙම පණිවිඩය යවනු ඇත මෙන්ම නව තීරුවක් ආරම්භ කිරීම වෙනුවට." } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Prečistiť komunity" + "value" : "Namiesto vytvorenia nového riadku v správe, aplikácia odošle správu." } }, "sl" : { "stringUnit" : { "state" : "translated", - "value" : "Obrezovanje skupnosti" + "value" : "Pritiskanje tipke Enter bo namesto začetka nove vrstice poslalo sporočilo." } }, "sq" : { "stringUnit" : { "state" : "translated", - "value" : "Pastro Communities" + "value" : "Klikimi i tastit Enter do të dërgojë mesazhin në vend që të fillojë një rresht të ri." } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : "Скрати Заједницу" + "value" : "Притиском на Enter тастер шаље се порука уместо започетог новог реда." } }, "sr-Latn" : { "stringUnit" : { "state" : "translated", - "value" : "Trim Communities" + "value" : "Dodir tastera Enter će poslati poruku umesto da započne novi red." } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Beskär gemenskaper" + "value" : "Tryck på returtangent sänder meddelande istället för att radbryta." } }, "sw" : { "stringUnit" : { "state" : "translated", - "value" : "Kata Community" + "value" : "Gusa kitufe cha Ingiza ili kutuma ujumbe badala ya kuanza mstari mpya." } }, "ta" : { "stringUnit" : { "state" : "translated", - "value" : "சமூகங்களை சுருக்கு" + "value" : "என்டர் விசையை அழுத்துவதன் மூலம் புதிய வரியை ஆரம்பிக்காமல் செய்தியினை அனுப்புகிறது." } }, "te" : { "stringUnit" : { "state" : "translated", - "value" : "కమ్యూనిటీలను ట్రిమ్ చేయండి" + "value" : "ఎంటర్ కీని టాప్ చేయడం ద్వారా కొత్త పంక్తి ప్రారంభం కాకుండా సందేశం పంపబడుతుంది." } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "Trim Communities" + "value" : "การแตะปุ่ม Enter จะส่งข้อความแทนการเริ่มบรรทัดใหม่" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Toplulukları Kırp" + "value" : "Enter tuşuna basmak yeni bir satıra geçmek yerine ileti gönderir." } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Автовидалення повідомлень спільнот" + "value" : "Натискання клавіші Enter буде відправляти повідомлення замість переходу на новий рядок." } }, "ur-IN" : { "stringUnit" : { "state" : "translated", - "value" : "کمیونٹیز ٹرم کریں" + "value" : "انٹر کی دبانے سے پیغام بھیجا جائے گا نہ کہ نئی سطر شروع کی جائے گی۔" } }, "uz" : { "stringUnit" : { "state" : "translated", - "value" : "Jamiyatlarni qisqartiring" + "value" : "Enter tugmasini bosish xabarni yuboradi, yangi satrdan boshlash o‘rniga." } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Cắt bớt Communities" + "value" : "Bấm phím Enter sẽ gửi tin nhắn thay vì bắt đầu một dòng mới." } }, "xh" : { "stringUnit" : { "state" : "translated", - "value" : "Coca i-Community" + "value" : "Tapping the Enter Key will send message instead of starting a new line." } }, "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "清理群组旧消息" + "value" : "按回车键发送消息而非换行" } }, "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "刪裁社群舊訊息" + "value" : "回車鍵發送訊息,而不是另起一行。" } } } }, - "conversationsMessageTrimmingTrimCommunitiesDescription" : { + "conversationsSendWithShiftEnter" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Shift+Enter ilə göndər" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odeslat klávesou Shift+Enter" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mit Umschalt- und Eingabetaste senden" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Send with Shift+Enter" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Envoyer avec Maj+Entrée" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verzenden met Shift+Enter" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wysyłaj za pomocą Shift+Enter" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Отправить с Shift+Enter" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skicka med Shift+Enter" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Надіслати з Shift+Enter" + } + } + } + }, + "conversationsSettingsAllMedia" : { "extractionState" : "manual", "localizations" : { "af" : { "stringUnit" : { "state" : "translated", - "value" : "Skrap boodskappe van Gemeenskap geselsies ouer as 6 maande, en waar daar meer as 2,000 boodskappe is." + "value" : "Alle Media" } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "حذف الرسائل القديمة من محادثات المجتمع التي تكون أقدم من 6 أشهر، وحيث يوجد أكثر من 2000 رسالة." + "value" : "جميع الوسائط" } }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "6 aydan köhnə və 2,000-dən çox mesajın olduğu İcma danışıqlarındakı mesajları sil." + "value" : "Bütün media" } }, "bal" : { "stringUnit" : { "state" : "translated", - "value" : "کمیونٹی کی بات چیت میں 6 ماہ سے زیادہ پرانے اور 2000 سے زیادہ پیغامات حذف کریں۔" + "value" : "تمام میڈیا" } }, "be" : { "stringUnit" : { "state" : "translated", - "value" : "Выдаліць паведамленні з гутаркоў супольнасці, старыя за 6 месяцаў, і калі там больш за 2,000 паведамленняў." + "value" : "Усе медыя" } }, "bg" : { "stringUnit" : { "state" : "translated", - "value" : "Изтрийте съобщения от Community разговори по-стари от 6 месеца, и където има над 2000 съобщения." + "value" : "Виж всички файлове" } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "কমিউনিটি কথোপকথনের ৬ মাসের বেশি পুরনো এবং যেখানে ২,০০০ এর বেশি মেসেজ আছে তা মুছুন।" + "value" : "সমস্ত মিডিয়া" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Suprimeix missatges de converses de la Community més antics de 6 mesos i quan hi ha més de 2,000 missatges." + "value" : "Tot el contingut multimèdia" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Z komunit vymazat zprávy starší než 6 měsíců a ponechat maximálně 2000 nejnovějších zpráv v každé komunitě." + "value" : "Všechna média" } }, "cy" : { "stringUnit" : { "state" : "translated", - "value" : "Dileu negeseuon o sgyrsiau Cymuned hŷn na 6 mis, a lle mae dros 2,000 o negeseuon." + "value" : "Pob Cyfrwng" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Slet beskeder fra Community-samtaler der er ældre end 6 måneder, og hvor der er over 2.000 beskeder." + "value" : "Alle medier" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Lösche Nachrichten von Community-Konversationen, die älter als 6 Monate sind, und wo es über 2.000 Nachrichten gibt." + "value" : "Alle Medieninhalte" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Διαγραφή μηνυμάτων από Community συνομιλίες άνω των 6 μηνών και όπου υπάρχουν πάνω από 2,000 μηνύματα." + "value" : "Όλα τα πολυμέσα" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Delete messages from Community conversations older than 6 months, and where there are over 2,000 messages." + "value" : "All Media" } }, "eo" : { "stringUnit" : { "state" : "translated", - "value" : "Forigi mesaĝojn de Komunumo konversacioj pli malnovajn ol 6 monatoj, kaj kie estas pli ol 2,000 mesaĝoj." + "value" : "Ĉiuj aŭdvidaĵoj" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Eliminar mensajes de conversaciones de Comunidad mayores a 6 meses, y donde hay más de 2,000 mensajes." + "value" : "Adjuntos" } }, "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Eliminar mensajes de conversaciones de Comunidad con más de 6 meses y donde haya más de 2,000 mensajes." + "value" : "Adjuntos" } }, "et" : { "stringUnit" : { "state" : "translated", - "value" : "Kustutage sõnumid Community vestlustest, mis on vanemad kui 6 kuud ja kus on üle 2 000 sõnumi." + "value" : "Kogu meedia" } }, "eu" : { "stringUnit" : { "state" : "translated", - "value" : "6 hilabete baino zaharragoak diren eta 2,000 mezu baino gehiago dituzten Community elkarrizketetako mezuak ezabatu." + "value" : "Multimedia guztia" } }, "fa" : { "stringUnit" : { "state" : "translated", - "value" : "حذف پیام از انجمن‌های قدیمی تر از ۶ ماه و انجمن‌هایی با بیش از 2000 پیام." + "value" : "تمام مدیا" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Poista viestit Community-keskusteluista, jotka ovat yli 6 kuukautta vanhoja ja joissa on yli 2,000 viestiä." + "value" : "Kaikki media" } }, "fil" : { "stringUnit" : { "state" : "translated", - "value" : "Burahin ang mga mensahe mula sa mga usapan sa Community na mas matagal na sa 6 buwan, at kung saan higit sa 2,000 ang mga mensahe." + "value" : "Lahat ng media" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Supprimez les messages des Communautés de plus de 6 mois et où il y a plus de 2 000 messages." + "value" : "Tous les médias" } }, "gl" : { "stringUnit" : { "state" : "translated", - "value" : "Eliminar mensaxes das conversas en Comunidade de máis de 6 meses, e onde hai máis de 2,000 mensaxes." + "value" : "Ficheiros multimedia" } }, "ha" : { "stringUnit" : { "state" : "translated", - "value" : "Goge saƙonni daga tattaunawar Community wanda ya fi watanni 6 da wucewa, kuma inda akwai saƙonni sama da 2,000." + "value" : "Duk Kafofin Watsa Labarai" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "מחק הודעות משיחות קהילה שגילן מעל 6 חודשים, ובהן יש יותר מ-2,000 הודעות." + "value" : "כל המדיה" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "6 महीने से अधिक पुराने सामुदायिक वार्तालापों से, और जहां 2,000 से अधिक संदेश हों, संदेशों को हटा दें।" + "value" : "सभी मीडिया" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Izbrišite poruke iz Community razgovora starije od 6 mjeseci i gdje ima više od 2.000 poruka." + "value" : "Sva multimedija" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Üzenetek törlése a közösségi beszélgetésekből, amelyek régebbiek, mint 6 hónap, és ahol több, mint 2.000 üzenet van." + "value" : "Összes médiafájl" } }, "hy-AM" : { "stringUnit" : { "state" : "translated", - "value" : "Ջնջել Community զրույցների հաղորդագրությունները, որոնք հին են 6 ամիսից ավել և որտեղ առկա է 2,000-ից ավել հաղորդագրություն" + "value" : "Բոլոր մեդիաները" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Hapus pesan dari percakapan Community yang lebih lama dari 6 bulan, dan di mana ada lebih dari 2.000 pesan." + "value" : "Semua Media" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Eliminare i messaggi dalle chat delle Comunità più vecchie di 6 mesi e con oltre 2.000 messaggi." + "value" : "Tutti i contenuti multimediali" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "6ヶ月以上のコミュニティと2,000以上のメッセージがあるコミュニティからメッセージを削除します。" + "value" : "すべてのメディア" } }, "ka" : { "stringUnit" : { "state" : "translated", - "value" : "წაშალეთ შეტყობინებები Community-ის საუბრებიდან, რომლებიც 6 თვეზე ძველია და 2,000-ზე მეტი შეტყობინებაა." + "value" : "ყველა მედია" } }, "km" : { "stringUnit" : { "state" : "translated", - "value" : "លុបសារចេញពីសន្ទនាសហគមន៍ ដែលមាន វ័យចាស់ជាង ៦ ខែ និងមានចំណុចប្រទាក់សារលើសពី 2000" + "value" : "ព័ត៌មានទាំងអស់" } }, "kn" : { "stringUnit" : { "state" : "translated", - "value" : "6 ತಿಂಗಳಿಗಿಂತ ಹಳೆಮಾದಿ ಮತ್ತು ೨,೦೦೦ ಸಂದೇಶಗಳು ಇರುವ ಸಮುದಾಯ ಸಂಭಾಷಣೆಗಳಿಂದ ಸಂದೇಶಗಳನ್ನು ಅಳಿಸಿ." + "value" : "ಎಲ್ಲ ಮಾಧ್ಯಮ" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "6개월 이상된 커뮤니티 대화의 메시지와 2,000개 이상의 메시지를 삭제합니다." + "value" : "모든 미디어" } }, "ku" : { "stringUnit" : { "state" : "translated", - "value" : "سڕینەوەی پەیامەکان لەلایەن گفتوگۆیی کۆمەڵگا ئێستا لە 6 مانگان زووتر، و هەڵەتوان زۆربەی 2,000 پەیام هەبێت." + "value" : "هەموو میدیایەکان" } }, "ku-TR" : { "stringUnit" : { "state" : "translated", - "value" : "Peyaman ji Civatan piştî 6 mehan an gava ku li wir zêdetirî 2000 peyaman çêbe jê bibe." + "value" : "Hemû Medya" } }, "lg" : { "stringUnit" : { "state" : "translated", - "value" : "Okuggya ebibukiribwa mu Community conversations mapya nga z’emiyezi 6 ez’ensi, y29,000." + "value" : "Emikutu Gyonna" } }, "lo" : { "stringUnit" : { "state" : "translated", - "value" : "ລຶບຂໍ້ຄວາມຈາກການສົນທະນາຂອງ Community ເກີນຫົກເດືອນ, ແລະມີເກີນສອງພັນຂໍ້ຄວາມ" + "value" : "ສື່ທັງໝົດ" } }, "lt" : { "stringUnit" : { "state" : "translated", - "value" : "Ištrinti žinutes iš Bendruomenės pokalbių, senesnius nei 6 mėnesiai ir kur yra virš 2 000 žinučių." + "value" : "Visa medija" } }, "lv" : { "stringUnit" : { "state" : "translated", - "value" : "Dzēst ziņojumus no Kopienas sarunām, kas vecāki par 6 mēnešiem un kur ir vairāk nekā 2 000 ziņojumu." + "value" : "Visa multivide" } }, "mk" : { "stringUnit" : { "state" : "translated", - "value" : "Избриши пораки од разговори со заедницата постари од 6 месеци и каде што има повеќе од 2,000 пораки." + "value" : "Сите медиуми" } }, "mn" : { "stringUnit" : { "state" : "translated", - "value" : "Community харилцан ярианаас 6 сараас дээш хугацааны, 2,000-аас дээш мессежтэй яриа устгана." + "value" : "Бүх Медианууд" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Padamkan mesej dari perbualan Community yang lebih lama dari 6 bulan, dan di mana terdapat lebih dari 2000 mesej." + "value" : "Semua Media" } }, "my" : { "stringUnit" : { "state" : "translated", - "value" : "Community စကားပြောဆိုမှုများမှ ၆ လကျော်အရွယ်ရှိပြီး မက်ဆေ့ချ် ၂,၀၀၀ ကျော်ရှိသော မက်ဆေ့ချ်များကို ဖျက်ပါ။" + "value" : "မီဒီယာအားလုံး" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Slett meldinger fra Community-samtaler eldre enn 6 måneder, og der det er over 2,000 meldinger." + "value" : "Alle medier" } }, "nb-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Slett meldinger fra Community-samtaler eldre enn 6 måneder og der det er over 2 000 meldinger." + "value" : "Alle medier" } }, "ne-NP" : { "stringUnit" : { "state" : "translated", - "value" : "६ महिना भन्दा पुराना र २,००० भन्दा बढी सन्देशहरू भएका समुदाय कुराकानीहरू मेटाउनुहोस्।" + "value" : "सबै मिडिया" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Community-berichten ouder dan 6 maanden verwijderen en afromen op 2000 berichten." + "value" : "Alle media" } }, "nn-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Slett meldingar frå Community-samtalar eldre enn 6 månader, og der det er over 2 000 meldingar." + "value" : "Alle medier" } }, "ny" : { "stringUnit" : { "state" : "translated", - "value" : "Chotsani mauthenga kuchokera pa zokambirana za Community zosapitilira miyezi 6, ndi pamene pali mauthenga opitilira 2,000." + "value" : "Zonse Zakanema" } }, "pa-IN" : { "stringUnit" : { "state" : "translated", - "value" : "ਕਮਿਊਨਿਟੀ ਗੱਲਬਾਤਾਂ ਤੋਂ 6 ਮਹੀਨੇ ਤੋਂ ਵੱਧ ਪੁਰੇ ਹੋਏ ਅਤੇ 2,000 ਸੰਦੇਸ਼ ਸੰਦੇਸ਼ ਹਟਾਓ।" + "value" : "ਸਾਰੀ ਮੀਡੀਆ" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Usuń wiadomości z konwersacji społecznościowych starszych niż 6 miesięcy i zawierających ponad 2000 wiadomości." + "value" : "Wszystkie media" } }, "ps" : { "stringUnit" : { "state" : "translated", - "value" : "د 6 میاشتو څخه زاړه او 2000 څخه زیات پیغامونه له Community مکالمو پاک کړئ." + "value" : "ټول میډیا" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Excluir mensagens de conversas da Comunidade mais antigas que 6 meses, e onde há mais de 2.000 mensagens." + "value" : "Todas as mídias" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Eliminar mensagens de conversas da Comunidade com mais de 6 meses, e onde há mais de 2,000 mensagens." + "value" : "Toda a Multimédia" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Șterge mesajele din conversațiile din comunități mai vechi de 6 luni și din conversațiile unde sunt peste 2.000 de mesaje." + "value" : "Toate fișierele media" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Удалить сообщения из переписок сообщества старше 6 месяцев и в которых более 2,000 сообщений." + "value" : "Все медиафайлы" } }, "sh" : { "stringUnit" : { "state" : "translated", - "value" : "Obriši poruke iz Community razgovora starijih od 6 mjeseci, gdje ima više od 2,000 poruka." + "value" : "Svi mediji" } }, "si-LK" : { "stringUnit" : { "state" : "translated", - "value" : "ප්‍රජාවේ සම්භාෂණ වලින් මාස 6කට වඩා පැරණි පණිවිඩ මකා දමන්න, සහ පණිවිඩ 2,000 ඉක්මවී ඇත." + "value" : "සියලු මාධ්යය" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Vymazať správy z Community konverzácií starších ako 6 mesiacov, a kde je viac ako 2,000 správ." + "value" : "Všetky média" } }, "sl" : { "stringUnit" : { "state" : "translated", - "value" : "Izbrišite sporočila iz pogovorov v skupnostih, ki so starejša od 6 mesecev in kjer je več kot 2000 sporočil." + "value" : "Vsi mediji" } }, "sq" : { "stringUnit" : { "state" : "translated", - "value" : "Fshiji mesazhet nga bisedat e Community më të vjetra se 6 muaj, dhe ku ka mbi 2,000 mesazhe." + "value" : "Krejt mediat" } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : "Обриши поруке из Community преписки старије од 6 месеци, и када има преко 2000 порука." + "value" : "Сви медији" } }, "sr-Latn" : { "stringUnit" : { "state" : "translated", - "value" : "Obriši poruke iz konverzacija u Community starije od 6 meseci, i gde ih ima više od 2,000." + "value" : "Svi mediji" } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Radera meddelanden från Community-konversationer äldre än 6 månader, och där det finns över 2 000 meddelanden." + "value" : "Alla media" } }, "sw" : { "stringUnit" : { "state" : "translated", - "value" : "Futa ujumbe kutoka Mazungumzo ya Community wenye umri zaidi ya miezi 6, na ambapo kuna zaidi ya jumbe 2,000." + "value" : "Vyombo vyote vya habari" } }, "ta" : { "stringUnit" : { "state" : "translated", - "value" : "6 மாதங்களுக்கு மேற்பட்ட மற்றும் 2,000 தகவல்களுக்கு மேற்பட்ட Community உரையாடலிலிருந்து தகவலைகளை நீக்கு." + "value" : "அனைத்து ஊடகங்கள்" } }, "te" : { "stringUnit" : { "state" : "translated", - "value" : "సంఘాల సంభాషణల నుండి 6 నెలల కంటే పాతవి మరియు 2,000 సందేశాల కంటే ఎక్కువ ఉన్న సందేశాలను తొలగించండి." + "value" : "అన్ని మీడియా" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "ลบข้อความใน Community การสนทนาที่อายุมากกว่า 6 เดือน และมีข้อความมากกว่า 2,000 ข้อความ" + "value" : "ไฟล์ทั้งหมด" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Topluluk sohbetlerinden 6 aydan eski ve 2.000'den fazla ileti içeren iletileri silin." + "value" : "Tüm Medya" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Видаляти повідомлення у спільнотах старше 6 місяців і де більше 2 000 повідомлень." + "value" : "Всі медіа" } }, "ur-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Community گفتگو سے پیغامات حذف کریں جو 6 مہینے سے زیادہ پرانے ہوں اور 2,000 سے زیادہ پیغامات ہوں۔" + "value" : "تمام میڈیا" } }, "uz" : { "stringUnit" : { "state" : "translated", - "value" : "Jamiyat suhbatlaridan 6 oydan oshgan va 2000 dan ortiq xabar mavjud bo'lgan xabarlarni o'chirish." + "value" : "Barcha Media" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Xóa tin nhắn từ cuộc hội thoại Community cũ hơn 6 tháng, và nơi có hơn 2.000 tin nhắn." + "value" : "Tất cả tệp phương tiện" } }, "xh" : { "stringUnit" : { "state" : "translated", - "value" : "Sangula imilayezo yeNguquko ezingaphaya kweinyanga ezi-6, nee malunga ne-2,000 yemilayezo" + "value" : "Yonke Imithombo yeendaba" } }, "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "删除来自拥有超过2,000条消息的社群内6个月以上的旧消息。" + "value" : "所有媒体" } }, "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "刪除擁有2000條以上訊息的社群中,六個月之前的舊訊息" + "value" : "所有媒體" } } } }, - "conversationsNew" : { + "conversationsSpellCheck" : { "extractionState" : "manual", "localizations" : { "af" : { "stringUnit" : { "state" : "translated", - "value" : "Nuwe gesprek" + "value" : "Speltoets" } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "محادثة جديدة" + "value" : "التدقيق الإملائي" } }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "Yeni danışıq" + "value" : "Orfoqrafiya yoxlanışı" } }, "bal" : { "stringUnit" : { "state" : "translated", - "value" : "گلبگ ءِ کَتگ" + "value" : "رسیلا چیک" } }, "be" : { "stringUnit" : { "state" : "translated", - "value" : "Новая размова" + "value" : "Праверка правапісу" } }, "bg" : { "stringUnit" : { "state" : "translated", - "value" : "Нов разговор" + "value" : "Проверка на правописа" } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "নতুন কথোপকথন" + "value" : "বানান ঠিক করুন" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Conversa nova" + "value" : "Revisar ortografia" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Nová konverzace" + "value" : "Kontrola pravopisu" } }, "cy" : { "stringUnit" : { "state" : "translated", - "value" : "Sgwrs newydd" + "value" : "Gwiriad Sillafu" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Ny samtale" + "value" : "Stavekontrol" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Neue Unterhaltung" + "value" : "Rechtschreibprüfung" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Νέα Συνομιλία" + "value" : "Ορθογραφικός Έλεγχος" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "New Conversation" + "value" : "Spell Check" } }, "eo" : { "stringUnit" : { "state" : "translated", - "value" : "Nova Konversacio" + "value" : "Literumkontrolo" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Nueva conversación" + "value" : "Revisión ortográfica" } }, "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Nueva conversación" + "value" : "Revisión ortográfica" } }, "et" : { "stringUnit" : { "state" : "translated", - "value" : "Uus vestlus" + "value" : "Õigekirjakontroll" } }, "eu" : { "stringUnit" : { "state" : "translated", - "value" : "Elkarrizketa Berria" + "value" : "Ortografia Egiaztapena" } }, "fa" : { "stringUnit" : { "state" : "translated", - "value" : "مکالمه جدید" + "value" : "بررسی املا" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Uusi keskustelu" + "value" : "Oikeinkirjoituksen tarkistus" } }, "fil" : { "stringUnit" : { "state" : "translated", - "value" : "Bagong Usapan" + "value" : "Suri sa baybayin" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Nouvelle conversation" + "value" : "Vérification orthographique" } }, "gl" : { "stringUnit" : { "state" : "translated", - "value" : "Nova conversa" + "value" : "Corrección Ortográfica" } }, "ha" : { "stringUnit" : { "state" : "translated", - "value" : "Sabon Tattaunawa" + "value" : "Binciken Kalmomi" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "שיחה חדשה" + "value" : "בדיקת איות" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "नई बातचीत" + "value" : "वर्तनी की जाँच" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Novi razgovor" + "value" : "Provjera pravopisa" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Új beszélgetés" + "value" : "Helyesírás ellenőrzése" } }, "hy-AM" : { "stringUnit" : { "state" : "translated", - "value" : "Նոր զրույց" + "value" : "Ուղղագրության ստուգում" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Percakapan Baru" + "value" : "Pemeriksa Ejaan" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Nuova chat" + "value" : "Controllo ortografico" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "新しい会話" + "value" : "スペルチェック" } }, "ka" : { "stringUnit" : { "state" : "translated", - "value" : "ახალი საუბარი" + "value" : "გამოცნობა" } }, "km" : { "stringUnit" : { "state" : "translated", - "value" : "ការសន្ទនាថ្មី" + "value" : "ពិនិត្យអក្ខរាវិរុទ្ធ" } }, "kn" : { "stringUnit" : { "state" : "translated", - "value" : "ಹೊಸ ಸಂಭಾಷಣೆ" + "value" : "ಸ್ವಯಂಚಾಲಿತ ಪರಿಶೀಲನೆ" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "새 대화" + "value" : "맞춤법 검사" } }, "ku" : { "stringUnit" : { "state" : "translated", - "value" : "گفتوگۆی نوێ" + "value" : "پشکنینی ڕستەكان" } }, "ku-TR" : { "stringUnit" : { "state" : "translated", - "value" : "Sohbeteke nû" + "value" : "Hevāyî Kontrol Bike" } }, "lg" : { "stringUnit" : { "state" : "translated", - "value" : "Eddoboozi empya" + "value" : "Spell Check" } }, "lt" : { "stringUnit" : { "state" : "translated", - "value" : "Naujas pokalbis" + "value" : "Rašybos tikrinimas" } }, "lv" : { "stringUnit" : { "state" : "translated", - "value" : "Jauna sarakste" + "value" : "Pareizrakstības Pārbaude" } }, "mk" : { "stringUnit" : { "state" : "translated", - "value" : "Нов разговор" + "value" : "Правописна Провера" } }, "mn" : { "stringUnit" : { "state" : "translated", - "value" : "Шинэ харилцан яриа" + "value" : "Үг бичих шалгалт" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Perbualan Baru" + "value" : "Menyemak Ejaan" } }, "my" : { "stringUnit" : { "state" : "translated", - "value" : "New Conversation" + "value" : "စကားလုံးစစ်ခြင်း" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Ny samtale" + "value" : "Stavekontroll" } }, "nb-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Ny Samtale" + "value" : "Stavekontroll" } }, "ne-NP" : { "stringUnit" : { "state" : "translated", - "value" : "नयाँ कुराकानी" + "value" : "हिज्जे जाँच" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Nieuw gesprek" + "value" : "Spellingcontrole" } }, "nn-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Ny samtale" + "value" : "Stavekontroll" } }, "ny" : { "stringUnit" : { "state" : "translated", - "value" : "Mushuk rimariy" + "value" : "Spell Check" } }, "pa-IN" : { "stringUnit" : { "state" : "translated", - "value" : "ਨਵੀਂ ਗੱਲਬਾਤ" + "value" : "ਹਜੇਸ਼ਉ ਚੈੱਕ" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Nowa rozmowa" + "value" : "Sprawdzanie pisowni" } }, "ps" : { "stringUnit" : { "state" : "translated", - "value" : "نوې خبرې اترې" + "value" : "املایي چک" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Nova Conversa" + "value" : "Corretor ortográfico" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Nova conversa" + "value" : "Verificação ortográfica" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Conversație nouă" + "value" : "Verificare ortografie" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Новая беседа" + "value" : "Проверка орфографии" } }, "sh" : { "stringUnit" : { "state" : "translated", - "value" : "Novi razgovor" + "value" : "Provjeravanje pravopisa" } }, "si-LK" : { "stringUnit" : { "state" : "translated", - "value" : "නව සංවාදය" + "value" : "අක්ෂර වින්‍යාස පරීක්ෂාව" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Nová konverzácia" + "value" : "Kontrola pravopisu" } }, "sl" : { "stringUnit" : { "state" : "translated", - "value" : "Nov pogovor" + "value" : "Preverjanje črkovanja" } }, "sq" : { "stringUnit" : { "state" : "translated", - "value" : "Bisedë e re" + "value" : "Kontrolli i drejtshkrimit" } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : "Нова преписка" + "value" : "Провера правописа" } }, "sr-Latn" : { "stringUnit" : { "state" : "translated", - "value" : "Nova konverzacija" + "value" : "Provera pravopisa" } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Ny konversation" + "value" : "Kontrollera stavning" } }, "sw" : { "stringUnit" : { "state" : "translated", - "value" : "Mazungumzo mapya" + "value" : "Ukaguzi wa Tahajia" } }, "ta" : { "stringUnit" : { "state" : "translated", - "value" : "புதிய உரையாடல்" + "value" : "சொல் சரிபார்ப்பு" } }, "te" : { "stringUnit" : { "state" : "translated", - "value" : "కొత్త సంభాషణ" + "value" : "పర్యాశలించడానికి చెక్ చేయి" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "การสนทนาใหม่" + "value" : "การตรวจสอบการสะกด" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Yeni Konuşma" + "value" : "Yazım Denetimi" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Нова бесіда" + "value" : "Перевірка орфографії" } }, "ur-IN" : { "stringUnit" : { "state" : "translated", - "value" : "نئی گفتگو" + "value" : "ہجے چیک" } }, "uz" : { "stringUnit" : { "state" : "translated", - "value" : "Yangi Suhbat" + "value" : "Imlo tekshiruvi" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Chuyện trò mới" + "value" : "Kiểm tra chính tả" } }, "xh" : { "stringUnit" : { "state" : "translated", - "value" : "Ingxoxo Entsha" + "value" : "Spell Check" } }, "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "新建会话" + "value" : "拼写检查" } }, "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "新對話" + "value" : "拼寫檢查" } } } }, - "conversationsNone" : { + "conversationsSpellCheckDescription" : { "extractionState" : "manual", "localizations" : { "af" : { "stringUnit" : { "state" : "translated", - "value" : "Jy het nog nie enige gesprekke nie" + "value" : "Aktiveer speltoetsing wanneer boodskappe getik word." } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "لا تملك أي محادثات حتى الآن" + "value" : "تفعيل التحقق الإملائي عند كتابة الرسائل." } }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "Hələ heç bir danışığınız yoxdur" + "value" : "Mesaj yazarkən orfoqrafik yoxlanışı fəallaşdır." } }, "bal" : { "stringUnit" : { "state" : "translated", - "value" : "شما نیش کنورزیشن بیت۔" + "value" : "پیغامات ٹائپ کرتے وقت ہجے کی تصدیق کو فعال کریں." } }, "be" : { "stringUnit" : { "state" : "translated", - "value" : "Вы пакуль не маеце размоў" + "value" : "Уключыць праверку правапісу пры наборы паведамленняў." } }, "bg" : { "stringUnit" : { "state" : "translated", - "value" : "Вие нямате никакви разговори досега" + "value" : "Активиране на автокорекция при писане на съобщения." } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "আপনার কোনো কথোপকথন নেই এখনো" + "value" : "বার্তা টাইপ করার সময় বানান পরীক্ষা সক্রিয় করুন।" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Encara no tens cap conversa" + "value" : "Activa la revisió ortogràfica quan escrius missatges." } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Zatím nemáte žádné konverzace" + "value" : "Povolit kontrolu pravopisu při psaní zpráv." } }, "cy" : { "stringUnit" : { "state" : "translated", - "value" : "Nid oes gennych unrhyw sgyrsiau eto" + "value" : "Galluogi gwirio sillafu wrth deipio negeseuon." } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Du har ingen samtaler endnu" + "value" : "Aktiver stavekontrol, når du skriver beskeder." } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Du hast noch keine Unterhaltungen" + "value" : "Rechtschreibprüfung bei der Eingabe von Nachrichten aktivieren." } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Δεν έχετε συνομιλίες ακόμα" + "value" : "Ενεργοποίηση ορθογραφικού ελέγχου κατά την πληκτρολόγηση μηνυμάτων." } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "You don't have any conversations yet" + "value" : "Enable spell check when typing messages." } }, "eo" : { "stringUnit" : { "state" : "translated", - "value" : "Vi ankoraŭ ne havas konversaciojn" + "value" : "Ebligi literumkontrolon dum skribadi mesaĝojn." } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Aún no tienes conversaciones" + "value" : "Activar el corrector ortográfico." } }, "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Aún no tienes conversaciones" + "value" : "Activar corrección ortográfica al escribir mensajes." } }, "et" : { "stringUnit" : { "state" : "translated", - "value" : "Teil pole veel ühtegi vestlust" + "value" : "Luba õigekirjakontrolli, kui kirjutate sõnumeid." } }, "eu" : { "stringUnit" : { "state" : "translated", - "value" : "Ez daukazu elkarrizketarik oraindik" + "value" : "Mezuak idaztean ortografia-egiaztatzea gaitu." } }, "fa" : { "stringUnit" : { "state" : "translated", - "value" : "شما هنوز هیچ مکالمه‌ای ندارید" + "value" : "چک کردن املای کلمات را در هنگام تایپ کردن فعال کنید." } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Sinulla ei ole vielä yhtään keskustelua" + "value" : "Käytä oikolukua kirjoitettaessa viestejä." } }, "fil" : { "stringUnit" : { "state" : "translated", - "value" : "Wala ka pang mga pag-uusap" + "value" : "I-enable ang pagsuri sa spell kapag nagta-type ng mga mensahe." } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Vous n'avez pas encore de conversations" - } - }, - "gl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aínda non tes ningunha conversa" + "value" : "Activer le correcteur d'orthographe pour la saisie des messages." } }, "ha" : { "stringUnit" : { "state" : "translated", - "value" : "Ba ku da zantuka a halin yanzu" + "value" : "Kunna dubawa na rubutu lokacin shigar da saƙonni." } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "עדיין אין לך שיחות" + "value" : "לאפשר בדיקת איות בעת הקלדת הודעות." } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "आपके पास अभी तक कोई वार्तालाप नहीं है" + "value" : "संदेश टाइप करते समय वर्तनी जांच सक्षम करें।" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Još nemate razgovora" + "value" : "Uključi provjeru pravopisa prilikom tipkanja poruka." } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Még nincsenek beszélgetéseid" + "value" : "A helyesírás-ellenőrzés engedélyezése üzenetek írásakor." } }, "hy-AM" : { "stringUnit" : { "state" : "translated", - "value" : "Դուք դեռևս ոչ մի զրույց ունեք" + "value" : "Միացնել ուղղագրության ստուգումը հաղորդագրություններ մուտքագրելիս:" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Anda belum memiliki percakapan satu pun" + "value" : "Aktifkan pemeriksaan ejaan saat mengetik pesan." } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Non hai ancora nessuna chat" + "value" : "Abilita i suggerimenti da tastiera." } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "まだ通知はありません" + "value" : "メッセージを入力するときにスペルチェックを有効にします" } }, "ka" : { "stringUnit" : { "state" : "translated", - "value" : "თქვენ ჯერ არ გაქვთ საუბრები" + "value" : "შეტყობინებების აკრეფისას ჩართე მართლწერის შემოწმება." } }, "km" : { "stringUnit" : { "state" : "translated", - "value" : "អ្នក​មិន​ទាន់​មានការសន្ទនាណាមួយនៅឡើយទេ" + "value" : "បើកការពិនិត្យអក្ខរាវិរុទ្ធនៅពេលវាយសារ។" } }, "kn" : { "stringUnit" : { "state" : "translated", - "value" : "ನೀವು ಇನ್ನೂ ಯಾವುದೇ ಸಂಭಾಷಣೆಗಳನ್ನು ಹೊಂದಿಲ್ಲ" + "value" : "ಸಂದೇಶಗಳನ್ನು ಟೈಪ್ ಮಾಡುವಾಗ ಸ್ಪೆಲ್ ಚೆಕ್ ಅನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಿ." } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "아직 대화가 없습니다" + "value" : "메시지를 입력할 때 맞춤법 검사를 활성화합니다." } }, "ku" : { "stringUnit" : { "state" : "translated", - "value" : "تۆ هیچ گفتگووەکت نییە یەکەوە" + "value" : "چالاککردنی ڕاستەکەوت لە کاتی نوسینی پەیامەکان." } }, "ku-TR" : { "stringUnit" : { "state" : "translated", - "value" : "Hêj tu diyalogekî nînin" + "value" : "Gava peyamên nivîsînê kontrola rastnivîsê bikar bînin." } }, "lg" : { "stringUnit" : { "state" : "translated", - "value" : "Tolina ngeri yenna ya kulagamu bukati." + "value" : "Tandika spell check bw'ogwandiika obubaka." } }, "lt" : { "stringUnit" : { "state" : "translated", - "value" : "Jūs dar neturite pokalbių" + "value" : "Įjungti rašybos tikrinimą, rašant žinutes." } }, "lv" : { "stringUnit" : { "state" : "translated", - "value" : "Tev vēl nav nevienas sarunas" + "value" : "Iespējot pareizrakstības pārbaudi, rakstot ziņojumus." } }, "mk" : { "stringUnit" : { "state" : "translated", - "value" : "Сè уште немате разговори" + "value" : "Овозможи проверка на правопис додека пишувате пораки." } }, "mn" : { "stringUnit" : { "state" : "translated", - "value" : "Танд одоогоор яриа байхгүй байна" + "value" : "Мессеж бичих үед гарын үсгийн алдааг шалгахыг идэвхжүүлэх." } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Anda belum mempunyai sebarang perbualan" + "value" : "Aktifkan pemeriksaan ejaan semasa menaip mesej." } }, "my" : { "stringUnit" : { "state" : "translated", - "value" : "သင့်တွင် စကားဝိုင်း မရှိသေးပါ" + "value" : "မက်ဆေ့ချ်ရိုက်နေစဉ် အမှားများစစ်ဆေးပါ။" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Du har ingen samtaler ennå" + "value" : "Aktiver stavekontroll når du skriver meldinger." } }, "nb-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Du har ingen samtaler ennå" + "value" : "Aktiver stavekontroll ved skriving av meldinger." } }, "ne-NP" : { "stringUnit" : { "state" : "translated", - "value" : "तपाईंसँग अहिलेसम्म कुनै कुराकानीहरू छैनन्" + "value" : "सन्देश टाइप गर्दै Spell चेक सक्षम गर्नुहोस्।" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "U heeft nog geen gesprekken" + "value" : "Spellingscontrole inschakelen tijdens het typen van berichten." } }, "nn-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Du har inga samtalar enno" + "value" : "Skru på stavekontroll når du skriv meldingar." } }, "ny" : { "stringUnit" : { "state" : "translated", - "value" : "Simunayambe kuyankhulana ndi aliyense" + "value" : "Yambitsa kuwunika kolakwitsa mukamalemba mauthenga." } }, "pa-IN" : { "stringUnit" : { "state" : "translated", - "value" : "ਤੁਹਾਡੇ ਕੋਲ ਹਾਲੇ ਤੱਕ ਕੋਈ ਗੱਲਬਾਤ ਨਹੀਂ ਹੋਈ ਹੈ" + "value" : "ਸੰਦਰਸ਼ਣ ਹਿਜੇ ਲਿਖਣ ਦੇ ਸਮੇਂ ਸੁਧਾਰ।" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Nie masz jeszcze żadnych konwersacji" + "value" : "Włącz sprawdzanie pisowni podczas pisania wiadomości." } }, "ps" : { "stringUnit" : { "state" : "translated", - "value" : "تاسو لا تراوسه هېڅ خبرې ندي کړي" + "value" : "د پیغامونو ټایپولو پر مهال د املا چک فعال کړئ." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Você ainda não tem nenhuma conversa" + "value" : "Habilitar verificação ortográfica ao digitar mensagens." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Ainda não tem conversas" + "value" : "Permitir corretor ortográfico ao escrever mensagens." } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Încă nu ai conversații" + "value" : "Activează verificarea ortografică pentru scrierea mesajelor." } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "У вас пока нет бесед" + "value" : "Включить проверку орфографии при наборе сообщений." } }, "sh" : { "stringUnit" : { "state" : "translated", - "value" : "Još nemaš nijednu poruku" + "value" : "Omogući provjeru pravopisa prilikom tipkanja poruka." } }, "si-LK" : { "stringUnit" : { "state" : "translated", - "value" : "ඔබට තවම සංවාදයක් නැත" + "value" : "පණිවිඩ ලිවීමේදී අක්ෂර වින්‍යාස පරීක්ෂාව සබල කරන්න." } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Zatiaľ nemáte žiadne konverzácie" + "value" : "Povoliť kontrolu pravopisu pri písaní správ." } }, "sl" : { "stringUnit" : { "state" : "translated", - "value" : "Nimate še nobenih pogovorov" + "value" : "Omogočite preverjanje črkovanja med tipkanjem sporočil." } }, "sq" : { "stringUnit" : { "state" : "translated", - "value" : "Ju nuk keni ende ndonjë bisedë" + "value" : "Aktivizo drejtshkrimin kur shkruan mesazhe." } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : "Још увек немате ниједан разговор" + "value" : "Омогући проверу правописа приликом куцања порука." } }, "sr-Latn" : { "stringUnit" : { "state" : "translated", - "value" : "Još nemate nijednu konverzaciju" + "value" : "Omogućava proveru pravopisa prilikom kucanja poruka." } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Du har inga konversationer än" + "value" : "Aktivera stavningskontroll när du skriver meddelanden." } }, "sw" : { "stringUnit" : { "state" : "translated", - "value" : "Hauna mazungumzo yoyote bado" + "value" : "Wezesha uangalizi wa tahajia unapopatajumbe." } }, "ta" : { "stringUnit" : { "state" : "translated", - "value" : "உங்களிடம் இதுவரை உரையாடல்கள் இல்லை" + "value" : "செய்திகளை தட்டச்சு செய்யும்பொழுது, எழுத்துப் பரிசோதனையை இயக்கவும்." } }, "te" : { "stringUnit" : { "state" : "translated", - "value" : "మీకు ఇంకా ఏ టిందనో సంభాషణలు లేవు" + "value" : "సందేశాలు టైప్ చేయడం ప్రారంభించినప్పుడు స్పెల్ చెక్ ప్రారంభించండి." } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "คุณยังไม่มีการสนทนา" + "value" : "เปิดใช้การตรวจสอบการสะกดเมื่อพิมพ์ข้อความ" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Henüz herhangi bir konuşmanız yok" + "value" : "İleti yazarken yazım denetimini etkinleştirin." } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "У вас ще немає жодних розмов" + "value" : "Увімкнути перевірку орфографії під час введення повідомлень." } }, "ur-IN" : { "stringUnit" : { "state" : "translated", - "value" : "آپ کے پاس ابھی تک کوئی گفتگو نہیں ہے" + "value" : "پیغامات لکھتے وقت ہجے کی جانچ فعال کریں." } }, "uz" : { "stringUnit" : { "state" : "translated", - "value" : "Sizda hali suhbatlar yo'q" + "value" : "Xabarlarni yozayotganda imlo tekshiruvini yoqish." } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Bạn chưa có cuộc trò chuyện nào" + "value" : "Bật kiểm tra chính tả khi nhập tin nhắn." } }, "xh" : { "stringUnit" : { "state" : "translated", - "value" : "Awunazo naziphi na iincoko okwangoku" + "value" : "Vumela ukupela ngokuzenzekelayo xa ubhala imilayezo." } }, "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "您还没有任何会话" + "value" : "在输入消息时启用拼写检查。" } }, "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "您還沒有任何對話" + "value" : "輸入訊息時進行拼寫檢查。" } } } }, - "conversationsSendWithEnterKey" : { + "conversationsStart" : { "extractionState" : "manual", "localizations" : { "af" : { "stringUnit" : { "state" : "translated", - "value" : "Stuur met Enter Sleutel" + "value" : "Begin Gesprek" } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "ارسل مع مفتاح الدخول" + "value" : "ابدأ محادثة" } }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "Enter düyməsi ilə göndər" + "value" : "Danışıq başlat" } }, "bal" : { "stringUnit" : { "state" : "translated", - "value" : "بھیج کے ساتھ انٹر کلید" + "value" : "گپتاری شروع کـــــــن" } }, "be" : { "stringUnit" : { "state" : "translated", - "value" : "Адпраўце, націснуўшы клавішу Enter" + "value" : "Пачаць гутарку" } }, "bg" : { "stringUnit" : { "state" : "translated", - "value" : "Изпращане с Enter клавиш" + "value" : "Започнете разговор" } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "এন্টার কি দিয়ে পাঠান" + "value" : "কথোপকথন শুরু করুন" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Envieu amb la tecla Enter" + "value" : "Comença una conversa" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Odeslat klávesou Enter" + "value" : "Zahájit konverzaci" } }, "cy" : { "stringUnit" : { "state" : "translated", - "value" : "Anfon gyda'r Allwedd Enter" + "value" : "Dechrau Sgwrs" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Send med Enter-tasten" + "value" : "Start samtale" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Mit Eingabetaste senden" + "value" : "Unterhaltung beginnen" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Αποστολή με το πλήκτρο Enter" + "value" : "Έναρξη Συνομιλίας" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Send with Enter Key" + "value" : "Start Conversation" } }, "eo" : { "stringUnit" : { "state" : "translated", - "value" : "Sendi per la Eniga klavo" + "value" : "Komenci Babilon" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Enviar con la tecla Into" + "value" : "Comenzar Conversación" } }, "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Enviar con la tecla de enter" + "value" : "Iniciar conversación" } }, "et" : { "stringUnit" : { "state" : "translated", - "value" : "Saada Enter-klahviga" + "value" : "Alusta vestlust" } }, "eu" : { "stringUnit" : { "state" : "translated", - "value" : "Bidali Enter Teklarekin" + "value" : "Has zaitez Elkarrizketa" } }, "fa" : { "stringUnit" : { "state" : "translated", - "value" : "ارسال با کلید Enter" + "value" : "شروع گفتگو" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Lähetä Enter-näppäimellä" + "value" : "Aloita keskustelu" } }, "fil" : { "stringUnit" : { "state" : "translated", - "value" : "Send with Enter Key" + "value" : "Simulan ang Pag-uusap" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Envoyer avec bouton Entrée" + "value" : "Démarrer une conversation" } }, "gl" : { "stringUnit" : { "state" : "translated", - "value" : "Enviar coa tecla Enter" + "value" : "Iniciar Conversa" } }, "ha" : { "stringUnit" : { "state" : "translated", - "value" : "Aika tare da Maɓallin Shigarwa" + "value" : "Fara Tattaunawa" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "שלח עם מקש Enter" + "value" : "התחל שיחה" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "एन्टर कुंजी के साथ भेजें" + "value" : "वार्तालाप शुरू करें" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Pošalji pomoću tipke Enter" + "value" : "Započni razgovor" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Küldés az enter billentyűvel" + "value" : "Beszélgetés indítása" } }, "hy-AM" : { "stringUnit" : { "state" : "translated", - "value" : "Ուղարկել Enter ստեղնով" + "value" : "Սկսել զրույցը" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Kirim dengan Tombol Enter" + "value" : "Mulai Percakapan" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Invia con il tasto Invio" + "value" : "Inizia chat" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "エンターキーで送信" + "value" : "会話を開始する" } }, "ka" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER ღილაკით გაგზავნა" + "value" : "საუბრის დაწყება" } }, "km" : { "stringUnit" : { "state" : "translated", - "value" : "ផ្ញើដោយប្រើខ្យល់ Key" + "value" : "ចាប់ផ្តើមសន្ទនា" } }, "kn" : { "stringUnit" : { "state" : "translated", - "value" : "ಎಂಟರ್ ಕೀ ಬಳಸಿ ಕಳುಹಿಸಿ" + "value" : "ಸಂಭಾಷಣೆಯನ್ನು ಪ್ರಾರಂಭಿಸಿ" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "Enter 키를 사용하여 보내기" + "value" : "대화 시작하기" } }, "ku" : { "stringUnit" : { "state" : "translated", - "value" : "ناردن بە کلیلی ئێنتر" + "value" : "دەستپێکردنی گفتوگۆ" } }, "ku-TR" : { "stringUnit" : { "state" : "translated", - "value" : "Bi Enter Bicîh Bibe" + "value" : "Sohbet Begin" } }, "lg" : { "stringUnit" : { "state" : "translated", - "value" : "Sindikira nga okozesa Enter Key" + "value" : "Tandika Okwogera" } }, "lt" : { "stringUnit" : { "state" : "translated", - "value" : "Siųsti naudojant Enter" + "value" : "Pradėti naują pokalbį" } }, "lv" : { "stringUnit" : { "state" : "translated", - "value" : "Nosūtīt ar Enter taustiņu" + "value" : "Sākt Sarunu" } }, "mk" : { "stringUnit" : { "state" : "translated", - "value" : "Испрати со копчето Enter" + "value" : "Започни Конверзација" } }, "mn" : { "stringUnit" : { "state" : "translated", - "value" : "Enter товчлуурыг ашиглан илгээх" + "value" : "Яриа эхлүүлэх" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Hantar dengan Kekunci Enter" + "value" : "Mulakan Perbualan" } }, "my" : { "stringUnit" : { "state" : "translated", - "value" : "Enter Key ဖြင့် ပို့ပါ" + "value" : "စကားပြောစတင်" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Send med Enter Key" + "value" : "Start samtale" } }, "nb-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Send med Enter Key" + "value" : "Start samtale" } }, "ne-NP" : { "stringUnit" : { "state" : "translated", - "value" : "Enter कुञ्जीबाट पठाउनुहोस्" + "value" : "कुराकानी सुरु गर्नुहोस्" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Verzenden met Enter toets" + "value" : "Gesprek starten" } }, "nn-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Send med Enter Key" + "value" : "Start samtale" } }, "ny" : { "stringUnit" : { "state" : "translated", - "value" : "Send with Enter Key" + "value" : "Start Conversation" } }, "pa-IN" : { "stringUnit" : { "state" : "translated", - "value" : "ਐਨਟੀਰ ਕੁੰਜੀ ਨਾਲ ਭੇਜੋ" + "value" : "ਗੱਲਬਾਤ ਸ਼ੁਰੂ ਕਰੋ" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Wyślij za pomocą klawisza Enter" + "value" : "Rozpocznij rozmowę" } }, "ps" : { "stringUnit" : { "state" : "translated", - "value" : "د Enter کیلي سره ولیږئ" + "value" : "خبرو اترو پیل کړئ" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Enviar com a tecla Enter" + "value" : "Iniciar Conversa" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Enviar com Enter" + "value" : "Iniciar Conversa" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Trimite cu tasta Enter" + "value" : "Începe o conversație" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Отправить с помощью клавиши Enter" + "value" : "Начать беседу" } }, "sh" : { "stringUnit" : { "state" : "translated", - "value" : "Pošalji sa Enter tipkom" + "value" : "Pokreni razgovor" } }, "si-LK" : { "stringUnit" : { "state" : "translated", - "value" : "Enter යතුර සමග යවන්න" + "value" : "සම්භාෂණය ආරම්භ කරන්න" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Odoslať správu tlačidlom Enter" + "value" : "Začať príhovor" } }, "sl" : { "stringUnit" : { "state" : "translated", - "value" : "Pošlji s tipko Enter" + "value" : "Začni pogovor" } }, "sq" : { "stringUnit" : { "state" : "translated", - "value" : "Dërgoni me Enter Key" + "value" : "Filloni Bisedën" } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : "Пошаљи са ентер тастером" + "value" : "Започни разговор" } }, "sr-Latn" : { "stringUnit" : { "state" : "translated", - "value" : "Pošalji pomoću tipke Enter" + "value" : "Započni razgovor" } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Sänd via tryckning på returtangent" + "value" : "Starta konversation" } }, "sw" : { "stringUnit" : { "state" : "translated", - "value" : "Tuma na Kibonyezo cha Enter" + "value" : "Anza Mazungumzo" } }, "ta" : { "stringUnit" : { "state" : "translated", - "value" : "Enter விசையால் அனுப்பு" + "value" : "உரையாடலைத் தொடங்கு" } }, "te" : { "stringUnit" : { "state" : "translated", - "value" : "ఎంటర్ కీతో పంపుము" + "value" : "సంభాషణ ప్రారంభించండి" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "ส่งด้วยปุ่ม Enter" + "value" : "เริ่มการสนทนา" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Enter tuşu ile gönder" + "value" : "Sohbet Başlat" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Надіслати клавішею Enter" + "value" : "Почати розмову" } }, "ur-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Enter Key کے ساتھ بھیجنا" + "value" : "گفتگو شروع کریں" } }, "uz" : { "stringUnit" : { "state" : "translated", - "value" : "Enter tugmasi bilan jo'natish" + "value" : "Suhbatni boshlash" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Gửi bằng Phím Enter" + "value" : "Bắt đầu cuộc trò chuyện" } }, "xh" : { "stringUnit" : { "state" : "translated", - "value" : "Thumela ngazo Ukhiye we-Enter" + "value" : "Start Conversation" } }, "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "使用回车键发送" + "value" : "开始会话" } }, "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "回車鍵發送" + "value" : "開始會話" } } } }, - "conversationsSendWithEnterKeyDescription" : { + "copied" : { "extractionState" : "manual", "localizations" : { "af" : { "stringUnit" : { "state" : "translated", - "value" : "Tikking van die Enter Key sal boodskappe stuur in plaas van om ‘n nuwe lyn te begin." + "value" : "Gekopieër" } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "النقر على Enter سوف يرسل الرسالة بدلاَ من بدء سطر جديد." + "value" : "تم النسخ" } }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "Enter düyməsinə toxunmaq, yeni bir sətir əlavə etmək əvəzinə mesajı göndərəcək." + "value" : "Kopyalandı" } }, "bal" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER کی تپی ھتادیپی پیام بجھڈی ہے، SHIFT + ENTER نوکی کورتھ شروع کـــــــن" + "value" : "نکل" } }, "be" : { "stringUnit" : { "state" : "translated", - "value" : "Націсканне Enter прывядзе да адпраўкі паведамлення замест пераходу на новы радок." + "value" : "Скапіравана" } }, "bg" : { "stringUnit" : { "state" : "translated", - "value" : "Натискането на клавиша Enter ще изпрати съобщение вместо да започне нов ред." + "value" : "Копирано" } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "এন্টার কী ট্যাপ করলে নতুন লাইন শুরু করার পরিবর্তে মেসেজ পাঠাবে।" + "value" : "কপি করা হয়েছে" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Si piqueu la tecla Intro, s'enviarà un missatge en lloc d'iniciar una línia nova." + "value" : "S'ha copiat" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Klepnutím na klávesu Enter odešlete zprávu namísto zahájení nového řádku." + "value" : "Zkopírováno" } }, "cy" : { "stringUnit" : { "state" : "translated", - "value" : "Tapio'r Allwedd Mynd i mewn bydd yn anfon neges yn lle dechrau llinell newydd." + "value" : "Copïwyd" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Når du trykker på Enter-tasten, sendes beskeder i stedet for at starte en ny linje." + "value" : "Kopieret" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Durch Tippen auf die Eingabetaste wird eine Nachricht gesendet, anstatt eine neue Zeile zu beginnen." + "value" : "Kopiert" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Πατώντας το πλήκτρο Enter θα σταλεί μήνυμα αντί να ξεκινήσει μια νέα γραμμή." + "value" : "Αντιγράφηκε" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Tapping the Enter Key will send message instead of starting a new line." + "value" : "Copied" } }, "eo" : { "stringUnit" : { "state" : "translated", - "value" : "Frapante la Enigareton sendos mesaĝon anstataŭ komenci novan linion." + "value" : "Kopiite" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Pulsar la tecla Intro enviará el mensaje en lugar de empezar una nueva línea." + "value" : "Copiado" } }, "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Pulsar la tecla Intro enviará el mensaje en lugar de empezar una nueva línea." + "value" : "Copiado" } }, "et" : { "stringUnit" : { "state" : "translated", - "value" : "Uuelt realt alustamise asemel saadab Enter-klahvi koputamine sõnumi." + "value" : "Kopeeritud" } }, "eu" : { "stringUnit" : { "state" : "translated", - "value" : "Enter Tekla sakatzeak mezua bidaliko du, lerro berri bat hasi beharrean." + "value" : "Kopiatu da" } }, "fa" : { "stringUnit" : { "state" : "translated", - "value" : "ضربه زدن روی کلید Enter به جای شروع یک خط جدید، پیام را ارسال خواهد کرد." + "value" : "کپی‌شده" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Rivinvaihdon sijaan Enter-näppäimen painallus lähettää viestin." + "value" : "Kopioitu" } }, "fil" : { "stringUnit" : { "state" : "translated", - "value" : "Kapag pinindot ang Enter Key ay magpapadala ito ng mensahe sa halip na magsimula ng bagong linya." + "value" : "Nakopya na" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Appuyer sur la touche Entrée enverra un message au lieu de commencer une nouvelle ligne." + "value" : "Copié" } }, "gl" : { "stringUnit" : { "state" : "translated", - "value" : "Tocar a tecla Enter enviará a mensaxe en vez de comezar unha nova liña." + "value" : "Copiouse" } }, "ha" : { "stringUnit" : { "state" : "translated", - "value" : "Matsawa maɓallin Shigarwa zai aika saƙo maimakon fara sabon layi." + "value" : "Kwafi" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "לחיצה על מקש Enter תשלח הודעה במקום לפתוח שורה חדשה." + "value" : "הועתק" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "एन्टर कुंजी को दबाने से संदेश भेजा जाएगा नई लाइन शुरू करने से बजाय।" + "value" : "कॉपी किया गया!" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Pritiskom na tipku Enter poslati će se poruka umjesto započinjanja novog retka." + "value" : "Kopirano" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Az Enter billentyű lenyomása elküldi az üzenetet új sor kezdése helyett." + "value" : "Másolva" } }, "hy-AM" : { "stringUnit" : { "state" : "translated", - "value" : "Enter ստեղնը սեղմելով՝ նոր տող սկսելու փոխարեն հաղորդագրություն կուղարկվի:" + "value" : "Պատճենվել է" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Mengetuk Tombol Enter akan mengirim pesan alih-alih memulai baris baru." + "value" : "Disalin" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Premere il tasto Invio invierà il messaggio invece d'iniziare una nuova riga." + "value" : "Copiato" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Enterキーをタップすると、改行ではなく、メッセージが送信されます。" + "value" : "コピーしました" } }, "ka" : { "stringUnit" : { "state" : "translated", - "value" : "Enter ღილაკზე დაჭერა აერტგზავნის შეტყობინებას, არა ახალი ხაზის დაწყებას." + "value" : "დაკოპირებულია" } }, "km" : { "stringUnit" : { "state" : "translated", - "value" : "ការ​ចុចលើឃី Enter នឹងផ្ញើសារជំនួសឱ្យការចុះបន្ទាត់ថ្មី។" + "value" : "បានចម្លង" } }, "kn" : { "stringUnit" : { "state" : "translated", - "value" : "ಎಂಟರ್ ಕಿವಿಯನ್ನು ಟ್ಯಾಪ್ ಮಾಡುವುದರಿಂದ ಹೊಸ ಸಾಲು ಪ್ರಾರಂಭಿಸುವ ಬದಲು ಸಂದೇಶವನ್ನು ಕಳುಹಿಸಲಾಗುತ್ತದೆ." + "value" : "ನಕಲು ಮಾಡಿದೆ" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "Enter를 누를 때 줄바꿈 대신 메시지를 전송합니다." + "value" : "복사됨" } }, "ku" : { "stringUnit" : { "state" : "translated", - "value" : "فەرمی پەیوەندیدانی کردنی کلیکی ئەنتەر پەیامیەک دەنێریت لە کاتی گۆڕینی هێڵی نوێ." + "value" : "لەبەرگرتن" } }, "ku-TR" : { "stringUnit" : { "state" : "translated", - "value" : "Bateya Li Keya Kêşkaran peyam ji bo şandina peyamê dike ser hûn kira xeta nûkirinê bide dest xetin." + "value" : "Kopîkirî ye" } }, "lg" : { "stringUnit" : { "state" : "translated", - "value" : "Okukonako ku Kiwandiiko kye Kikuta kiyinza okuweereza obubaka ne kuttandika olunyiriri olupya." + "value" : "Koppa" + } + }, + "lo" : { + "stringUnit" : { + "state" : "translated", + "value" : "ຄັດລອກເເລ້ວ" } }, "lt" : { "stringUnit" : { "state" : "translated", - "value" : "Enter klavišas siųs žinutę vietoje naujos eilutės pradžios." + "value" : "Nukopijuota" } }, "lv" : { "stringUnit" : { "state" : "translated", - "value" : "Pieskaroties Enter taustiņam, tiks nosūtīta ziņa, nevis sākta jauna rinda." + "value" : "Nokopēts" } }, "mk" : { "stringUnit" : { "state" : "translated", - "value" : "Допирањето на копчето Enter ќе испрати порака наместо да започне нов ред." + "value" : "Копирано" } }, "mn" : { "stringUnit" : { "state" : "translated", - "value" : "Enter товч дарахад шинэ мөр эхлүүлэхийн оронд мессеж илгээнэ." + "value" : "Хуулсан" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Mengetuk Kekunci Masuk akan menghantar mesej daripada memulakan barisan baharu." + "value" : "Disalin" } }, "my" : { "stringUnit" : { "state" : "translated", - "value" : "Enter key ကိုနှိပ်ခြင်းဖြင့် စာကြောင်းအသစ်ကို စမည့်အစား မက်ဆေ့ခ်ျကို ပေးပို့ပါမည်။" + "value" : "ကို ကူးယူပြီးပါပြီ" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Ved å trykke på Enter nøkkel vil sende melding i stedet for å starte en ny linje." + "value" : "Kopiert" } }, "nb-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Ved å trykke på Enter nøkkel vil sende melding i stedet for å starte en ny linje." + "value" : "Kopiert" } }, "ne-NP" : { "stringUnit" : { "state" : "translated", - "value" : "ENTER कुञ्जीले सन्देश पठाउनेछ भनेको नयाँ लाइन सुरु गर्नुको सट्टा।" + "value" : "प्रतिलिपि बनाइएको" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Met de Enter Toets direct het bericht versturen in plaats van een nieuwe regel beginnen." + "value" : "Gekopieerd" } }, "nn-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Ved å trykke på Enter nøkkel vil sende melding i stedet for å starte en ny linje." + "value" : "Kopiert" } }, "ny" : { "stringUnit" : { "state" : "translated", - "value" : "Dinani Makiyi Olimba kuti mutumize uthenga m'malo moyamba mzere watsopano." + "value" : "Chotengera" } }, "pa-IN" : { "stringUnit" : { "state" : "translated", - "value" : "ਇੰਟਰ ਕੀ ਨੂੰ ਟੈਪ ਕਰਨ ਨਾਲ ਸੁਨੇਹਾ ਭੇਜਿਆ ਜਾਵੇਗਾ ਤਦ ਇਹ ਇਕ ਨਵੀਂ ਲਾਈਨ ਸ਼ੁਰੂ ਕਰਨ ਦੇ ਬਜਾਏ।" + "value" : "ਨਕਲ ਕੀਤੀ" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Naciśnięcie klawisza Enter spowoduje wysłanie wiadomości zamiast rozpoczęcia nowej linijki." + "value" : "Skopiowano" } }, "ps" : { "stringUnit" : { "state" : "translated", - "value" : "د انټر کلیک کول به پیغام واستوي پرځای د نوي کرښې پیل کولو." + "value" : "کاپي" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Tocar na tecla Enter enviará uma mensagem em vez de iniciar uma nova linha." + "value" : "Copiado" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Tocar na tecla Enter enviará uma mensagem em vez de iniciar uma nova linha." + "value" : "Copiado" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Atingerea tastei Enter va trimite un mesaj în loc de a iniția o nouă linie." + "value" : "S-a copiat" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Нажатие Enter будет отправлять сообщение, а не начинать новую строку." + "value" : "Скопировано" } }, "sh" : { "stringUnit" : { "state" : "translated", - "value" : "Pritiskom na tipku Enter poslat će se poruka umjesto da se započne novi redak." + "value" : "Kopirano" } }, "si-LK" : { "stringUnit" : { "state" : "translated", - "value" : "ඇතුළුවීමේ යතුර තට්ටු කිරීම මෙම පණිවිඩය යවනු ඇත මෙන්ම නව තීරුවක් ආරම්භ කිරීම වෙනුවට." + "value" : "පිටපත් විය" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Namiesto vytvorenia nového riadku v správe, aplikácia odošle správu." + "value" : "Skopírované" } }, "sl" : { "stringUnit" : { "state" : "translated", - "value" : "Pritiskanje tipke Enter bo namesto začetka nove vrstice poslalo sporočilo." + "value" : "Kopirano" } }, "sq" : { "stringUnit" : { "state" : "translated", - "value" : "Klikimi i tastit Enter do të dërgojë mesazhin në vend që të fillojë një rresht të ri." + "value" : "U kopjua" } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : "Притиском на Enter тастер шаље се порука уместо започетог новог реда." + "value" : "Копирана" } }, "sr-Latn" : { "stringUnit" : { "state" : "translated", - "value" : "Dodir tastera Enter će poslati poruku umesto da započne novi red." + "value" : "Kopirano" } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Tryck på returtangent sänder meddelande istället för att radbryta." + "value" : "Kopierad" } }, "sw" : { "stringUnit" : { "state" : "translated", - "value" : "Gusa kitufe cha Ingiza ili kutuma ujumbe badala ya kuanza mstari mpya." + "value" : "Imenakiliwa" } }, "ta" : { "stringUnit" : { "state" : "translated", - "value" : "என்டர் விசையை அழுத்துவதன் மூலம் புதிய வரியை ஆரம்பிக்காமல் செய்தியினை அனுப்புகிறது." + "value" : "நகலெடுக்கப்பட்டது" } }, "te" : { "stringUnit" : { "state" : "translated", - "value" : "ఎంటర్ కీని టాప్ చేయడం ద్వారా కొత్త పంక్తి ప్రారంభం కాకుండా సందేశం పంపబడుతుంది." + "value" : "ప్రతి తీసుకోబడింది" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "การแตะปุ่ม Enter จะส่งข้อความแทนการเริ่มบรรทัดใหม่" + "value" : "คัดลอกแล้ว" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Enter tuşuna basmak yeni bir satıra geçmek yerine ileti gönderir." + "value" : "Kopyalandı" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Натискання клавіші Enter буде відправляти повідомлення замість переходу на новий рядок." + "value" : "Скопійовано" } }, "ur-IN" : { "stringUnit" : { "state" : "translated", - "value" : "انٹر کی دبانے سے پیغام بھیجا جائے گا نہ کہ نئی سطر شروع کی جائے گی۔" + "value" : "کاپی کیا گیا" } }, "uz" : { "stringUnit" : { "state" : "translated", - "value" : "Enter tugmasini bosish xabarni yuboradi, yangi satrdan boshlash o‘rniga." + "value" : "Nusxalandi" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Bấm phím Enter sẽ gửi tin nhắn thay vì bắt đầu một dòng mới." + "value" : "Đã sao chép" } }, "xh" : { "stringUnit" : { "state" : "translated", - "value" : "Tapping the Enter Key will send message instead of starting a new line." + "value" : "Ikopiwe" } }, "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "按回车键发送消息而非换行" + "value" : "已复制" } }, "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "回車鍵發送訊息,而不是另起一行。" + "value" : "已複製" } } } }, - "conversationsSettingsAllMedia" : { + "copy" : { "extractionState" : "manual", "localizations" : { "af" : { "stringUnit" : { "state" : "translated", - "value" : "Alle Media" + "value" : "Kopieer" } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "جميع الوسائط" + "value" : "نسخ" } }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "Bütün media" + "value" : "Kopyala" } }, "bal" : { "stringUnit" : { "state" : "translated", - "value" : "تمام میڈیا" + "value" : "کاپی" } }, "be" : { "stringUnit" : { "state" : "translated", - "value" : "Усе медыя" + "value" : "Скапіяваць" } }, "bg" : { "stringUnit" : { "state" : "translated", - "value" : "Виж всички файлове" + "value" : "Копиране" } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "সমস্ত মিডিয়া" + "value" : "কপি করুন" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Tot el contingut multimèdia" + "value" : "Copiar" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Všechna média" + "value" : "Kopírovat" } }, "cy" : { "stringUnit" : { "state" : "translated", - "value" : "Pob Cyfrwng" + "value" : "Copïo" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Alle medier" + "value" : "Kopiér" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Alle Medieninhalte" + "value" : "Kopieren" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Όλα τα πολυμέσα" + "value" : "Αντιγραφή" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "All Media" + "value" : "Copy" } }, "eo" : { "stringUnit" : { "state" : "translated", - "value" : "Ĉiuj aŭdvidaĵoj" + "value" : "Kopii" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Adjuntos" + "value" : "Copiar" } }, "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Adjuntos" + "value" : "Copiar" } }, "et" : { "stringUnit" : { "state" : "translated", - "value" : "Kogu meedia" + "value" : "Kopeeri" } }, "eu" : { "stringUnit" : { "state" : "translated", - "value" : "Multimedia guztia" + "value" : "Kopiatu" } }, "fa" : { "stringUnit" : { "state" : "translated", - "value" : "تمام مدیا" + "value" : "کپی" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Kaikki media" + "value" : "Kopioi" } }, "fil" : { "stringUnit" : { "state" : "translated", - "value" : "Lahat ng media" + "value" : "Kopyahin" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Tous les médias" + "value" : "Copier" } }, "gl" : { "stringUnit" : { "state" : "translated", - "value" : "Ficheiros multimedia" + "value" : "Copiar" } }, "ha" : { "stringUnit" : { "state" : "translated", - "value" : "Duk Kafofin Watsa Labarai" + "value" : "Kwafi" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "כל המדיה" + "value" : "העתק" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "सभी मीडिया" + "value" : "कॉपी करें" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Sva multimedija" + "value" : "Kopiraj" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Összes médiafájl" + "value" : "Másolás" } }, "hy-AM" : { "stringUnit" : { "state" : "translated", - "value" : "Բոլոր մեդիաները" + "value" : "Պատճենել" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Semua Media" + "value" : "Salin" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Tutti i contenuti multimediali" + "value" : "Copia" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "すべてのメディア" + "value" : "コピーする" } }, "ka" : { "stringUnit" : { "state" : "translated", - "value" : "ყველა მედია" + "value" : "დაკოპირება" } }, "km" : { "stringUnit" : { "state" : "translated", - "value" : "ព័ត៌មានទាំងអស់" + "value" : "ចម្លង" } }, "kn" : { "stringUnit" : { "state" : "translated", - "value" : "ಎಲ್ಲ ಮಾಧ್ಯಮ" + "value" : "ನಕಲಿಸಿ" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "모든 미디어" + "value" : "복사" } }, "ku" : { "stringUnit" : { "state" : "translated", - "value" : "هەموو میدیایەکان" + "value" : "لەبەرگرتن" } }, "ku-TR" : { "stringUnit" : { "state" : "translated", - "value" : "Hemû Medya" + "value" : "Kopî bike" } }, "lg" : { "stringUnit" : { "state" : "translated", - "value" : "Emikutu Gyonna" + "value" : "Koppa" } }, "lo" : { "stringUnit" : { "state" : "translated", - "value" : "ສື່ທັງໝົດ" + "value" : "ເສັກກີ້າບ" } }, "lt" : { "stringUnit" : { "state" : "translated", - "value" : "Visa medija" + "value" : "Kopijuoti" } }, "lv" : { "stringUnit" : { "state" : "translated", - "value" : "Visa multivide" + "value" : "Kopēt" } }, "mk" : { "stringUnit" : { "state" : "translated", - "value" : "Сите медиуми" + "value" : "Копирај" } }, "mn" : { "stringUnit" : { "state" : "translated", - "value" : "Бүх Медианууд" + "value" : "Хуулах" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Semua Media" + "value" : "Salin" } }, "my" : { "stringUnit" : { "state" : "translated", - "value" : "မီဒီယာအားလုံး" + "value" : "ကူးယူမည်" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Alle medier" + "value" : "Kopier" } }, "nb-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Alle medier" + "value" : "Kopier" } }, "ne-NP" : { "stringUnit" : { "state" : "translated", - "value" : "सबै मिडिया" + "value" : "प्रतिलिपि गर्नुहोस्" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Alle media" + "value" : "Kopieer" } }, "nn-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Alle medier" + "value" : "Kopier" } }, "ny" : { "stringUnit" : { "state" : "translated", - "value" : "Zonse Zakanema" + "value" : "Chotsani" } }, "pa-IN" : { "stringUnit" : { "state" : "translated", - "value" : "ਸਾਰੀ ਮੀਡੀਆ" + "value" : "ਨਕਲ" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Wszystkie media" + "value" : "Kopiuj" } }, "ps" : { "stringUnit" : { "state" : "translated", - "value" : "ټول میډیا" + "value" : "د حساب ID کاپي" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Todas as mídias" + "value" : "Copiar" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Toda a Multimédia" + "value" : "Copiar" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Toate fișierele media" + "value" : "Copiază" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Все медиафайлы" + "value" : "Скопировать" } }, "sh" : { "stringUnit" : { "state" : "translated", - "value" : "Svi mediji" + "value" : "Kopiraj" } }, "si-LK" : { "stringUnit" : { "state" : "translated", - "value" : "සියලු මාධ්යය" + "value" : "පිටපත්" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Všetky média" + "value" : "Kopírovať" } }, "sl" : { "stringUnit" : { "state" : "translated", - "value" : "Vsi mediji" + "value" : "Kopiraj" } }, "sq" : { "stringUnit" : { "state" : "translated", - "value" : "Krejt mediat" + "value" : "Kopjoje" } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : "Сви медији" + "value" : "Копирај" } }, "sr-Latn" : { "stringUnit" : { "state" : "translated", - "value" : "Svi mediji" + "value" : "Kopiraj" } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Alla media" + "value" : "Kopiera" } }, "sw" : { "stringUnit" : { "state" : "translated", - "value" : "Vyombo vyote vya habari" + "value" : "Nakili" } }, "ta" : { "stringUnit" : { "state" : "translated", - "value" : "அனைத்து ஊடகங்கள்" + "value" : "நகலெடு" } }, "te" : { "stringUnit" : { "state" : "translated", - "value" : "అన్ని మీడియా" + "value" : "కాపీ చేయండి" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "ไฟล์ทั้งหมด" + "value" : "คัดลอก" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Tüm Medya" + "value" : "Kopyala" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Всі медіа" + "value" : "Копіювати" } }, "ur-IN" : { "stringUnit" : { "state" : "translated", - "value" : "تمام میڈیا" + "value" : "کاپی کریں" } }, "uz" : { "stringUnit" : { "state" : "translated", - "value" : "Barcha Media" + "value" : "Nusxalash" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Tất cả tệp phương tiện" + "value" : "Sao chép" } }, "xh" : { "stringUnit" : { "state" : "translated", - "value" : "Yonke Imithombo yeendaba" + "value" : "Kopa" } }, "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "所有媒体" + "value" : "复制" } }, "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "所有媒體" + "value" : "拷貝" } } } }, - "conversationsSpellCheck" : { + "create" : { "extractionState" : "manual", "localizations" : { "af" : { "stringUnit" : { "state" : "translated", - "value" : "Speltoets" + "value" : "Skep" } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "التدقيق الإملائي" + "value" : "إنشاء" } }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "Orfoqrafiya yoxlanışı" + "value" : "Yarat" } }, "bal" : { "stringUnit" : { "state" : "translated", - "value" : "رسیلا چیک" + "value" : "بناؤ" } }, "be" : { "stringUnit" : { "state" : "translated", - "value" : "Праверка правапісу" + "value" : "Стварыць" } }, "bg" : { "stringUnit" : { "state" : "translated", - "value" : "Проверка на правописа" + "value" : "Създай" } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "বানান ঠিক করুন" + "value" : "তৈরি করুন" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Revisar ortografia" + "value" : "Crear" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Kontrola pravopisu" + "value" : "Vytvořit" } }, "cy" : { "stringUnit" : { "state" : "translated", - "value" : "Gwiriad Sillafu" + "value" : "Creu" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Stavekontrol" + "value" : "Opret" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Rechtschreibprüfung" + "value" : "Erstellen" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Ορθογραφικός Έλεγχος" + "value" : "Δημιουργία" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Spell Check" + "value" : "Create" } }, "eo" : { "stringUnit" : { "state" : "translated", - "value" : "Literumkontrolo" + "value" : "Krei" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Revisión ortográfica" + "value" : "Crear" } }, "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Revisión ortográfica" + "value" : "Crear" } }, "et" : { "stringUnit" : { "state" : "translated", - "value" : "Õigekirjakontroll" + "value" : "Loo" } }, "eu" : { "stringUnit" : { "state" : "translated", - "value" : "Ortografia Egiaztapena" + "value" : "Sortu" } }, "fa" : { "stringUnit" : { "state" : "translated", - "value" : "بررسی املا" + "value" : "ایجاد" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Oikeinkirjoituksen tarkistus" + "value" : "Luo" } }, "fil" : { "stringUnit" : { "state" : "translated", - "value" : "Suri sa baybayin" + "value" : "Lumikha" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Vérification orthographique" + "value" : "Créer" } }, "gl" : { "stringUnit" : { "state" : "translated", - "value" : "Corrección Ortográfica" + "value" : "Crear" } }, "ha" : { "stringUnit" : { "state" : "translated", - "value" : "Binciken Kalmomi" + "value" : "Ƙirƙiri" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "בדיקת איות" + "value" : "צור" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "वर्तनी की जाँच" + "value" : "बनाएं" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Provjera pravopisa" + "value" : "Stvori" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Helyesírás ellenőrzése" + "value" : "Létrehozás" } }, "hy-AM" : { "stringUnit" : { "state" : "translated", - "value" : "Ուղղագրության ստուգում" + "value" : "Ստեղծել" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Pemeriksa Ejaan" + "value" : "Buat" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Controllo ortografico" + "value" : "Crea" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "スペルチェック" + "value" : "作成する" } }, "ka" : { "stringUnit" : { "state" : "translated", - "value" : "გამოცნობა" + "value" : "შექმნა" } }, "km" : { "stringUnit" : { "state" : "translated", - "value" : "ពិនិត្យអក្ខរាវិរុទ្ធ" + "value" : "បង្កើត" } }, "kn" : { "stringUnit" : { "state" : "translated", - "value" : "ಸ್ವಯಂಚಾಲಿತ ಪರಿಶೀಲನೆ" + "value" : "ರಚಿಸಿ" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "맞춤법 검사" + "value" : "만들기" } }, "ku" : { "stringUnit" : { "state" : "translated", - "value" : "پشکنینی ڕستەكان" + "value" : "دروستکردن" } }, "ku-TR" : { "stringUnit" : { "state" : "translated", - "value" : "Hevāyî Kontrol Bike" + "value" : "Çêke" } }, "lg" : { "stringUnit" : { "state" : "translated", - "value" : "Spell Check" + "value" : "Kilira" + } + }, + "lo" : { + "stringUnit" : { + "state" : "translated", + "value" : "ລູດ" } }, "lt" : { "stringUnit" : { "state" : "translated", - "value" : "Rašybos tikrinimas" + "value" : "Sukurti" } }, "lv" : { "stringUnit" : { "state" : "translated", - "value" : "Pareizrakstības Pārbaude" + "value" : "Izveidot" } }, "mk" : { "stringUnit" : { "state" : "translated", - "value" : "Правописна Провера" + "value" : "Креирај" } }, "mn" : { "stringUnit" : { "state" : "translated", - "value" : "Үг бичих шалгалт" + "value" : "Үүсгэх" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Menyemak Ejaan" + "value" : "Buat" } }, "my" : { "stringUnit" : { "state" : "translated", - "value" : "စကားလုံးစစ်ခြင်း" + "value" : "ဖန်တီးပါ" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Stavekontroll" + "value" : "Opprett" } }, "nb-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Stavekontroll" + "value" : "Opprett" } }, "ne-NP" : { "stringUnit" : { "state" : "translated", - "value" : "हिज्जे जाँच" + "value" : "सिर्जना गर्नुहोस्" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Spellingcontrole" + "value" : "Aanmaken" } }, "nn-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Stavekontroll" + "value" : "Opprett" } }, "ny" : { "stringUnit" : { "state" : "translated", - "value" : "Spell Check" + "value" : "Yeretsani" } }, "pa-IN" : { "stringUnit" : { "state" : "translated", - "value" : "ਹਜੇਸ਼ਉ ਚੈੱਕ" + "value" : "ਬਨਾਓ" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Sprawdzanie pisowni" + "value" : "Utwórz" } }, "ps" : { "stringUnit" : { "state" : "translated", - "value" : "املایي چک" + "value" : "د نوي اړیکې سره خبرې اترې پیل کړئ" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Corretor ortográfico" + "value" : "Criar" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Verificação ortográfica" + "value" : "Criar" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Verificare ortografie" + "value" : "Creează" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Проверка орфографии" + "value" : "Создать" } }, "sh" : { "stringUnit" : { "state" : "translated", - "value" : "Provjeravanje pravopisa" + "value" : "Kreiraj" } }, "si-LK" : { "stringUnit" : { "state" : "translated", - "value" : "අක්ෂර වින්‍යාස පරීක්ෂාව" + "value" : "සාදන්න" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Kontrola pravopisu" + "value" : "Vytvoriť" } }, "sl" : { "stringUnit" : { "state" : "translated", - "value" : "Preverjanje črkovanja" + "value" : "Ustvari" } }, "sq" : { "stringUnit" : { "state" : "translated", - "value" : "Kontrolli i drejtshkrimit" + "value" : "Krijo" } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : "Провера правописа" + "value" : "Креирај" } }, "sr-Latn" : { "stringUnit" : { "state" : "translated", - "value" : "Provera pravopisa" + "value" : "Kreiraj" } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Kontrollera stavning" + "value" : "Skapa" } }, "sw" : { "stringUnit" : { "state" : "translated", - "value" : "Ukaguzi wa Tahajia" + "value" : "Unda" } }, "ta" : { "stringUnit" : { "state" : "translated", - "value" : "சொல் சரிபார்ப்பு" + "value" : "உருவாக்கு" } }, "te" : { "stringUnit" : { "state" : "translated", - "value" : "పర్యాశలించడానికి చెక్ చేయి" + "value" : "సృష్టించు" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "การตรวจสอบการสะกด" + "value" : "Create" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Yazım Denetimi" + "value" : "Oluştur" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Перевірка орфографії" + "value" : "Створити" } }, "ur-IN" : { "stringUnit" : { "state" : "translated", - "value" : "ہجے چیک" + "value" : "بنائیں" } }, "uz" : { "stringUnit" : { "state" : "translated", - "value" : "Imlo tekshiruvi" + "value" : "Yaratish" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Kiểm tra chính tả" + "value" : "Tạo" } }, "xh" : { "stringUnit" : { "state" : "translated", - "value" : "Spell Check" + "value" : "Yenza" } }, "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "拼写检查" + "value" : "创建" } }, "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "拼寫檢查" + "value" : "建立" } } } }, - "conversationsSpellCheckDescription" : { + "creatingCall" : { "extractionState" : "manual", "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aktiveer speltoetsing wanneer boodskappe getik word." - } - }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "تفعيل التحقق الإملائي عند كتابة الرسائل." + "value" : "إنشاء مكالمة" } }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "Mesaj yazarkən orfoqrafik yoxlanışı fəallaşdır." - } - }, - "bal" : { - "stringUnit" : { - "state" : "translated", - "value" : "پیغامات ٹائپ کرتے وقت ہجے کی تصدیق کو فعال کریں." - } - }, - "be" : { - "stringUnit" : { - "state" : "translated", - "value" : "Уключыць праверку правапісу пры наборы паведамленняў." - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Активиране на автокорекция при писане на съобщения." - } - }, - "bn" : { - "stringUnit" : { - "state" : "translated", - "value" : "বার্তা টাইপ করার সময় বানান পরীক্ষা সক্রিয় করুন।" + "value" : "Zəng yaradılır" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Activa la revisió ortogràfica quan escrius missatges." + "value" : "Creant Trucada" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Povolit kontrolu pravopisu při psaní zpráv." - } - }, - "cy" : { - "stringUnit" : { - "state" : "translated", - "value" : "Galluogi gwirio sillafu wrth deipio negeseuon." + "value" : "Vytváření hovoru" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Aktiver stavekontrol, når du skriver beskeder." + "value" : "Kalder op" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Rechtschreibprüfung bei der Eingabe von Nachrichten aktivieren." - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ενεργοποίηση ορθογραφικού ελέγχου κατά την πληκτρολόγηση μηνυμάτων." + "value" : "Anruf wird erstellt" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Enable spell check when typing messages." + "value" : "Creating Call" } }, "eo" : { "stringUnit" : { "state" : "translated", - "value" : "Ebligi literumkontrolon dum skribadi mesaĝojn." + "value" : "Kreante vokon" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Activar el corrector ortográfico." + "value" : "Creando llamada" } }, "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Activar corrección ortográfica al escribir mensajes." - } - }, - "et" : { - "stringUnit" : { - "state" : "translated", - "value" : "Luba õigekirjakontrolli, kui kirjutate sõnumeid." - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mezuak idaztean ortografia-egiaztatzea gaitu." - } - }, - "fa" : { - "stringUnit" : { - "state" : "translated", - "value" : "چک کردن املای کلمات را در هنگام تایپ کردن فعال کنید." - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Käytä oikolukua kirjoitettaessa viestejä." - } - }, - "fil" : { - "stringUnit" : { - "state" : "translated", - "value" : "I-enable ang pagsuri sa spell kapag nagta-type ng mga mensahe." + "value" : "Creando llamada" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Activer le correcteur d'orthographe pour la saisie des messages." - } - }, - "ha" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kunna dubawa na rubutu lokacin shigar da saƙonni." - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "לאפשר בדיקת איות בעת הקלדת הודעות." + "value" : "Création de l'appel" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "संदेश टाइप करते समय वर्तनी जांच सक्षम करें।" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Uključi provjeru pravopisa prilikom tipkanja poruka." + "value" : "कॉल बनाया जा रहा है" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "A helyesírás-ellenőrzés engedélyezése üzenetek írásakor." - } - }, - "hy-AM" : { - "stringUnit" : { - "state" : "translated", - "value" : "Միացնել ուղղագրության ստուգումը հաղորդագրություններ մուտքագրելիս:" + "value" : "Hívás készítése" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Aktifkan pemeriksaan ejaan saat mengetik pesan." + "value" : "Membuat Panggilan" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Abilita i suggerimenti da tastiera." + "value" : "Creazione chiamata" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "メッセージを入力するときにスペルチェックを有効にします" + "value" : "通話を作成中" } }, "ka" : { "stringUnit" : { "state" : "translated", - "value" : "შეტყობინებების აკრეფისას ჩართე მართლწერის შემოწმება." - } - }, - "km" : { - "stringUnit" : { - "state" : "translated", - "value" : "បើកការពិនិត្យអក្ខរាវិរុទ្ធនៅពេលវាយសារ។" - } - }, - "kn" : { - "stringUnit" : { - "state" : "translated", - "value" : "ಸಂದೇಶಗಳನ್ನು ಟೈಪ್ ಮಾಡುವಾಗ ಸ್ಪೆಲ್ ಚೆಕ್ ಅನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಿ." + "value" : "ირეკება" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "메시지를 입력할 때 맞춤법 검사를 활성화합니다." - } - }, - "ku" : { - "stringUnit" : { - "state" : "translated", - "value" : "چالاککردنی ڕاستەکەوت لە کاتی نوسینی پەیامەکان." - } - }, - "ku-TR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gava peyamên nivîsînê kontrola rastnivîsê bikar bînin." - } - }, - "lg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tandika spell check bw'ogwandiika obubaka." - } - }, - "lt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Įjungti rašybos tikrinimą, rašant žinutes." - } - }, - "lv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Iespējot pareizrakstības pārbaudi, rakstot ziņojumus." - } - }, - "mk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Овозможи проверка на правопис додека пишувате пораки." - } - }, - "mn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Мессеж бичих үед гарын үсгийн алдааг шалгахыг идэвхжүүлэх." - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aktifkan pemeriksaan ejaan semasa menaip mesej." - } - }, - "my" : { - "stringUnit" : { - "state" : "translated", - "value" : "မက်ဆေ့ချ်ရိုက်နေစဉ် အမှားများစစ်ဆေးပါ။" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aktiver stavekontroll når du skriver meldinger." - } - }, - "nb-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aktiver stavekontroll ved skriving av meldinger." - } - }, - "ne-NP" : { - "stringUnit" : { - "state" : "translated", - "value" : "सन्देश टाइप गर्दै Spell चेक सक्षम गर्नुहोस्।" + "value" : "통화 생성 중" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Spellingscontrole inschakelen tijdens het typen van berichten." - } - }, - "nn-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Skru på stavekontroll når du skriv meldingar." - } - }, - "ny" : { - "stringUnit" : { - "state" : "translated", - "value" : "Yambitsa kuwunika kolakwitsa mukamalemba mauthenga." - } - }, - "pa-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "ਸੰਦਰਸ਼ਣ ਹਿਜੇ ਲਿਖਣ ਦੇ ਸਮੇਂ ਸੁਧਾਰ।" + "value" : "Oproep starten" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Włącz sprawdzanie pisowni podczas pisania wiadomości." + "value" : "Tworzenie połączenia" } }, - "ps" : { + "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "د پیغامونو ټایپولو پر مهال د املا چک فعال کړئ." + "value" : "A criar chamada" } }, - "pt-BR" : { + "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Habilitar verificação ortográfica ao digitar mensagens." + "value" : "Se creează apelul" } }, - "pt-PT" : { + "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Permitir corretor ortográfico ao escrever mensagens." + "value" : "Создание вызова" } }, - "ro" : { + "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Activează verificarea ortografică pentru scrierea mesajelor." + "value" : "Skapar samtalet" } }, - "ru" : { + "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Включить проверку орфографии при наборе сообщений." + "value" : "Arama Oluşturuluyor" } }, - "sh" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Omogući provjeru pravopisa prilikom tipkanja poruka." + "value" : "Викликаємо" } }, - "si-LK" : { + "vi" : { "stringUnit" : { "state" : "translated", - "value" : "පණිවිඩ ලිවීමේදී අක්ෂර වින්‍යාස පරීක්ෂාව සබල කරන්න." + "value" : "Đang tạo cuộc gọi" } }, - "sk" : { + "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "Povoliť kontrolu pravopisu pri písaní správ." + "value" : "正在创建通话" } }, - "sl" : { + "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "Omogočite preverjanje črkovanja med tipkanjem sporočil." + "value" : "正在建立通話" } - }, - "sq" : { + } + } + }, + "currentPassword" : { + "extractionState" : "manual", + "localizations" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "Aktivizo drejtshkrimin kur shkruan mesazhe." + "value" : "Hazırkı parol" } }, - "sr" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Омогући проверу правописа приликом куцања порука." + "value" : "Aktuální heslo" } }, - "sr-Latn" : { + "de" : { "stringUnit" : { "state" : "translated", - "value" : "Omogućava proveru pravopisa prilikom kucanja poruka." + "value" : "Aktuelles Passwort" } }, - "sv-SE" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Aktivera stavningskontroll när du skriver meddelanden." + "value" : "Current Password" } }, - "sw" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Wezesha uangalizi wa tahajia unapopatajumbe." + "value" : "Mot de passe actuel" } }, - "ta" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "செய்திகளை தட்டச்சு செய்யும்பொழுது, எழுத்துப் பரிசோதனையை இயக்கவும்." + "value" : "Huidig wachtwoord" } }, - "te" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "సందేశాలు టైప్ చేయడం ప్రారంభించినప్పుడు స్పెల్ చెక్ ప్రారంభించండి." + "value" : "Obecne hasło" } }, - "th" : { + "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "เปิดใช้การตรวจสอบการสะกดเมื่อพิมพ์ข้อความ" + "value" : "Nuvarande lösenord" } }, - "tr" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "İleti yazarken yazım denetimini etkinleştirin." + "value" : "Поточний пароль" } - }, - "uk" : { + } + } + }, + "currentPlan" : { + "extractionState" : "manual", + "localizations" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "Увімкнути перевірку орфографії під час введення повідомлень." + "value" : "Hazırkı plan" } }, - "ur-IN" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "پیغامات لکھتے وقت ہجے کی جانچ فعال کریں." + "value" : "Současný tarif" } }, - "uz" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Xabarlarni yozayotganda imlo tekshiruvini yoqish." + "value" : "Current Plan" } }, - "vi" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Bật kiểm tra chính tả khi nhập tin nhắn." + "value" : "Forfait actuel" } }, - "xh" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Vumela ukupela ngokuzenzekelayo xa ubhala imilayezo." + "value" : "Huidig abonnement" } }, - "zh-CN" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "在输入消息时启用拼写检查。" + "value" : "Obecny plan" } }, - "zh-TW" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "輸入訊息時進行拼寫檢查。" + "value" : "Поточна передплата" } } } }, - "conversationsStart" : { + "cut" : { "extractionState" : "manual", "localizations" : { "af" : { "stringUnit" : { "state" : "translated", - "value" : "Begin Gesprek" + "value" : "Sny" } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "ابدأ محادثة" + "value" : "قص" } }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "Danışıq başlat" + "value" : "Kəs" } }, "bal" : { "stringUnit" : { "state" : "translated", - "value" : "گپتاری شروع کـــــــن" + "value" : "کٹ" } }, "be" : { "stringUnit" : { "state" : "translated", - "value" : "Пачаць гутарку" + "value" : "Выразаць" } }, "bg" : { "stringUnit" : { "state" : "translated", - "value" : "Започнете разговор" + "value" : "Изрязване" } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "কথোপকথন শুরু করুন" + "value" : "কাটুন" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Comença una conversa" + "value" : "Retalla" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Zahájit konverzaci" + "value" : "Vyjmout" } }, "cy" : { "stringUnit" : { "state" : "translated", - "value" : "Dechrau Sgwrs" + "value" : "Torri" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Start samtale" + "value" : "Klip" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Unterhaltung beginnen" + "value" : "Ausschneiden" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Έναρξη Συνομιλίας" + "value" : "Αποκοπή" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Start Conversation" + "value" : "Cut" } }, "eo" : { "stringUnit" : { "state" : "translated", - "value" : "Komenci Babilon" + "value" : "Eltondi" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Comenzar Conversación" + "value" : "Cortar" } }, "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Iniciar conversación" + "value" : "Cortar" } }, "et" : { "stringUnit" : { "state" : "translated", - "value" : "Alusta vestlust" + "value" : "Lõika" } }, "eu" : { "stringUnit" : { "state" : "translated", - "value" : "Has zaitez Elkarrizketa" + "value" : "Ebaki" } }, "fa" : { "stringUnit" : { "state" : "translated", - "value" : "شروع گفتگو" + "value" : "برش" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Aloita keskustelu" + "value" : "Leikkaa" } }, "fil" : { "stringUnit" : { "state" : "translated", - "value" : "Simulan ang Pag-uusap" + "value" : "I-cut" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Démarrer une conversation" + "value" : "Couper" } }, "gl" : { "stringUnit" : { "state" : "translated", - "value" : "Iniciar Conversa" + "value" : "Cortar" } }, "ha" : { "stringUnit" : { "state" : "translated", - "value" : "Fara Tattaunawa" + "value" : "Yanke" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "התחל שיחה" + "value" : "גזור" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "वार्तालाप शुरू करें" + "value" : "कट" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Započni razgovor" + "value" : "Izreži" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Beszélgetés indítása" + "value" : "Kivágás" } }, "hy-AM" : { "stringUnit" : { "state" : "translated", - "value" : "Սկսել զրույցը" + "value" : "Կտրել" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Mulai Percakapan" + "value" : "Potong" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Inizia chat" + "value" : "Taglia" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "会話を開始する" + "value" : "切り取り" } }, "ka" : { "stringUnit" : { "state" : "translated", - "value" : "საუბრის დაწყება" + "value" : "ამოჭრა" } }, "km" : { "stringUnit" : { "state" : "translated", - "value" : "ចាប់ផ្តើមសន្ទនា" + "value" : "កាត់" } }, "kn" : { "stringUnit" : { "state" : "translated", - "value" : "ಸಂಭಾಷಣೆಯನ್ನು ಪ್ರಾರಂಭಿಸಿ" + "value" : "ಕತ್ತರಿಸಿ" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "대화 시작하기" + "value" : "잘라내기" } }, "ku" : { "stringUnit" : { "state" : "translated", - "value" : "دەستپێکردنی گفتوگۆ" + "value" : "قطع کردن" } }, "ku-TR" : { "stringUnit" : { "state" : "translated", - "value" : "Sohbet Begin" + "value" : "Biqusîne" } }, "lg" : { "stringUnit" : { "state" : "translated", - "value" : "Tandika Okwogera" + "value" : "temula" + } + }, + "lo" : { + "stringUnit" : { + "state" : "translated", + "value" : "ຕັດ" } }, "lt" : { "stringUnit" : { "state" : "translated", - "value" : "Pradėti naują pokalbį" + "value" : "Iškirpti" } }, "lv" : { "stringUnit" : { "state" : "translated", - "value" : "Sākt Sarunu" + "value" : "Izgriezt" } }, "mk" : { "stringUnit" : { "state" : "translated", - "value" : "Започни Конверзација" + "value" : "Сечи" } }, "mn" : { "stringUnit" : { "state" : "translated", - "value" : "Яриа эхлүүлэх" + "value" : "Таслах" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Mulakan Perbualan" + "value" : "Potong" } }, "my" : { "stringUnit" : { "state" : "translated", - "value" : "စကားပြောစတင်" + "value" : "ချည်းကုတ်" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Start samtale" + "value" : "Klipp ut" } }, "nb-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Start samtale" + "value" : "Klipp ut" } }, "ne-NP" : { "stringUnit" : { "state" : "translated", - "value" : "कुराकानी सुरु गर्नुहोस्" + "value" : "काट्नुहोस्" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Gesprek starten" + "value" : "Knippen" } }, "nn-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Start samtale" + "value" : "Klipp ut" } }, "ny" : { "stringUnit" : { "state" : "translated", - "value" : "Start Conversation" + "value" : "Dula" } }, "pa-IN" : { "stringUnit" : { "state" : "translated", - "value" : "ਗੱਲਬਾਤ ਸ਼ੁਰੂ ਕਰੋ" + "value" : "ਕੱਟੋ" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Rozpocznij rozmowę" + "value" : "Wytnij" } }, "ps" : { "stringUnit" : { "state" : "translated", - "value" : "خبرو اترو پیل کړئ" + "value" : "معلومات له منځه ندي تللي" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Iniciar Conversa" + "value" : "Cortar" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Iniciar Conversa" + "value" : "Cortar" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Începe o conversație" + "value" : "Decupează" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Начать беседу" + "value" : "Вырезать" } }, "sh" : { "stringUnit" : { "state" : "translated", - "value" : "Pokreni razgovor" + "value" : "Izreži" } }, "si-LK" : { "stringUnit" : { "state" : "translated", - "value" : "සම්භාෂණය ආරම්භ කරන්න" + "value" : "කපන්න" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Začať príhovor" + "value" : "Vystrihnúť" } }, "sl" : { "stringUnit" : { "state" : "translated", - "value" : "Začni pogovor" + "value" : "Odreži" } }, "sq" : { "stringUnit" : { "state" : "translated", - "value" : "Filloni Bisedën" + "value" : "Prije" } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : "Започни разговор" + "value" : "Изрежи" } }, "sr-Latn" : { "stringUnit" : { "state" : "translated", - "value" : "Započni razgovor" + "value" : "Iseci" } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Starta konversation" + "value" : "Klipp ut" } }, "sw" : { "stringUnit" : { "state" : "translated", - "value" : "Anza Mazungumzo" + "value" : "Kata" } }, "ta" : { "stringUnit" : { "state" : "translated", - "value" : "உரையாடலைத் தொடங்கு" + "value" : "பிரி" } }, "te" : { "stringUnit" : { "state" : "translated", - "value" : "సంభాషణ ప్రారంభించండి" + "value" : "కట్ చేయడం" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "เริ่มการสนทนา" + "value" : "ตัด" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Sohbet Başlat" + "value" : "Kes" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Почати розмову" + "value" : "Вирізати" } }, "ur-IN" : { "stringUnit" : { "state" : "translated", - "value" : "گفتگو شروع کریں" + "value" : "کاٹیں" } }, "uz" : { "stringUnit" : { "state" : "translated", - "value" : "Suhbatni boshlash" + "value" : "Kesish" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Bắt đầu cuộc trò chuyện" + "value" : "Cắt" } }, "xh" : { "stringUnit" : { "state" : "translated", - "value" : "Start Conversation" + "value" : "Sika" } }, "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "开始会话" + "value" : "剪切" } }, "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "開始會話" + "value" : "剪下" } } } }, - "copied" : { + "darkMode" : { "extractionState" : "manual", "localizations" : { - "af" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "Gekopieër" + "value" : "Qaranlıq rejim" } }, - "ar" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "تم النسخ" + "value" : "Tmavý režim" } }, - "az" : { + "de" : { "stringUnit" : { "state" : "translated", - "value" : "Kopyalandı" + "value" : "Dunkelmodus" } }, - "bal" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "نکل" + "value" : "Dark Mode" } }, - "be" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Скапіравана" + "value" : "Mode sombre" } }, - "bg" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Копирано" + "value" : "Donkere modus" } }, - "bn" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "কপি করা হয়েছে" + "value" : "Tryb ciemny" } }, - "ca" : { + "ro" : { "stringUnit" : { "state" : "translated", - "value" : "S'ha copiat" + "value" : "Mod întunecat" } }, - "cs" : { + "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Zkopírováno" + "value" : "Тёмный режим" } }, - "cy" : { + "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Copïwyd" + "value" : "Mörkt läge" } }, - "da" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Kopieret" + "value" : "Темний режим" + } + } + } + }, + "databaseErrorClearDataWarning" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu cihazdan bütün mesajları, qoşmaları və hesab məlumatlarını silmək və yeni hesab yaratmaq istədiyinizə əminsinizmi?" } }, - "de" : { + "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Kopiert" + "value" : "Estàs segur que vols suprimir tots els missatges, fitxers adjunts i dades del compte d'aquest dispositiu i crear un compte nou?" } }, - "el" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Αντιγράφηκε" + "value" : "Opravdu chcete smazat všechny zprávy, přílohy a data účtu z tohoto zařízení a vytvořit nový účet?" } }, - "en" : { + "da" : { "stringUnit" : { "state" : "translated", - "value" : "Copied" + "value" : "Er du sikker på, at du vil slette alle beskeder, vedhæftede filer og kontodata fra denne enhed og oprette en ny konto?" } }, - "eo" : { + "de" : { "stringUnit" : { "state" : "translated", - "value" : "Kopiite" + "value" : "Möchtest du wirklich alle Nachrichten, Anhänge und Kontodaten von diesem Gerät löschen und ein neues Konto erstellen?" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Are you sure you want to delete all messages, attachments, and account data from this device and create a new account?" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Copiado" + "value" : "¿Estás seguro de que quieres eliminar todos los mensajes, archivos adjuntos y datos de la cuenta de este dispositivo y crear una cuenta nueva?" } }, "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Copiado" + "value" : "¿Estás seguro de que quieres eliminar todos los mensajes, archivos adjuntos y datos de la cuenta de este dispositivo y crear una cuenta nueva?" } }, - "et" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Kopeeritud" + "value" : "Êtes-vous sûr de vouloir supprimer tous les messages, pièces jointes et données de compte de cet appareil et créer un nouveau compte ?" } }, - "eu" : { + "hi" : { "stringUnit" : { "state" : "translated", - "value" : "Kopiatu da" + "value" : "क्या आप वाकई इस डिवाइस से सभी संदेश, अटैचमेंट और खाता डेटा हटाना चाहते हैं और एक नया खाता बनाना चाहते हैं?" } }, - "fa" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "کپی‌شده" + "value" : "Biztosan törli az összes üzenetet, mellékletet és fiókadatot erről az eszközről, és új fiókot hoz létre?" } }, - "fi" : { + "it" : { "stringUnit" : { "state" : "translated", - "value" : "Kopioitu" + "value" : "Sei sicuro di voler eliminare tutti i messaggi, gli allegati e i dati dell'account da questo dispositivo e creare un nuovo account?" } }, - "fil" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Nakopya na" + "value" : "このデバイス上のすべてのメッセージ、添付ファイル、アカウントデータを削除し、新しいアカウントを作成してもよろしいですか?" } }, - "fr" : { + "ko" : { "stringUnit" : { "state" : "translated", - "value" : "Copié" + "value" : "정말로 이 기기에서 모든 메시지, 첨부 파일, 계정 데이터를 삭제하고 새 계정을 생성하시겠습니까?" } }, - "gl" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Copiouse" + "value" : "Weet u zeker dat u alle berichten, bijlagen en accountgegevens van dit apparaat wilt verwijderen en een nieuw account wilt aanmaken?" } }, - "ha" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Kwafi" + "value" : "Czy na pewno chcesz usunąć wszystkie wiadomości, załączniki i dane konta z tego urządzenia i utworzyć nowe konto?" } }, - "he" : { + "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "הועתק" + "value" : "Tem a certeza de que pretende apagar todas as mensagens, anexos e dados da conta deste dispositivo e criar uma nova conta?" } }, - "hi" : { + "ro" : { "stringUnit" : { "state" : "translated", - "value" : "कॉपी किया गया!" + "value" : "Ești sigur/ă că dorești să ștergi toate mesajele, atașamentele și datele contului de pe acest dispozitiv și să creezi un cont nou?" } }, - "hr" : { + "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Kopirano" + "value" : "Вы уверены, что хотите удалить все сообщения, вложения и данные учетной записи с этого устройства и создать новую учетную запись?" } }, - "hu" : { + "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Másolva" + "value" : "Är du säker på att du vill radera alla meddelanden, bilagor och kontodata från denna enhet och skapa ett nytt konto?" } }, - "hy-AM" : { + "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Պատճենվել է" + "value" : "Tüm mesajları, ekleri ve hesap verilerini bu cihazdan silip yeni bir hesap oluşturmak istediğinizden emin misiniz?" } }, - "id" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Disalin" + "value" : "Ви впевнені, що хочете видалити всі повідомлення, вкладення та дані облікового запису з цього пристрою та створити новий обліковий запис?" } }, - "it" : { + "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "Copiato" + "value" : "您确定要删除此设备上的所有消息、附件和帐户数据,并创建新帐户吗?" } }, - "ja" : { + "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "コピーしました" + "value" : "您確定要從此裝置中刪除所有訊息、附件及帳號資料,並建立一個新帳號嗎?" + } + } + } + }, + "databaseErrorGeneric" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verilənlər bazası xətası baş verdi.

Problemləri həll etmək üçün paylaşmaq üçün proqram qeydlərinizi ixrac edin. Bu uğursuz olarsa, {app_name} proqramını yenidən quraşdırın və hesabınızı bərpa edin." } }, - "ka" : { + "ca" : { "stringUnit" : { "state" : "translated", - "value" : "დაკოპირებულია" + "value" : "S'ha produït un error de base de dades.

Exporta els registres de l'aplicació per compartir i resoldre problemes. Si això no té èxit, reinstal·la {app_name} i restaura el teu compte." } }, - "km" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "បានចម្លង" + "value" : "Došlo k chybě databáze.

Exportujte své aplikační logy a sdílejte je pro účely diagnostiky. Pokud to nebude úspěšné, přeinstalujte {app_name} a obnovte svůj účet." } }, - "kn" : { + "da" : { "stringUnit" : { "state" : "translated", - "value" : "ನಕಲು ಮಾಡಿದೆ" + "value" : "Der opstod en databasefejl.

Eksporter dine applikationslogs til deling for fejlfinding. Hvis dette ikke lykkes, geninstaller {app_name} og gendan din konto." } }, - "ko" : { + "de" : { "stringUnit" : { "state" : "translated", - "value" : "복사됨" + "value" : "Ein Datenbankfehler ist aufgetreten.

Exportiere deine App-Logs, um diese für eine Fehleranalyse zu teilen. Wenn dies nicht erfolgreich ist, installiere die {app_name} neu und stelle deinen Account wieder her." } }, - "ku" : { + "el" : { "stringUnit" : { "state" : "translated", - "value" : "لەبەرگرتن" + "value" : "Παρουσιάστηκε σφάλμα βάσης δεδομένων.

Εξαγάγετε τα αρχεία καταγραφής της εφαρμογής σας για κοινή χρήση στην αντιμετώπιση προβλημάτων. Αν αυτό δεν είναι επιτυχές, επανεγκαταστήστε το {app_name} και επαναφέρετε τον λογαριασμό σας." } }, - "ku-TR" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Kopîkirî ye" + "value" : "A database error occurred.

Export your application logs to share for troubleshooting. If this is unsuccessful, reinstall {app_name} and restore your account." } }, - "lg" : { + "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Koppa" + "value" : "Ocurrió un error en la base de datos.

Exporta tus registros de aplicación para compartirlos con fines de resolución de problemas. Si esto no funciona, reinstala {app_name} y restaura tu cuenta." } }, - "lo" : { + "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "ຄັດລອກເເລ້ວ" + "value" : "Ocurrió un error en la base de datos.

Exporta tus registros de aplicación para compartirlos con fines de resolución de problemas. Si esto no funciona, reinstala {app_name} y restaura tu cuenta." } }, - "lt" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Nukopijuota" + "value" : "Une erreur de base de données s'est produite.

Exportez les journaux de votre application pour les partager à des fins de dépannage. Si cela échoue, réinstallez {app_name} et restaurez votre compte." } }, - "lv" : { + "hi" : { "stringUnit" : { "state" : "translated", - "value" : "Nokopēts" + "value" : "डेटाबेस त्रुटि हुई है।

समस्या निवारण के लिए अपने एप्लिकेशन लॉग्स को शेयर करने के लिए निर्यात करें। यदि यह असफल रहता है, तो {app_name} को फिर से इंस्टॉल करें और अपना खाता पुनः प्राप्त करें।" } }, - "mk" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Копирано" + "value" : "Adatbázishiba történt.

Exportálja az alkalmazás naplóit, hogy megoszhassa azokat a hibaelhárításhoz. Ha ez nem sikerül, telepítse újra a(z) {app_name} alkalmazást és állítsa vissza a fiókját." } }, - "mn" : { + "it" : { "stringUnit" : { "state" : "translated", - "value" : "Хуулсан" + "value" : "Si è verificato un errore nel database.

Esporta i log dell'applicazione per condividerli e facilitare la risoluzione del problema. Se non funziona, reinstalla {app_name} e ripristina il tuo account." } }, - "ms" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Disalin" + "value" : "データベースエラーが発生しました。

トラブルシューティングのために、アプリのログをエクスポートして共有してください。この操作が失敗した場合は、{app_name} を再インストールし、アカウントを復元してください。" } }, - "my" : { + "ko" : { "stringUnit" : { "state" : "translated", - "value" : "ကို ကူးယူပြီးပါပြီ" + "value" : "데이터베이스 오류가 발생했습니다.

문제 해결을 위해 애플리케이션 로그를 내보내서 공유하십시오. 실패할 경우, {app_name}을 다시 설치하고 계정을 복원하십시오." } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Kopiert" + "value" : "En databasefeil har oppstått.

Eksporter dine applikasjon logger for å dele feilsøkingen. Hvis dette ikke vellykkes, installer {app_name} på nytt og gjenopprett kontoen din." } }, - "nb-NO" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Kopiert" + "value" : "Er is een databasefout opgetreden.

Exporteer uw applicatie logs om te delen voor probleemoplossing. Als dit niet lukt, installeer {app_name} opnieuw en herstel uw account." } }, - "ne-NP" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "प्रतिलिपि बनाइएको" + "value" : "Wystąpił błąd bazy danych.

Wyeksportuj dzienniki aplikacji do udostępnienia w celu rozwiązania problemu. Jeśli to się nie powiedzie, zainstaluj ponownie {app_name} i przywróć swoje konto." } }, - "nl" : { + "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Gekopieerd" + "value" : "Ocorreu um erro na base de dados.

Exporte os registos da sua aplicação para partilhar para apoio à resolução de problemas. Se isto não resultar, reinstale o {app_name} e recupere a sua conta." } }, - "nn-NO" : { + "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Kopiert" + "value" : "A apărut o eroare în baza de date.

Exportați jurnalele aplicației pentru a le partaja în vederea depanării. Dacă nu reușiți, reinstalați {app_name} și restaurați-vă contul." } }, - "ny" : { + "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Chotengera" + "value" : "Произошла ошибка базы данных.

Экспортируйте журналы приложения для использования в целях устранения неполадок. Если это не поможет, переустановите {app_name} и восстановите учётную запись." } }, - "pa-IN" : { + "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "ਨਕਲ ਕੀਤੀ" + "value" : "Ett databasfel har inträffat.

Exportera dina applikationsloggar för att dela dem för felsökning. Om detta misslyckas, installera om {app_name} och återställ ditt konto." } }, - "pl" : { + "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Skopiowano" + "value" : "Veritabanında bir sorun oluştu.

Sorun giderme için uygulama günlüklerinizi dışa aktarın. Eğer başarısız olunursa {app_name} uygulamasını yeniden yükleyip, hesabınızı geri yükleyin." } }, - "ps" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "کاپي" + "value" : "Сталася помилка бази даних.

Експортуйте журнали програми, щоб надати їх для усунення несправностей. Якщо це не допоможе, перевстановіть {app_name} та відновіть свій обліковий запис." } }, - "pt-BR" : { + "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "Copiado" + "value" : "发生数据库错误。

请导出您的应用日志以进行故障排除。如果不成功,请重新安装{app_name}并恢复您的帐户。" } }, - "pt-PT" : { + "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "Copiado" + "value" : "發生資料庫錯誤。

請匯出您的應用程式日誌以便分享並協助故障排除。如果仍無法解決,請重新安裝 {app_name} 並還原您的帳號。" + } + } + } + }, + "databaseErrorRestoreDataWarning" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu cihazdan bütün mesajları, qoşmaları və hesab məlumatlarını silmək və hesabınızı şəbəkədən bərpa etmək istədiyinizə əminsinizmi?" } }, - "ro" : { + "ca" : { "stringUnit" : { "state" : "translated", - "value" : "S-a copiat" + "value" : "Estàs segur que vols suprimir tots els missatges, fitxers adjunts i dades del compte d'aquest dispositiu i restaurar el teu compte de la xarxa?" } }, - "ru" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Скопировано" + "value" : "Opravdu chcete smazat všechny zprávy, přílohy a data účtu z tohoto zařízení a obnovit svůj účet ze sítě?" } }, - "sh" : { + "da" : { "stringUnit" : { "state" : "translated", - "value" : "Kopirano" + "value" : "Er du sikker på, at du vil slette alle beskeder, vedhæftede filer og kontodata fra denne enhed og gendanne din konto fra netværket?" } }, - "si-LK" : { + "de" : { "stringUnit" : { "state" : "translated", - "value" : "පිටපත් විය" + "value" : "Möchtest du wirklich alle Nachrichten, Anhänge und Kontodaten von diesem Gerät löschen und dein Konto aus dem Netzwerk wiederherstellen?" } }, - "sk" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Skopírované" + "value" : "Are you sure you want to delete all messages, attachments, and account data from this device and restore your account from the network?" } }, - "sl" : { + "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Kopirano" + "value" : "¿Estás seguro de que quieres eliminar todos los mensajes, archivos adjuntos y datos de la cuenta de este dispositivo y restaurar tu cuenta desde la red?" } }, - "sq" : { + "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "U kopjua" + "value" : "¿Estás seguro de que quieres eliminar todos los mensajes, archivos adjuntos y datos de la cuenta de este dispositivo y restaurar tu cuenta desde la red?" } }, - "sr" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Копирана" + "value" : "Êtes-vous sûr de vouloir supprimer tous les messages, pièces jointes et données de compte de cet appareil et restaurer votre compte depuis le réseau ?" } }, - "sr-Latn" : { + "hi" : { "stringUnit" : { "state" : "translated", - "value" : "Kopirano" + "value" : "क्या आप वाकई इस डिवाइस से सभी संदेश, अटैचमेंट और खाता डेटा हटाना चाहते हैं और नेटवर्क से अपना खाता पुनः प्राप्त करना चाहते हैं?" } }, - "sv-SE" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Kopierad" + "value" : "Biztosan törli az összes üzenetet, mellékletet és fiókadatot erről az eszközről, és vissza állítja a fiókját a hálózatról?" } }, - "sw" : { + "it" : { "stringUnit" : { "state" : "translated", - "value" : "Imenakiliwa" + "value" : "Sei sicuro di voler eliminare tutti i messaggi, gli allegati e i dati dell'account da questo dispositivo e ripristinare il tuo account dalla rete?" } }, - "ta" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "நகலெடுக்கப்பட்டது" + "value" : "このデバイス上のすべてのメッセージ、添付ファイル、アカウントデータを削除し、ネットワークからアカウントを復元してもよろしいですか?" } }, - "te" : { + "ko" : { "stringUnit" : { "state" : "translated", - "value" : "ప్రతి తీసుకోబడింది" + "value" : "정말로 이 기기에서 모든 메시지, 첨부 파일, 계정 데이터를 삭제하고 네트워크에서 계정을 복원하시겠습니까?" } }, - "th" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "คัดลอกแล้ว" + "value" : "Weet u zeker dat u alle berichten, bijlagen en accountgegevens van dit apparaat wilt verwijderen en uw account wilt herstellen vanuit het netwerk?" } }, - "tr" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Kopyalandı" + "value" : "Czy na pewno chcesz usunąć wszystkie wiadomości, załączniki i dane konta z tego urządzenia i przywrócić konto z sieci?" } }, - "uk" : { + "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Скопійовано" + "value" : "Tem a certeza de que pretende apagar todas as mensagens, anexos e dados da conta deste dispositivo e restaurar a sua conta a partir da rede?" } }, - "ur-IN" : { + "ro" : { "stringUnit" : { "state" : "translated", - "value" : "کاپی کیا گیا" + "value" : "Ești sigur că vrei să ștergi toate mesajele, atașamentele și datele contului de pe acest dispozitiv și să restaurezi contul tău din rețea?" } }, - "uz" : { + "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Nusxalandi" + "value" : "Вы уверены, что хотите удалить все сообщения, вложения и данные учетной записи с этого устройства и восстановить свою учетную запись из сети?" } }, - "vi" : { + "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Đã sao chép" + "value" : "Är du säker på att du vill ta bort alla meddelanden, bilagor och kontodata från den här enheten och återställa ditt konto från nätverket?" } }, - "xh" : { + "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Ikopiwe" + "value" : "Tüm mesajları, ekleri ve hesap verilerini bu cihazdan silip hesabınızı ağdan geri yüklemek istediğinizden emin misiniz?" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ви впевнені, що хочете видалити всі повідомлення, вкладення та дані облікового запису з цього пристрою та відновити свій обліковий запис із мережі?" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bạn có chắc chắn muốn xóa tất cả tin nhắn, tệp đính kèm, và dữ liệu tài khoản khỏi thiết bị này và khôi phục lại tài khoản của bạn từ mạng lưới?" } }, "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "已复制" + "value" : "您确定要删除此设备上的所有消息、附件和帐户数据,并从网络中恢复你的帐户吗?" } }, "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "已複製" + "value" : "您確定要從此裝置中刪除所有訊息、附件及帳號資料,並從網路中還原您的帳號嗎?" } } } }, - "copy" : { + "databaseErrorTimeout" : { "extractionState" : "manual", "localizations" : { "af" : { "stringUnit" : { "state" : "translated", - "value" : "Kopieer" + "value" : "Ons het opgemerk {app_name} neem lank om te begin.

Jy kan aanhou wag, jou toestel logs uitvoer om te deel vir foutsporing, of probeer om {app_name} te herbegin." } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "نسخ" + "value" : "لقد لاحظنا أن {app_name} يستغرق وقتًا طويلاً لبدء.

يمكنك مواصلة الانتظار، تصدير سجلات الجهاز للمشاركة في استكشاف الأخطاء وإصلاحها، أو محاولة إعادة تشغيل {app_name}." } }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "Kopyala" + "value" : "{app_name} tətbiqinin başladılmasının çox vaxt apardığına fikir verdik.

Gözləməyə davam edə, problemin aradan qaldırılması üçün cihazınızın jurnallarını xaricə köçürə və ya {app_name}-u yenidən başlatmağa çalışa bilərsiniz." } }, "bal" : { "stringUnit" : { "state" : "translated", - "value" : "کاپی" + "value" : "ما دیستگ کہ {app_name} ءِ بندات کنگ ءَ بازیں وھدے لگ اِیت۔

شما دیم ءَ اوشتات کن اِت، وتی ڈیوائس ءِ لاگاں پہ جیڑہ ءِ گیش ءُ گیوار ءَ شیئر کنگ ءِ ھاترا برآمد کن اِت یا {app_name} ءَ پدا بندات کنگ ءِ جُھد ءَ کن اِت۔" } }, "be" : { "stringUnit" : { "state" : "translated", - "value" : "Скапіяваць" + "value" : "Мы заўважылі, што {app_name} патрабуе шмат часу для запуску.

Вы можаце працягваць чакаць, экспартаваць журналы вашай прылады для спагнання праблем, альбо паспрабаваць перазапусціць {app_name}." } }, "bg" : { "stringUnit" : { "state" : "translated", - "value" : "Копиране" + "value" : "Забелязахме, че стартирането на {app_name} отнема много време.

Можете да продължите да чакате, да експортирате дневници на устройството си, за да ги споделите за отстраняване на неизправности, или да опитате да рестартирате {app_name}." } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "কপি করুন" + "value" : "We've noticed {app_name} is taking a long time to start.

You can continue to wait, export your device logs to share for troubleshooting, or try restarting {app_name}." } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Copiar" + "value" : "Hem notat que {app_name} està trigant molt a començar.

Podeu continuar esperant, exportar els registres del dispositiu per compartir-los per solucionar problemes, o intentar reiniciar {app_name}." } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Kopírovat" + "value" : "Všimli jsme si, že spuštění aplikace {app_name} trvá dlouho.

Můžete pokračovat v čekání, exportovat logy zařízení k řešení problémů nebo zkusit restartovat {app_name}." } }, "cy" : { "stringUnit" : { "state" : "translated", - "value" : "Copïo" + "value" : "Rydym wedi sylwi bod {app_name} yn cymryd llawer o amser i ddechrau.

Gallwch barhau i aros, allforio logiau eich dyfais i'w rhannu ar gyfer datrys problemau, neu geisio ailgychwyn {app_name}." } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Kopiér" + "value" : "Vi har bemærket, at {app_name} tager lang tid at starte.

Du kan fortsætte med at vente, eksportere dine enhedslogfiler for at dele dem til fejlfinding eller prøve at genstarte {app_name}." } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Kopieren" + "value" : "Wir haben bemerkt, dass {app_name} lange zum Starten braucht.

Du kannst weiter warten, deine Geräteprotokolle zur Fehlerbehebung exportieren oder versuchen, {app_name} neu zu starten." } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Αντιγραφή" + "value" : "Παρατηρήσαμε ότι το {app_name} χρειάζεται πολύ χρόνο για να ξεκινήσει.

Μπορείτε να συνεχίσετε να περιμένετε, να εξάγετε τα αρχεία καταγραφής της συσκευής σας για να τα μοιραστείτε για την αντιμετώπιση προβλημάτων ή να επανεκκινήσετε το {app_name}." } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Copy" + "value" : "We've noticed {app_name} is taking a long time to start.

You can continue to wait, export your device logs to share for troubleshooting, or try restarting {app_name}." } }, "eo" : { "stringUnit" : { "state" : "translated", - "value" : "Kopii" + "value" : "Ni rimarkis ke {app_name} bezonas longe por komenci.

Vi povas daŭrigi atendadon, eksporti viajn aparato-protokolojn por dividi por cimo-serĉado, aŭ reprovi relanĉi {app_name}." } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Copiar" + "value" : "Hemos notado que {app_name} está tardando mucho en arrancar.

Puedes esperar, exportar los registros de tu dispositivo para compartirlos para la resolución de problemas, o intentar reiniciar {app_name}." } }, "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Copiar" + "value" : "Hemos notado que {app_name} está tardando mucho en iniciar.

Puedes seguir esperando, exportar los registros de tu dispositivo para compartirlos y solucionar problemas, o intentar reiniciar {app_name}." } }, "et" : { "stringUnit" : { "state" : "translated", - "value" : "Kopeeri" + "value" : "Oleme märganud, et {app_name} käivitamine võtab kaua aega.

Võite jätkata ootamist, eksportida oma seadme logisid tõrkeotsingu eesmärgil jagamiseks või proovida {app_name}'i taaskäivitamist." } }, "eu" : { "stringUnit" : { "state" : "translated", - "value" : "Kopiatu" + "value" : "{app_name} martxan jartzeko denbora gehiegi hartzen ari dela nabaritu dugu.

Jarrai itzazu itxaroten, esportatu zure gailu-erregistroak konpontzeko partekatzeko edo saiatu {app_name} berrabiarazten." } }, "fa" : { "stringUnit" : { "state" : "translated", - "value" : "کپی" + "value" : "ما متوجه شده‌ایم که شروع {app_name} زمان زیادی می‌برد.

می‌توانید همچنان منتظر بمانید، گزارش‌های دستگاه خود را برای اشتراک‌گذاری برای عیب‌یابی صادر کنید، یا سعی کنید {app_name} را مجدداً راه‌اندازی کنید." } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Kopioi" + "value" : "Huomasimme, että {app_name} käynnistyy hitaasti.

Voit jatkaa odottamista, viedä laitteesi lokit jaettavaksi vianmäärityksessä tai yrittää käynnistää {app_name} uudelleen." } }, "fil" : { "stringUnit" : { "state" : "translated", - "value" : "Kopyahin" + "value" : "Napansin naming matagal bago mag-start ang {app_name}.

Puwede kang maghintay nalang, i-export ang iyong device logs para i-share para sa troubleshooting, o subukan i-restart ang {app_name}." } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Copier" + "value" : "Nous avons remarqué que {app_name} met beaucoup de temps à démarrer.

Vous pouvez continuer à attendre, exporter les journaux de votre appareil pour les partager pour le dépannage ou essayer de redémarrer {app_name}." } }, "gl" : { "stringUnit" : { "state" : "translated", - "value" : "Copiar" + "value" : "Notamos que {app_name} está a tardar moito en iniciar.

Podes esperar, exportar os teus rexistros do dispositivo para compartir e solucionar problemas, ou tentar reiniciar {app_name}." } }, "ha" : { "stringUnit" : { "state" : "translated", - "value" : "Kwafi" + "value" : "Mun lura cewa {app_name} yana ɗaukar dogon lokaci don farawa.

Za ku iya ci gaba da jira, fitar da log ɗin na'urarku don rabawa don magance matsaloli, ko sake farawa {app_name}." } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "העתק" + "value" : "שמנו לב ל-{app_name} לוקח הרבה זמן להתחיל.

תוכל להמשיך להמתין, לייצא את יומני המכשיר שלך לשיתוף לצורך פתרון בעיות, או לנסות להפעיל מחדש את {app_name}." } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "कॉपी करें" + "value" : "हमने देखा कि {app_name} प्रारंभ होने में बहुत समय ले रहा है।

आप प्रतीक्षा करना जारी रख सकते हैं, अपने डिवाइस लॉग को निर्यात कर सकते हैं ताकि समस्या निवारण के लिए साझा कर सकें, या {app_name} पुनरारंभ करने का प्रयास कर सकते हैं।" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Kopiraj" + "value" : "Primijetili smo da pokretanje {app_name} traje dugo.

Možete nastaviti čekati, izvesti zapise uređaja za dijeljenje radi rješavanja problema ili pokušati ponovo pokrenuti {app_name}." } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Másolás" + "value" : "Észrevettük, hogy {app_name} indítása sokáig tart.

Továbbra is várhatsz, exportálhatod az eszköz naplóit a hibaelhárításhoz, vagy megpróbálhatod újraindítani {app_name}-t." } }, "hy-AM" : { "stringUnit" : { "state" : "translated", - "value" : "Պատճենել" + "value" : "Մենք նկատել ենք, որ {app_name} շատ երկար է սկսում աշխատել։

Դուք կարող եք շարունակել սպասել, արտահանել ձեր սարքի տեղեկամատյանները կիսելու համար հետաքրքրությունների համար, կամ փորձել վերագործարկել {app_name}-ը։" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Salin" + "value" : "Kami menyadari {app_name} membutuhkan waktu lama untuk memulai.

Anda dapat terus menunggu, mengekspor log perangkat Anda untuk dibagikan dalam pemecahan masalah, atau mencoba memulai ulang {app_name}." } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Copia" + "value" : "Abbiamo notato che {app_name} ci impiega molto tempo ad avviarsi.

Puoi continuare ad attendere, esportare i log del dispositivo per la risoluzione dei problemi o provare a riavviare {app_name}." } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "コピーする" + "value" : "{app_name}が起動するのに時間がかかっていることを確認しました。

引き続きお待ちいただくか、トラブルシューティングのためにデバイスログをエクスポートして共有するか、{app_name}を再起動してみてください。" } }, "ka" : { "stringUnit" : { "state" : "translated", - "value" : "დაკოპირება" + "value" : "გავიგეთ {app_name}-ის გაშვება დიდ დროს იკავებს.

თქვენ შეგიძლიათ დაელოდოთ, ექსპორტირდეთ თქვენი მოწყობილობის ჟურნალები რათა გაუზიაროთ პრობლემების დიაგნოსტირებისთვის, ან სცადოთ {app_name}-ის გადატვირთვა." } }, "km" : { "stringUnit" : { "state" : "translated", - "value" : "ចម្លង" + "value" : "យើងគិតថា {app_name} ពុំអាចចាប់ផ្ដើមបានយ៉ាងចំហរមួយរយៈ

អ្នកអាចរង់ចាំតទៅ ហៅទិន្នន័យឧបករណ៍របស់អ្នកដើម្បីជួយដោះស្រាយ បើទោះអញ្ចឹងក៏ដោយ សាកល្បងចាប់ផ្ដើម {app_name}។" } }, "kn" : { "stringUnit" : { "state" : "translated", - "value" : "ನಕಲಿಸಿ" + "value" : "ನಾವು ಗಮನಿಸಿದ್ದೇವೆ {app_name} ಆರಂಭಿಸಲು ಹೆಚ್ಚು ಸಮಯ ತೆಗೆದುಕೊಳ್ಳುತ್ತಿದೆ.

ನೀವು ನಿರೀಕ್ಷಿಸಬಹುದು, ನಿಮ್ಮ ಸಾಧನ ರೆಕಾರ್ಡುಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಲು ದೋಷ ಪರಿಹಾರದೊಂದಿಗೆ ಎಕ್ಸ್‌ಪೋರ್ಟ್ ಮಾಡಬಹುದು ಅಥವಾ {app_name} ಪುನಃಪ್ರಾರಂಭಿಸಲು ಪ್ರಯತ್ನಿಸಬಹುದು." } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "복사" + "value" : "{app_name} 이 오랜 시간동안 응답하지 않은 것으로 보입니다.

계속 기다리거나, 기기의 로그를 내보내 도움을 요청하거나, {app_name} 을 재시작 해보세요." } }, "ku" : { "stringUnit" : { "state" : "translated", - "value" : "لەبەرگرتن" + "value" : "دەبینین {app_name} بوونی دواخستنی درێژە.

تۆ دەتوانیت بەردەوام ببهێنین، کۆگایەکانی ئامرازەکەت بەکەشێنی بۆ پشکنین، یان هەوڵبدە بە سەرەکی ={app_name}دوژخستنەوە." } }, "ku-TR" : { "stringUnit" : { "state" : "translated", - "value" : "Kopî bike" + "value" : "Em teswîr kirin {app_name} bimînte ye.

Hûn dikarin perê nîşan bibinine rewşa sernîşana hûn perê logoya ten do dike bibirûje, an berbijîhengê {app_name} dîsa baş.herşe cereyan bike." } }, "lg" : { "stringUnit" : { "state" : "translated", - "value" : "Koppa" - } - }, - "lo" : { - "stringUnit" : { - "state" : "translated", - "value" : "ເສັກກີ້າບ" + "value" : "Tukuboolabye {app_name} enetwala ebweru okuggwa.

Muliisa kulinda, kusitumidde ebirukanya ebyekusibiza okugabana oludde, oba kwemuddira {app_name}." } }, "lt" : { "stringUnit" : { "state" : "translated", - "value" : "Kopijuoti" + "value" : "Pastebėjome, kad {app_name} užtrunka ilgai paleisti.

Galite toliau laukti, eksportuoti savo įrenginio žurnalus, kad galėtumėte juos pasidalinti dėl trikčių šalinimo, arba bandykite iš naujo paleisti {app_name}." } }, "lv" : { "stringUnit" : { "state" : "translated", - "value" : "Kopēt" + "value" : "Mēs esam pamanījuši, ka {app_name} aizņem daudz laika, lai startētu.

Jūs varat turpināt gaidīt, eksportēt sava ierīces žurnālus, lai dalītos problēmas novēršanā, vai pamēģiniet restartēt {app_name}." } }, "mk" : { "stringUnit" : { "state" : "translated", - "value" : "Копирај" + "value" : "Забележавме дека {app_name} троши многу време за старт.

Можете да продолжите да чекате, да ги извезете дневниците на вашиот уред за решавање на проблеми или да се обидете да го рестартирате {app_name}." } }, "mn" : { "stringUnit" : { "state" : "translated", - "value" : "Хуулах" + "value" : "{app_name} эхлүүлэх их хугацаа зарцуулагдаж байна.

Хүлээсээр байх, төхөөрөмжийн тэмдэглэлийг экспортлоход хуваалцаж асуудал шийдэх, эсвэл {app_name}-г дахин эхлүүлэх боломжтой." } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Salin" + "value" : "Kami perasan {app_name} mengambil masa yang lama untuk bermula.

Anda boleh terus menunggu, eksport log peranti anda untuk perkongsian penyelesaian masalah atau cuba mulakan semula {app_name}." } }, "my" : { "stringUnit" : { "state" : "translated", - "value" : "ကူးယူမည်" + "value" : "We've noticed {app_name} is taking a long time to start.

You can continue to wait, export your device logs to share for troubleshooting, or try restarting {app_name}." } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Kopier" + "value" : "Vi har lagt merke til at {app_name} tar lang tid å starte.

Du kan vente videre, eksportere loggene på enheten din for å dele for feilsøking, eller prøve å starte {app_name} på nytt." } }, "nb-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Kopier" + "value" : "Vi har lagt merke til at {app_name} tar lang tid å starte.

Du kan fortsette å vente, eksportere enhetsloggene for å dele for feilsøking, eller prøve å starte {app_name} på nytt." } }, "ne-NP" : { "stringUnit" : { "state" : "translated", - "value" : "प्रतिलिपि गर्नुहोस्" + "value" : "हामीले नोटिस गर्यौं कि {app_name} सुरु हुन धेरै समय लिइरहेको छ।

तपाईं प्रतीक्षा जारी राख्न सक्नुहुन्छ, समस्या समाधानको लागि तपाईंको उपकरणको लक निकाल्न साझा गर्न सक्नुहुन्छ, वा {app_name} पुन: सुरु गर्न प्रयास गर्न सक्नुहुन्छ।" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Kopieer" + "value" : "We hebben gemerkt dat {app_name} veel tijd nodig heeft om op te starten.

U kunt doorgaan met wachten, uw apparaatlogs exporteren om te delen voor probleemoplossing, of proberen {app_name} opnieuw op te starten." } }, "nn-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Kopier" + "value" : "Vi har merka at {app_name} tar lang tid på å starte.

Du kan vente, eksportere loggar frå eininga di for deling til feilsøking, eller prøve å starte {app_name} på nytt." } }, "ny" : { "stringUnit" : { "state" : "translated", - "value" : "Chotsani" + "value" : "Timazindikira {app_name} kutenga nthawi kuti ayambe.

Inu mungapitirize kudikira, kutulutsira chipangizo malipoti kuti azipeza mavuto, kapena yesani kuyambiranso {app_name}." } }, "pa-IN" : { "stringUnit" : { "state" : "translated", - "value" : "ਨਕਲ" + "value" : "ਅਸੀਂ ਨੋਟ ਕੀਤਾ ਹੈ ਕਿ {app_name} ਨੂੰ ਸ਼ੁਰੂ ਕਰਨ ਵਿੱਚ ਬਹੁਤ ਸਮਾਂ ਲੱਗ ਰਿਹਾ ਹੈ।

ਤੁਸੀਂ ਇੰਤਜ਼ਾਰ ਕਰ ਸਕਦੇ ਹੋ, ਆਪਣੇ ਔਜ਼ਾਰ ਦੇ ਲੌਗ ਨਿਕਾਸੀ ਕਰ ਸਕਦੇ ਹੋ ਚੋਣ ਕਰਨ ਲਈ Troubleshooting ਲਈ ਸਾਂਝੇ ਕਰਨ ਲਈ, ਜਾਂ {app_name} ਨੂੰ ਮੁੜ ਸ਼ੁਰੂ ਕਰਨ ਦੀ ਕੋਸ਼ਿਸ਼ ਕਰੋ।" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Kopiuj" + "value" : "Zauważyliśmy, że uruchomienie aplikacji {app_name} zajmuje dużo czasu.

Możesz kontynuować oczekiwanie, wyeksportować dzienniki urządzenia do udostępnienia w celu rozwiązania problemów lub spróbować ponownie uruchomić aplikację {app_name}." } }, "ps" : { "stringUnit" : { "state" : "translated", - "value" : "د حساب ID کاپي" + "value" : "موږ ولیدل چې {app_name} د پیل کولو لپاره ډیر وخت نیسي.

تاسو کولی شئ انتظار ته دوام ورکړئ، د ستونزو د حل لپاره د شریکولو لپاره د خپل وسیله لاګ صادر کړئ، یا د {app_name} بیا پیلولو هڅه وکړئ." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Copiar" + "value" : "Observamos que {app_name} está demorando muito para iniciar.

Você pode continuar esperando, exportar os logs do seu dispositivo para compartilhar para solução de problemas, ou tentar reiniciar o {app_name}." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Copiar" + "value" : "Percebemos que {app_name} está a demorar muito a iniciar.

Pode continuar a esperar, exportar os registos do seu dispositivo e partilhar para analisarmos o problema, ou tentar reiniciar o {app_name}." } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Copiază" + "value" : "Am observat că {app_name} durează mult timp să pornească.

Puteți continua să așteptați, să exportați jurnalele dispozitivului pentru a le partaja pentru depanare sau să încercați să reporniți {app_name}." } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Скопировать" + "value" : "Мы заметили, что {app_name} занимает много времени для запуска.

Вы можете продолжить ждать, экспортировать журналы вашего устройства для устранения неполадок или попробовать перезапустить {app_name}." } }, "sh" : { "stringUnit" : { "state" : "translated", - "value" : "Kopiraj" + "value" : "Primetili smo da pokretanje {app_name} traje dugo.

Možete nastaviti da čekate, izvesti logove uređaja za deljenje radi otklanjanja grešaka, ili pokušati ponovo pokrenuti {app_name}." } }, "si-LK" : { "stringUnit" : { "state" : "translated", - "value" : "පිටපත්" + "value" : "අප සැකසූවානම් {app_name} ආරම්භ කිරීමට වැඩි කාලයක් ගත වන බව දැක ඇත.

ඔබට සිතියෙන්නේ, උපකරණ ලොග් දත්ත අපට යැවිය හැකි, නැවත {app_name} ආරම්භ කර බලන්න." } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Kopírovať" + "value" : "Všimli sme si, že {app_name} sa dlho spúšťa.

Môžete pokračovať v čakaní, exportovať záznamy z vášho zariadenia kvôli riešeniu problémov alebo skúsiť reštartovať {app_name}." } }, "sl" : { "stringUnit" : { "state" : "translated", - "value" : "Kopiraj" + "value" : "Opažamo, da {app_name} potrebuje dolgo časa za zagon.

Lahko nadaljujete s čakanjem, izvozite dnevniške datoteke naprave za odpravljanje težav ali poskusite znova zagnati {app_name}." } }, "sq" : { "stringUnit" : { "state" : "translated", - "value" : "Kopjoje" + "value" : "Ne kemi vërejtur që {app_name} po merr shumë kohë për tu nisur.

Ju mund të prisni, eksportoni regjistrat e pajisjes suaj për ndihmë në zgjidhjen e problemeve, ose provoni të rinisni {app_name}." } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : "Копирај" + "value" : "Приметили смо да {app_name} треба дуго времена да се покрене.

Можете наставити да чекате, извести дневнике уређаја да их делите за решавање проблема или покушати поново покренути {app_name}." } }, "sr-Latn" : { "stringUnit" : { "state" : "translated", - "value" : "Kopiraj" + "value" : "Primetili smo da aplikaciji {app_name} treba dugo vremena da se pokrene.

Možete da nastavite da čekate, izvezete logove uređaja za deljenje radi rešavanja problema ili pokušate ponovo da pokrenete {app_name}." } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Kopiera" + "value" : "Vi har märkt att {app_name} tar lång tid att starta.

Du kan fortsätta vänta, exportera dina felsökningsloggar för att dela för felsökning, eller försöka starta om {app_name}." } }, "sw" : { "stringUnit" : { "state" : "translated", - "value" : "Nakili" + "value" : "Tumeona {app_name} inachukua muda mrefu kuanza.

Unaweza kuendelea kusubiri, kuhamisha kumbukumbu za kifaa chako kushiriki kwa kutatua shida, au jaribu kuanzisha {app_name} upya." } }, "ta" : { "stringUnit" : { "state" : "translated", - "value" : "நகலெடு" + "value" : "{app_name} தொடங்க அதிக நேரம் ஆகிறதே எனக் காணப்படுகின்றது.

நீங்கள் தொடர்ந்தும் காத்திருக்கலாம், உங்களின் சாதன பதிவு பட்டியலை வெளியிட்டு பகிரவும் அல்லது {app_name} புனரஇயக்க முயற்சிக்கவும்." } }, "te" : { "stringUnit" : { "state" : "translated", - "value" : "కాపీ చేయండి" + "value" : "మేము గమనించాము {app_name} ప్రారంభమవ్వడానికి చాలా సమయం పడుతోంది.

మీరు వేచి ఉండవచ్చు, సమస్యను నిర్ధారించడానికి పరికరం లాగ్‌లను ఎగుమతి చేసి షేర్ చేయవచ్చు లేదా {app_name} రీస్టార్ట్ చేయవచ్చు." } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "คัดลอก" + "value" : "เราได้สังเกตว่า {app_name} ใช้เวลานานในการเริ่มต้น

คุณสามารถรอต่อไป ส่งออกบันทึกอุปกรณ์ของคุณเพื่อแบ่งปันเพื่อแก้ไขปัญหา หรือลองรีสตาร์ท {app_name}" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Kopyala" + "value" : "{app_name} uygulamasının başlatılması uzun sürüyor fark ettik.

Beklemeye devam edebilir, cihaz günlüklerinizi paylaşmak için dışa aktarabilir veya {app_name} yeniden başlatmayı deneyebilirsiniz." } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Копіювати" + "value" : "Ми помітили, що {app_name} довго запускається.

Ви можете продовжити чекати, експортувати журнали вашого пристрою для аналізу або спробувати перезапустити {app_name}." } }, "ur-IN" : { "stringUnit" : { "state" : "translated", - "value" : "کاپی کریں" + "value" : "ہم نے دیکھا ہے کہ {app_name} کو شروع ہونے میں کافی وقت لگ رہا ہے۔

آپ انتظار کرنا جاری رکھ سکتے ہیں، مسئلہ حل کرنے کے لیے اشتراک کرنے کے لیے اپنے آلے کے لاگز کو برآمد کر سکتے ہیں، یا {app_name} کو دوبارہ شروع کرنے کی کوشش کر سکتے ہیں۔" } }, "uz" : { "stringUnit" : { "state" : "translated", - "value" : "Nusxalash" + "value" : "{app_name} ishga tushishiga koʻp vaqt ketayotganini aniqladik.

Kutishda davom etishingiz, muammolarni bartaraf etish uchun qurilma jurnallarini baham koʻrish uchun eksport qilishingiz yoki {app_name} ilovasini qayta ishga tushirishga urinib koʻrishingiz mumkin." } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Sao chép" + "value" : "Chúng tôi nhận thấy {app_name} mất nhiều thời gian để khởi động.

Bạn có thể tiếp tục chờ, xuất nhật ký thiết bị để chia sẻ hỗ trợ khắc phục sự cố, hoặc thử khởi động lại {app_name}." } }, "xh" : { "stringUnit" : { "state" : "translated", - "value" : "Kopa" + "value" : "Siqaphele ukuba i-{app_name} ithatha ixesha elide ukuqala.

Ungaqhubeka ulinde, uthumele iingxelo zesixhobo sakho ukwabelana ngazo ukulungisa iingxaki, okanye uzame ukuqala ngokutsha {app_name}." } }, "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "复制" + "value" : "我们注意到{app_name}启动时间过长。

您可以选择继续等待,导出设备日志以分享故障排除,或尝试重新启动{app_name}。" } }, "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "拷貝" + "value" : "我們注意到 {app_name} 啟動時間過長。

您可以繼續等待,匯出您的設備日誌以便排除故障,或者嘗試重新啟動 {app_name}。" } } } }, - "create" : { + "databaseErrorUpdate" : { "extractionState" : "manual", "localizations" : { "af" : { "stringUnit" : { "state" : "translated", - "value" : "Skep" + "value" : "Jou app databasis is onversoenbaar met hierdie weergawe van {app_name}. Herinstalleer die app en herstel jou rekening om 'n nuwe databasis te genereer en voort te gaan met die gebruik van {app_name}.

Waarskuwing: Dit sal lei tot die verlies van alle boodskappe en aanhegsels ouer as twee weke." } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "إنشاء" + "value" : "قاعدة بيانات تطبيقك غير متوافقة مع هذا الإصدار من {app_name}. أعد تثبيت التطبيق واستعد حسابك لإنشاء قاعدة بيانات جديدة ومتابعة استخدام {app_name}.

تحذير: سيؤدي هذا إلى فقدان جميع الرسائل والمرفقات التي يزيد عمرها عن أسبوعين." } }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "Yarat" + "value" : "Tətbiqinizin veri bazası, {app_name} tətbiqinin versiyası ilə uyumlu deyil. Yeni bir veri bazası yaratmaq və {app_name} istifadə etməyə davam etmək üçün tətbiqi yenidən quraşdırın və hesabınızı bərpa edin.

Xəbərdarlıq: Bu, iki həftədən köhnə olan bütün mesajların və qoşmaların itkisi ilə nəticələnəcək." } }, "bal" : { "stringUnit" : { "state" : "translated", - "value" : "بناؤ" + "value" : "{app_name} کس ای ورژنئے نکہ ایپ دیټابیس ناہم آهن. اِیپ نوک بزا من اَکاونٹ بازگری کن تا پن نوک دیټابیس پیدا بکن تا {app_name} دابی مرت استفاده بکن.

چیتپا: ماہیت زامبلاونکین دو ہفتہ ناہند، تمام پیامانءِ و اٹیچمنٹاں گم بیت." } }, "be" : { "stringUnit" : { "state" : "translated", - "value" : "Стварыць" + "value" : "Ваша база даных прыкладання несумяшчальная з гэтай версіяй {app_name}. Пераўсталюйце прыкладанне і аднавіце ўліковы запіс, каб стварыць новую базу даных і працягнуць выкарыстанне {app_name}.

Увага: гэта прывядзе да страты ўсіх паведамленняў і ўкладанняў старэйшых за два тыдні." } }, "bg" : { "stringUnit" : { "state" : "translated", - "value" : "Създай" + "value" : "Вашата база данни на приложението е несъвместима с тази версия на {app_name}. Инсталирайте повторно приложението и възстановете своя акаунт, за да генерирате нова база данни и да продължите да използвате {app_name}.

Внимание: Това ще доведе до загуба на всички съобщения и прикачени файлове по-стари от две седмици." } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "তৈরি করুন" + "value" : "আপনার অ্যাপ্লিকেশন ডাটাবেস {app_name} এর এই সংস্করণের সাথে অসঙ্গতিপূর্ণ। অ্যাপ পুনরায় ইনস্টল করুন এবং আপনার অ্যাকাউন্ট পুনরুদ্ধার করুন একটি নতুন ডাটাবেস তৈরি করতে এবং {app_name} ব্যবহার করতে থাকুন।

সতর্কতা: এর ফলে আপনার সমস্ত বার্তা এবং সংযুক্তিগুলি দুই সপ্তাহের বেশি পুরানো হারিয়ে যাবে।" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Crear" + "value" : "La base de dades de la vostra aplicació no és compatible amb aquesta versió de {app_name}. Reinstal·leu l'aplicació i restaureu el vostre compte per generar una nova base de dades i continuar utilitzant {app_name}.

Avís: Això donarà lloc a la pèrdua de tots els missatges i adjunts anteriors a dues setmanes." } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Vytvořit" + "value" : "Databáze vaší aplikace není kompatibilní s touto verzí {app_name}. Přeinstalujte aplikaci a obnovte svůj účet pro vytvoření nové databáze a pokračování v používání {app_name}.

Varování: To povede ke ztrátě všech zpráv a příloh starších než dva týdny." } }, "cy" : { "stringUnit" : { "state" : "translated", - "value" : "Creu" + "value" : "Mae eich cronfa ddata ap yn anghydnaws â'r fersiwn hon o {app_name}. Ailosodwch yr ap a darganfod eich cyfrif i greu cronfa ddata newydd a pharhau i ddefnyddio {app_name}.

Rhybudd: Bydd hyn yn arwain at golli’r holl negeseuon a’r atodiadau sy’n hŷn na phythefnos." } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Opret" + "value" : "Din app-database er inkompatibel med denne version af {app_name}. Geninstaller appen og gendan din konto for at generere en ny database og fortsætte med at bruge {app_name}.

Advarsel: Dette vil resultere i tab af alle beskeder og vedhæftninger, der er ældre end to uger." } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Erstellen" + "value" : "Deine App-Datenbank ist mit dieser Version von {app_name} nicht kompatibel. Installiere die App neu und stelle deinen Account wieder her, um eine neue Datenbank zu erstellen und {app_name} weiter zu verwenden.

Warnung: Dadurch gehen alle Nachrichten und Anhänge verloren, die älter als zwei Wochen sind." } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Δημιουργία" + "value" : "Η βάση δεδομένων της εφαρμογής σας δεν είναι συμβατή με αυτήν την έκδοση του {app_name}. Επανεγκαταστήστε την εφαρμογή και αποκαταστήστε τον λογαριασμό σας για να δημιουργήσετε μια νέα βάση δεδομένων και να συνεχίσετε να χρησιμοποιείτε το {app_name}.

Προειδοποίηση: Αυτό θα έχει ως αποτέλεσμα την απώλεια όλων των μηνυμάτων και των συνημμένων που είναι παλαιότερα των δύο εβδομάδων." } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Create" + "value" : "Your app database is incompatible with this version of {app_name}. Reinstall the app and restore your account to generate a new database and continue using {app_name}.

Warning: This will result in the loss of all messages and attachments older than two weeks." } }, "eo" : { "stringUnit" : { "state" : "translated", - "value" : "Krei" + "value" : "Via aplikaĵa datumbazo ne kongruas kun ĉi tiu versio de {app_name}. Reinstalu la aplikaĵon kaj restarigu vian konton por generi novan datumbazon kaj daŭrigi la uzadon de {app_name}.

Averto: Ĉi tio rezultos en la perdo de ĉiuj mesaĝoj kaj aldonaĵoj pli aĝaj ol du semajnoj." } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Crear" + "value" : "Tu base de datos de la app es incompatible con esta versión de {app_name}. Reinstala la app y restaura tu cuenta para generar una nueva base de datos y continuar usando {app_name}.

Advertencia: Esto resultará en la pérdida de todos los mensajes y archivos adjuntos mayores a dos semanas." } }, "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Crear" + "value" : "Su base de datos de la aplicación no es compatible con esta versión de {app_name}. Reinstala la aplicación y restaura tu cuenta para generar una nueva base de datos y continuar usando {app_name}.

Advertencia: Esto resultará en la pérdida de todos los mensajes y archivos adjuntos anteriores a dos semanas." } }, "et" : { "stringUnit" : { "state" : "translated", - "value" : "Loo" + "value" : "Teie rakenduse andmebaas ei ühildu selle {app_name} versiooniga. Installige rakendus uuesti ja taastage oma konto, et luua uus andmebaas ja jätkata {app_name} kasutamist.

Hoiatus: See kaotab kõik vanemad kui kaks nädalat sõnumid ja manused." } }, "eu" : { "stringUnit" : { "state" : "translated", - "value" : "Sortu" + "value" : "Zure aplikazio-datubasea ez da bateragarria {app_name} bertsio honekin. Berrinstalatu aplikazioa eta leheneratu zure kontua datubase berri bat sortzeko eta {app_name} erabiltzen jarraitzeko.

Abisua: Honek bi astetik gorako mezu eta eranskinen galera eragingo du." } }, "fa" : { "stringUnit" : { "state" : "translated", - "value" : "ایجاد" + "value" : "پایگاه داده برنامه شما با این نسخه از {app_name} سازگار نیست. برنامه را دوباره نصب کنید و حساب خود را بازیابی کنید تا یک پایگاه داده جدید ایجاد کنید و به استفاده از {app_name} ادامه دهید.

هشدار: این باعث از دست رفتن همه پیام‌ها و پیوست‌های قدیمی‌تر از دو هفته می‌شود." } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Luo" + "value" : "Sovellustietokanta ei ole yhteensopiva tämän {app_name} version kanssa. Asenna sovellus uudelleen ja palauta tilisi, jotta voit luoda uuden tietokannan ja jatkaa {app_name} käyttöä.

Varoitus: Tämä johtaa yli kaksi viikkoa vanhojen viestien ja liitteiden menetykseen." } }, "fil" : { "stringUnit" : { "state" : "translated", - "value" : "Lumikha" + "value" : "Ang iyong app database ay hindi compatible sa bersyon na ito ng {app_name}. I-reinstall ang app at i-restore ang iyong account upang makabuo ng bagong database at ipagpatuloy ang paggamit ng {app_name}.

Babala: Ito ay magreresulta sa pagkawala ng lahat ng mensahe at attachments na higit sa dalawang linggo." } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Créer" + "value" : "La base de données de votre application est incompatible avec cette version de {app_name}. Réinstallez l'application et restaurez votre compte pour générer une nouvelle base de données et continuer à utiliser {app_name}.

Avertissement : Cela entraînera la perte de tous les messages et pièces jointes datant de plus de deux semaines." } }, "gl" : { "stringUnit" : { "state" : "translated", - "value" : "Crear" + "value" : "A base de datos da túa aplicación non é compatible con esta versión de {app_name}. Reinstala a aplicación e restaura a túa conta para xerar unha nova base de datos e continuar usando {app_name}.

Advertencia: Isto resultará na perda de todas as mensaxes e adxuntos anteriores a dúas semanas." } }, "ha" : { "stringUnit" : { "state" : "translated", - "value" : "Ƙirƙiri" + "value" : "Bayanan aikace-aikacen ku ba su dace da wannan sigar {app_name}. Sake shigar da aikace-aikacen kuma dawo da asusunka don samar da sabon database kuma ci gaba da amfani da {app_name}.

Gargadi: Wannan zai haifar da rasa duk sakonni da fayiloli fiye da makonni biyu." } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "צור" + "value" : "מאגר נתונים של האפליקציה שלך אינו תואם לגרסה זו של {app_name}. התקן מחדש את האפליקציה ושחזר את החשבון שלך כדי ליצור מאגר נתונים חדש ולהמשיך להשתמש ב-{app_name}.

אזהרה: פעולה זו תשאיר את כל ההודעות והצרופות הישנות משבועיים." } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "बनाएं" + "value" : "{app_name} का यह संस्करण आपके ऐप डेटाबेस के साथ असंगत है। ऐप को पुन: स्थापित करें और अपना खाता पुनर्स्थापित करें ताकि नया डेटाबेस उत्पन्न हो सके और {app_name} का उपयोग जारी रख सकें।

चेतावनी: इससे दो सप्ताह से पुराने सभी संदेश और संलग्नक खो जाएंगे।" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Stvori" + "value" : "Vaša aplikacijska baza podataka nije kompatibilna s ovom verzijom {app_name}. Ponovno instalirajte aplikaciju i vratite svoj račun kako biste generirali novu bazu podataka i nastavili koristiti {app_name}.

Upozorenje: Ovo će rezultirati gubitkom svih poruka i privitaka starijih od dva tjedna." } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Létrehozás" + "value" : "Az alkalmazás adatbázisa nem kompatibilis a {app_name} jelenlegi verziójával. Telepítsd újra az alkalmazást, és állítsd vissza fiókját egy új adatbázis létrehozásához és a {app_name} további használathoz.

Figyelmeztetés: Ez minden két hétnél régebbi üzenet és melléklet elvesztését eredményezi." } }, "hy-AM" : { "stringUnit" : { "state" : "translated", - "value" : "Ստեղծել" + "value" : "Ձեր հավելվածի տվյալների բազան համադրված չէ այս {app_name} տարբերակի հետ։ Վերակայանացրեք հավելվածը և վերագործարկեք ձեր հաշիվը նոր տվյալների բազա ստեղծելու և {app_name} շարունակելու համար։

Զգուշացում: Սա կհանգեցնի բոլոր հաղորդագրությունների և կցաթղթերի երկու շաբաթից ավելի հնության կորստին։" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Buat" + "value" : "Database aplikasi Anda tidak kompatibel dengan versi {app_name} ini. Instal ulang aplikasi dan pulihkan akun Anda untuk menghasilkan database baru dan terus menggunakan {app_name}.

Peringatan: Ini akan menyebabkan hilangnya semua pesan dan lampiran yang lebih dari dua minggu." } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Crea" + "value" : "Il database della tua app non è compatibile con questa versione di {app_name}. Reinstalla l'app e ripristina il tuo account per generare un nuovo database e continuare a utilizzare {app_name}.

Attenzione: Questo comporterà la perdita di tutti i messaggi e di tutti gli allegati più vecchi di due settimane." } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "作成する" + "value" : "お使いのアプリデータベースはこのバージョンの {app_name} と互換性がありません。アプリを再インストールしてアカウントを復元し、新しいデータベースを生成して {app_name} を使用し続けてください。

警告: これにより、2週間以上前のすべてのメッセージと添付ファイルが失われます。" } }, "ka" : { "stringUnit" : { "state" : "translated", - "value" : "შექმნა" + "value" : "თქვენი აპლიკაციის მონაცემთა ბაზა არ შეესაბამება {app_name}-ის ამ ვერსიას. ხელახლა დააინსტალირეთ აპლიკაცია და აღადგინეთ თქვენი ანგარიში ახალი მონაცემთა ბაზის შესაქმნელად და {app_name} გამოყენების გაგრძელებისთვის.

გაფრთხილება: ეს გამოიწვევს ყველა მესიჯის და ფაილის დაკარგვას, რომლებიც ორ კვირაზე მეტი ხნისაა." } }, "km" : { "stringUnit" : { "state" : "translated", - "value" : "បង្កើត" + "value" : "ឃ្លាំងទិន្នន័យរបស់កម្មវិធីរបស់អ្នកមិនអាចបើកជាមួយ {app_name} នេះទេ។ កញ្ចួញកម្មវិធីឡើងវិញ ហើយស្ដារគណនីរបស់អ្នកដើម្បីបង្កើតឃ្លាំងទិន្នន័យថ្មី និង​បន្តប្រើ {app_name}។

ការព្រមាន៖ នេះនឹងលុបបាត់ជាស្ថាពរ នូវសារទាំងអស់ និងឯកសារភ្ជាប់ដែលអាយុចាស់ជាងពីរសប្ដាហ៍។" } }, "kn" : { "stringUnit" : { "state" : "translated", - "value" : "ರಚಿಸಿ" + "value" : "ನಿಮ್ಮ ಅಪ್ಲಿಕೇಶನ್ ಡೇಟಾಬೇಸ್ {app_name} ಆವೃತ್ತಿಯೊಂದಿಗೆ ಅನುರೂಪವಲ್ಲ. ಹೊಸ ಡೇಟಾಬೇಸ್ನನ್ನು ರಚಿಸಲು ಅಪ್ಲಿಕೇಶನನನ್ನು ಪುನಃಹುಹಾಯಿಸಿ ಮತ್ತು ನಿಮ್ಮ ಖಾತೆಯನ್ನು ಪುನಃಸ್ಥಾಪಿಸಿ ಮತ್ತು {app_name} ಬಳಸಿದ್ದು ನಿರಂತರ ಪ್ರದರ್ಶಿಸಲು.

ಎಚ್ಚರಿಕೆ: ಇದು ಎರಡು ವಾರಗಳಿಗಿಂತ ಹೆಚ್ಚಿನ ವಯಸ್ಸಿನ ಎಲ್ಲಾ ಸಂದೇಶಗಳು ಮತ್ತು ಜೊತೆಯುಡುಕಳವನ್ನು ಕಳೆದುಹಾಕುತ್ತದೆ." } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "만들기" + "value" : "{app_name}의 이 버전은 앱 데이터베이스와 호환되지 않습니다. 앱을 재설치하고 계정을 복원하여 새 데이터베이스를 생성하고 {app_name}을 계속 사용하십시오.

경고: 이렇게 하면 2주 이상 된 모든 메시지와 첨부 파일이 손실됩니다." } }, "ku" : { "stringUnit" : { "state" : "translated", - "value" : "دروستکردن" + "value" : "بنکەی زانیاری بەرنامەکەت ناگەلێرد بەم وه‌ شەپکنیکی {app_name}. بەخێربەستەوە بەرنامەکە و ئەژمێرت پاشەکەوتی بکەیت بۆ بەکاربردنی {app_name}.

ئاگاداری: ئەمە دوای دیترین مەکموو بەرەوکردنەکان و هاوبەشەکان بەکارە لأختە دەکاتە دوو هەفتە ئەگەری پەیامەکان." } }, "ku-TR" : { "stringUnit" : { "state" : "translated", - "value" : "Çêke" + "value" : "بنکەی زانیاری بەرنامەکەت ناگەلێرد بەم وه‌ شەپکنیکی {app_name}. بەخێربەستەوە بەرنامەکە و ئەژمێرت پاشەکەوتی بکەیت بۆ بەکاربردنی {app_name}.

ئاگاداری: ئەمە دوای دیترین مەکموو بەرەوکردنەکان و هاوبەشەکان بەکارە لأختە دەکاتە دوو هەفتە ئەگەری پەیامەکان." } }, "lg" : { "stringUnit" : { "state" : "translated", - "value" : "Kilira" - } - }, - "lo" : { - "stringUnit" : { - "state" : "translated", - "value" : "ລູດ" + "value" : "Database y’app yo si yagwanira ne verison ya {app_name}. Tegyamu app ne kuba okwongera ku Account yo okwongera okukozesa {app_name}.

Warning: Kino kyakuleeteka okufiirwa kwa bubaka bwona n’empapula ezisazeewo ezinyuuse emywaka ebiri ebalagala." } }, "lt" : { "stringUnit" : { "state" : "translated", - "value" : "Sukurti" + "value" : "Jūsų programos duomenų bazė nesuderinama su šia {app_name} versija. Iš naujo įdiekite programą ir atkurkite savo paskyrą, kad sugeneruotumėte naują duomenų bazę ir toliau naudotumėte {app_name}.

Įspėjimas: Dėl to visos pranešimų ir priedų, senesnių nei dvi savaitės, bus prarastos." } }, "lv" : { "stringUnit" : { "state" : "translated", - "value" : "Izveidot" + "value" : "Jūsu lietotnes datubāze nav saderīga ar šo {app_name} versiju. Pārsūtiet lietotni un atjaunojiet savu kontu, lai izveidotu jaunu datubāzi un turpinātu izmantot {app_name}.

Brīdinājums: Tas rezultēsies visu ziņojumu un pielikumu, kas ir vecāki par divām nedēļām, zaudēšanā." } }, "mk" : { "stringUnit" : { "state" : "translated", - "value" : "Креирај" + "value" : "Вашата база на податоци на апликацијата не е компатибилна со оваа верзија на {app_name}. Повторно инсталирајте ја апликацијата и вратете го вашиот профил за да генерирате нова база на податоци и да продолжите да го користите {app_name}.

Предупредување: Ова ќе резултира со губење на сите пораки и прикачувања постари од две недели." } }, "mn" : { "stringUnit" : { "state" : "translated", - "value" : "Үүсгэх" + "value" : "Таны програмын өгөгдлийн сан {app_name}-ийн энэ хувилбарт нийцэхгүй байна. Програмыг дахин суулгаж, профайлыг сэргээснээр шинэ өгөгдлийн сан үүсгэж, {app_name} ашиглах боломжтой болно.

Сануулга: Энэ нь хоёр долоо хоногоос дээш хугацаатай бүх мессежүүд болон хавсралтууд алдагдах болно." } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Buat" + "value" : "Pangkalan data aplikasi anda tidak serasi dengan versi {app_name} ini. Pasang semula aplikasi ini dan pulihkan akaun anda untuk menjana pangkalan data baru dan terus menggunakan {app_name}.

Amaran: Ini akan menyebabkan kehilangan semua mesej dan lampiran yang lebih lama daripada dua minggu." } }, "my" : { "stringUnit" : { "state" : "translated", - "value" : "ဖန်တီးပါ" + "value" : "သင့်အက်ပ်ဒေတာဘေ့စ်သည် {app_name} ၏ ဤဗားရှင်းနှင့် မက်စ်ပေါ်နိုင်ပါ။ အက်ပ်ကို ပြန်ထည့်သွင်းပြီး သင့်အကောင့်ကို ပြန်လည်ထားပြီး {app_name} ကို ဆက်လက်သုံးဆောင်ရန် အချက်ပြပီးနောက် ငါးပတ်အတွင်းလက်ရှိမက်ဆေ့ခ်ျနှင့်လိုက်ဖက်မှုအတင်ပျောက်သွားနိုင်သည်။

သတိပေးချက်: ဤလုပ်ဆောင်ချက်ကြောင့် အဆိုပါကာလထက်ပိုကြာသော မက်ဆေ့ခ်ျများနှင့် ပျက်စီးပိုင်ဆိုင်မှုများ ပျောက်ဆုံးသွားမည်။" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Opprett" + "value" : "App-databasen din er inkompatibel med denne versjonen av {app_name}. Installer appen på nytt og gjenopprett kontoen din for å generere en ny database og fortsette å bruke {app_name}.

Advarsel: Dette vil resultere i tap av alle meldinger og vedlegg eldre enn to uker." } }, "nb-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Opprett" + "value" : "Database til appen din er inkompatibel med denne versjonen av {app_name}. Installer appen på nytt og gjenopprett kontoen din for å generere en ny database og fortsette å bruke {app_name}.

Advarsel: Dette vil resultere i tap av alle meldinger og vedlegg eldre enn to uker." } }, "ne-NP" : { "stringUnit" : { "state" : "translated", - "value" : "सिर्जना गर्नुहोस्" + "value" : "तपाईंको एप डाटाबेस {app_name} को यो संस्करणसँग असंगत छ। एपलाई पुनः स्थापना गर्नुहोस् र आफ्नो खाता पुनर्स्थापना गर्नुहोस् नयाँ डाटाबेस सिर्जना गर्न र {app_name} प्रयोग गर्न जारी राख्न।

चेतावनी: यसले दुई हप्ता भन्दा पुरानो सबै सन्देशहरू र अट्याचमेन्टहरूको हानि हुनेछ।" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Aanmaken" + "value" : "Uw app-database is niet compatibel met deze versie van {app_name}. Installeer de app opnieuw en herstel uw account om een nieuwe database te genereren en {app_name} te blijven gebruiken.

Waarschuwing: Dit leidt tot verlies van alle berichten en bijlagen ouder dan twee weken." } }, "nn-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Opprett" + "value" : "Databasen til appen din er ikkje kompatibel med denne versjonen av {app_name}. Installer appen på nytt og gjenopprett kontoen din for å generera ein ny database og fortset å bruka {app_name}.

Advarsel: Dette vil resultera i at alle meldingar og vedlegg eldre enn to veker går tapt." } }, "ny" : { "stringUnit" : { "state" : "translated", - "value" : "Yeretsani" + "value" : "Deta la pulogalamu yanu silikugwirizana ndi mtundu uwu wa {app_name}. Yikani pulogalamu yatsopanoyi ndikubwezerani akaunti yanu kuti mupange deta yatsopano ndikupitiriza kugwiritsa ntchito {app_name}.

Chenjezo: Izi zidzachititsa kuti mutaye mauthenga onse ndi zoyikapo zoposa masabata awo pafupifupi ziwiri." } }, "pa-IN" : { "stringUnit" : { "state" : "translated", - "value" : "ਬਨਾਓ" + "value" : "ਤੁਹਾਡੇ ਐਪ ਦਾ ਡਾਟਾਬੇਸ ਇਸ ਸੰਸਕਰਣ ਨਾਲ ਅਨੁਕੂਲ ਨਹੀਂ ਹੈ {app_name}। ਐਪ ਨੂੰ ਦੁਬਾਰਾ ਇੰਸਟਾਲ ਕਰੋ ਅਤੇ ਆਪਣਾ ਖਾਤਾ ਬਹਾਲ ਕਰੋ ਇੱਕ ਨਵਾਂ ਡਾਟਾਬੇਸ ਬਣਾਉਣ ਅਤੇ ਜਾਰੀ ਰੱਖਣ ਲਈ {app_name} ਵਰਤੋਂ.

ਚੇਤਾਵਨੀ: ਇਸ ਨਾਲ ਦੋ ਹਫ਼ਤਿਆਂ ਤੋਂ ਪੁਰਾਣੇ ਸਾਰੇ ਸੰਦੇਸ਼ ਅਤੇ ਅਟੈਕਮੈਂਟ ਗੁਆਚ ਜਾਣਗੇ।" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Utwórz" + "value" : "Twoja baza danych aplikacji jest niezgodna z tą wersją aplikacji {app_name}. Aby wygenerować nową bazę danych i dalej korzystać z aplikacji {app_name}, zainstaluj aplikację ponownie i przywróć swoje konto.

Uwaga: spowoduje to utratę wszystkich wiadomości i załączników starszych niż dwa tygodnie." } }, "ps" : { "stringUnit" : { "state" : "translated", - "value" : "د نوي اړیکې سره خبرې اترې پیل کړئ" + "value" : "ستاسو د اپ ڈیٹ ډیټابیس د {app_name} دې نسخې سره همغږي نه لري. د اپلیکیشن بیا نصب کړئ او خپل حساب بیا جوړ کړئ ترڅو یو نوی ډیټابیس جوړ کړئ او {app_name} کارول دوام ورکړئ.

خبرداری: دا به د دوو هفتو څخه زوړ ټول پیغامونه او ملحقات له لاسه ورکیدو لامل شي." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Criar" + "value" : "O banco de dados do seu aplicativo é incompatível com esta versão do {app_name}. Reinstale o aplicativo e restaure sua conta para gerar um novo banco de dados e continuar usando {app_name}.

Aviso: Isso resultará na perda de todas as mensagens e anexos com mais de duas semanas." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Criar" + "value" : "A base de dados do seu aplicativo é incompatível com esta versão do {app_name}. Reinstale o aplicativo e restaure a sua conta para gerar uma nova base de dados e continuar a usar o {app_name}.

Aviso: Isso resultará na perda de todas as mensagens e anexos com mais de duas semanas." } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Creează" + "value" : "Baza de date a aplicației dumneavoastră este incompatibilă cu această versiune de {app_name}. Reinstalați aplicația și restaurați contul pentru a genera o nouă bază de date și a continua să folosiți {app_name}.

Atenție: Aceasta va conduce la pierderea tuturor mesajelor și atașamentelor mai vechi de două săptămâni." } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Создать" + "value" : "База данных вашего приложения несовместима с этой версией {app_name}. Переустановите приложение и восстановите свой аккаунт, чтобы создать новую базу данных и продолжить использовать {app_name}.

Внимание: Это приведет к потере всех сообщений и вложений старше двух недель." } }, "sh" : { "stringUnit" : { "state" : "translated", - "value" : "Kreiraj" + "value" : "Tvoja baza podataka aplikacije nije kompatibilna s ovom verzijom {app_name}. Ponovo instaliraj aplikaciju i obnovi svoj račun da stvoriš novu bazu podataka i nastaviš koristiti {app_name}.

Upozorenje: Ovo će dovesti do gubitka svih poruka i privitaka starijih od dvije sedmice." } }, "si-LK" : { "stringUnit" : { "state" : "translated", - "value" : "සාදන්න" + "value" : "ඔබගේ යෙදුම් දත්ත කේතය {app_name} මෙහෙයුම් එක්වී නොමැත. යෙදුම නැවත ස්ථාපනය කර ඔබේ ගිණුම ප්‍රතිස්ථාපනය කර යුත්සේ {app_name} භාවිතා කිරීමට නව දත්ත ගබඩායක් සෑදී යමි.

අවවාදයයි: මෙය සතියක පරණ මායිම් සහ සියළු පණිවිඩ හා සම්බන්ධතා දත්ත කාසිවී අහිමියාවක් ලැබේ." } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Vytvoriť" + "value" : "Vaša databáza aplikácie nie je kompatibilná s touto verziou {app_name}. Preinštalujte aplikáciu a obnovte svoj účet, aby sa vygenerovala nová databáza a mohli ste pokračovať v používaní {app_name}.

Upozornenie: Týmto dôjde k strate všetkých správ a príloh starších ako dva týždne." } }, "sl" : { "stringUnit" : { "state" : "translated", - "value" : "Ustvari" + "value" : "Vaša baza podatkov aplikacije ni združljiva s to različico {app_name}. Ponovno namestite aplikacijo in obnovite svoj račun, da ustvarite novo bazo podatkov in nadaljujete z uporabo {app_name}.

Opozorilo: To bo povzročilo izgubo vseh sporočil in prilog, starejših od dveh tednov." } }, "sq" : { "stringUnit" : { "state" : "translated", - "value" : "Krijo" + "value" : "Baza e të dhënave të aplikacionit tuaj është e papajtueshme me këtë version të {app_name}. Rinstaloni aplikacionin dhe riktheni llogarinë tuaj për të gjeneruar një bazë të re të të dhënave dhe për të vazhduar përdorimin e {app_name}.

Paralajmërim: Kjo do të rezultojë në humbjen e të gjitha mesazheve dhe bashkëngjitjeve më të vjetra se dy javë." } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : "Креирај" + "value" : "Ваша база података апликације није компатибилна са овом верзијом {app_name}. Поново инсталирајте апликацију и повратите ваш налог да бисте генерисали нову базу података и наставили да користите {app_name}.

Упозорење: Ово ће резултирати губитком свих порука и прилога старијих од две недеље." } }, "sr-Latn" : { "stringUnit" : { "state" : "translated", - "value" : "Kreiraj" + "value" : "Baza podataka vaše aplikacije nije kompatibilna sa ovom verzijom {app_name}. Ponovo instalirajte aplikaciju i vratite vaš nalog kako biste generisali novu bazu podataka i nastavili sa korišćenjem {app_name}.

Upozorenje: Ovo će rezultirati gubitkom wszystkich poruka i privitaka starijih od dve nedelje." } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Skapa" + "value" : "Din appdatabas är inkompatibel med den här versionen av {app_name}. Installera om appen och återställ ditt konto för att skapa en ny databas och fortsätta använda {app_name}.

Varning: Detta resulterar i förlust av alla meddelanden och bilagor äldre än två veckor." } }, "sw" : { "stringUnit" : { "state" : "translated", - "value" : "Unda" + "value" : "Hifadhidata ya programu yako hailingani na toleo hili la {app_name}. Sakinusha programu na urejeshe akaunti yako ili kuunda hifadhidata mpya na uendelee kutumia {app_name}.

Onyo: Hii itasababisha kupoteza ujumbe na viambatanisho vyote vilivyo zaidi ya wiki mbili." } }, "ta" : { "stringUnit" : { "state" : "translated", - "value" : "உருவாக்கு" + "value" : "உங்கள் பயன்பாட்டு தரவுத்தொகுப்பு இந்த {app_name} பதிப்புடன் இணக்கமில்லை. பயன்பாட்டை மறுதொன்று முடியும் மற்றும் உங்கள் கணக்கை மறுஆதிக்கவும் புதிய தரவுத்தொகுப்பை உருவாக்கி {app_name} பயன்பாட்டைப் பயன்படுத்தி தொடரவும்.

புரிந்து கொள்ளுங்கள்: இது இரண்டு வாரத்திற்கு முதலில் உள்ள அனைத்து செய்தி மற்றும் இணைப்புகளை இழக்கச் செய்யும்." } }, "te" : { "stringUnit" : { "state" : "translated", - "value" : "సృష్టించు" + "value" : "మీ యాప్ డేటాబేస్ {app_name} యొక్క ఈ వెర్షన్ తో అనుకూలంగా లేదు. యాప్‌ను పున సంస్థాపించి మీ ఖాతాను పునరుద్ధరించండి ఒక కొత్త డేటాబేస్ ను సృష్టించి {app_name} కొనసాగించడానికి.

హెచ్చరిక: ఇది రెండు వారాల క్రితం ఉన్న అన్ని సందేశాలు మరియు అటాచ్మెంట్లు కోల్పోవడానికి అనుమతిస్తుంది." } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "Create" + "value" : "ฐานข้อมูลแอปของคุณไม่สามารถใช้งานร่วมกับเวอร์ชันนี้ของ {app_name} ติดตั้งใหม่และกู้คืนบัญชีเพื่อสร้างฐานข้อมูลใหม่และใช้งาน {app_name} ต่อไป

คำเตือน: สิ่งนี้จะส่งผลให้สูญเสียข้อความและไฟล์แนบทั้งหมดที่มีอายุเกินสองสัปดาห์" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Oluştur" + "value" : "{app_name} uygulamanızın veritabanı bu sürüm ile uyumsuz. Uygulamayı yeniden yükleyin ve yeni bir veritabanı oluşturmak ve {app_name} kullanmaya devam etmek için hesabınızı geri yükleyin.

Uyarı: Bu, iki haftadan daha eski olan tüm ileti ve eklerin kaybolmasına neden olacaktır." } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Створити" + "value" : "База даних вашого додатку несумісна з цією версією {app_name}. Перевстановіть додаток та відновіть свій обліковий запис, щоб створити нову базу даних і продовжувати користуватися {app_name}.

Увага: Це призведе до втрати всіх повідомлень та вкладень, старших двох тижнів." } }, "ur-IN" : { "stringUnit" : { "state" : "translated", - "value" : "بنائیں" + "value" : "آپ کا ایپ ڈیٹا بیس {app_name} کے اس ورژن سے مطابقت نہیں رکھتا ہے۔ ایپ کو دوبارہ انسٹال کریں اور نیا ڈیٹا بیس بنانے کے لیے اپنا اکاؤنٹ بحال کریں اور {app_name} کا استعمال جاری رکھیں۔

انتباہ: اس کے نتیجے میں دو ہفتوں سے پرانے تمام پیغامات اور منسلکات ضائع ہو جائیں گے۔" } }, "uz" : { "stringUnit" : { "state" : "translated", - "value" : "Yaratish" + "value" : "Sizning ilova ma'lumotlar bazasi ushbu {app_name} versiyasi bilan mos kelmaydi. Ilovani qayta o'rnating va hisobingizni tiklang, yangi ma'lumotlar bazasini yaratish va {app_name} dan foydalanishni davom ettirish uchun.

Ogohlantirish: bu ikki haftalikdan katta barcha xabarlar va ilovalarni yo'qotishga olib keladi." } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Tạo" + "value" : "Cơ sở dữ liệu ứng dụng của bạn không tương thích với phiên bản {app_name} này. Cài đặt lại ứng dụng và khôi phục tài khoản của bạn để tạo một cơ sở dữ liệu mới và tiếp tục sử dụng {app_name}.

Cảnh báo: Điều này sẽ dẫn đến việc mất tất cả tin nhắn và tệp đính kèm cũ hơn hai tuần." } }, "xh" : { "stringUnit" : { "state" : "translated", - "value" : "Yenza" + "value" : "I-database yakho yohlelo lwe-app ayinakuvisisana nohlobo lwangoku lwe {app_name}. Faka app kwakhona kwaye ubuyisele i-akhawunti yakho ukuphuhlisa database entsha kwaye uqhubeke usebenzisa {app_name}.

Isilumkiso: Oku kuya kubangela ukulahleka kwemiyalezo yonke kunye nezinto ezihambelanayo ezindala kunokuba yiveki ezimbini." } }, "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "创建" + "value" : "您的应用数据库与此版本的{app_name}不兼容。请重新安装应用并恢复您的账户以生成新的数据库并继续使用{app_name}。

警告:该操作将导致两周前的所有消息和附件丢失。" } }, "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "建立" - } - } - } - }, - "creatingCall" : { - "extractionState" : "manual", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "إنشاء مكالمة" - } - }, - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zəng yaradılır" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Creant Trucada" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vytváření hovoru" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kalder op" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Anruf wird erstellt" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Creating Call" - } - }, - "eo" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kreante vokon" - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Creando llamada" - } - }, - "es-ES" : { - "stringUnit" : { - "state" : "translated", - "value" : "Creando llamada" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Création de l'appel" - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "कॉल बनाया जा रहा है" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "Membuat Panggilan" - } - }, - "ka" : { - "stringUnit" : { - "state" : "translated", - "value" : "ირეკება" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "통화 생성 중" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Oproep starten" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tworzenie połączenia" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Создание вызова" - } - }, - "sv-SE" : { - "stringUnit" : { - "state" : "translated", - "value" : "Skapar samtalet" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Arama Oluşturuluyor" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Викликаємо" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Đang tạo cuộc gọi" - } - }, - "zh-CN" : { - "stringUnit" : { - "state" : "translated", - "value" : "正在创建通话" + "value" : "您的應用程式資料庫與此版本的 {app_name} 不相容。重新安裝應用程式並恢復您的帳戶以生成新的資料庫並繼續使用 {app_name}。

警告:這將導致丟失所有兩週前的訊息和附件。" } } } }, - "cut" : { + "databaseOptimizing" : { "extractionState" : "manual", "localizations" : { "af" : { "stringUnit" : { "state" : "translated", - "value" : "Sny" + "value" : "Optimalisering databasis" } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "قص" + "value" : "تحسين قاعدة البيانات" } }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "Kəs" + "value" : "Veri bazası optimallaşdırılır" } }, "bal" : { "stringUnit" : { "state" : "translated", - "value" : "کٹ" + "value" : "ڈیٹابیس شخصکورانی" } }, "be" : { "stringUnit" : { "state" : "translated", - "value" : "Выразаць" + "value" : "Аптымізацыя базы даных" } }, "bg" : { "stringUnit" : { "state" : "translated", - "value" : "Изрязване" + "value" : "Опресняване на базата данни" } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "কাটুন" + "value" : "ডাটাবেস অপ্টিমাইজ করা হচ্ছে" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Retalla" + "value" : "Optimitzant la base de dades" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Vyjmout" + "value" : "Optimalizace databáze" } }, "cy" : { "stringUnit" : { "state" : "translated", - "value" : "Torri" + "value" : "Optimeiddio Cronfa Ddata" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Klip" + "value" : "Optimerer Database" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Ausschneiden" + "value" : "Datenbank wird optimiert" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Αποκοπή" + "value" : "Βελτιστοποίηση Βάσης Δεδομένων" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Cut" + "value" : "Optimizing Database" } }, "eo" : { "stringUnit" : { "state" : "translated", - "value" : "Eltondi" + "value" : "Optimumigante Datumbazon" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Cortar" + "value" : "Optimizando base de datos" } }, "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Cortar" + "value" : "Optimizando base de datos" } }, "et" : { "stringUnit" : { "state" : "translated", - "value" : "Lõika" + "value" : "Andmebaasi optimeerimine" } }, "eu" : { "stringUnit" : { "state" : "translated", - "value" : "Ebaki" + "value" : "Datu Basea Optimizatzen" } }, "fa" : { "stringUnit" : { "state" : "translated", - "value" : "برش" + "value" : "در حال بهینه‌سازی پایگاه داده" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Leikkaa" + "value" : "Optimoidaan tietokanta" } }, "fil" : { "stringUnit" : { "state" : "translated", - "value" : "I-cut" + "value" : "Ina-optimize ang Database" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Couper" + "value" : "Optimisation de la base de données" } }, "gl" : { "stringUnit" : { "state" : "translated", - "value" : "Cortar" + "value" : "Optimizando a Base de Datos" } }, "ha" : { "stringUnit" : { "state" : "translated", - "value" : "Yanke" + "value" : "Ƙarƙare Bayanai" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "גזור" + "value" : "מעדכן מסד נתונים" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "कट" + "value" : "डाटाबेस का अनुकूलन" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Izreži" + "value" : "Optimizacija Baze podataka" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Kivágás" + "value" : "Adatbázis optimalizálása" } }, "hy-AM" : { "stringUnit" : { "state" : "translated", - "value" : "Կտրել" + "value" : "Շտեմարանն օպտիմալացվում է" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Potong" + "value" : "Mengoptimalkan Database" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Taglia" + "value" : "Ottimizzazione del database" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "切り取り" + "value" : "データベースを更新中" } }, "ka" : { "stringUnit" : { "state" : "translated", - "value" : "ამოჭრა" + "value" : "მონაცემთა ბაზის ოპტიმიზაცია" } }, "km" : { "stringUnit" : { "state" : "translated", - "value" : "កាត់" + "value" : "កំពុងធ្វើបច្ចុប្បន្នភាពមូលដ្ខានទិន្នន័យ" } }, "kn" : { "stringUnit" : { "state" : "translated", - "value" : "ಕತ್ತರಿಸಿ" + "value" : "ಡೇಟಾಬೇಸ್ ಆಪ್ಟಿಮೈಸಿಂಗ್" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "잘라내기" + "value" : "데이터베이스 최적화" } }, "ku" : { "stringUnit" : { "state" : "translated", - "value" : "قطع کردن" + "value" : "باشکردنی بنکەدراوە" } }, "ku-TR" : { "stringUnit" : { "state" : "translated", - "value" : "Biqusîne" + "value" : "Danegehê baştir dike" } }, "lg" : { "stringUnit" : { "state" : "translated", - "value" : "temula" - } - }, - "lo" : { - "stringUnit" : { - "state" : "translated", - "value" : "ຕັດ" + "value" : "Okuza Database" } }, "lt" : { "stringUnit" : { "state" : "translated", - "value" : "Iškirpti" + "value" : "Optimizuojama duomenų bazė" } }, "lv" : { "stringUnit" : { "state" : "translated", - "value" : "Izgriezt" + "value" : "Optimizē datu bāzi" } }, "mk" : { "stringUnit" : { "state" : "translated", - "value" : "Сечи" + "value" : "Оптимизирање на базата на податоци" } }, "mn" : { "stringUnit" : { "state" : "translated", - "value" : "Таслах" + "value" : "Мэдээллийн сантай оптимжуулалт хийх" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Potong" + "value" : "Mengoptimumkan Pangkalan Data" } }, "my" : { "stringUnit" : { "state" : "translated", - "value" : "ချည်းကုတ်" + "value" : "ဒေတာဘေ့စ် ဖြည့်စွမ်းနေသည်" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Klipp ut" + "value" : "Optimaliserer databasen" } }, "nb-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Klipp ut" + "value" : "Optimaliserer databasen" } }, "ne-NP" : { "stringUnit" : { "state" : "translated", - "value" : "काट्नुहोस्" + "value" : "डाटाबेस अनकुल पारिदै" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Knippen" + "value" : "Optimaliseer Database" } }, "nn-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Klipp ut" + "value" : "Optimaliserer databasen" } }, "ny" : { "stringUnit" : { "state" : "translated", - "value" : "Dula" + "value" : "Kupanga bwino Database" } }, "pa-IN" : { "stringUnit" : { "state" : "translated", - "value" : "ਕੱਟੋ" + "value" : "ਡਾਟਾਬੇਸ ਦਾ ਅਦਾਕਾਰ ਬਣਾ ਰਿਹਾ ਹੈ" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Wytnij" + "value" : "Optymalizacja bazy danych" } }, "ps" : { "stringUnit" : { "state" : "translated", - "value" : "معلومات له منځه ندي تللي" + "value" : "ډیټابیس سمول" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Cortar" + "value" : "Otimizando base de dados" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Cortar" + "value" : "Otimizando a base de dados" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Decupează" + "value" : "Optimizare bază de date" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Вырезать" + "value" : "Оптимизация базы данных" } }, "sh" : { "stringUnit" : { "state" : "translated", - "value" : "Izreži" + "value" : "Optimizacija baze podataka" } }, "si-LK" : { "stringUnit" : { "state" : "translated", - "value" : "කපන්න" + "value" : "දත්ත සමුදාය ප්‍රශස්ත කිරීම" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Vystrihnúť" + "value" : "Optimalizácia databázy" } }, "sl" : { "stringUnit" : { "state" : "translated", - "value" : "Odreži" + "value" : "Optimizacija podatkovne baze" } }, "sq" : { "stringUnit" : { "state" : "translated", - "value" : "Prije" + "value" : "Optimizimi i Bazës së të Dhënave" } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : "Изрежи" + "value" : "Оптимизација базе података" } }, "sr-Latn" : { "stringUnit" : { "state" : "translated", - "value" : "Iseci" + "value" : "Optimizacija baze podataka" } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Klipp ut" + "value" : "Optimerar databas" } }, "sw" : { "stringUnit" : { "state" : "translated", - "value" : "Kata" + "value" : "Kuhamisha Hifadhidata" } }, "ta" : { "stringUnit" : { "state" : "translated", - "value" : "பிரி" + "value" : "தரவுத்தொகுப்பை மேம்படுத்துகிறது" } }, "te" : { "stringUnit" : { "state" : "translated", - "value" : "కట్ చేయడం" + "value" : "డేటాబేస్‌ను మెరుగ్గాచేయడం" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "ตัด" + "value" : "การเพิ่มประสิทธิภาพฐานข้อมูล" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Kes" + "value" : "Veritabanı En İyi Duruma Getiriliyor" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Вирізати" + "value" : "Оптимізація бази даних" } }, "ur-IN" : { "stringUnit" : { "state" : "translated", - "value" : "کاٹیں" + "value" : "ڈیٹابیس کو بہتر بنانا" } }, "uz" : { "stringUnit" : { "state" : "translated", - "value" : "Kesish" + "value" : "Ma'lumotlar bazasi optimallashtirilmoqda" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Cắt" + "value" : "Đang tối ưu hóa cơ sở dữ liệu" } }, "xh" : { "stringUnit" : { "state" : "translated", - "value" : "Sika" + "value" : "Ukuphucula iDatha" } }, "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "剪切" + "value" : "正在优化数据库" } }, "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "剪下" - } - } - } - }, - "databaseErrorClearDataWarning" : { - "extractionState" : "manual", - "localizations" : { - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bu cihazdan bütün mesajları, qoşmaları və hesab məlumatlarını silmək və yeni hesab yaratmaq istədiyinizə əminsinizmi?" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Estàs segur que vols suprimir tots els missatges, fitxers adjunts i dades del compte d'aquest dispositiu i crear un compte nou?" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Opravdu chcete smazat všechny zprávy, přílohy a data účtu z tohoto zařízení a vytvořit nový účet?" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Er du sikker på, at du vil slette alle beskeder, vedhæftede filer og kontodata fra denne enhed og oprette en ny konto?" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Are you sure you want to delete all messages, attachments, and account data from this device and create a new account?" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Êtes-vous sûr de vouloir supprimer tous les messages, pièces jointes et données de compte de cet appareil et créer un nouveau compte ?" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Biztosan törli az összes üzenetet, mellékletet és fiókadatot erről az eszközről, és új fiókot hoz létre?" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "정말로 이 기기에서 모든 메시지, 첨부 파일, 계정 데이터를 삭제하고 새 계정을 생성하시겠습니까?" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Weet u zeker dat u alle berichten, bijlagen en accountgegevens van dit apparaat wilt verwijderen en een nieuw account wilt aanmaken?" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Czy na pewno chcesz usunąć wszystkie wiadomości, załączniki i dane konta z tego urządzenia i utworzyć nowe konto?" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ви впевнені, що хочете видалити всі повідомлення, вкладення та дані облікового запису з цього пристрою та створити новий обліковий запис?" - } - } - } - }, - "databaseErrorGeneric" : { - "extractionState" : "manual", - "localizations" : { - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "Verilənlər bazası xətası baş verdi.

Problemləri həll etmək üçün paylaşmaq üçün proqram qeydlərinizi ixrac edin. Bu uğursuz olarsa, {app_name} proqramını yenidən quraşdırın və hesabınızı bərpa edin." - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "S'ha produït un error de base de dades.

Exporta els registres de l'aplicació per compartir i resoldre problemes. Si això no té èxit, reinstal·la {app_name} i restaura el teu compte." - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Došlo k chybě databáze.

Exportujte své aplikační logy a sdílejte je pro účely diagnostiky. Pokud to nebude úspěšné, přeinstalujte {app_name} a obnovte svůj účet." - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Der opstod en databasefejl.

Eksporter dine applikationslogs til deling for fejlfinding. Hvis dette ikke lykkes, geninstaller {app_name} og gendan din konto." - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ein Datenbankfehler ist aufgetreten.

Exportiere deine App-Logs, um diese für eine Fehleranalyse zu teilen. Wenn dies nicht erfolgreich ist, installiere die {app_name} neu und stelle deinen Account wieder her." - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "A database error occurred.

Export your application logs to share for troubleshooting. If this is unsuccessful, reinstall {app_name} and restore your account." - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ocurrió un error en la base de datos.

Exporta tus registros de aplicación para compartirlos con fines de resolución de problemas. Si esto no funciona, reinstala {app_name} y restaura tu cuenta." - } - }, - "es-ES" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ocurrió un error en la base de datos.

Exporta tus registros de aplicación para compartirlos con fines de resolución de problemas. Si esto no funciona, reinstala {app_name} y restaura tu cuenta." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Une erreur de base de données s'est produite.

Exportez les journaux de votre application pour les partager à des fins de dépannage. Si cela échoue, réinstallez {app_name} et restaurez votre compte." - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "डेटाबेस त्रुटि हुई है।

समस्या निवारण के लिए अपने एप्लिकेशन लॉग्स को शेयर करने के लिए निर्यात करें। यदि यह असफल रहता है, तो {app_name} को फिर से इंस्टॉल करें और अपना खाता पुनः प्राप्त करें।" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "データベースエラーが発生しました。

\nトラブルシューティングのために、アプリのログをエクスポートして共有してください。この操作が失敗した場合は、{app_name} を再インストールし、アカウントを復元してください。" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "데이터베이스 오류가 발생했습니다.

문제 해결을 위해 애플리케이션 로그를 내보내서 공유하십시오. 실패할 경우, {app_name}을 다시 설치하고 계정을 복원하십시오." - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Er is een databasefout opgetreden.

Exporteer uw applicatie logs om te delen voor probleemoplossing. Als dit niet lukt, installeer {app_name} opnieuw en herstel uw account." - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wystąpił błąd bazy danych.

Wyeksportuj dzienniki aplikacji do udostępnienia w celu rozwiązania problemu. Jeśli to się nie powiedzie, zainstaluj ponownie {app_name} i przywróć swoje konto." - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "A apărut o eroare în baza de date.

Exportați jurnalele aplicației pentru a le partaja în vederea depanării. Dacă nu reușiți, reinstalați {app_name} și restaurați-vă contul." - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Veritabanında bir sorun oluştu.

Sorun giderme için uygulama günlüklerinizi dışa aktarın. Eğer başarısız olunursa {app_name} uygulamasını yeniden yükleyip, hesabınızı geri yükleyin." - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Сталася помилка бази даних.

Експортуйте журнали програми, щоб надати їх для усунення несправностей. Якщо це не допоможе, перевстановіть {app_name} та відновіть свій обліковий запис." - } - }, - "zh-CN" : { - "stringUnit" : { - "state" : "translated", - "value" : "发生数据库错误。

请导出您的应用日志以进行故障排除。如果不成功,请重新安装{app_name}并恢复您的帐户。" - } - } - } - }, - "databaseErrorRestoreDataWarning" : { - "extractionState" : "manual", - "localizations" : { - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bu cihazdan bütün mesajları, qoşmaları və hesab məlumatlarını silmək və hesabınızı şəbəkədən bərpa etmək istədiyinizə əminsinizmi?" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Estàs segur que vols suprimir tots els missatges, fitxers adjunts i dades del compte d'aquest dispositiu i restaurar el teu compte de la xarxa?" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Opravdu chcete smazat všechny zprávy, přílohy a data účtu z tohoto zařízení a obnovit svůj účet ze sítě?" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Er du sikker på, at du vil slette alle beskeder, vedhæftede filer og kontodata fra denne enhed og gendanne din konto fra netværket?" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Are you sure you want to delete all messages, attachments, and account data from this device and restore your account from the network?" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Êtes-vous sûr de vouloir supprimer tous les messages, pièces jointes et données de compte de cet appareil et restaurer votre compte depuis le réseau ?" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Biztosan törli az összes üzenetet, mellékletet és fiókadatot erről az eszközről, és vissza állítja a fiókját a hálózatról?" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "정말로 이 기기에서 모든 메시지, 첨부 파일, 계정 데이터를 삭제하고 네트워크에서 계정을 복원하시겠습니까?" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Weet u zeker dat u alle berichten, bijlagen en accountgegevens van dit apparaat wilt verwijderen en uw account wilt herstellen vanuit het netwerk?" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Czy na pewno chcesz usunąć wszystkie wiadomości, załączniki i dane konta z tego urządzenia i przywrócić konto z sieci?" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ви впевнені, що хочете видалити всі повідомлення, вкладення та дані облікового запису з цього пристрою та відновити свій обліковий запис із мережі?" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bạn có chắc chắn muốn xóa tất cả tin nhắn, tệp đính kèm, và dữ liệu tài khoản khỏi thiết bị này và khôi phục lại tài khoản của bạn từ mạng lưới?" + "value" : "最佳化資料庫中" } } } }, - "databaseErrorTimeout" : { + "debugLog" : { "extractionState" : "manual", "localizations" : { "af" : { "stringUnit" : { "state" : "translated", - "value" : "Ons het opgemerk {app_name} neem lank om te begin.

Jy kan aanhou wag, jou toestel logs uitvoer om te deel vir foutsporing, of probeer om {app_name} te herbegin." + "value" : "Ontfout Log" } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "لقد لاحظنا أن {app_name} يستغرق وقتًا طويلاً لبدء.

يمكنك مواصلة الانتظار، تصدير سجلات الجهاز للمشاركة في استكشاف الأخطاء وإصلاحها، أو محاولة إعادة تشغيل {app_name}." + "value" : "سِجل تصحيح الأخطاء" } }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "{app_name} tətbiqinin başladılmasının çox vaxt apardığına fikir verdik.

Gözləməyə davam edə, problemin aradan qaldırılması üçün cihazınızın jurnallarını xaricə köçürə və ya {app_name}-u yenidən başlatmağa çalışa bilərsiniz." + "value" : "Sazlama jurnalı" } }, "bal" : { "stringUnit" : { "state" : "translated", - "value" : "ما دیستگ کہ {app_name} ءِ بندات کنگ ءَ بازیں وھدے لگ اِیت۔

شما دیم ءَ اوشتات کن اِت، وتی ڈیوائس ءِ لاگاں پہ جیڑہ ءِ گیش ءُ گیوار ءَ شیئر کنگ ءِ ھاترا برآمد کن اِت یا {app_name} ءَ پدا بندات کنگ ءِ جُھد ءَ کن اِت۔" + "value" : "ڈیبگ لگ" } }, "be" : { "stringUnit" : { "state" : "translated", - "value" : "Мы заўважылі, што {app_name} патрабуе шмат часу для запуску.

Вы можаце працягваць чакаць, экспартаваць журналы вашай прылады для спагнання праблем, альбо паспрабаваць перазапусціць {app_name}." + "value" : "Логі адладкі" } }, "bg" : { "stringUnit" : { "state" : "translated", - "value" : "Забелязахме, че стартирането на {app_name} отнема много време.

Можете да продължите да чакате, да експортирате дневници на устройството си, за да ги споделите за отстраняване на неизправности, или да опитате да рестартирате {app_name}." + "value" : "Debug Log" } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "We've noticed {app_name} is taking a long time to start.

You can continue to wait, export your device logs to share for troubleshooting, or try restarting {app_name}." + "value" : "ডিবাগ লগ" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Hem notat que {app_name} està trigant molt a començar.

Podeu continuar esperant, exportar els registres del dispositiu per compartir-los per solucionar problemes, o intentar reiniciar {app_name}." + "value" : "Registre de depuració" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Všimli jsme si, že spuštění aplikace {app_name} trvá dlouho.

Můžete pokračovat v čekání, exportovat logy zařízení k řešení problémů nebo zkusit restartovat {app_name}." + "value" : "Ladící log" } }, "cy" : { "stringUnit" : { "state" : "translated", - "value" : "Rydym wedi sylwi bod {app_name} yn cymryd llawer o amser i ddechrau.

Gallwch barhau i aros, allforio logiau eich dyfais i'w rhannu ar gyfer datrys problemau, neu geisio ailgychwyn {app_name}." + "value" : "Cofnod Dadfygio" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Vi har bemærket, at {app_name} tager lang tid at starte.

Du kan fortsætte med at vente, eksportere dine enhedslogfiler for at dele dem til fejlfinding eller prøve at genstarte {app_name}." + "value" : "Debug log" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Wir haben bemerkt, dass {app_name} lange zum Starten braucht.

Du kannst weiter warten, deine Geräteprotokolle zur Fehlerbehebung exportieren oder versuchen, {app_name} neu zu starten." + "value" : "Debug-Protokoll" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Παρατηρήσαμε ότι το {app_name} χρειάζεται πολύ χρόνο για να ξεκινήσει.

Μπορείτε να συνεχίσετε να περιμένετε, να εξάγετε τα αρχεία καταγραφής της συσκευής σας για να τα μοιραστείτε για την αντιμετώπιση προβλημάτων ή να επανεκκινήσετε το {app_name}." + "value" : "Αρχείο καταγραφής Αποσφαλμάτωσης" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "We've noticed {app_name} is taking a long time to start.

You can continue to wait, export your device logs to share for troubleshooting, or try restarting {app_name}." + "value" : "Debug Log" } }, "eo" : { "stringUnit" : { "state" : "translated", - "value" : "Ni rimarkis ke {app_name} bezonas longe por komenci.

Vi povas daŭrigi atendadon, eksporti viajn aparato-protokolojn por dividi por cimo-serĉado, aŭ reprovi relanĉi {app_name}." + "value" : "Sencimiga protokolo" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Hemos notado que {app_name} está tardando mucho en arrancar.

Puedes esperar, exportar los registros de tu dispositivo para compartirlos para la resolución de problemas, o intentar reiniciar {app_name}." + "value" : "Registro de depuración" } }, "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Hemos notado que {app_name} está tardando mucho en iniciar.

Puedes seguir esperando, exportar los registros de tu dispositivo para compartirlos y solucionar problemas, o intentar reiniciar {app_name}." + "value" : "Registro de depuración" } }, "et" : { "stringUnit" : { "state" : "translated", - "value" : "Oleme märganud, et {app_name} käivitamine võtab kaua aega.

Võite jätkata ootamist, eksportida oma seadme logisid tõrkeotsingu eesmärgil jagamiseks või proovida {app_name}'i taaskäivitamist." + "value" : "Silumislogi" } }, "eu" : { "stringUnit" : { "state" : "translated", - "value" : "{app_name} martxan jartzeko denbora gehiegi hartzen ari dela nabaritu dugu.

Jarrai itzazu itxaroten, esportatu zure gailu-erregistroak konpontzeko partekatzeko edo saiatu {app_name} berrabiarazten." + "value" : "Arazketa egunkaria" } }, "fa" : { "stringUnit" : { "state" : "translated", - "value" : "ما متوجه شده‌ایم که شروع {app_name} زمان زیادی می‌برد.

می‌توانید همچنان منتظر بمانید، گزارش‌های دستگاه خود را برای اشتراک‌گذاری برای عیب‌یابی صادر کنید، یا سعی کنید {app_name} را مجدداً راه‌اندازی کنید." + "value" : "گزارش اشکال‌زدایی" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Huomasimme, että {app_name} käynnistyy hitaasti.

Voit jatkaa odottamista, viedä laitteesi lokit jaettavaksi vianmäärityksessä tai yrittää käynnistää {app_name} uudelleen." + "value" : "Virheenkorjausloki" } }, "fil" : { "stringUnit" : { "state" : "translated", - "value" : "Napansin naming matagal bago mag-start ang {app_name}.

Puwede kang maghintay nalang, i-export ang iyong device logs para i-share para sa troubleshooting, o subukan i-restart ang {app_name}." + "value" : "I-debug ang Log" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Nous avons remarqué que {app_name} met beaucoup de temps à démarrer.

Vous pouvez continuer à attendre, exporter les journaux de votre appareil pour les partager pour le dépannage ou essayer de redémarrer {app_name}." + "value" : "Journal de débogage" } }, "gl" : { "stringUnit" : { "state" : "translated", - "value" : "Notamos que {app_name} está a tardar moito en iniciar.

Podes esperar, exportar os teus rexistros do dispositivo para compartir e solucionar problemas, ou tentar reiniciar {app_name}." + "value" : "Rexistro de depuración" } }, "ha" : { "stringUnit" : { "state" : "translated", - "value" : "Mun lura cewa {app_name} yana ɗaukar dogon lokaci don farawa.

Za ku iya ci gaba da jira, fitar da log ɗin na'urarku don rabawa don magance matsaloli, ko sake farawa {app_name}." + "value" : "Kuskuren Tsari" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "שמנו לב ל-{app_name} לוקח הרבה זמן להתחיל.

תוכל להמשיך להמתין, לייצא את יומני המכשיר שלך לשיתוף לצורך פתרון בעיות, או לנסות להפעיל מחדש את {app_name}." + "value" : "יומן תקלות" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "हमने देखा कि {app_name} प्रारंभ होने में बहुत समय ले रहा है।

आप प्रतीक्षा करना जारी रख सकते हैं, अपने डिवाइस लॉग को निर्यात कर सकते हैं ताकि समस्या निवारण के लिए साझा कर सकें, या {app_name} पुनरारंभ करने का प्रयास कर सकते हैं।" + "value" : "डीबग लॉग" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Primijetili smo da pokretanje {app_name} traje dugo.

Možete nastaviti čekati, izvesti zapise uređaja za dijeljenje radi rješavanja problema ili pokušati ponovo pokrenuti {app_name}." + "value" : "Evidencija o otklanjanju grešaka" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Észrevettük, hogy {app_name} indítása sokáig tart.

Továbbra is várhatsz, exportálhatod az eszköz naplóit a hibaelhárításhoz, vagy megpróbálhatod újraindítani {app_name}-t." + "value" : "Hibakeresési napló" } }, "hy-AM" : { "stringUnit" : { "state" : "translated", - "value" : "Մենք նկատել ենք, որ {app_name} շատ երկար է սկսում աշխատել։

Դուք կարող եք շարունակել սպասել, արտահանել ձեր սարքի տեղեկամատյանները կիսելու համար հետաքրքրությունների համար, կամ փորձել վերագործարկել {app_name}-ը։" + "value" : "Վրիպազերծման մատյան" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Kami menyadari {app_name} membutuhkan waktu lama untuk memulai.

Anda dapat terus menunggu, mengekspor log perangkat Anda untuk dibagikan dalam pemecahan masalah, atau mencoba memulai ulang {app_name}." + "value" : "Catatan Awakutu" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Abbiamo notato che {app_name} ci impiega molto tempo ad avviarsi.

Puoi continuare ad attendere, esportare i log del dispositivo per la risoluzione dei problemi o provare a riavviare {app_name}." + "value" : "Log di debug" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "{app_name}が起動するのに時間がかかっていることを確認しました。

引き続きお待ちいただくか、トラブルシューティングのためにデバイスログをエクスポートして共有するか、{app_name}を再起動してみてください。" + "value" : "デバッグログ" } }, "ka" : { "stringUnit" : { "state" : "translated", - "value" : "გავიგეთ {app_name}-ის გაშვება დიდ დროს იკავებს.

თქვენ შეგიძლიათ დაელოდოთ, ექსპორტირდეთ თქვენი მოწყობილობის ჟურნალები რათა გაუზიაროთ პრობლემების დიაგნოსტირებისთვის, ან სცადოთ {app_name}-ის გადატვირთვა." + "value" : "გამართვის ჟურნალი" } }, "km" : { "stringUnit" : { "state" : "translated", - "value" : "យើងគិតថា {app_name} ពុំអាចចាប់ផ្ដើមបានយ៉ាងចំហរមួយរយៈ

អ្នកអាចរង់ចាំតទៅ ហៅទិន្នន័យឧបករណ៍របស់អ្នកដើម្បីជួយដោះស្រាយ បើទោះអញ្ចឹងក៏ដោយ សាកល្បងចាប់ផ្ដើម {app_name}។" + "value" : "កំណត់ត្រាបញ្ហា" } }, "kn" : { "stringUnit" : { "state" : "translated", - "value" : "ನಾವು ಗಮನಿಸಿದ್ದೇವೆ {app_name} ಆರಂಭಿಸಲು ಹೆಚ್ಚು ಸಮಯ ತೆಗೆದುಕೊಳ್ಳುತ್ತಿದೆ.

ನೀವು ನಿರೀಕ್ಷಿಸಬಹುದು, ನಿಮ್ಮ ಸಾಧನ ರೆಕಾರ್ಡುಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಲು ದೋಷ ಪರಿಹಾರದೊಂದಿಗೆ ಎಕ್ಸ್‌ಪೋರ್ಟ್ ಮಾಡಬಹುದು ಅಥವಾ {app_name} ಪುನಃಪ್ರಾರಂಭಿಸಲು ಪ್ರಯತ್ನಿಸಬಹುದು." + "value" : "ಡೀಬಗ್ ಲಾಗ್" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "{app_name} 이 오랜 시간동안 응답하지 않은 것으로 보입니다.

계속 기다리거나, 기기의 로그를 내보내 도움을 요청하거나, {app_name} 을 재시작 해보세요." + "value" : "디버그 로그" } }, "ku" : { "stringUnit" : { "state" : "translated", - "value" : "دەبینین {app_name} بوونی دواخستنی درێژە.

تۆ دەتوانیت بەردەوام ببهێنین، کۆگایەکانی ئامرازەکەت بەکەشێنی بۆ پشکنین، یان هەوڵبدە بە سەرەکی ={app_name}دوژخستنەوە." + "value" : "لەگەڵکردنی پەیام" } }, "ku-TR" : { "stringUnit" : { "state" : "translated", - "value" : "Em teswîr kirin {app_name} bimînte ye.

Hûn dikarin perê nîşan bibinine rewşa sernîşana hûn perê logoya ten do dike bibirûje, an berbijîhengê {app_name} dîsa baş.herşe cereyan bike." + "value" : "Loga pînekirina çewtiyan" } }, "lg" : { "stringUnit" : { "state" : "translated", - "value" : "Tukuboolabye {app_name} enetwala ebweru okuggwa.

Muliisa kulinda, kusitumidde ebirukanya ebyekusibiza okugabana oludde, oba kwemuddira {app_name}." + "value" : "Log ya Debug" + } + }, + "lo" : { + "stringUnit" : { + "state" : "translated", + "value" : "ບັນທຶກການດັດແກ້" } }, "lt" : { "stringUnit" : { "state" : "translated", - "value" : "Pastebėjome, kad {app_name} užtrunka ilgai paleisti.

Galite toliau laukti, eksportuoti savo įrenginio žurnalus, kad galėtumėte juos pasidalinti dėl trikčių šalinimo, arba bandykite iš naujo paleisti {app_name}." + "value" : "Derinimo žurnalas" } }, "lv" : { "stringUnit" : { "state" : "translated", - "value" : "Mēs esam pamanījuši, ka {app_name} aizņem daudz laika, lai startētu.

Jūs varat turpināt gaidīt, eksportēt sava ierīces žurnālus, lai dalītos problēmas novēršanā, vai pamēģiniet restartēt {app_name}." + "value" : "Atkļūdošanas žurnāls" } }, "mk" : { "stringUnit" : { "state" : "translated", - "value" : "Забележавме дека {app_name} троши многу време за старт.

Можете да продолжите да чекате, да ги извезете дневниците на вашиот уред за решавање на проблеми или да се обидете да го рестартирате {app_name}." + "value" : "Дневник на грешки" } }, "mn" : { "stringUnit" : { "state" : "translated", - "value" : "{app_name} эхлүүлэх их хугацаа зарцуулагдаж байна.

Хүлээсээр байх, төхөөрөмжийн тэмдэглэлийг экспортлоход хуваалцаж асуудал шийдэх, эсвэл {app_name}-г дахин эхлүүлэх боломжтой." + "value" : "Засварын бүртгэл" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Kami perasan {app_name} mengambil masa yang lama untuk bermula.

Anda boleh terus menunggu, eksport log peranti anda untuk perkongsian penyelesaian masalah atau cuba mulakan semula {app_name}." + "value" : "Log Nyahpepijat" } }, "my" : { "stringUnit" : { "state" : "translated", - "value" : "We've noticed {app_name} is taking a long time to start.

You can continue to wait, export your device logs to share for troubleshooting, or try restarting {app_name}." + "value" : "Debug မှတ်တမ်း" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Vi har lagt merke til at {app_name} tar lang tid å starte.

Du kan vente videre, eksportere loggene på enheten din for å dele for feilsøking, eller prøve å starte {app_name} på nytt." + "value" : "Feilsøkingslogg" } }, "nb-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Vi har lagt merke til at {app_name} tar lang tid å starte.

Du kan fortsette å vente, eksportere enhetsloggene for å dele for feilsøking, eller prøve å starte {app_name} på nytt." + "value" : "Feilsøkingslogg" } }, "ne-NP" : { "stringUnit" : { "state" : "translated", - "value" : "हामीले नोटिस गर्यौं कि {app_name} सुरु हुन धेरै समय लिइरहेको छ।

तपाईं प्रतीक्षा जारी राख्न सक्नुहुन्छ, समस्या समाधानको लागि तपाईंको उपकरणको लक निकाल्न साझा गर्न सक्नुहुन्छ, वा {app_name} पुन: सुरु गर्न प्रयास गर्न सक्नुहुन्छ।" + "value" : "डिबग लग" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "We hebben gemerkt dat {app_name} veel tijd nodig heeft om op te starten.

U kunt doorgaan met wachten, uw apparaatlogs exporteren om te delen voor probleemoplossing, of proberen {app_name} opnieuw op te starten." + "value" : "Foutopsporingslogboek" } }, "nn-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Vi har merka at {app_name} tar lang tid på å starte.

Du kan vente, eksportere loggar frå eininga di for deling til feilsøking, eller prøve å starte {app_name} på nytt." + "value" : "Feilsøkingslogg" } }, "ny" : { "stringUnit" : { "state" : "translated", - "value" : "Timazindikira {app_name} kutenga nthawi kuti ayambe.

Inu mungapitirize kudikira, kutulutsira chipangizo malipoti kuti azipeza mavuto, kapena yesani kuyambiranso {app_name}." + "value" : "Lowani mu Debug Log" } }, "pa-IN" : { "stringUnit" : { "state" : "translated", - "value" : "ਅਸੀਂ ਨੋਟ ਕੀਤਾ ਹੈ ਕਿ {app_name} ਨੂੰ ਸ਼ੁਰੂ ਕਰਨ ਵਿੱਚ ਬਹੁਤ ਸਮਾਂ ਲੱਗ ਰਿਹਾ ਹੈ।

ਤੁਸੀਂ ਇੰਤਜ਼ਾਰ ਕਰ ਸਕਦੇ ਹੋ, ਆਪਣੇ ਔਜ਼ਾਰ ਦੇ ਲੌਗ ਨਿਕਾਸੀ ਕਰ ਸਕਦੇ ਹੋ ਚੋਣ ਕਰਨ ਲਈ Troubleshooting ਲਈ ਸਾਂਝੇ ਕਰਨ ਲਈ, ਜਾਂ {app_name} ਨੂੰ ਮੁੜ ਸ਼ੁਰੂ ਕਰਨ ਦੀ ਕੋਸ਼ਿਸ਼ ਕਰੋ।" + "value" : "ਡਿਬੱਗ ਲਾਗ" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Zauważyliśmy, że uruchomienie aplikacji {app_name} zajmuje dużo czasu.

Możesz kontynuować oczekiwanie, wyeksportować dzienniki urządzenia do udostępnienia w celu rozwiązania problemów lub spróbować ponownie uruchomić aplikację {app_name}." + "value" : "Dziennik debugowania" } }, "ps" : { "stringUnit" : { "state" : "translated", - "value" : "موږ ولیدل چې {app_name} د پیل کولو لپاره ډیر وخت نیسي.

تاسو کولی شئ انتظار ته دوام ورکړئ، د ستونزو د حل لپاره د شریکولو لپاره د خپل وسیله لاګ صادر کړئ، یا د {app_name} بیا پیلولو هڅه وکړئ." + "value" : "دیباگ لاګ" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Observamos que {app_name} está demorando muito para iniciar.

Você pode continuar esperando, exportar os logs do seu dispositivo para compartilhar para solução de problemas, ou tentar reiniciar o {app_name}." + "value" : "Log de Depuração" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Percebemos que {app_name} está a demorar muito a iniciar.

Pode continuar a esperar, exportar os registos do seu dispositivo e partilhar para analisarmos o problema, ou tentar reiniciar o {app_name}." + "value" : "Log de debug" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Am observat că {app_name} durează mult timp să pornească.

Puteți continua să așteptați, să exportați jurnalele dispozitivului pentru a le partaja pentru depanare sau să încercați să reporniți {app_name}." + "value" : "Jurnal Depanare" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Мы заметили, что {app_name} занимает много времени для запуска.

Вы можете продолжить ждать, экспортировать журналы вашего устройства для устранения неполадок или попробовать перезапустить {app_name}." + "value" : "Журнал отладки" } }, "sh" : { "stringUnit" : { "state" : "translated", - "value" : "Primetili smo da pokretanje {app_name} traje dugo.

Možete nastaviti da čekate, izvesti logove uređaja za deljenje radi otklanjanja grešaka, ili pokušati ponovo pokrenuti {app_name}." + "value" : "Debug log" } }, "si-LK" : { "stringUnit" : { "state" : "translated", - "value" : "අප සැකසූවානම් {app_name} ආරම්භ කිරීමට වැඩි කාලයක් ගත වන බව දැක ඇත.

ඔබට සිතියෙන්නේ, උපකරණ ලොග් දත්ත අපට යැවිය හැකි, නැවත {app_name} ආරම්භ කර බලන්න." + "value" : "දෝශ නිරාකරණ ලොගය" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Všimli sme si, že {app_name} sa dlho spúšťa.

Môžete pokračovať v čakaní, exportovať záznamy z vášho zariadenia kvôli riešeniu problémov alebo skúsiť reštartovať {app_name}." + "value" : "Ladiaci log" } }, "sl" : { "stringUnit" : { "state" : "translated", - "value" : "Opažamo, da {app_name} potrebuje dolgo časa za zagon.

Lahko nadaljujete s čakanjem, izvozite dnevniške datoteke naprave za odpravljanje težav ali poskusite znova zagnati {app_name}." + "value" : "Sistemska zabeležba" } }, "sq" : { "stringUnit" : { "state" : "translated", - "value" : "Ne kemi vërejtur që {app_name} po merr shumë kohë për tu nisur.

Ju mund të prisni, eksportoni regjistrat e pajisjes suaj për ndihmë në zgjidhjen e problemeve, ose provoni të rinisni {app_name}." + "value" : "Regjistër Diagnostikimesh" } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : "Приметили смо да {app_name} треба дуго времена да се покрене.

Можете наставити да чекате, извести дневнике уређаја да их делите за решавање проблема или покушати поново покренути {app_name}." + "value" : "Извештај о грешкама" } }, "sr-Latn" : { "stringUnit" : { "state" : "translated", - "value" : "Primetili smo da aplikaciji {app_name} treba dugo vremena da se pokrene.

Možete da nastavite da čekate, izvezete logove uređaja za deljenje radi rešavanja problema ili pokušate ponovo da pokrenete {app_name}." + "value" : "Izveštaj o greškama" } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Vi har märkt att {app_name} tar lång tid att starta.

Du kan fortsätta vänta, exportera dina felsökningsloggar för att dela för felsökning, eller försöka starta om {app_name}." + "value" : "Felsökningslogg" } }, "sw" : { "stringUnit" : { "state" : "translated", - "value" : "Tumeona {app_name} inachukua muda mrefu kuanza.

Unaweza kuendelea kusubiri, kuhamisha kumbukumbu za kifaa chako kushiriki kwa kutatua shida, au jaribu kuanzisha {app_name} upya." + "value" : "Logi ya Kurekebisha" } }, "ta" : { "stringUnit" : { "state" : "translated", - "value" : "{app_name} தொடங்க அதிக நேரம் ஆகிறதே எனக் காணப்படுகின்றது.

நீங்கள் தொடர்ந்தும் காத்திருக்கலாம், உங்களின் சாதன பதிவு பட்டியலை வெளியிட்டு பகிரவும் அல்லது {app_name} புனரஇயக்க முயற்சிக்கவும்." + "value" : "பிழைத்திருத்த பதிவு" } }, "te" : { "stringUnit" : { "state" : "translated", - "value" : "మేము గమనించాము {app_name} ప్రారంభమవ్వడానికి చాలా సమయం పడుతోంది.

మీరు వేచి ఉండవచ్చు, సమస్యను నిర్ధారించడానికి పరికరం లాగ్‌లను ఎగుమతి చేసి షేర్ చేయవచ్చు లేదా {app_name} రీస్టార్ట్ చేయవచ్చు." + "value" : "డీబగ్ లాగ్" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "เราได้สังเกตว่า {app_name} ใช้เวลานานในการเริ่มต้น

คุณสามารถรอต่อไป ส่งออกบันทึกอุปกรณ์ของคุณเพื่อแบ่งปันเพื่อแก้ไขปัญหา หรือลองรีสตาร์ท {app_name}" + "value" : "Debug Log" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "{app_name} uygulamasının başlatılması uzun sürüyor fark ettik.

Beklemeye devam edebilir, cihaz günlüklerinizi paylaşmak için dışa aktarabilir veya {app_name} yeniden başlatmayı deneyebilirsiniz." + "value" : "Hata Ayıklama Raporu" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Ми помітили, що {app_name} довго запускається.

Ви можете продовжити чекати, експортувати журнали вашого пристрою для аналізу або спробувати перезапустити {app_name}." + "value" : "Журнал відладки" } }, "ur-IN" : { "stringUnit" : { "state" : "translated", - "value" : "ہم نے دیکھا ہے کہ {app_name} کو شروع ہونے میں کافی وقت لگ رہا ہے۔

آپ انتظار کرنا جاری رکھ سکتے ہیں، مسئلہ حل کرنے کے لیے اشتراک کرنے کے لیے اپنے آلے کے لاگز کو برآمد کر سکتے ہیں، یا {app_name} کو دوبارہ شروع کرنے کی کوشش کر سکتے ہیں۔" + "value" : "Debug Log" } }, "uz" : { "stringUnit" : { "state" : "translated", - "value" : "{app_name} ishga tushishiga koʻp vaqt ketayotganini aniqladik.

Kutishda davom etishingiz, muammolarni bartaraf etish uchun qurilma jurnallarini baham koʻrish uchun eksport qilishingiz yoki {app_name} ilovasini qayta ishga tushirishga urinib koʻrishingiz mumkin." + "value" : "Tahlil yozuvi" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Chúng tôi nhận thấy {app_name} mất nhiều thời gian để khởi động.

Bạn có thể tiếp tục chờ, xuất nhật ký thiết bị để chia sẻ hỗ trợ khắc phục sự cố, hoặc thử khởi động lại {app_name}." + "value" : "Nhật ký sửa lỗi" } }, "xh" : { "stringUnit" : { "state" : "translated", - "value" : "Siqaphele ukuba i-{app_name} ithatha ixesha elide ukuqala.

Ungaqhubeka ulinde, uthumele iingxelo zesixhobo sakho ukwabelana ngazo ukulungisa iingxaki, okanye uzame ukuqala ngokutsha {app_name}." + "value" : "Ilogi yeSipseko" } }, "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "我们注意到{app_name}启动时间过长。

您可以选择继续等待,导出设备日志以分享故障排除,或尝试重新启动{app_name}。" + "value" : "调试日志" } }, "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "我們注意到 {app_name} 啟動時間過長。

您可以繼續等待,匯出您的設備日誌以便排除故障,或者嘗試重新啟動 {app_name}。" + "value" : "除錯紀錄" } } } }, - "databaseErrorUpdate" : { + "decline" : { "extractionState" : "manual", "localizations" : { "af" : { "stringUnit" : { "state" : "translated", - "value" : "Jou app databasis is onversoenbaar met hierdie weergawe van {app_name}. Herinstalleer die app en herstel jou rekening om 'n nuwe databasis te genereer en voort te gaan met die gebruik van {app_name}.

Waarskuwing: Dit sal lei tot die verlies van alle boodskappe en aanhegsels ouer as twee weke." + "value" : "Weier" } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "قاعدة بيانات تطبيقك غير متوافقة مع هذا الإصدار من {app_name}. أعد تثبيت التطبيق واستعد حسابك لإنشاء قاعدة بيانات جديدة ومتابعة استخدام {app_name}.

تحذير: سيؤدي هذا إلى فقدان جميع الرسائل والمرفقات التي يزيد عمرها عن أسبوعين." + "value" : "رفض" } }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "Tətbiqinizin databazası {app_name} tətbiqinin versiyası ilə uyumlu deyil. Yeni bir databaza yaratmaq və {app_name} istifadə etməyə davam etmək üçün tətbiqi yenidən quraşdırın və hesabınızı bərpa edin.

Xəbərdarlıq: Bu, iki həftədən köhnə olan bütün mesajların və qoşmaların itkisi ilə nəticələnəcək." + "value" : "Rədd et" } }, "bal" : { "stringUnit" : { "state" : "translated", - "value" : "{app_name} کس ای ورژنئے نکہ ایپ دیټابیس ناہم آهن. اِیپ نوک بزا من اَکاونٹ بازگری کن تا پن نوک دیټابیس پیدا بکن تا {app_name} دابی مرت استفاده بکن.

چیتپا: ماہیت زامبلاونکین دو ہفتہ ناہند، تمام پیامانءِ و اٹیچمنٹاں گم بیت." + "value" : "نامنظور" } }, "be" : { "stringUnit" : { "state" : "translated", - "value" : "Ваша база даных прыкладання несумяшчальная з гэтай версіяй {app_name}. Пераўсталюйце прыкладанне і аднавіце ўліковы запіс, каб стварыць новую базу даных і працягнуць выкарыстанне {app_name}.

Увага: гэта прывядзе да страты ўсіх паведамленняў і ўкладанняў старэйшых за два тыдні." + "value" : "Адхіліць" } }, "bg" : { "stringUnit" : { "state" : "translated", - "value" : "Вашата база данни на приложението е несъвместима с тази версия на {app_name}. Инсталирайте повторно приложението и възстановете своя акаунт, за да генерирате нова база данни и да продължите да използвате {app_name}.

Внимание: Това ще доведе до загуба на всички съобщения и прикачени файлове по-стари от две седмици." + "value" : "Отхвърляне" } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "আপনার অ্যাপ্লিকেশন ডাটাবেস {app_name} এর এই সংস্করণের সাথে অসঙ্গতিপূর্ণ। অ্যাপ পুনরায় ইনস্টল করুন এবং আপনার অ্যাকাউন্ট পুনরুদ্ধার করুন একটি নতুন ডাটাবেস তৈরি করতে এবং {app_name} ব্যবহার করতে থাকুন।

সতর্কতা: এর ফলে আপনার সমস্ত বার্তা এবং সংযুক্তিগুলি দুই সপ্তাহের বেশি পুরানো হারিয়ে যাবে।" + "value" : "অগ্রাহ্য করুন" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "La base de dades de la vostra aplicació no és compatible amb aquesta versió de {app_name}. Reinstal·leu l'aplicació i restaureu el vostre compte per generar una nova base de dades i continuar utilitzant {app_name}.

Avís: Això donarà lloc a la pèrdua de tots els missatges i adjunts anteriors a dues setmanes." + "value" : "Declineu" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Databáze vaší aplikace není kompatibilní s touto verzí {app_name}. Přeinstalujte aplikaci a obnovte svůj účet pro vytvoření nové databáze a pokračování v používání {app_name}.

Varování: To povede ke ztrátě všech zpráv a příloh starších než dva týdny." + "value" : "Odmítnout" } }, "cy" : { "stringUnit" : { "state" : "translated", - "value" : "Mae eich cronfa ddata ap yn anghydnaws â'r fersiwn hon o {app_name}. Ailosodwch yr ap a darganfod eich cyfrif i greu cronfa ddata newydd a pharhau i ddefnyddio {app_name}.

Rhybudd: Bydd hyn yn arwain at golli’r holl negeseuon a’r atodiadau sy’n hŷn na phythefnos." + "value" : "Gwrthod" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Din app-database er inkompatibel med denne version af {app_name}. Geninstaller appen og gendan din konto for at generere en ny database og fortsætte med at bruge {app_name}.

Advarsel: Dette vil resultere i tab af alle beskeder og vedhæftninger, der er ældre end to uger." + "value" : "Afvis" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Deine App-Datenbank ist mit dieser Version von {app_name} nicht kompatibel. Installiere die App neu und stelle deinen Account wieder her, um eine neue Datenbank zu erstellen und {app_name} weiter zu verwenden.

Warnung: Dadurch gehen alle Nachrichten und Anhänge verloren, die älter als zwei Wochen sind." + "value" : "Ablehnen" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Η βάση δεδομένων της εφαρμογής σας δεν είναι συμβατή με αυτήν την έκδοση του {app_name}. Επανεγκαταστήστε την εφαρμογή και αποκαταστήστε τον λογαριασμό σας για να δημιουργήσετε μια νέα βάση δεδομένων και να συνεχίσετε να χρησιμοποιείτε το {app_name}.

Προειδοποίηση: Αυτό θα έχει ως αποτέλεσμα την απώλεια όλων των μηνυμάτων και των συνημμένων που είναι παλαιότερα των δύο εβδομάδων." + "value" : "Απόρριψη" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Your app database is incompatible with this version of {app_name}. Reinstall the app and restore your account to generate a new database and continue using {app_name}.

Warning: This will result in the loss of all messages and attachments older than two weeks." + "value" : "Decline" } }, "eo" : { "stringUnit" : { "state" : "translated", - "value" : "Via aplikaĵa datumbazo ne kongruas kun ĉi tiu versio de {app_name}. Reinstalu la aplikaĵon kaj restarigu vian konton por generi novan datumbazon kaj daŭrigi la uzadon de {app_name}.

Averto: Ĉi tio rezultos en la perdo de ĉiuj mesaĝoj kaj aldonaĵoj pli aĝaj ol du semajnoj." + "value" : "Malakcepti" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Tu base de datos de la app es incompatible con esta versión de {app_name}. Reinstala la app y restaura tu cuenta para generar una nueva base de datos y continuar usando {app_name}.

Advertencia: Esto resultará en la pérdida de todos los mensajes y archivos adjuntos mayores a dos semanas." + "value" : "Rechazar" } }, "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Su base de datos de la aplicación no es compatible con esta versión de {app_name}. Reinstala la aplicación y restaura tu cuenta para generar una nueva base de datos y continuar usando {app_name}.

Advertencia: Esto resultará en la pérdida de todos los mensajes y archivos adjuntos anteriores a dos semanas." + "value" : "Rechazar" } }, "et" : { "stringUnit" : { "state" : "translated", - "value" : "Teie rakenduse andmebaas ei ühildu selle {app_name} versiooniga. Installige rakendus uuesti ja taastage oma konto, et luua uus andmebaas ja jätkata {app_name} kasutamist.

Hoiatus: See kaotab kõik vanemad kui kaks nädalat sõnumid ja manused." + "value" : "Keeldu" } }, "eu" : { "stringUnit" : { "state" : "translated", - "value" : "Zure aplikazio-datubasea ez da bateragarria {app_name} bertsio honekin. Berrinstalatu aplikazioa eta leheneratu zure kontua datubase berri bat sortzeko eta {app_name} erabiltzen jarraitzeko.

Abisua: Honek bi astetik gorako mezu eta eranskinen galera eragingo du." + "value" : "Baztertu" } }, "fa" : { "stringUnit" : { "state" : "translated", - "value" : "پایگاه داده برنامه شما با این نسخه از {app_name} سازگار نیست. برنامه را دوباره نصب کنید و حساب خود را بازیابی کنید تا یک پایگاه داده جدید ایجاد کنید و به استفاده از {app_name} ادامه دهید.

هشدار: این باعث از دست رفتن همه پیام‌ها و پیوست‌های قدیمی‌تر از دو هفته می‌شود." + "value" : "رد تماس" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Sovellustietokanta ei ole yhteensopiva tämän {app_name} version kanssa. Asenna sovellus uudelleen ja palauta tilisi, jotta voit luoda uuden tietokannan ja jatkaa {app_name} käyttöä.

Varoitus: Tämä johtaa yli kaksi viikkoa vanhojen viestien ja liitteiden menetykseen." + "value" : "Kieltäydy" } }, "fil" : { "stringUnit" : { "state" : "translated", - "value" : "Ang iyong app database ay hindi compatible sa bersyon na ito ng {app_name}. I-reinstall ang app at i-restore ang iyong account upang makabuo ng bagong database at ipagpatuloy ang paggamit ng {app_name}.

Babala: Ito ay magreresulta sa pagkawala ng lahat ng mensahe at attachments na higit sa dalawang linggo." + "value" : "Tanggihan" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "La base de données de votre application est incompatible avec cette version de {app_name}. Réinstallez l'application et restaurez votre compte pour générer une nouvelle base de données et continuer à utiliser {app_name}.

Avertissement : Cela entraînera la perte de tous les messages et pièces jointes datant de plus de deux semaines." + "value" : "Refuser" } }, "gl" : { "stringUnit" : { "state" : "translated", - "value" : "A base de datos da túa aplicación non é compatible con esta versión de {app_name}. Reinstala a aplicación e restaura a túa conta para xerar unha nova base de datos e continuar usando {app_name}.

Advertencia: Isto resultará na perda de todas as mensaxes e adxuntos anteriores a dúas semanas." + "value" : "Rexeitar" } }, "ha" : { "stringUnit" : { "state" : "translated", - "value" : "Bayanan aikace-aikacen ku ba su dace da wannan sigar {app_name}. Sake shigar da aikace-aikacen kuma dawo da asusunka don samar da sabon database kuma ci gaba da amfani da {app_name}.

Gargadi: Wannan zai haifar da rasa duk sakonni da fayiloli fiye da makonni biyu." + "value" : "Ki ɗiɗe" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "מאגר נתונים של האפליקציה שלך אינו תואם לגרסה זו של {app_name}. התקן מחדש את האפליקציה ושחזר את החשבון שלך כדי ליצור מאגר נתונים חדש ולהמשיך להשתמש ב-{app_name}.

אזהרה: פעולה זו תשאיר את כל ההודעות והצרופות הישנות משבועיים." + "value" : "דחה" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "{app_name} का यह संस्करण आपके ऐप डेटाबेस के साथ असंगत है। ऐप को पुन: स्थापित करें और अपना खाता पुनर्स्थापित करें ताकि नया डेटाबेस उत्पन्न हो सके और {app_name} का उपयोग जारी रख सकें।

चेतावनी: इससे दो सप्ताह से पुराने सभी संदेश और संलग्नक खो जाएंगे।" + "value" : "अस्वीकृत करें" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Vaša aplikacijska baza podataka nije kompatibilna s ovom verzijom {app_name}. Ponovno instalirajte aplikaciju i vratite svoj račun kako biste generirali novu bazu podataka i nastavili koristiti {app_name}.

Upozorenje: Ovo će rezultirati gubitkom svih poruka i privitaka starijih od dva tjedna." + "value" : "Odbaci" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Az alkalmazás adatbázisa nem kompatibilis a {app_name} jelenlegi verziójával. Telepítsd újra az alkalmazást, és állítsd vissza fiókját egy új adatbázis létrehozásához és a {app_name} további használathoz.

Figyelmeztetés: Ez minden két hétnél régebbi üzenet és melléklet elvesztését eredményezi." + "value" : "Elutasítás" } }, "hy-AM" : { "stringUnit" : { "state" : "translated", - "value" : "Ձեր հավելվածի տվյալների բազան համադրված չէ այս {app_name} տարբերակի հետ։ Վերակայանացրեք հավելվածը և վերագործարկեք ձեր հաշիվը նոր տվյալների բազա ստեղծելու և {app_name} շարունակելու համար։

Զգուշացում: Սա կհանգեցնի բոլոր հաղորդագրությունների և կցաթղթերի երկու շաբաթից ավելի հնության կորստին։" + "value" : "Մերժել" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Database aplikasi Anda tidak kompatibel dengan versi {app_name} ini. Instal ulang aplikasi dan pulihkan akun Anda untuk menghasilkan database baru dan terus menggunakan {app_name}.

Peringatan: Ini akan menyebabkan hilangnya semua pesan dan lampiran yang lebih dari dua minggu." + "value" : "Tolak" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Il database della tua app non è compatibile con questa versione di {app_name}. Reinstalla l'app e ripristina il tuo account per generare un nuovo database e continuare a utilizzare {app_name}.

Attenzione: Questo comporterà la perdita di tutti i messaggi e di tutti gli allegati più vecchi di due settimane." + "value" : "Rifiuta" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "お使いのアプリデータベースはこのバージョンの {app_name} と互換性がありません。アプリを再インストールしてアカウントを復元し、新しいデータベースを生成して {app_name} を使用し続けてください。

警告: これにより、2週間以上前のすべてのメッセージと添付ファイルが失われます。" + "value" : "拒否" } }, "ka" : { "stringUnit" : { "state" : "translated", - "value" : "თქვენი აპლიკაციის მონაცემთა ბაზა არ შეესაბამება {app_name}-ის ამ ვერსიას. ხელახლა დააინსტალირეთ აპლიკაცია და აღადგინეთ თქვენი ანგარიში ახალი მონაცემთა ბაზის შესაქმნელად და {app_name} გამოყენების გაგრძელებისთვის.

გაფრთხილება: ეს გამოიწვევს ყველა მესიჯის და ფაილის დაკარგვას, რომლებიც ორ კვირაზე მეტი ხნისაა." + "value" : "უარყოფა" } }, "km" : { "stringUnit" : { "state" : "translated", - "value" : "ឃ្លាំងទិន្នន័យរបស់កម្មវិធីរបស់អ្នកមិនអាចបើកជាមួយ {app_name} នេះទេ។ កញ្ចួញកម្មវិធីឡើងវិញ ហើយស្ដារគណនីរបស់អ្នកដើម្បីបង្កើតឃ្លាំងទិន្នន័យថ្មី និង​បន្តប្រើ {app_name}។

ការព្រមាន៖ នេះនឹងលុបបាត់ជាស្ថាពរ នូវសារទាំងអស់ និងឯកសារភ្ជាប់ដែលអាយុចាស់ជាងពីរសប្ដាហ៍។" + "value" : "បដិសេធ" } }, "kn" : { "stringUnit" : { "state" : "translated", - "value" : "ನಿಮ್ಮ ಅಪ್ಲಿಕೇಶನ್ ಡೇಟಾಬೇಸ್ {app_name} ಆವೃತ್ತಿಯೊಂದಿಗೆ ಅನುರೂಪವಲ್ಲ. ಹೊಸ ಡೇಟಾಬೇಸ್ನನ್ನು ರಚಿಸಲು ಅಪ್ಲಿಕೇಶನನನ್ನು ಪುನಃಹುಹಾಯಿಸಿ ಮತ್ತು ನಿಮ್ಮ ಖಾತೆಯನ್ನು ಪುನಃಸ್ಥಾಪಿಸಿ ಮತ್ತು {app_name} ಬಳಸಿದ್ದು ನಿರಂತರ ಪ್ರದರ್ಶಿಸಲು.

ಎಚ್ಚರಿಕೆ: ಇದು ಎರಡು ವಾರಗಳಿಗಿಂತ ಹೆಚ್ಚಿನ ವಯಸ್ಸಿನ ಎಲ್ಲಾ ಸಂದೇಶಗಳು ಮತ್ತು ಜೊತೆಯುಡುಕಳವನ್ನು ಕಳೆದುಹಾಕುತ್ತದೆ." + "value" : "ನಿರಾಕರಿಸಿ" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "{app_name}의 이 버전은 앱 데이터베이스와 호환되지 않습니다. 앱을 재설치하고 계정을 복원하여 새 데이터베이스를 생성하고 {app_name}을 계속 사용하십시오.

경고: 이렇게 하면 2주 이상 된 모든 메시지와 첨부 파일이 손실됩니다." + "value" : "거절" } }, "ku" : { "stringUnit" : { "state" : "translated", - "value" : "بنکەی زانیاری بەرنامەکەت ناگەلێرد بەم وه‌ شەپکنیکی {app_name}. بەخێربەستەوە بەرنامەکە و ئەژمێرت پاشەکەوتی بکەیت بۆ بەکاربردنی {app_name}.

ئاگاداری: ئەمە دوای دیترین مەکموو بەرەوکردنەکان و هاوبەشەکان بەکارە لأختە دەکاتە دوو هەفتە ئەگەری پەیامەکان." + "value" : "رەتی کردن" } }, "ku-TR" : { "stringUnit" : { "state" : "translated", - "value" : "بنکەی زانیاری بەرنامەکەت ناگەلێرد بەم وه‌ شەپکنیکی {app_name}. بەخێربەستەوە بەرنامەکە و ئەژمێرت پاشەکەوتی بکەیت بۆ بەکاربردنی {app_name}.

ئاگاداری: ئەمە دوای دیترین مەکموو بەرەوکردنەکان و هاوبەشەکان بەکارە لأختە دەکاتە دوو هەفتە ئەگەری پەیامەکان." + "value" : "Red bike" } }, "lg" : { "stringUnit" : { "state" : "translated", - "value" : "Database y’app yo si yagwanira ne verison ya {app_name}. Tegyamu app ne kuba okwongera ku Account yo okwongera okukozesa {app_name}.

Warning: Kino kyakuleeteka okufiirwa kwa bubaka bwona n’empapula ezisazeewo ezinyuuse emywaka ebiri ebalagala." + "value" : "Gana" + } + }, + "lo" : { + "stringUnit" : { + "state" : "translated", + "value" : "ປະຕິເສດ" } }, "lt" : { "stringUnit" : { "state" : "translated", - "value" : "Jūsų programos duomenų bazė nesuderinama su šia {app_name} versija. Iš naujo įdiekite programą ir atkurkite savo paskyrą, kad sugeneruotumėte naują duomenų bazę ir toliau naudotumėte {app_name}.

Įspėjimas: Dėl to visos pranešimų ir priedų, senesnių nei dvi savaitės, bus prarastos." + "value" : "Atmesti" } }, "lv" : { "stringUnit" : { "state" : "translated", - "value" : "Jūsu lietotnes datubāze nav saderīga ar šo {app_name} versiju. Pārsūtiet lietotni un atjaunojiet savu kontu, lai izveidotu jaunu datubāzi un turpinātu izmantot {app_name}.

Brīdinājums: Tas rezultēsies visu ziņojumu un pielikumu, kas ir vecāki par divām nedēļām, zaudēšanā." + "value" : "Noraidīt" } }, "mk" : { "stringUnit" : { "state" : "translated", - "value" : "Вашата база на податоци на апликацијата не е компатибилна со оваа верзија на {app_name}. Повторно инсталирајте ја апликацијата и вратете го вашиот профил за да генерирате нова база на податоци и да продолжите да го користите {app_name}.

Предупредување: Ова ќе резултира со губење на сите пораки и прикачувања постари од две недели." + "value" : "Одбиј" } }, "mn" : { "stringUnit" : { "state" : "translated", - "value" : "Таны програмын өгөгдлийн сан {app_name}-ийн энэ хувилбарт нийцэхгүй байна. Програмыг дахин суулгаж, профайлыг сэргээснээр шинэ өгөгдлийн сан үүсгэж, {app_name} ашиглах боломжтой болно.

Сануулга: Энэ нь хоёр долоо хоногоос дээш хугацаатай бүх мессежүүд болон хавсралтууд алдагдах болно." + "value" : "Татгалзах" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Pangkalan data aplikasi anda tidak serasi dengan versi {app_name} ini. Pasang semula aplikasi ini dan pulihkan akaun anda untuk menjana pangkalan data baru dan terus menggunakan {app_name}.

Amaran: Ini akan menyebabkan kehilangan semua mesej dan lampiran yang lebih lama daripada dua minggu." + "value" : "Tolak" } }, "my" : { "stringUnit" : { "state" : "translated", - "value" : "သင့်အက်ပ်ဒေတာဘေ့စ်သည် {app_name} ၏ ဤဗားရှင်းနှင့် မက်စ်ပေါ်နိုင်ပါ။ အက်ပ်ကို ပြန်ထည့်သွင်းပြီး သင့်အကောင့်ကို ပြန်လည်ထားပြီး {app_name} ကို ဆက်လက်သုံးဆောင်ရန် အချက်ပြပီးနောက် ငါးပတ်အတွင်းလက်ရှိမက်ဆေ့ခ်ျနှင့်လိုက်ဖက်မှုအတင်ပျောက်သွားနိုင်သည်။

သတိပေးချက်: ဤလုပ်ဆောင်ချက်ကြောင့် အဆိုပါကာလထက်ပိုကြာသော မက်ဆေ့ခ်ျများနှင့် ပျက်စီးပိုင်ဆိုင်မှုများ ပျောက်ဆုံးသွားမည်။" + "value" : "ငြင်းပယ်" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "App-databasen din er inkompatibel med denne versjonen av {app_name}. Installer appen på nytt og gjenopprett kontoen din for å generere en ny database og fortsette å bruke {app_name}.

Advarsel: Dette vil resultere i tap av alle meldinger og vedlegg eldre enn to uker." + "value" : "Avvis" } }, "nb-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Database til appen din er inkompatibel med denne versjonen av {app_name}. Installer appen på nytt og gjenopprett kontoen din for å generere en ny database og fortsette å bruke {app_name}.

Advarsel: Dette vil resultere i tap av alle meldinger og vedlegg eldre enn to uker." + "value" : "Avvis" } }, "ne-NP" : { "stringUnit" : { "state" : "translated", - "value" : "तपाईंको एप डाटाबेस {app_name} को यो संस्करणसँग असंगत छ। एपलाई पुनः स्थापना गर्नुहोस् र आफ्नो खाता पुनर्स्थापना गर्नुहोस् नयाँ डाटाबेस सिर्जना गर्न र {app_name} प्रयोग गर्न जारी राख्न।

चेतावनी: यसले दुई हप्ता भन्दा पुरानो सबै सन्देशहरू र अट्याचमेन्टहरूको हानि हुनेछ।" + "value" : "अस्वीकार गर्नुहोस्" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Uw app-database is niet compatibel met deze versie van {app_name}. Installeer de app opnieuw en herstel uw account om een nieuwe database te genereren en {app_name} te blijven gebruiken.

Waarschuwing: Dit leidt tot verlies van alle berichten en bijlagen ouder dan twee weken." + "value" : "Afwijzen" } }, "nn-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Databasen til appen din er ikkje kompatibel med denne versjonen av {app_name}. Installer appen på nytt og gjenopprett kontoen din for å generera ein ny database og fortset å bruka {app_name}.

Advarsel: Dette vil resultera i at alle meldingar og vedlegg eldre enn to veker går tapt." + "value" : "Avslå" } }, "ny" : { "stringUnit" : { "state" : "translated", - "value" : "Deta la pulogalamu yanu silikugwirizana ndi mtundu uwu wa {app_name}. Yikani pulogalamu yatsopanoyi ndikubwezerani akaunti yanu kuti mupange deta yatsopano ndikupitiriza kugwiritsa ntchito {app_name}.

Chenjezo: Izi zidzachititsa kuti mutaye mauthenga onse ndi zoyikapo zoposa masabata awo pafupifupi ziwiri." + "value" : "Kuba" } }, "pa-IN" : { "stringUnit" : { "state" : "translated", - "value" : "ਤੁਹਾਡੇ ਐਪ ਦਾ ਡਾਟਾਬੇਸ ਇਸ ਸੰਸਕਰਣ ਨਾਲ ਅਨੁਕੂਲ ਨਹੀਂ ਹੈ {app_name}। ਐਪ ਨੂੰ ਦੁਬਾਰਾ ਇੰਸਟਾਲ ਕਰੋ ਅਤੇ ਆਪਣਾ ਖਾਤਾ ਬਹਾਲ ਕਰੋ ਇੱਕ ਨਵਾਂ ਡਾਟਾਬੇਸ ਬਣਾਉਣ ਅਤੇ ਜਾਰੀ ਰੱਖਣ ਲਈ {app_name} ਵਰਤੋਂ.

ਚੇਤਾਵਨੀ: ਇਸ ਨਾਲ ਦੋ ਹਫ਼ਤਿਆਂ ਤੋਂ ਪੁਰਾਣੇ ਸਾਰੇ ਸੰਦੇਸ਼ ਅਤੇ ਅਟੈਕਮੈਂਟ ਗੁਆਚ ਜਾਣਗੇ।" + "value" : "ਇਨਕਾਰ" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Twoja baza danych aplikacji jest niezgodna z tą wersją aplikacji {app_name}. Aby wygenerować nową bazę danych i dalej korzystać z aplikacji {app_name}, zainstaluj aplikację ponownie i przywróć swoje konto.

Uwaga: spowoduje to utratę wszystkich wiadomości i załączników starszych niż dwa tygodnie." + "value" : "Odrzuć" } }, "ps" : { "stringUnit" : { "state" : "translated", - "value" : "ستاسو د اپ ڈیٹ ډیټابیس د {app_name} دې نسخې سره همغږي نه لري. د اپلیکیشن بیا نصب کړئ او خپل حساب بیا جوړ کړئ ترڅو یو نوی ډیټابیس جوړ کړئ او {app_name} کارول دوام ورکړئ.

خبرداری: دا به د دوو هفتو څخه زوړ ټول پیغامونه او ملحقات له لاسه ورکیدو لامل شي." + "value" : "رد کول" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "O banco de dados do seu aplicativo é incompatível com esta versão do {app_name}. Reinstale o aplicativo e restaure sua conta para gerar um novo banco de dados e continuar usando {app_name}.

Aviso: Isso resultará na perda de todas as mensagens e anexos com mais de duas semanas." + "value" : "Recusar" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "A base de dados do seu aplicativo é incompatível com esta versão do {app_name}. Reinstale o aplicativo e restaure a sua conta para gerar uma nova base de dados e continuar a usar o {app_name}.

Aviso: Isso resultará na perda de todas as mensagens e anexos com mais de duas semanas." + "value" : "Recusar" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Baza de date a aplicației dumneavoastră este incompatibilă cu această versiune de {app_name}. Reinstalați aplicația și restaurați contul pentru a genera o nouă bază de date și a continua să folosiți {app_name}.

Atenție: Aceasta va conduce la pierderea tuturor mesajelor și atașamentelor mai vechi de două săptămâni." + "value" : "Respinge" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "База данных вашего приложения несовместима с этой версией {app_name}. Переустановите приложение и восстановите свой аккаунт, чтобы создать новую базу данных и продолжить использовать {app_name}.

Внимание: Это приведет к потере всех сообщений и вложений старше двух недель." + "value" : "Отклонить" } }, "sh" : { "stringUnit" : { "state" : "translated", - "value" : "Tvoja baza podataka aplikacije nije kompatibilna s ovom verzijom {app_name}. Ponovo instaliraj aplikaciju i obnovi svoj račun da stvoriš novu bazu podataka i nastaviš koristiti {app_name}.

Upozorenje: Ovo će dovesti do gubitka svih poruka i privitaka starijih od dvije sedmice." + "value" : "Odbij" } }, "si-LK" : { "stringUnit" : { "state" : "translated", - "value" : "ඔබගේ යෙදුම් දත්ත කේතය {app_name} මෙහෙයුම් එක්වී නොමැත. යෙදුම නැවත ස්ථාපනය කර ඔබේ ගිණුම ප්‍රතිස්ථාපනය කර යුත්සේ {app_name} භාවිතා කිරීමට නව දත්ත ගබඩායක් සෑදී යමි.

අවවාදයයි: මෙය සතියක පරණ මායිම් සහ සියළු පණිවිඩ හා සම්බන්ධතා දත්ත කාසිවී අහිමියාවක් ලැබේ." + "value" : "ප්‍රතික්‍ෂේප" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Vaša databáza aplikácie nie je kompatibilná s touto verziou {app_name}. Preinštalujte aplikáciu a obnovte svoj účet, aby sa vygenerovala nová databáza a mohli ste pokračovať v používaní {app_name}.

Upozornenie: Týmto dôjde k strate všetkých správ a príloh starších ako dva týždne." + "value" : "Odmietnuť" } }, "sl" : { "stringUnit" : { "state" : "translated", - "value" : "Vaša baza podatkov aplikacije ni združljiva s to različico {app_name}. Ponovno namestite aplikacijo in obnovite svoj račun, da ustvarite novo bazo podatkov in nadaljujete z uporabo {app_name}.

Opozorilo: To bo povzročilo izgubo vseh sporočil in prilog, starejših od dveh tednov." + "value" : "Zavrni" } }, "sq" : { "stringUnit" : { "state" : "translated", - "value" : "Baza e të dhënave të aplikacionit tuaj është e papajtueshme me këtë version të {app_name}. Rinstaloni aplikacionin dhe riktheni llogarinë tuaj për të gjeneruar një bazë të re të të dhënave dhe për të vazhduar përdorimin e {app_name}.

Paralajmërim: Kjo do të rezultojë në humbjen e të gjitha mesazheve dhe bashkëngjitjeve më të vjetra se dy javë." + "value" : "Refuzoje" } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : "Ваша база података апликације није компатибилна са овом верзијом {app_name}. Поново инсталирајте апликацију и повратите ваш налог да бисте генерисали нову базу података и наставили да користите {app_name}.

Упозорење: Ово ће резултирати губитком свих порука и прилога старијих од две недеље." + "value" : "Одбиј" } }, "sr-Latn" : { "stringUnit" : { "state" : "translated", - "value" : "Baza podataka vaše aplikacije nije kompatibilna sa ovom verzijom {app_name}. Ponovo instalirajte aplikaciju i vratite vaš nalog kako biste generisali novu bazu podataka i nastavili sa korišćenjem {app_name}.

Upozorenje: Ovo će rezultirati gubitkom wszystkich poruka i privitaka starijih od dve nedelje." + "value" : "Odbi" } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Din appdatabas är inkompatibel med den här versionen av {app_name}. Installera om appen och återställ ditt konto för att skapa en ny databas och fortsätta använda {app_name}.

Varning: Detta resulterar i förlust av alla meddelanden och bilagor äldre än två veckor." + "value" : "Avböj" } }, "sw" : { "stringUnit" : { "state" : "translated", - "value" : "Hifadhidata ya programu yako hailingani na toleo hili la {app_name}. Sakinusha programu na urejeshe akaunti yako ili kuunda hifadhidata mpya na uendelee kutumia {app_name}.

Onyo: Hii itasababisha kupoteza ujumbe na viambatanisho vyote vilivyo zaidi ya wiki mbili." + "value" : "Kataa" } }, "ta" : { "stringUnit" : { "state" : "translated", - "value" : "உங்கள் பயன்பாட்டு தரவுத்தொகுப்பு இந்த {app_name} பதிப்புடன் இணக்கமில்லை. பயன்பாட்டை மறுதொன்று முடியும் மற்றும் உங்கள் கணக்கை மறுஆதிக்கவும் புதிய தரவுத்தொகுப்பை உருவாக்கி {app_name} பயன்பாட்டைப் பயன்படுத்தி தொடரவும்.

புரிந்து கொள்ளுங்கள்: இது இரண்டு வாரத்திற்கு முதலில் உள்ள அனைத்து செய்தி மற்றும் இணைப்புகளை இழக்கச் செய்யும்." + "value" : "மறுக்கவும்" } }, "te" : { "stringUnit" : { "state" : "translated", - "value" : "మీ యాప్ డేటాబేస్ {app_name} యొక్క ఈ వెర్షన్ తో అనుకూలంగా లేదు. యాప్‌ను పున సంస్థాపించి మీ ఖాతాను పునరుద్ధరించండి ఒక కొత్త డేటాబేస్ ను సృష్టించి {app_name} కొనసాగించడానికి.

హెచ్చరిక: ఇది రెండు వారాల క్రితం ఉన్న అన్ని సందేశాలు మరియు అటాచ్మెంట్లు కోల్పోవడానికి అనుమతిస్తుంది." + "value" : "నిరాకరించు" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "ฐานข้อมูลแอปของคุณไม่สามารถใช้งานร่วมกับเวอร์ชันนี้ของ {app_name} ติดตั้งใหม่และกู้คืนบัญชีเพื่อสร้างฐานข้อมูลใหม่และใช้งาน {app_name} ต่อไป

คำเตือน: สิ่งนี้จะส่งผลให้สูญเสียข้อความและไฟล์แนบทั้งหมดที่มีอายุเกินสองสัปดาห์" + "value" : "ปฏิเสธ" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "{app_name} uygulamanızın veritabanı bu sürüm ile uyumsuz. Uygulamayı yeniden yükleyin ve yeni bir veritabanı oluşturmak ve {app_name} kullanmaya devam etmek için hesabınızı geri yükleyin.

Uyarı: Bu, iki haftadan daha eski olan tüm ileti ve eklerin kaybolmasına neden olacaktır." + "value" : "Reddet" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "База даних вашого додатку несумісна з цією версією {app_name}. Перевстановіть додаток та відновіть свій обліковий запис, щоб створити нову базу даних і продовжувати користуватися {app_name}.

Увага: Це призведе до втрати всіх повідомлень та вкладень, старших двох тижнів." + "value" : "Відхилити" } }, "ur-IN" : { "stringUnit" : { "state" : "translated", - "value" : "آپ کا ایپ ڈیٹا بیس {app_name} کے اس ورژن سے مطابقت نہیں رکھتا ہے۔ ایپ کو دوبارہ انسٹال کریں اور نیا ڈیٹا بیس بنانے کے لیے اپنا اکاؤنٹ بحال کریں اور {app_name} کا استعمال جاری رکھیں۔

انتباہ: اس کے نتیجے میں دو ہفتوں سے پرانے تمام پیغامات اور منسلکات ضائع ہو جائیں گے۔" + "value" : "منسوخ کریں" } }, "uz" : { "stringUnit" : { "state" : "translated", - "value" : "Sizning ilova ma'lumotlar bazasi ushbu {app_name} versiyasi bilan mos kelmaydi. Ilovani qayta o'rnating va hisobingizni tiklang, yangi ma'lumotlar bazasini yaratish va {app_name} dan foydalanishni davom ettirish uchun.

Ogohlantirish: bu ikki haftalikdan katta barcha xabarlar va ilovalarni yo'qotishga olib keladi." + "value" : "Rad etish" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Cơ sở dữ liệu ứng dụng của bạn không tương thích với phiên bản {app_name} này. Cài đặt lại ứng dụng và khôi phục tài khoản của bạn để tạo một cơ sở dữ liệu mới và tiếp tục sử dụng {app_name}.

Cảnh báo: Điều này sẽ dẫn đến việc mất tất cả tin nhắn và tệp đính kèm cũ hơn hai tuần." + "value" : "Từ chối" } }, "xh" : { "stringUnit" : { "state" : "translated", - "value" : "I-database yakho yohlelo lwe-app ayinakuvisisana nohlobo lwangoku lwe {app_name}. Faka app kwakhona kwaye ubuyisele i-akhawunti yakho ukuphuhlisa database entsha kwaye uqhubeke usebenzisa {app_name}.

Isilumkiso: Oku kuya kubangela ukulahleka kwemiyalezo yonke kunye nezinto ezihambelanayo ezindala kunokuba yiveki ezimbini." + "value" : "Caphula" } }, "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "您的应用数据库与此版本的{app_name}不兼容。请重新安装应用并恢复您的账户以生成新的数据库并继续使用{app_name}。

警告:该操作将导致两周前的所有消息和附件丢失。" + "value" : "拒绝" } }, "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "您的應用程式資料庫與此版本的 {app_name} 不相容。重新安裝應用程式並恢復您的帳戶以生成新的資料庫並繼續使用 {app_name}。

警告:這將導致丟失所有兩週前的訊息和附件。" + "value" : "拒絕" } } } }, - "databaseOptimizing" : { + "delete" : { "extractionState" : "manual", "localizations" : { "af" : { "stringUnit" : { "state" : "translated", - "value" : "Optimalisering databasis" + "value" : "Skrap" } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "تحسين قاعدة البيانات" + "value" : "حذف" } }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "Databaza optimallaşdırılır" + "value" : "Sil" } }, "bal" : { "stringUnit" : { "state" : "translated", - "value" : "ڈیٹابیس شخصکورانی" + "value" : "حذف کرنا" } }, "be" : { "stringUnit" : { "state" : "translated", - "value" : "Аптымізацыя базы даных" + "value" : "Выдаліць" } }, "bg" : { "stringUnit" : { "state" : "translated", - "value" : "Опресняване на базата данни" + "value" : "Изтриване" } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "ডাটাবেস অপ্টিমাইজ করা হচ্ছে" + "value" : "মুছে ফেলুন" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Optimitzant la base de dades" + "value" : "Suprimeix" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Optimalizace databáze" + "value" : "Smazat" } }, "cy" : { "stringUnit" : { "state" : "translated", - "value" : "Optimeiddio Cronfa Ddata" + "value" : "Dileu" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Optimerer Database" + "value" : "Slet" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Optimiere Datenbank" + "value" : "Löschen" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Βελτιστοποίηση Βάσης Δεδομένων" + "value" : "Διαγραφή" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Optimizing Database" + "value" : "Delete" } }, "eo" : { "stringUnit" : { "state" : "translated", - "value" : "Optimumigante Datumbazon" + "value" : "Forigi" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Optimizando base de datos" + "value" : "Eliminar" } }, "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Optimizando base de datos" + "value" : "Eliminar" } }, "et" : { "stringUnit" : { "state" : "translated", - "value" : "Andmebaasi optimeerimine" + "value" : "Kustuta" } }, "eu" : { "stringUnit" : { "state" : "translated", - "value" : "Datu Basea Optimizatzen" + "value" : "Ezabatu" } }, "fa" : { "stringUnit" : { "state" : "translated", - "value" : "در حال بهینه‌سازی پایگاه داده" + "value" : "حذف" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Optimoidaan tietokanta" + "value" : "Poista" } }, "fil" : { "stringUnit" : { "state" : "translated", - "value" : "Ina-optimize ang Database" + "value" : "Burahin" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Optimisation de la base de données" + "value" : "Supprimer" } }, "gl" : { "stringUnit" : { "state" : "translated", - "value" : "Optimizando a Base de Datos" + "value" : "Borrar" } }, "ha" : { "stringUnit" : { "state" : "translated", - "value" : "Ƙarƙare Bayanai" + "value" : "Goge" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "מעדכן מסד נתונים" + "value" : "מחק" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "डाटाबेस का अनुकूलन" + "value" : "मिटाएं" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Optimizacija Baze podataka" + "value" : "Obriši" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Adatbázis optimalizálása" + "value" : "Törlés" } }, "hy-AM" : { "stringUnit" : { "state" : "translated", - "value" : "Շտեմարանն օպտիմալացվում է" + "value" : "Ջնջել" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Mengoptimalkan Database" + "value" : "Hapus" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Ottimizzazione del database" + "value" : "Elimina" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "データベースを更新中" + "value" : "削除" } }, "ka" : { "stringUnit" : { "state" : "translated", - "value" : "მონაცემთა ბაზის ოპტიმიზაცია" + "value" : "წაშლა" } }, "km" : { "stringUnit" : { "state" : "translated", - "value" : "កំពុងធ្វើបច្ចុប្បន្នភាពមូលដ្ខានទិន្នន័យ" + "value" : "លុប" } }, "kn" : { "stringUnit" : { "state" : "translated", - "value" : "ಡೇಟಾಬೇಸ್ ಆಪ್ಟಿಮೈಸಿಂಗ್" + "value" : "ಅಳಿಸಿ" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "데이터베이스 최적화" + "value" : "삭제" } }, "ku" : { "stringUnit" : { "state" : "translated", - "value" : "باشکردنی بنکەدراوە" + "value" : "سڕینەوە" } }, "ku-TR" : { "stringUnit" : { "state" : "translated", - "value" : "Danegehê baştir dike" + "value" : "Jê bibe" } }, "lg" : { "stringUnit" : { "state" : "translated", - "value" : "Okuza Database" + "value" : "Jjamu" + } + }, + "lo" : { + "stringUnit" : { + "state" : "translated", + "value" : "ລຶບ" } }, "lt" : { "stringUnit" : { "state" : "translated", - "value" : "Optimizuojama duomenų bazė" + "value" : "Ištrinti" } }, "lv" : { "stringUnit" : { "state" : "translated", - "value" : "Optimizē datu bāzi" + "value" : "Dzēst" } }, "mk" : { "stringUnit" : { "state" : "translated", - "value" : "Оптимизирање на базата на податоци" + "value" : "Избриши" } }, "mn" : { "stringUnit" : { "state" : "translated", - "value" : "Мэдээллийн сантай оптимжуулалт хийх" + "value" : "Устгах" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Mengoptimumkan Pangkalan Data" + "value" : "Padam" } }, "my" : { "stringUnit" : { "state" : "translated", - "value" : "ဒေတာဘေ့စ် ဖြည့်စွမ်းနေသည်" + "value" : "ဖျက်မည်" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Optimaliserer databasen" + "value" : "Slett" } }, "nb-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Optimaliserer databasen" + "value" : "Slett" } }, "ne-NP" : { "stringUnit" : { "state" : "translated", - "value" : "डाटाबेस अनकुल पारिदै" + "value" : "मेटाउनुहोस्" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Optimaliseer Database" + "value" : "Verwijderen" } }, "nn-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Optimaliserer databasen" + "value" : "Slett" } }, "ny" : { "stringUnit" : { "state" : "translated", - "value" : "Kupanga bwino Database" + "value" : "Chotsani" } }, "pa-IN" : { "stringUnit" : { "state" : "translated", - "value" : "ਡਾਟਾਬੇਸ ਦਾ ਅਦਾਕਾਰ ਬਣਾ ਰਿਹਾ ਹੈ" + "value" : "ਹਟਾਓ" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Optymalizacja bazy danych" + "value" : "Usuń" } }, "ps" : { "stringUnit" : { "state" : "translated", - "value" : "ډیټابیس سمول" + "value" : "ړنګول" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Otimizando base de dados" + "value" : "Excluir" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Otimizando a base de dados" + "value" : "Apagar" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Optimizare bază de date" + "value" : "Șterge" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Оптимизация базы данных" + "value" : "Удалить" } }, "sh" : { "stringUnit" : { "state" : "translated", - "value" : "Optimizacija baze podataka" + "value" : "Obriši" } }, "si-LK" : { "stringUnit" : { "state" : "translated", - "value" : "දත්ත සමුදාය ප්‍රශස්ත කිරීම" + "value" : "මකන්න" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Optimalizácia databázy" + "value" : "Vymazať" } }, "sl" : { "stringUnit" : { "state" : "translated", - "value" : "Optimizacija podatkovne baze" + "value" : "Izbriši" } }, "sq" : { "stringUnit" : { "state" : "translated", - "value" : "Optimizimi i Bazës së të Dhënave" + "value" : "Fshije" } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : "Оптимизација базе података" + "value" : "Обриши" } }, "sr-Latn" : { "stringUnit" : { "state" : "translated", - "value" : "Optimizacija baze podataka" + "value" : "Obriši" } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Optimerar databas" + "value" : "Radera" } }, "sw" : { "stringUnit" : { "state" : "translated", - "value" : "Kuhamisha Hifadhidata" + "value" : "Futa" } }, "ta" : { "stringUnit" : { "state" : "translated", - "value" : "தரவுத்தொகுப்பை மேம்படுத்துகிறது" + "value" : "நீக்கு" } }, "te" : { "stringUnit" : { "state" : "translated", - "value" : "డేటాబేస్‌ను మెరుగ్గాచేయడం" + "value" : "సందేశాన్ని తొలగించు" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "การเพิ่มประสิทธิภาพฐานข้อมูล" + "value" : "ลบ" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Veritabanı En İyi Duruma Getiriliyor" + "value" : "Sil" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Оптимізація бази даних" + "value" : "Видалити" } }, "ur-IN" : { "stringUnit" : { "state" : "translated", - "value" : "ڈیٹابیس کو بہتر بنانا" + "value" : "حذف کریں" } }, "uz" : { "stringUnit" : { "state" : "translated", - "value" : "Ma'lumotlar bazasi optimallashtirilmoqda" + "value" : "O'chirish" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Đang tối ưu hóa cơ sở dữ liệu" + "value" : "Xóa" } }, "xh" : { "stringUnit" : { "state" : "translated", - "value" : "Ukuphucula iDatha" + "value" : "Sangula" } }, "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "正在优化数据库" + "value" : "删除" } }, "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "最佳化資料庫中" + "value" : "刪除" } } } }, - "debugLog" : { + "deleteAfterGroupFirstReleaseConfigOutdated" : { "extractionState" : "manual", "localizations" : { "af" : { "stringUnit" : { "state" : "translated", - "value" : "Ontfout Log" + "value" : "Sommige van jou toestelle gebruik verouderde weergawes. Sinchronisering mag onbetroubaar wees totdat hulle opgedateer word." } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "سِجل تصحيح الأخطاء" + "value" : "بعض أجهزتك تستخدم إصدارات قديمة. قد تكون المزامنة غير موثوقة حتى يتم تحديثها." } }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "Sazlama jurnalı" + "value" : "Bəzi cihazlarınız köhnə versiyaları istifadə edir. Güncəllənənə qədər sinxronlaşdırma güvənli olmaya bilər." } }, "bal" : { "stringUnit" : { "state" : "translated", - "value" : "ڈیبگ لگ" + "value" : "کچھ دستگاهاتیں یونی اپڈیتاہ ضخ و عرض چکس" } }, "be" : { "stringUnit" : { "state" : "translated", - "value" : "Логі адладкі" + "value" : "Некаторыя з вашых прылад выкарыстоўваюць састарэлую версію. Сінхранізацыя можа быць ненадзейнай, пакуль яны не будуць абноўлены." } }, "bg" : { "stringUnit" : { "state" : "translated", - "value" : "Debug Log" + "value" : "Някои от вашите устройства използват остарели версии. Синхронизацията може да бъде ненадеждна, докато не бъдат актуализирани." } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "ডিবাগ লগ" + "value" : "আপনার কিছু ডিভাইস পুরোনো ভার্সন ব্যবহার করছে। যতক্ষন না আপডেট হয়, সিঙ্কিং ভরসাযোগ্য নাও হতে পারে।" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Registre de depuració" + "value" : "Alguns dels vostres dispositius tenen versions antigues. La sincronització pot no ser fialbe fins que estiguin actualitzats." } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Ladící log" + "value" : "Některá z vašich zařízení používají zastaralé verze. Synchronizace může být nespolehlivá, dokud nebudou aktualizovány." } }, "cy" : { "stringUnit" : { "state" : "translated", - "value" : "Cofnod Dadfygio" + "value" : "Mae rhai o'ch dyfeisiau'n defnyddio fersiynau allan o ddyddiad. Efallai na fydd cydamseru'n ddibynadwy nes cânt eu diweddaru." } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Debug log" + "value" : "Nogle af dine enheder bruger forældede versioner. Synkroniseringen kan være upålidelig, indtil de er opdaterede." } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Debug-Protokoll" + "value" : "Einige deiner Geräte verwenden veraltete Versionen. Die Synchronisierung kann unzuverlässig sein, bis sie aktualisiert werden." } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Αρχείο καταγραφής Αποσφαλμάτωσης" + "value" : "Μερικές από τις συσκευές σας χρησιμοποιούν ξεπερασμένες εκδόσεις. Ο συγχρονισμός μπορεί να είναι αναξιόπιστος μέχρι να ενημερωθούν." } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Debug Log" + "value" : "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated." } }, "eo" : { "stringUnit" : { "state" : "translated", - "value" : "Sencimiga protokolo" + "value" : "Iuj el viaj aparatoj uzas malnoviĝintajn versiojn. Sinkronigado povas esti nefidinda ĝis ili ĝisdatigos." } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Registro de depuración" + "value" : "Algunos de tus dispositivos están utilizando versiones desactualizadas. La sincronización puede ser poco confiable hasta que se actualicen." } }, "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Registro de depuración" + "value" : "Algunos de tus dispositivos están utilizando versiones desactualizadas. La sincronización puede ser poco confiable hasta que se actualicen." } }, "et" : { "stringUnit" : { "state" : "translated", - "value" : "Silumislogi" + "value" : "Mõned teie seadmed kasutavad aegunud versioone. Sünkroonimine võib olla ebausaldusväärne, kuni neid värskendatakse." } }, "eu" : { "stringUnit" : { "state" : "translated", - "value" : "Arazketa egunkaria" + "value" : "Zure gailu batzuk bertsio zaharkituak erabiltzen ari dira. Sinkronizazioa ez da fidagarria izango eguneratu arte." } }, "fa" : { "stringUnit" : { "state" : "translated", - "value" : "گزارش اشکال‌زدایی" + "value" : "برخی از دستگاه‌های شما از نسخه‌های قدیمی استفاده می‌کنند. همگام سازی ممکن است غیر قابل اعتماد باشد تا زمانی که به روز شوند." } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Virheenkorjausloki" + "value" : "Jotkin laitteesi käyttävät vanhentuneita versioita ja synkronoinnin toiminnassa voi olla ongelmia, kunnes ne on päivitetty." } }, "fil" : { "stringUnit" : { "state" : "translated", - "value" : "I-debug ang Log" + "value" : "Ang ilan sa iyong mga device ay gumagamit ng mga lipas na bersyon. Ang pag-sync ay maaaring hindi maaasahan hanggang sila ay ma-update." } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Journal de débogage" + "value" : "Certains de vos appareils utilisent des versions obsolètes. La synchronisation peut être instable jusqu'à ce qu'ils soient mis à jour." } }, "gl" : { "stringUnit" : { "state" : "translated", - "value" : "Rexistro de depuración" + "value" : "Algúns dos teus dispositivos están usando versións desactualizadas. A sincronización pode non ser de confianza ata que se actualicen." } }, "ha" : { "stringUnit" : { "state" : "translated", - "value" : "Kuskuren Tsari" + "value" : "Wasu daga cikin na'urorinku suna amfani da tsoffin sigogi. Hada na'urorin na iya zama maras tabbas har sai an sabunta su." } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "יומן תקלות" + "value" : "חלק מהמכשירים שלך משתמשים בגרסאות מיושנות. הסנכרון עשוי להיות לא יציב עד שהם יעודכנו." } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "डीबग लॉग" + "value" : "आपके कुछ डिवाइस पुराने संस्करणों का उपयोग कर रहे हैं। जब तक वे अपडेट नहीं हो जाते, सिंक अविश्वसनीय हो सकता है।" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Evidencija o otklanjanju grešaka" + "value" : "Neki od vaših uređaja koriste zastarjele verzije. Sinkronizacija može biti nepouzdana dok se ne ažuriraju." } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Hibakeresési napló" + "value" : "Egyes eszközeid elavult verziókat használnak. A szinkronizálás megbízhatatlan lehet a frissítésig." } }, "hy-AM" : { "stringUnit" : { "state" : "translated", - "value" : "Վրիպազերծման մատյան" + "value" : "Ձեր որոշ սարքեր օգտագործում են հնացած տարբերակներ։ Համաժամեցումը կարող է լինել ոչ կայուն մինչ դրանք թարմացվեն։" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Catatan Awakutu" + "value" : "Beberapa perangkat Anda menggunakan versi lama. Sinkronisasi mungkin tidak dapat diandalkan hingga diperbarui." } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Log di debug" + "value" : "Alcuni dei tuoi dispositivi stanno utilizzando una versione obsoleta. La sincronizzazione potrebbe risultare inaffidabile finché non verranno aggiornati." } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "デバッグログ" + "value" : "一部のデバイスは古いバージョンを使用しています。同期が更新されるまで信頼性が低い場合があります。" } }, "ka" : { "stringUnit" : { "state" : "translated", - "value" : "გამართვის ჟურნალი" + "value" : "ზოგიერთი თქვენი მოწყობილობა იყენებს მოძველებულ ვერსიებს. სინქრონიზაცია შეიძლება არაკეთილსაწინააღმდეგო იყოს სანამ ისინი განახლებენ." } }, "km" : { "stringUnit" : { "state" : "translated", - "value" : "កំណត់ត្រាបញ្ហា" + "value" : "ឧបករណ៍របស់អ្នកមួយចំនួនបានប្រើប្រាស់កំណែហួសសម័យ។ ការសមកាលកម្មអាចមិនគួរឱ្យទុកចិត្តរហូតពួកគេត្រូវបានអាប់ដេត។" } }, "kn" : { "stringUnit" : { "state" : "translated", - "value" : "ಡೀಬಗ್ ಲಾಗ್" + "value" : "ನಿಮ್ಮ ಕೆಲವು ಉಪಕರಣಗಳು ಹಳೆಯ ಆವೃತ್ತಿಗಳನ್ನು ಬಳಸುತ್ತಿವೆ. ಅವುಗಳನ್ನು ಅಪ್‌ಡೇಟ್ ಮಾಡಿದರೆ ಸಂಬಂಧಿಕ ವೇಳೆಾಂಗಿತ ದೂರವಿರಲುಂದು ಸಂಕರವಾಗಿದದ್ದು ಎನ್ಕಉಂಬುದೋನು." } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "디버그 로그" + "value" : "일부 기기가 구버전을 사용 중입니다. 업데이트되지 않은 경우 동기화가 불안정할 수 있습니다." } }, "ku" : { "stringUnit" : { "state" : "translated", - "value" : "لەگەڵکردنی پەیام" + "value" : "هەندێك لە ئامێرەکانت بە كۆمپەرسیەکانە کۆن بەکاردەهێنن. یەکخستن کردنی دەکرێت بە پەیوەندیدار نەبێت تاکوو یەکدەکرێن." } }, "ku-TR" : { "stringUnit" : { "state" : "translated", - "value" : "Loga pînekirina çewtiyan" + "value" : "Hin cîhazên te versîyonên paşmayî diemilînin. Senkronîzekirin dibe ku ne pêbawer be, heya ku ew neyên rojanekirin." } }, "lg" : { "stringUnit" : { "state" : "translated", - "value" : "Log ya Debug" - } - }, - "lo" : { - "stringUnit" : { - "state" : "translated", - "value" : "ບັນທຶກການດັດແກ້" + "value" : "Ebimu ku byuma byo birina amakyaaga g'omukadde. Okukola kw'ebigatta okulowoofu kunaawera okutuusa lwe bidikusibwa." } }, "lt" : { "stringUnit" : { "state" : "translated", - "value" : "Derinimo žurnalas" + "value" : "Kai kurie jūsų įrenginiai naudoja pasenusias versijas. Sinchronizavimas gali būti nestabilus, kol jie nebus atnaujinti." } }, "lv" : { "stringUnit" : { "state" : "translated", - "value" : "Atkļūdošanas žurnāls" + "value" : "Dažas no tavām ierīcēm izmanto novecojušas versijas. Sinhronizācija var būt neuzticama, kamēr tās netiks atjauninātas." } }, "mk" : { "stringUnit" : { "state" : "translated", - "value" : "Дневник на грешки" + "value" : "Некои од твоите уреди користат застарени верзии. Синхронизацијата може да биде непостојана додека не ги ажурираш." } }, "mn" : { "stringUnit" : { "state" : "translated", - "value" : "Засварын бүртгэл" + "value" : "Таны зарим төхөөрөмж хуучирсан хувилбаруудыг ашиглаж байна. Тааруулах үйл явц найдвартай байхгүй байж магадгүй." } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Log Nyahpepijat" + "value" : "Beberapa peranti anda menggunakan versi yang telah lapuk. Penyelarasan mungkin tidak boleh dipercayai sehingga ia dikemas kini." } }, "my" : { "stringUnit" : { "state" : "translated", - "value" : "Debug မှတ်တမ်း" + "value" : "သင့်စက်အချို့မှာ နောက်ဆုံးပေါ် ဗားရှင်းကို မသုံးထားပါ ။ ပြန်လုပ်ခွင့်မပြုပြီး ဖော်ပြမှုတို့အပြီး Update လုပ်ပါ" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Feilsøkingslogg" + "value" : "Noen av enhetene dine bruker utdaterte versjoner. Synkronisering kan være upålitelig frem til de er oppdatert." } }, "nb-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Feilsøkingslogg" + "value" : "Noen av enhetene dine bruker utdaterte versjoner. Synkronisering kan være upålitelig inntil de er oppdatert." } }, "ne-NP" : { "stringUnit" : { "state" : "translated", - "value" : "डिबग लग" + "value" : "तपाईंका केही उपकरणहरू पुराना संस्करणहरू प्रयोग गरिरहेका छन्। तिनीहरूको अद्यावधिक नभएसम्म समकालन अस्थिर हुन सक्छ।" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Foutopsporingslogboek" + "value" : "Sommige van je apparaten gebruiken verouderde versies. Synchroniseren kan onbetrouwbaar zijn totdat ze zijn bijgewerkt." } }, "nn-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Feilsøkingslogg" + "value" : "Nokre av einingane dine brukar utdaterte versjonar. Synkronisering kan vere upåliteleg til desse er oppdaterte." } }, "ny" : { "stringUnit" : { "state" : "translated", - "value" : "Lowani mu Debug Log" + "value" : "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated." } }, "pa-IN" : { "stringUnit" : { "state" : "translated", - "value" : "ਡਿਬੱਗ ਲਾਗ" + "value" : "ਤੁਹਾਡੇ ਕੁਝ ਉਪਕਰਣ ਪੁਰਾਣਾ ਵਰਜਨ ਵਰਤ ਰਹੇ ਹਨ। ਸਿੰਕ ਰੀਲਾਇਸਬਲ ਨਹੀ ਹੋ ਸਕਦਾ ਜਦ ਤਕ ਕਿ ਉਹ ਨੂੰ ਅੱਪਡੇਟ ਨਹੀਂ ਕੀਤੇ ਜਾਂਦੇ।" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Dziennik debugowania" + "value" : "Niektóre urządzenia korzystają z nieaktualnych wersji. Synchronizacja może być zawodna do czasu ich aktualizacji." } }, "ps" : { "stringUnit" : { "state" : "translated", - "value" : "دیباگ لاګ" + "value" : "ځینې ستاسو وسایل زوړ نسخې کاروي. همغږي د دې وسایلو تر تازه کولو پورې بې باوره کېدی شي." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Log de Depuração" + "value" : "Alguns dos seus dispositivos estão usando versões desatualizadas. A sincronização pode não ser confiável até que sejam atualizados." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Log de debug" + "value" : "Alguns dos seus dispositivos estão a usar versões antigas. A sincronização pode ser pouco confiável até que sejam atualizados." } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Jurnal Depanare" + "value" : "Unele dintre dispozitivele tale folosesc versiuni învechite. Sincronizarea poate fi nesigură până când acestea sunt actualizate." } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Журнал отладки" + "value" : "Некоторые из ваших устройств используют устаревшие версии. Синхронизация может быть ненадежной до тех пор, пока они не будут обновлены." } }, "sh" : { "stringUnit" : { "state" : "translated", - "value" : "Debug log" + "value" : "Neki od vaših uređaja koriste zastarjele verzije. Sinhronizacija može biti nepouzdana dok se ne ažuriraju." } }, "si-LK" : { "stringUnit" : { "state" : "translated", - "value" : "දෝශ නිරාකරණ ලොගය" + "value" : "ඔබගේ උපාංග කිහිපයක් පැරණි අනුවාද භාවිත කරයි. දක්නට ඇති බවකට පරීක්ෂා විය හැක." } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Ladiaci log" + "value" : "Niektoré vaše zariadenia používajú zastarané verzie. Synchronizácia môže byť nespoľahlivá, kým nebudú aktualizované." } }, "sl" : { "stringUnit" : { "state" : "translated", - "value" : "Sistemska zabeležba" + "value" : "Nekatere vaše naprave uporabljajo zastarele različice. Sinhronizacija morda ne bo zanesljiva, dokler ne bodo posodobljene." } }, "sq" : { "stringUnit" : { "state" : "translated", - "value" : "Regjistër Diagnostikimesh" + "value" : "Disa nga pajisjet tuaja po përdorin versione të vjetra. Sinkronizimi mund të mos jetë i besueshëm deri sa të përditësohen." } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : "Извештај о грешкама" + "value" : "Неки од твојих уређаја користе застареле верзије. Синхронизација може бити непоуздана до ажурирања." } }, "sr-Latn" : { "stringUnit" : { "state" : "translated", - "value" : "Izveštaj o greškama" + "value" : "Neki od vaših uređaja koriste zastarele verzije. Sinhronizacija može biti nepouzdana dok se ne ažuriraju." } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Felsökningslogg" + "value" : "Några av dina enheter använder gamla versioner. Synkronisering kan vara opålitlig tills de uppdateras." } }, "sw" : { "stringUnit" : { "state" : "translated", - "value" : "Logi ya Kurekebisha" + "value" : "Baadhi ya vifaa vyako vina matoleo yasiyo ya kisasa. Usawazishaji unaweza kuwa si wa kuaminika hadi yamesasishwa." } }, "ta" : { "stringUnit" : { "state" : "translated", - "value" : "பிழைத்திருத்த பதிவு" + "value" : "உங்கள் சாதனங்களில் சில பழைய பதிப்புகளைப் பயன்படுத்திக் கொண்டிருக்கின்றன. சிங்கிங் வாங்கുവையில் குறிப்பாக விசையமைப்புகளைப் பயன்படுத்தவும்." } }, "te" : { "stringUnit" : { "state" : "translated", - "value" : "డీబగ్ లాగ్" + "value" : "మీ పరికరాలలో కొన్ని పాత సంస్కరణలను ఉపయోగిస్తున్నాయి. అవి నవీకరించేవరకూ సింక్ అవ్వడంలో విశ్వసనీయత ఉండకపోవచ్చు." } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "Debug Log" + "value" : "อุปกรณ์บางอย่างของคุณใช้เวอร์ชันที่ล้าสมัย การซิงค์อาจไม่น่าเชื่อถือจนกว่าจะได้รับการอัปเดต" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Hata Ayıklama Raporu" + "value" : "Bazı cihazlarınız eski sürümleri kullanıyor. Güncellenene kadar senkronizasyon güvenilir olmayabilir." } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Журнал відладки" + "value" : "Деякі з ваших пристроїв використовують застарілу версію застосунку. Синхронізація може не вдатись, доки вони не будуть оновлені." } }, "ur-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Debug Log" + "value" : "آپ کے کچھ آلات پرانی ورژن استعمال کر رہے ہیں۔ ہم آہنگی قابل اعتماد نہیں ہوسکتی جب تک کہ انہیں اپ ڈیٹ نہ کیا جائے۔" } }, "uz" : { "stringUnit" : { "state" : "translated", - "value" : "Tahlil yozuvi" + "value" : "Ayrim qurilmalaringiz eskirgan versiyalarni ishlatmoqda. Sinxronizatsiya yangilanmaganicha ishonchsiz bo'lishi mumkin." } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Nhật ký sửa lỗi" + "value" : "Một số thiết bị của bạn đang sử dụng các phiên bản đã lỗi thời. Đồng bộ hóa có thể không đáng tin cậy cho đến khi chúng được cập nhật." } }, "xh" : { "stringUnit" : { "state" : "translated", - "value" : "Ilogi yeSipseko" + "value" : "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated." } }, "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "调试日志" + "value" : "您的某些设备正在使用旧版本客户端。同步功能可能在更新之前不稳定。" } }, "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "除錯紀錄" + "value" : "您的部分裝置正在使用過時的版本。在更新之前,同步可能不可靠。" } } } }, - "decline" : { + "deleteAfterGroupPR1BlockThisUser" : { "extractionState" : "manual", "localizations" : { "af" : { "stringUnit" : { "state" : "translated", - "value" : "Weier" + "value" : "Blokkeer Hierdie Gebruiker" } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "رفض" + "value" : "حظر هذا المستخدم" } }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "Rədd et" + "value" : "Bu istifadəçini əngəllə" } }, "bal" : { "stringUnit" : { "state" : "translated", - "value" : "نامنظور" + "value" : "اس صارف کو روکو" } }, "be" : { "stringUnit" : { "state" : "translated", - "value" : "Адхіліць" + "value" : "Заблакіраваць гэтага карыстальніка" } }, "bg" : { "stringUnit" : { "state" : "translated", - "value" : "Отхвърляне" + "value" : "Блокирай този потребител" } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "অগ্রাহ্য করুন" + "value" : "এই ব্যবহারকারীকে ব্লক করুন" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Declineu" + "value" : "Bloquejar a aquest usuari/a" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Odmítnout" + "value" : "Blokovat tohoto uživatele" } }, "cy" : { "stringUnit" : { "state" : "translated", - "value" : "Gwrthod" + "value" : "Rhwystro'r Defnyddiwr Hwn" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Afvis" + "value" : "Bloker Denne Bruger" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Ablehnen" + "value" : "Diese Person blockieren" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Απόρριψη" + "value" : "Φραγή Αυτού του Χρήστη" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Decline" + "value" : "Block This User" } }, "eo" : { "stringUnit" : { "state" : "translated", - "value" : "Malakcepti" + "value" : "Bloki tiun uzanton" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Rechazar" + "value" : "Bloquear usuario" } }, "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Rechazar" + "value" : "Bloquear usuario" } }, "et" : { "stringUnit" : { "state" : "translated", - "value" : "Keeldu" + "value" : "Blokeeri see kasutaja" } }, "eu" : { "stringUnit" : { "state" : "translated", - "value" : "Baztertu" + "value" : "Block This User" } }, "fa" : { "stringUnit" : { "state" : "translated", - "value" : "رد تماس" + "value" : "مسدود کردن کاربر" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Kieltäydy" + "value" : "Estä tämä käyttäjä" } }, "fil" : { "stringUnit" : { "state" : "translated", - "value" : "Tanggihan" + "value" : "I-block Ang Taong Ito" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Refuser" + "value" : "Bloquer cet utilisateur" } }, "gl" : { "stringUnit" : { "state" : "translated", - "value" : "Rexeitar" + "value" : "Bloquear a este usuario" } }, "ha" : { "stringUnit" : { "state" : "translated", - "value" : "Ki ɗiɗe" + "value" : "To'she Wannan Mai Amfani" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "דחה" + "value" : "חסום משתמש זה" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "अस्वीकृत करें" + "value" : "इस उपयोगकर्ता को ब्लॉक करें" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Odbaci" + "value" : "Blokiraj ovog korisnika" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Elutasítás" + "value" : "Felhasználó letiltása" } }, "hy-AM" : { "stringUnit" : { "state" : "translated", - "value" : "Մերժել" + "value" : "Արգելափակել այս օգտատիրոջը" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Tolak" + "value" : "Blokir Pengguna Ini" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Rifiuta" + "value" : "Blocca questo utente" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "拒否" + "value" : "このユーザーをブロックする" } }, "ka" : { "stringUnit" : { "state" : "translated", - "value" : "უარყოფა" + "value" : "დაბლოკეთ ეს მომხმარებელი" } }, "km" : { "stringUnit" : { "state" : "translated", - "value" : "បដិសេធ" + "value" : "ទប់ស្កាត់អ្នក​ប្រើ​" } }, "kn" : { "stringUnit" : { "state" : "translated", - "value" : "ನಿರಾಕರಿಸಿ" + "value" : "ಈ ಬಳಕೆದಾರರನ್ನು ತಡೆಯಿರಿ" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "거절" + "value" : "이 사용자를 차단" } }, "ku" : { "stringUnit" : { "state" : "translated", - "value" : "رەتی کردن" + "value" : "دوورخستنەوەی ئەم بەکارهێنەرە" } }, "ku-TR" : { "stringUnit" : { "state" : "translated", - "value" : "Red bike" + "value" : "Blok Bike Vê Bikarhênerê" } }, "lg" : { "stringUnit" : { "state" : "translated", - "value" : "Gana" + "value" : "Block This User" } }, "lo" : { "stringUnit" : { "state" : "translated", - "value" : "ປະຕິເສດ" + "value" : "ຫ້າມຜູ້ນັກນັບນີ້" } }, "lt" : { "stringUnit" : { "state" : "translated", - "value" : "Atmesti" + "value" : "Užblokuoti šį naudotoją" } }, "lv" : { "stringUnit" : { "state" : "translated", - "value" : "Noraidīt" + "value" : "Bloķēt šo lietotāju" } }, "mk" : { "stringUnit" : { "state" : "translated", - "value" : "Одбиј" + "value" : "Блокирај го овој корисник" } }, "mn" : { "stringUnit" : { "state" : "translated", - "value" : "Татгалзах" + "value" : "Энэ хэрэглэгчийг хориглох" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Tolak" + "value" : "Sekat Pengguna Ini" } }, "my" : { "stringUnit" : { "state" : "translated", - "value" : "ငြင်းပယ်" + "value" : "ဤသုံးစွဲသူကို ဘလော့မည်" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Avvis" + "value" : "Blokker denne brukeren" } }, "nb-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Avvis" + "value" : "Blokker denne brukeren" } }, "ne-NP" : { "stringUnit" : { "state" : "translated", - "value" : "अस्वीकार गर्नुहोस्" + "value" : "यस प्रयोगकर्तालाई ब्लक गर्नुहोस्" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Afwijzen" + "value" : "Deze gebruiker blokkeren" } }, "nn-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Avslå" + "value" : "Blokker denne brukaren" } }, "ny" : { "stringUnit" : { "state" : "translated", - "value" : "Kuba" + "value" : "Block This User" } }, "pa-IN" : { "stringUnit" : { "state" : "translated", - "value" : "ਇਨਕਾਰ" + "value" : "ਇਸ ਉਪਭੋਗਤਾ ਨੂੰ ਬਲੌਕ ਕਰੋ" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Odrzuć" + "value" : "Zablokuj tego użytkownika" } }, "ps" : { "stringUnit" : { "state" : "translated", - "value" : "رد کول" + "value" : "دا کاروونکی بلاک کړئ" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Recusar" + "value" : "Bloquear este usuário" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Recusar" + "value" : "Bloquear este utilizador" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Respinge" + "value" : "Blochează acest utilizator" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Отклонить" + "value" : "Заблокировать этого пользователя" } }, "sh" : { "stringUnit" : { "state" : "translated", - "value" : "Odbij" + "value" : "Blokiraj ovog korisnika" } }, "si-LK" : { "stringUnit" : { "state" : "translated", - "value" : "ප්‍රතික්‍ෂේප" + "value" : "මෙම පරිශීලකයා අවහිර කරන්න" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Odmietnuť" + "value" : "Blokovať tohto používateľa" } }, "sl" : { "stringUnit" : { "state" : "translated", - "value" : "Zavrni" + "value" : "Blokiraj uporabnika" } }, "sq" : { "stringUnit" : { "state" : "translated", - "value" : "Refuzoje" + "value" : "Bllokoni këtë përdorues" } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : "Одбиј" + "value" : "Блокирати овог корисника" } }, "sr-Latn" : { "stringUnit" : { "state" : "translated", - "value" : "Odbi" + "value" : "Blokiraj ovog korisnika" } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Avböj" + "value" : "Blockera denna användare" } }, "sw" : { "stringUnit" : { "state" : "translated", - "value" : "Kataa" + "value" : "Zuia Mtumiaji Huyu" } }, "ta" : { "stringUnit" : { "state" : "translated", - "value" : "மறுக்கவும்" + "value" : "இந்த பயனரைத் தடை செய்யவும்" } }, "te" : { "stringUnit" : { "state" : "translated", - "value" : "నిరాకరించు" + "value" : "ఈ వినియోగదారుని నిరోధించు" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "ปฏิเสธ" + "value" : "บล็อกคนนี้" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Reddet" + "value" : "Bu Kullanıcıyı Engelle" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Відхилити" + "value" : "Заблокувати користувача" } }, "ur-IN" : { "stringUnit" : { "state" : "translated", - "value" : "منسوخ کریں" + "value" : "اس صارف کو بلاک کریں" } }, "uz" : { "stringUnit" : { "state" : "translated", - "value" : "Rad etish" + "value" : "Ushbu foydalanuvchini bloklash" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Từ chối" + "value" : "Chặn Người dùng này" } }, "xh" : { "stringUnit" : { "state" : "translated", - "value" : "Caphula" + "value" : "Vimba Lo Msebenzisi" } }, "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "拒绝" + "value" : "屏蔽该用户" } }, "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "拒絕" + "value" : "封鎖此使用者" } } } }, - "delete" : { + "deleteAfterGroupPR1BlockUser" : { "extractionState" : "manual", "localizations" : { "af" : { "stringUnit" : { "state" : "translated", - "value" : "Skrap" + "value" : "Blokkeer Gebruiker" } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "حذف" + "value" : "حظر مستخدم" } }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "Sil" + "value" : "İstifadəçini əngəllə" } }, "bal" : { "stringUnit" : { "state" : "translated", - "value" : "حذف کرنا" + "value" : "صارف کو روکو" } }, "be" : { "stringUnit" : { "state" : "translated", - "value" : "Выдаліць" + "value" : "Заблакіраваць карыстальніка" } }, "bg" : { "stringUnit" : { "state" : "translated", - "value" : "Изтриване" + "value" : "Блокиране на потребител" } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "মুছে ফেলুন" + "value" : "ব্যবহারকারীকে ব্লক করুন" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Suprimeix" + "value" : "Bloquejar usuari" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Smazat" + "value" : "Blokovat uživatele" } }, "cy" : { "stringUnit" : { "state" : "translated", - "value" : "Dileu" + "value" : "Rhwystro Defnyddiwr" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Slet" + "value" : "Bloker Bruger" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Löschen" + "value" : "Person blockieren" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Διαγραφή" + "value" : "Φραγή Χρήστη" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Delete" + "value" : "Block User" } }, "eo" : { "stringUnit" : { "state" : "translated", - "value" : "Forigi" + "value" : "Bloki uzanton" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Eliminar" + "value" : "Bloquear usuario" } }, "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Eliminar" + "value" : "Bloquear usuario" } }, "et" : { "stringUnit" : { "state" : "translated", - "value" : "Kustuta" + "value" : "Blokeeri kasutaja" } }, "eu" : { "stringUnit" : { "state" : "translated", - "value" : "Ezabatu" + "value" : "Block User" } }, "fa" : { "stringUnit" : { "state" : "translated", - "value" : "حذف" + "value" : "مسدود کردن کاربر" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Poista" + "value" : "Estä käyttäjä" } }, "fil" : { "stringUnit" : { "state" : "translated", - "value" : "Burahin" + "value" : "I-block Ang User" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Supprimer" + "value" : "Bloquer l'utilisateur" } }, "gl" : { "stringUnit" : { "state" : "translated", - "value" : "Borrar" + "value" : "Bloquear usuario" } }, "ha" : { "stringUnit" : { "state" : "translated", - "value" : "Goge" + "value" : "To'she Mai Amfani" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "מחק" + "value" : "חסום משתמש" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "मिटाएं" + "value" : "उपयोगकर्ता को ब्लॉक करें" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Obriši" + "value" : "Blokiraj korisnika" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Törlés" + "value" : "Felhasználó letiltása" } }, "hy-AM" : { "stringUnit" : { "state" : "translated", - "value" : "Ջնջել" + "value" : "Արգելափակել օգտատիրոջը" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Hapus" + "value" : "Blokir Pengguna" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Elimina" + "value" : "Blocca utente" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "削除" + "value" : "ユーザーをブロック" } }, "ka" : { "stringUnit" : { "state" : "translated", - "value" : "წაშლა" + "value" : "დაბლოკეთ მომხმარებელი" } }, "km" : { "stringUnit" : { "state" : "translated", - "value" : "លុប" + "value" : "ទប់ស្កាត់អ្នក​ប្រើ​" } }, "kn" : { "stringUnit" : { "state" : "translated", - "value" : "ಅಳಿಸಿ" + "value" : "ಬಳಕೆದಾರರನ್ನು ತಡೆಯಿರಿ" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "삭제" + "value" : "사용자 차단" } }, "ku" : { "stringUnit" : { "state" : "translated", - "value" : "سڕینەوە" + "value" : "دەسەڵاتدانە کەسی" } }, "ku-TR" : { "stringUnit" : { "state" : "translated", - "value" : "Jê bibe" + "value" : "Bikarhênerê Blok Bike" } }, "lg" : { "stringUnit" : { "state" : "translated", - "value" : "Jjamu" + "value" : "Block User" } }, "lo" : { "stringUnit" : { "state" : "translated", - "value" : "ລຶບ" + "value" : "ຫ້າມຜູ້ນັກ" } }, "lt" : { "stringUnit" : { "state" : "translated", - "value" : "Ištrinti" + "value" : "Užblokuoti naudotoją" } }, "lv" : { "stringUnit" : { "state" : "translated", - "value" : "Dzēst" + "value" : "Bloķēt lietotāju" } }, "mk" : { "stringUnit" : { "state" : "translated", - "value" : "Избриши" + "value" : "Блокирај корисник" } }, "mn" : { "stringUnit" : { "state" : "translated", - "value" : "Устгах" + "value" : "Хэрэглэгчийг хаах" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Padam" + "value" : "Sekat Pengguna" } }, "my" : { "stringUnit" : { "state" : "translated", - "value" : "ဖျက်မည်" + "value" : "သုံးစွဲသူကို ဘလော့မည်" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Slett" + "value" : "Blokker bruker" } }, "nb-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Slett" + "value" : "Blokker bruker" } }, "ne-NP" : { "stringUnit" : { "state" : "translated", - "value" : "मेटाउनुहोस्" + "value" : "प्रयोगकर्ता ब्लक गर्नुहोस्" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Verwijderen" + "value" : "Blokkeer gebruiker" } }, "nn-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Slett" + "value" : "Blokker brukar" } }, "ny" : { "stringUnit" : { "state" : "translated", - "value" : "Chotsani" + "value" : "Block User" } }, "pa-IN" : { "stringUnit" : { "state" : "translated", - "value" : "ਹਟਾਓ" + "value" : "ਉਪਭੋਗਤਾ ਨੂੰ ਬਲੌਕ ਕਰੋ" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Usuń" + "value" : "Zablokuj użytkownika" } }, "ps" : { "stringUnit" : { "state" : "translated", - "value" : "ړنګول" + "value" : "کاروونکی بلاک کړئ" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Excluir" + "value" : "Bloquear Usuário" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Apagar" + "value" : "Bloquear utilizador" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Șterge" + "value" : "Blochează utilizator" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Удалить" + "value" : "Заблокировать пользователя" } }, "sh" : { "stringUnit" : { "state" : "translated", - "value" : "Obriši" + "value" : "Blokiraj korisnika" } }, "si-LK" : { "stringUnit" : { "state" : "translated", - "value" : "මකන්න" + "value" : "අවහිර කරන්න" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Vymazať" + "value" : "Zablokovať používateľa" } }, "sl" : { "stringUnit" : { "state" : "translated", - "value" : "Izbriši" + "value" : "Blokiraj uporabnika" } }, "sq" : { "stringUnit" : { "state" : "translated", - "value" : "Fshije" + "value" : "Bllokoni përdoruesin" } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : "Обриши" + "value" : "Блокирај корисника" } }, "sr-Latn" : { "stringUnit" : { "state" : "translated", - "value" : "Obriši" + "value" : "Blokiraj korisnika" } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Radera" + "value" : "Blockera användare" } }, "sw" : { "stringUnit" : { "state" : "translated", - "value" : "Futa" + "value" : "Zuia Mtumiaji" } }, "ta" : { "stringUnit" : { "state" : "translated", - "value" : "நீக்கு" + "value" : "பயனரைத் தடை செய்யவும்" } }, "te" : { "stringUnit" : { "state" : "translated", - "value" : "సందేశాన్ని తొలగించు" + "value" : "వినియోగదారుని నిరోధించు" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "ลบ" + "value" : "บล็อกผู้ใช้" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Sil" + "value" : "Kullanıcıyı Engelle" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Видалити" + "value" : "Заблокувати користувача" } }, "ur-IN" : { "stringUnit" : { "state" : "translated", - "value" : "حذف کریں" + "value" : "صارف کو بلاک کریں" } }, "uz" : { "stringUnit" : { "state" : "translated", - "value" : "O'chirish" + "value" : "Foydalanuvchini bloklash" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Xóa" + "value" : "Chặn Người dùng này" } }, "xh" : { "stringUnit" : { "state" : "translated", - "value" : "Sangula" + "value" : "Vimba Umsebenzisi" } }, "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "删除" + "value" : "屏蔽用户" } }, "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "刪除" + "value" : "封鎖使用者" } } } }, - "deleteAfterGroupFirstReleaseConfigOutdated" : { + "deleteAfterGroupPR1GroupSettings" : { "extractionState" : "manual", "localizations" : { "af" : { "stringUnit" : { "state" : "translated", - "value" : "Sommige van jou toestelle gebruik verouderde weergawes. Sinchronisering mag onbetroubaar wees totdat hulle opgedateer word." + "value" : "Groep Instellings" } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "بعض أجهزتك تستخدم إصدارات قديمة. قد تكون المزامنة غير موثوقة حتى يتم تحديثها." + "value" : "إعدادات المجموعة" } }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "Bəzi cihazlarınız köhnə versiyaları istifadə edir. Güncəllənənə qədər sinxronlaşdırma güvənli olmaya bilər." + "value" : "Qrup ayarları" } }, "bal" : { "stringUnit" : { "state" : "translated", - "value" : "کچھ دستگاهاتیں یونی اپڈیتاہ ضخ و عرض چکس" + "value" : "گروپءِ ستینز" } }, "be" : { "stringUnit" : { "state" : "translated", - "value" : "Некаторыя з вашых прылад выкарыстоўваюць састарэлую версію. Сінхранізацыя можа быць ненадзейнай, пакуль яны не будуць абноўлены." + "value" : "Налады групы" } }, "bg" : { "stringUnit" : { "state" : "translated", - "value" : "Някои от вашите устройства използват остарели версии. Синхронизацията може да бъде ненадеждна, докато не бъдат актуализирани." + "value" : "Настройки на групата" } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "আপনার কিছু ডিভাইস পুরোনো ভার্সন ব্যবহার করছে। যতক্ষন না আপডেট হয়, সিঙ্কিং ভরসাযোগ্য নাও হতে পারে।" + "value" : "গ্রুপ সেটিংস" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Alguns dels vostres dispositius tenen versions antigues. La sincronització pot no ser fialbe fins que estiguin actualitzats." + "value" : "Configuració del Grup" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Některá z vašich zařízení používají zastaralé verze. Synchronizace může být nespolehlivá, dokud nebudou aktualizovány." + "value" : "Nastavení skupiny" } }, "cy" : { "stringUnit" : { "state" : "translated", - "value" : "Mae rhai o'ch dyfeisiau'n defnyddio fersiynau allan o ddyddiad. Efallai na fydd cydamseru'n ddibynadwy nes cânt eu diweddaru." + "value" : "Gosodiadau Grŵp" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Nogle af dine enheder bruger forældede versioner. Synkroniseringen kan være upålidelig, indtil de er opdaterede." + "value" : "Gruppens indstillinger" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Einige deiner Geräte verwenden veraltete Versionen. Die Synchronisierung kann unzuverlässig sein, bis sie aktualisiert werden." + "value" : "Gruppen-Einstellungen" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Μερικές από τις συσκευές σας χρησιμοποιούν ξεπερασμένες εκδόσεις. Ο συγχρονισμός μπορεί να είναι αναξιόπιστος μέχρι να ενημερωθούν." + "value" : "Ρυθμίσεις Ομάδας" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated." + "value" : "Group Settings" } }, "eo" : { "stringUnit" : { "state" : "translated", - "value" : "Iuj el viaj aparatoj uzas malnoviĝintajn versiojn. Sinkronigado povas esti nefidinda ĝis ili ĝisdatigos." + "value" : "Grupaj Agordoj" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Algunos de tus dispositivos están utilizando versiones desactualizadas. La sincronización puede ser poco confiable hasta que se actualicen." + "value" : "Configuración del grupo" } }, "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Algunos de tus dispositivos están utilizando versiones desactualizadas. La sincronización puede ser poco confiable hasta que se actualicen." + "value" : "Configuración del grupo" } }, "et" : { "stringUnit" : { "state" : "translated", - "value" : "Mõned teie seadmed kasutavad aegunud versioone. Sünkroonimine võib olla ebausaldusväärne, kuni neid värskendatakse." + "value" : "Grupi sätted" } }, "eu" : { "stringUnit" : { "state" : "translated", - "value" : "Zure gailu batzuk bertsio zaharkituak erabiltzen ari dira. Sinkronizazioa ez da fidagarria izango eguneratu arte." + "value" : "Taldearen Ezarpenak" } }, "fa" : { "stringUnit" : { "state" : "translated", - "value" : "برخی از دستگاه‌های شما از نسخه‌های قدیمی استفاده می‌کنند. همگام سازی ممکن است غیر قابل اعتماد باشد تا زمانی که به روز شوند." + "value" : "تنظیمات گروه" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Jotkin laitteesi käyttävät vanhentuneita versioita ja synkronoinnin toiminnassa voi olla ongelmia, kunnes ne on päivitetty." + "value" : "Ryhmäasetukset" } }, "fil" : { "stringUnit" : { "state" : "translated", - "value" : "Ang ilan sa iyong mga device ay gumagamit ng mga lipas na bersyon. Ang pag-sync ay maaaring hindi maaasahan hanggang sila ay ma-update." + "value" : "Mga Setting ng Grupo" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Certains de vos appareils utilisent des versions obsolètes. La synchronisation peut être instable jusqu'à ce qu'ils soient mis à jour." + "value" : "Paramètres de groupe" } }, "gl" : { "stringUnit" : { "state" : "translated", - "value" : "Algúns dos teus dispositivos están usando versións desactualizadas. A sincronización pode non ser de confianza ata que se actualicen." + "value" : "Axustes do grupo" } }, "ha" : { "stringUnit" : { "state" : "translated", - "value" : "Wasu daga cikin na'urorinku suna amfani da tsoffin sigogi. Hada na'urorin na iya zama maras tabbas har sai an sabunta su." + "value" : "Saitunan rukunin" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "חלק מהמכשירים שלך משתמשים בגרסאות מיושנות. הסנכרון עשוי להיות לא יציב עד שהם יעודכנו." + "value" : "הגדרות קבוצה" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "आपके कुछ डिवाइस पुराने संस्करणों का उपयोग कर रहे हैं। जब तक वे अपडेट नहीं हो जाते, सिंक अविश्वसनीय हो सकता है।" + "value" : "समूह सेटिंग्स" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Neki od vaših uređaja koriste zastarjele verzije. Sinkronizacija može biti nepouzdana dok se ne ažuriraju." + "value" : "Postavke grupe" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Egyes eszközeid elavult verziókat használnak. A szinkronizálás megbízhatatlan lehet a frissítésig." + "value" : "Csoport beállítások" } }, "hy-AM" : { "stringUnit" : { "state" : "translated", - "value" : "Ձեր որոշ սարքեր օգտագործում են հնացած տարբերակներ։ Համաժամեցումը կարող է լինել ոչ կայուն մինչ դրանք թարմացվեն։" + "value" : "Խմբի կարգավորումներ" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Beberapa perangkat Anda menggunakan versi lama. Sinkronisasi mungkin tidak dapat diandalkan hingga diperbarui." + "value" : "Pengaturan grup" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Alcuni dei tuoi dispositivi stanno utilizzando una versione obsoleta. La sincronizzazione potrebbe risultare inaffidabile finché non verranno aggiornati." + "value" : "Impostazioni gruppo" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "一部のデバイスは古いバージョンを使用しています。同期が更新されるまで信頼性が低い場合があります。" + "value" : "グループ設定" } }, "ka" : { "stringUnit" : { "state" : "translated", - "value" : "ზოგიერთი თქვენი მოწყობილობა იყენებს მოძველებულ ვერსიებს. სინქრონიზაცია შეიძლება არაკეთილსაწინააღმდეგო იყოს სანამ ისინი განახლებენ." + "value" : "ჯგუფის პარამეტრები" } }, "km" : { "stringUnit" : { "state" : "translated", - "value" : "ឧបករណ៍របស់អ្នកមួយចំនួនបានប្រើប្រាស់កំណែហួសសម័យ។ ការសមកាលកម្មអាចមិនគួរឱ្យទុកចិត្តរហូតពួកគេត្រូវបានអាប់ដេត។" + "value" : "ការកំណត់ក្រុម" } }, "kn" : { "stringUnit" : { "state" : "translated", - "value" : "ನಿಮ್ಮ ಕೆಲವು ಉಪಕರಣಗಳು ಹಳೆಯ ಆವೃತ್ತಿಗಳನ್ನು ಬಳಸುತ್ತಿವೆ. ಅವುಗಳನ್ನು ಅಪ್‌ಡೇಟ್ ಮಾಡಿದರೆ ಸಂಬಂಧಿಕ ವೇಳೆಾಂಗಿತ ದೂರವಿರಲುಂದು ಸಂಕರವಾಗಿದದ್ದು ಎನ್ಕಉಂಬುದೋನು." + "value" : "ಗುಂಪು ಸಂಯೋಜನೆಗಳು" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "일부 기기가 구버전을 사용 중입니다. 업데이트되지 않은 경우 동기화가 불안정할 수 있습니다." + "value" : "그룹 설정" } }, "ku" : { "stringUnit" : { "state" : "translated", - "value" : "هەندێك لە ئامێرەکانت بە كۆمپەرسیەکانە کۆن بەکاردەهێنن. یەکخستن کردنی دەکرێت بە پەیوەندیدار نەبێت تاکوو یەکدەکرێن." + "value" : "ڕێکەوتکردنی گروپ" } }, "ku-TR" : { "stringUnit" : { "state" : "translated", - "value" : "Hin cîhazên te versîyonên paşmayî diemilînin. Senkronîzekirin dibe ku ne pêbawer be, heya ku ew neyên rojanekirin." + "value" : "Mîhengên Komê" } }, "lg" : { "stringUnit" : { "state" : "translated", - "value" : "Ebimu ku byuma byo birina amakyaaga g'omukadde. Okukola kw'ebigatta okulowoofu kunaawera okutuusa lwe bidikusibwa." + "value" : "Setingi ze Kibinja" } }, "lt" : { "stringUnit" : { "state" : "translated", - "value" : "Kai kurie jūsų įrenginiai naudoja pasenusias versijas. Sinchronizavimas gali būti nestabilus, kol jie nebus atnaujinti." + "value" : "Grupės nustatymai" } }, "lv" : { "stringUnit" : { "state" : "translated", - "value" : "Dažas no tavām ierīcēm izmanto novecojušas versijas. Sinhronizācija var būt neuzticama, kamēr tās netiks atjauninātas." + "value" : "Grupas iestatījumi" } }, "mk" : { "stringUnit" : { "state" : "translated", - "value" : "Некои од твоите уреди користат застарени верзии. Синхронизацијата може да биде непостојана додека не ги ажурираш." + "value" : "Поставки за групата" } }, "mn" : { "stringUnit" : { "state" : "translated", - "value" : "Таны зарим төхөөрөмж хуучирсан хувилбаруудыг ашиглаж байна. Тааруулах үйл явц найдвартай байхгүй байж магадгүй." + "value" : "Бүлгийн тохиргоо" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Beberapa peranti anda menggunakan versi yang telah lapuk. Penyelarasan mungkin tidak boleh dipercayai sehingga ia dikemas kini." + "value" : "Tetapan Kumpulan" } }, "my" : { "stringUnit" : { "state" : "translated", - "value" : "သင့်စက်အချို့မှာ နောက်ဆုံးပေါ် ဗားရှင်းကို မသုံးထားပါ ။ ပြန်လုပ်ခွင့်မပြုပြီး ဖော်ပြမှုတို့အပြီး Update လုပ်ပါ" + "value" : "အုပ်စုဆက်တင်များ" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Noen av enhetene dine bruker utdaterte versjoner. Synkronisering kan være upålitelig frem til de er oppdatert." + "value" : "Gruppeinnstillinger" } }, "nb-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Noen av enhetene dine bruker utdaterte versjoner. Synkronisering kan være upålitelig inntil de er oppdatert." + "value" : "Gruppeinnstillinger" } }, "ne-NP" : { "stringUnit" : { "state" : "translated", - "value" : "तपाईंका केही उपकरणहरू पुराना संस्करणहरू प्रयोग गरिरहेका छन्। तिनीहरूको अद्यावधिक नभएसम्म समकालन अस्थिर हुन सक्छ।" + "value" : "समूह सेटिङ्हरू" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Sommige van je apparaten gebruiken verouderde versies. Synchroniseren kan onbetrouwbaar zijn totdat ze zijn bijgewerkt." + "value" : "Groepsinstellingen" } }, "nn-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Nokre av einingane dine brukar utdaterte versjonar. Synkronisering kan vere upåliteleg til desse er oppdaterte." + "value" : "Gruppeinnstillinger" } }, "ny" : { "stringUnit" : { "state" : "translated", - "value" : "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated." + "value" : "Zikhazikiko za Gulu" } }, "pa-IN" : { "stringUnit" : { "state" : "translated", - "value" : "ਤੁਹਾਡੇ ਕੁਝ ਉਪਕਰਣ ਪੁਰਾਣਾ ਵਰਜਨ ਵਰਤ ਰਹੇ ਹਨ। ਸਿੰਕ ਰੀਲਾਇਸਬਲ ਨਹੀ ਹੋ ਸਕਦਾ ਜਦ ਤਕ ਕਿ ਉਹ ਨੂੰ ਅੱਪਡੇਟ ਨਹੀਂ ਕੀਤੇ ਜਾਂਦੇ।" + "value" : "ਗਰੁੱਪ ਸੈਟਿੰਗਾਂ" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Niektóre urządzenia korzystają z nieaktualnych wersji. Synchronizacja może być zawodna do czasu ich aktualizacji." + "value" : "Ustawienia grupy" } }, "ps" : { "stringUnit" : { "state" : "translated", - "value" : "ځینې ستاسو وسایل زوړ نسخې کاروي. همغږي د دې وسایلو تر تازه کولو پورې بې باوره کېدی شي." + "value" : "د ډلې تنظیمات" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Alguns dos seus dispositivos estão usando versões desatualizadas. A sincronização pode não ser confiável até que sejam atualizados." + "value" : "Configurações do Grupo" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Alguns dos seus dispositivos estão a usar versões antigas. A sincronização pode ser pouco confiável até que sejam atualizados." + "value" : "Definições do grupo" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Unele dintre dispozitivele tale folosesc versiuni învechite. Sincronizarea poate fi nesigură până când acestea sunt actualizate." + "value" : "Setările grupului" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Некоторые из ваших устройств используют устаревшие версии. Синхронизация может быть ненадежной до тех пор, пока они не будут обновлены." + "value" : "Настройки группы" } }, "sh" : { "stringUnit" : { "state" : "translated", - "value" : "Neki od vaših uređaja koriste zastarjele verzije. Sinhronizacija može biti nepouzdana dok se ne ažuriraju." + "value" : "Postavke grupe" } }, "si-LK" : { "stringUnit" : { "state" : "translated", - "value" : "ඔබගේ උපාංග කිහිපයක් පැරණි අනුවාද භාවිත කරයි. දක්නට ඇති බවකට පරීක්ෂා විය හැක." + "value" : "සමූහ සේටින්" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Niektoré vaše zariadenia používajú zastarané verzie. Synchronizácia môže byť nespoľahlivá, kým nebudú aktualizované." + "value" : "Nastavenia skupiny" } }, "sl" : { "stringUnit" : { "state" : "translated", - "value" : "Nekatere vaše naprave uporabljajo zastarele različice. Sinhronizacija morda ne bo zanesljiva, dokler ne bodo posodobljene." + "value" : "Nastavitve skupine" } }, "sq" : { "stringUnit" : { "state" : "translated", - "value" : "Disa nga pajisjet tuaja po përdorin versione të vjetra. Sinkronizimi mund të mos jetë i besueshëm deri sa të përditësohen." + "value" : "Cilësimet e grupit" } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : "Неки од твојих уређаја користе застареле верзије. Синхронизација може бити непоуздана до ажурирања." + "value" : "Подешавања групе" } }, "sr-Latn" : { "stringUnit" : { "state" : "translated", - "value" : "Neki od vaših uređaja koriste zastarele verzije. Sinhronizacija može biti nepouzdana dok se ne ažuriraju." + "value" : "Podešavanja grupe" } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Några av dina enheter använder gamla versioner. Synkronisering kan vara opålitlig tills de uppdateras." + "value" : "Gruppinställningar" } }, "sw" : { "stringUnit" : { "state" : "translated", - "value" : "Baadhi ya vifaa vyako vina matoleo yasiyo ya kisasa. Usawazishaji unaweza kuwa si wa kuaminika hadi yamesasishwa." + "value" : "Mipangilio ya Kikundi" } }, "ta" : { "stringUnit" : { "state" : "translated", - "value" : "உங்கள் சாதனங்களில் சில பழைய பதிப்புகளைப் பயன்படுத்திக் கொண்டிருக்கின்றன. சிங்கிங் வாங்கുവையில் குறிப்பாக விசையமைப்புகளைப் பயன்படுத்தவும்." + "value" : "குழு அமைப்புகள்" } }, "te" : { "stringUnit" : { "state" : "translated", - "value" : "మీ పరికరాలలో కొన్ని పాత సంస్కరణలను ఉపయోగిస్తున్నాయి. అవి నవీకరించేవరకూ సింక్ అవ్వడంలో విశ్వసనీయత ఉండకపోవచ్చు." + "value" : "సమూహ సెట్టింగ్‌లు" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "อุปกรณ์บางอย่างของคุณใช้เวอร์ชันที่ล้าสมัย การซิงค์อาจไม่น่าเชื่อถือจนกว่าจะได้รับการอัปเดต" + "value" : "การตั้งค่ากลุ่ม" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Bazı cihazlarınız eski sürümleri kullanıyor. Güncellenene kadar senkronizasyon güvenilir olmayabilir." + "value" : "Grup Ayarları" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Деякі з ваших пристроїв використовують застарілу версію застосунку. Синхронізація може не вдатись, доки вони не будуть оновлені." + "value" : "Налаштування групи" } }, "ur-IN" : { "stringUnit" : { "state" : "translated", - "value" : "آپ کے کچھ آلات پرانی ورژن استعمال کر رہے ہیں۔ ہم آہنگی قابل اعتماد نہیں ہوسکتی جب تک کہ انہیں اپ ڈیٹ نہ کیا جائے۔" + "value" : "گروپ کی ترتیبات" } }, "uz" : { "stringUnit" : { "state" : "translated", - "value" : "Ayrim qurilmalaringiz eskirgan versiyalarni ishlatmoqda. Sinxronizatsiya yangilanmaganicha ishonchsiz bo'lishi mumkin." + "value" : "Guruhning sozlamalari" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Một số thiết bị của bạn đang sử dụng các phiên bản đã lỗi thời. Đồng bộ hóa có thể không đáng tin cậy cho đến khi chúng được cập nhật." + "value" : "Cài đặt nhóm" } }, "xh" : { "stringUnit" : { "state" : "translated", - "value" : "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated." + "value" : "UsesiQela" } }, "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "您的某些设备正在使用旧版本客户端。同步功能可能在更新之前不稳定。" + "value" : "群组设置" } }, "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "您的部分裝置正在使用過時的版本。在更新之前,同步可能不可靠。" + "value" : "群組設定" } } } }, - "deleteAfterGroupPR1BlockThisUser" : { + "deleteAfterGroupPR1MentionsOnly" : { "extractionState" : "manual", "localizations" : { "af" : { "stringUnit" : { "state" : "translated", - "value" : "Blokkeer Hierdie Gebruiker" + "value" : "Slegs waarsku vir vermeldings" } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "حظر هذا المستخدم" + "value" : "إشعار للإشارات فقط" } }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "Bu istifadəçini əngəllə" + "value" : "Yalnız adçəkmələr üçün bildir" } }, "bal" : { "stringUnit" : { "state" : "translated", - "value" : "اس صارف کو روکو" + "value" : "ساب گون ناچ" } }, "be" : { "stringUnit" : { "state" : "translated", - "value" : "Заблакіраваць гэтага карыстальніка" + "value" : "Апавяшчэнне толькі для згадак" } }, "bg" : { "stringUnit" : { "state" : "translated", - "value" : "Блокирай този потребител" + "value" : "Известия само за споменавания" } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "এই ব্যবহারকারীকে ব্লক করুন" + "value" : "Notify for Mentions Only" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Bloquejar a aquest usuari/a" + "value" : "Notify for Mentions Only" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Blokovat tohoto uživatele" + "value" : "Upozorňovat pouze na zmínky" } }, "cy" : { "stringUnit" : { "state" : "translated", - "value" : "Rhwystro'r Defnyddiwr Hwn" + "value" : "Dim ond hysbysu am Gyfeiriadau" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Bloker Denne Bruger" + "value" : "Underret kun når jeg bliver omtalt" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Diese Person blockieren" + "value" : "Nur für Erwähnungen benachrichtigen" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Φραγή Αυτού του Χρήστη" + "value" : "Ειδοποίηση Μόνο για Αναφορές" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Block This User" + "value" : "Notify for Mentions Only" } }, "eo" : { "stringUnit" : { "state" : "translated", - "value" : "Bloki tiun uzanton" + "value" : "Notify for Mentions Only" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Bloquear usuario" + "value" : "Notificar Solo Menciones" } }, "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Bloquear usuario" + "value" : "Notificar Solo Menciones" } }, "et" : { "stringUnit" : { "state" : "translated", - "value" : "Blokeeri see kasutaja" + "value" : "Teavita vaid mainimistest" } }, "eu" : { "stringUnit" : { "state" : "translated", - "value" : "Block This User" + "value" : "Jakinarazi Aipamenetarako bakarrik" } }, "fa" : { "stringUnit" : { "state" : "translated", - "value" : "مسدود کردن کاربر" + "value" : "فقط برای ذکر شده ها اطلاع دهید" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Estä tämä käyttäjä" + "value" : "Huomioi vain mainitut" } }, "fil" : { "stringUnit" : { "state" : "translated", - "value" : "I-block Ang Taong Ito" + "value" : "Notify for Mentions Only" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Bloquer cet utilisateur" + "value" : "Activer les notifications que sur mention" } }, "gl" : { "stringUnit" : { "state" : "translated", - "value" : "Bloquear a este usuario" + "value" : "Notificar só por mencións" } }, "ha" : { "stringUnit" : { "state" : "translated", - "value" : "To'she Wannan Mai Amfani" + "value" : "Sanar Da Aka Kira Na Mentions Kadai" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "חסום משתמש זה" + "value" : "Notify for Mentions Only" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "इस उपयोगकर्ता को ब्लॉक करें" + "value" : "केवल उल्लेखों के लिए सूचित करें" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Blokiraj ovog korisnika" + "value" : "Obavijesti samo kod spominjanja" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Felhasználó letiltása" + "value" : "Értesítés csak említések esetén" } }, "hy-AM" : { "stringUnit" : { "state" : "translated", - "value" : "Արգելափակել այս օգտատիրոջը" + "value" : "Խոսել միայն հիշատակումների համար" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Blokir Pengguna Ini" + "value" : "Beri Tahu Hanya untuk Sebutan" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Blocca questo utente" + "value" : "Notifiche solo per le menzioni" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "このユーザーをブロックする" + "value" : "メンションのみ" } }, "ka" : { "stringUnit" : { "state" : "translated", - "value" : "დაბლოკეთ ეს მომხმარებელი" + "value" : "მხოლოდ მოხსენებები გამომყენებელთათვის" } }, "km" : { "stringUnit" : { "state" : "translated", - "value" : "ទប់ស្កាត់អ្នក​ប្រើ​" + "value" : "Notify for Mentions Only" } }, "kn" : { "stringUnit" : { "state" : "translated", - "value" : "ಈ ಬಳಕೆದಾರರನ್ನು ತಡೆಯಿರಿ" + "value" : "ಉಲ್ಲೇಖಗಳು ಮಾತ್ರ" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "이 사용자를 차단" + "value" : "멘션만 알림 받기" } }, "ku" : { "stringUnit" : { "state" : "translated", - "value" : "دوورخستنەوەی ئەم بەکارهێنەرە" + "value" : "تەنها بۆ بیرکردنەوەکان ئاگادار بکەرەوە" } }, "ku-TR" : { "stringUnit" : { "state" : "translated", - "value" : "Blok Bike Vê Bikarhênerê" + "value" : "Tenê tiya din ji bo peyamên şaneke edə par kendiakirinani wallah" } }, "lg" : { "stringUnit" : { "state" : "translated", - "value" : "Block This User" - } - }, - "lo" : { - "stringUnit" : { - "state" : "translated", - "value" : "ຫ້າມຜູ້ນັກນັບນີ້" + "value" : "Tegeera okuyitibwa nze kwokka" } }, "lt" : { "stringUnit" : { "state" : "translated", - "value" : "Užblokuoti šį naudotoją" + "value" : "Notify for Mentions Only" } }, "lv" : { "stringUnit" : { "state" : "translated", - "value" : "Bloķēt šo lietotāju" + "value" : "Paziņot tikai pieminēšanas gadījumā" } }, "mk" : { "stringUnit" : { "state" : "translated", - "value" : "Блокирај го овој корисник" + "value" : "Извести само за спомнувања" } }, "mn" : { "stringUnit" : { "state" : "translated", - "value" : "Энэ хэрэглэгчийг хориглох" + "value" : "Зөвхөн дурсамжийн талаар мэдэгдээрэй" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Sekat Pengguna Ini" + "value" : "Pemberitahuan hanya untuk Sebutan" } }, "my" : { "stringUnit" : { "state" : "translated", - "value" : "ဤသုံးစွဲသူကို ဘလော့မည်" + "value" : "အဖွဲ့၀င်များ၏ ဆိုင်းချက်အတွက်သာ အသိပေးချက်" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Blokker denne brukeren" + "value" : "Varsle kun når jeg blir omtalt" } }, "nb-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Blokker denne brukeren" + "value" : "Varsle kun når jeg blir omtalt" } }, "ne-NP" : { "stringUnit" : { "state" : "translated", - "value" : "यस प्रयोगकर्तालाई ब्लक गर्नुहोस्" + "value" : "केवल उल्लेखहरूको लागि सूचित गर्नुहोस्" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Deze gebruiker blokkeren" + "value" : "Alleen melding bij vermeldingen" } }, "nn-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Blokker denne brukaren" + "value" : "Varsle kun når jeg blir omtalt" } }, "ny" : { "stringUnit" : { "state" : "translated", - "value" : "Block This User" + "value" : "Zidziwitso za ntchitoyi zimatumizidwa mukafuna kutsatira off - features." } }, "pa-IN" : { "stringUnit" : { "state" : "translated", - "value" : "ਇਸ ਉਪਭੋਗਤਾ ਨੂੰ ਬਲੌਕ ਕਰੋ" + "value" : "ਕੇਵਲ ਜ਼ਿਕਰ ਲਈ ਸੂਚਿਤ ਕਰੋ" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Zablokuj tego użytkownika" + "value" : "Powiadom tylko o wzmiankach" } }, "ps" : { "stringUnit" : { "state" : "translated", - "value" : "دا کاروونکی بلاک کړئ" + "value" : "یوازې یادونه لپاره خبرتیا" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Bloquear este usuário" + "value" : "Notificar apenas menções" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Bloquear este utilizador" + "value" : "Notificar apenas quando sou mencionado" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Blochează acest utilizator" + "value" : "Notifică doar pentru mențiuni" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Заблокировать этого пользователя" + "value" : "Уведомлять только об упоминаниях" } }, "sh" : { "stringUnit" : { "state" : "translated", - "value" : "Blokiraj ovog korisnika" + "value" : "Samo obavijesti za spomene" } }, "si-LK" : { "stringUnit" : { "state" : "translated", - "value" : "මෙම පරිශීලකයා අවහිර කරන්න" + "value" : "සඳහන් කිරීම් සඳහා පමණක් දැනුම් දෙන්න" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Blokovať tohto používateľa" + "value" : "Upozorniť Len ak Spomenutý" } }, "sl" : { "stringUnit" : { "state" : "translated", - "value" : "Blokiraj uporabnika" + "value" : "Obvesti le za omembe" } }, "sq" : { "stringUnit" : { "state" : "translated", - "value" : "Bllokoni këtë përdorues" + "value" : "Veç për Përmendjet" } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : "Блокирати овог корисника" + "value" : "Обавештавај само за помене" } }, "sr-Latn" : { "stringUnit" : { "state" : "translated", - "value" : "Blokiraj ovog korisnika" + "value" : "Obavesti samo kada se pominje" } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Blockera denna användare" + "value" : "Notifiera endast för omnämnanden" } }, "sw" : { "stringUnit" : { "state" : "translated", - "value" : "Zuia Mtumiaji Huyu" + "value" : "Taarifa za bubu tu kwa kutajwa" } }, "ta" : { "stringUnit" : { "state" : "translated", - "value" : "இந்த பயனரைத் தடை செய்யவும்" + "value" : "குறிப்பில் மட்டும் அறிவிக்கவும்" } }, "te" : { "stringUnit" : { "state" : "translated", - "value" : "ఈ వినియోగదారుని నిరోధించు" + "value" : "కేవలం పేర్లు" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "บล็อกคนนี้" + "value" : "แจ้งเฉพาะการกล่าวถึง" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Bu Kullanıcıyı Engelle" + "value" : "Yalnızca Bahsedildiğinde Bildir" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Заблокувати користувача" + "value" : "Сповіщати лише про згадки" } }, "ur-IN" : { "stringUnit" : { "state" : "translated", - "value" : "اس صارف کو بلاک کریں" + "value" : "صرف تذکرات کے لئے اطلاع دیں" } }, "uz" : { "stringUnit" : { "state" : "translated", - "value" : "Ushbu foydalanuvchini bloklash" + "value" : "Faqat tilash haqida habar qilish" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Chặn Người dùng này" + "value" : "Notify for Mentions Only" } }, "xh" : { "stringUnit" : { "state" : "translated", - "value" : "Vimba Lo Msebenzisi" + "value" : "Zaziso ZeMentions Kuphela" } }, "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "屏蔽该用户" + "value" : "仅在提及时通知" } }, "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "封鎖此使用者" + "value" : "提及我時才通知" } } } }, - "deleteAfterGroupPR1BlockUser" : { + "deleteAfterGroupPR1MentionsOnlyDescription" : { "extractionState" : "manual", "localizations" : { "af" : { "stringUnit" : { "state" : "translated", - "value" : "Blokkeer Gebruiker" + "value" : "Wanneer geaktiveer, sal jy net kennisgewings ontvang vir boodskappe wat jou noem." } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "حظر مستخدم" + "value" : "عند التمكين، سيتم إشعارك بالرسائل التي تشير إليك فقط." } }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "İstifadəçini əngəllə" + "value" : "Fəal olduqda, yalnız adınız çəkilən mesajlar barədə məlumatlandırılacaqsınız." } }, "bal" : { "stringUnit" : { "state" : "translated", - "value" : "صارف کو روکو" + "value" : "ہنرگنتہ، شمای چہگان پیغاماتی آ اینیگ ذکری بکود بیتنگ، گپ چندکندگ۔" } }, "be" : { "stringUnit" : { "state" : "translated", - "value" : "Заблакіраваць карыстальніка" + "value" : "Калі гэта ўключана, вы будзеце атрымліваць апавяшчэнні толькі аб паведамленнях, у якіх вы згадваецеся." } }, "bg" : { "stringUnit" : { "state" : "translated", - "value" : "Блокиране на потребител" + "value" : "Когато е включено, ще получавате известия само за съобщения, които ви споменават." } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "ব্যবহারকারীকে ব্লক করুন" + "value" : "When enabled, you'll only be notified for messages mentioning you." } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Bloquejar usuari" + "value" : "Quan estigui activat, només rebreu notificacions per missatges que us mencionin." } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Blokovat uživatele" + "value" : "Když je povoleno, budete upozorněni pouze na zprávy, které vás zmiňují." } }, "cy" : { "stringUnit" : { "state" : "translated", - "value" : "Rhwystro Defnyddiwr" + "value" : "Pan fydd ar gael, dim ond y negeseuon sy'n eich crybwyll byddwch chi'n cael eich hysbysu amdanynt." } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Bloker Bruger" + "value" : "Når aktiveret, vil du kun blive underrettet for beskeder, der nævner dig." } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Person blockieren" + "value" : "Wenn aktiviert, wirst du nur über Nachrichten benachrichtigt, in denen du erwähnt wirst." } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Φραγή Χρήστη" + "value" : "Όταν ενεργοποιηθεί, θα ειδοποιηθείτε μόνο για μηνύματα που σας αναφέρουν." } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Block User" + "value" : "When enabled, you'll only be notified for messages mentioning you." } }, "eo" : { "stringUnit" : { "state" : "translated", - "value" : "Bloki uzanton" + "value" : "Kiam ebligita, vi ricevos notifikojn nur pri mesaĝoj kiuj mencios vin." } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Bloquear usuario" + "value" : "Cuando está activado, sólo se te notificará de mensajes que te mencionen." } }, "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Bloquear usuario" + "value" : "Cuando está activado, sólo se te notificará de mensajes que te mencionen." } }, "et" : { "stringUnit" : { "state" : "translated", - "value" : "Blokeeri kasutaja" + "value" : "Kui lubatud, teavitatakse teid ainult sõnumitest, mis mainivad teid." } }, "eu" : { "stringUnit" : { "state" : "translated", - "value" : "Block User" + "value" : "Gaituta dagoenean, zu aipatzen dituzten mezuek soilik abisatuko zaituzte." } }, "fa" : { "stringUnit" : { "state" : "translated", - "value" : "مسدود کردن کاربر" + "value" : "وقتی فعال باشد، فقط برای پیام هایی که از شما نام می برند مطلع می شوید." } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Estä käyttäjä" + "value" : "Kun toiminto on käytössä, saat ilmoituksia vain sinut mainitsevista viesteistä." } }, "fil" : { "stringUnit" : { "state" : "translated", - "value" : "I-block Ang User" + "value" : "Kapag naka-enable, ikaw ay makakatanggap lamang ng notifications para sa mga mensaheng tinutukoy ka." } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Bloquer l'utilisateur" + "value" : "Quand activé, vous recevrez les notifications uniquement pour les messages qui vous mentionnent." } }, "gl" : { "stringUnit" : { "state" : "translated", - "value" : "Bloquear usuario" + "value" : "Cando estea activado, só recibirás notificacións para mensaxes que te mencionen." } }, "ha" : { "stringUnit" : { "state" : "translated", - "value" : "To'she Mai Amfani" + "value" : "Lokacin da aka kunna, za a sanar da ku kawai don saƙonni da suke ambatarku." } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "חסום משתמש" + "value" : "כאשר אופציה זו מופעלת, תקבל/י הודעות על הודעות בהן הוזכרת בלבד." } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "उपयोगकर्ता को ब्लॉक करें" + "value" : "सक्षम होने पर, आपको केवल उन्हीं संदेशों के लिए सूचित किया जाएगा जो आपको उल्लेखित करते हैं।" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Blokiraj korisnika" + "value" : "Kad je omogućeno, bit ćete obaviješteni samo o porukama koje vas spominju." } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Felhasználó letiltása" + "value" : "Ha engedélyezve van, csak a téged említő üzenetekről kapsz értesítést." } }, "hy-AM" : { "stringUnit" : { "state" : "translated", - "value" : "Արգելափակել օգտատիրոջը" + "value" : "Միացնելու դեպքում, դուք ծանուցվելու եք միայն ձեզ նշող հաղորդագրությունների համար։" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Blokir Pengguna" + "value" : "Saat diaktifkan, Anda hanya akan diberi tahu untuk pesan yang menyebut Anda." } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Blocca utente" + "value" : "Se abilitato, verrai notificato solo per i messaggi in cui vieni menzionano." } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "ユーザーをブロック" + "value" : "有効にすると、あなたに言及するメッセージのみが通知されます。" } }, "ka" : { "stringUnit" : { "state" : "translated", - "value" : "დაბლოკეთ მომხმარებელი" + "value" : "როდესაც ჩართულია, მხოლოდ იმ შეტყობინებებზე მიიღებთ შეტყობინებებს, რომლებიც თქვენ გეხებიან." } }, "km" : { "stringUnit" : { "state" : "translated", - "value" : "ទប់ស្កាត់អ្នក​ប្រើ​" + "value" : "ជាប្រព្រឹត្តិការណ៍ អ្នកនឹងត្រូវបានជូនដំណឹងសម្រាប់សារដែលបានហៅ ឬកិច្ចសារ" } }, "kn" : { "stringUnit" : { "state" : "translated", - "value" : "ಬಳಕೆದಾರರನ್ನು ತಡೆಯಿರಿ" + "value" : "ಚಾಲನೆ ಮಾಡಿದಾಗ, ನಿಮಗೆ ಮಾತ್ರ ನಿಮ್ಮಲ್ಲಿ ಉಲ್ಲೇಖಿಸಿದ ಸಂದೇಶಗಳಿಗೆ ನೋಟಿಫಿಕೇಶನ್ ದೊರೆಯುತ್ತದೆ." } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "사용자 차단" + "value" : "활성화되면 당신이 언급된 메시지만 알림을 받게 됩니다." } }, "ku" : { "stringUnit" : { "state" : "translated", - "value" : "دەسەڵاتدانە کەسی" + "value" : "کاتێک داچوو، تۆ تەنها باسی دەکەیت بۆ نامەکانی پاراستنی تۆ." } }, "ku-TR" : { "stringUnit" : { "state" : "translated", - "value" : "Bikarhênerê Blok Bike" + "value" : "Gava pêveka aktîv bike, hûn ten foraan bêjin mesajên yedi nexşanda te kirîna hûncarî." } }, "lg" : { "stringUnit" : { "state" : "translated", - "value" : "Block User" - } - }, - "lo" : { - "stringUnit" : { - "state" : "translated", - "value" : "ຫ້າມຜູ້ນັກ" + "value" : "Nga okolezebwa, onajanjaluka bubi ku bubakabwe bujanjaluko" } }, "lt" : { "stringUnit" : { "state" : "translated", - "value" : "Užblokuoti naudotoją" + "value" : "Kai įjungta, būsite informuoti tik apie žinutes, kuriose esate paminėti." } }, "lv" : { "stringUnit" : { "state" : "translated", - "value" : "Bloķēt lietotāju" + "value" : "Ja iespējots, tev tiks paziņots tikai par ziņojumiem, kuros esi pieminēts." } }, "mk" : { "stringUnit" : { "state" : "translated", - "value" : "Блокирај корисник" + "value" : "Кога е овозможено, ќе добивате известувања само за пораките кои ве споменуваат." } }, "mn" : { "stringUnit" : { "state" : "translated", - "value" : "Хэрэглэгчийг хаах" + "value" : "Идэвхжүүлсэн үед зөвхөн таныг дурдсан мессежүүдэд л мэдэгдэл ирнэ." } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Sekat Pengguna" + "value" : "Apabila diaktifkan, anda hanya akan diberitahu untuk mesej yang menyebut anda." } }, "my" : { "stringUnit" : { "state" : "translated", - "value" : "သုံးစွဲသူကို ဘလော့မည်" + "value" : "When enabled, you'll only be notified for messages mentioning you." } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Blokker bruker" + "value" : "Når aktivert vil du bare bli varslet når meldinger omtaler deg." } }, "nb-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Blokker bruker" + "value" : "Når aktivert vil du bare bli varslet når meldinger omtaler deg." } }, "ne-NP" : { "stringUnit" : { "state" : "translated", - "value" : "प्रयोगकर्ता ब्लक गर्नुहोस्" + "value" : "जब यो सक्षम गर्ने, तपाईंलाई केवल तपाईंको उल्लेख गर्ने सन्देशहरूको लागि सूचना दिइनेछ।" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Blokkeer gebruiker" + "value" : "Wanneer ingeschakeld, wordt u alleen op de hoogte gesteld voor berichten waarin u vermeld wordt." } }, "nn-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Blokker brukar" + "value" : "Når aktivert vil du bare bli varslet når meldinger omtaler deg." } }, "ny" : { "stringUnit" : { "state" : "translated", - "value" : "Block User" + "value" : "Mukayambitsa, mudzalengezedwa zokha kwa mauthenga omwe akukuthandizani." } }, "pa-IN" : { "stringUnit" : { "state" : "translated", - "value" : "ਉਪਭੋਗਤਾ ਨੂੰ ਬਲੌਕ ਕਰੋ" + "value" : "ਜਦੋਂ ਇਹ ਸਬੰਧਿਤ ਹੁੰਦਾ ਹੈ, ਤੁਸੀਂ ਸਿਰਫ ਮੇਨਸ਼ਨ ਕੀਤੇ ਜਾਣ ਵਾਲੇ ਸੰਦੇਸ਼ਾਂ ਲਈ ਸੂਚਿਤ ਹੋਵੋਗੇ।" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Zablokuj użytkownika" + "value" : "Po włączeniu tej opcji będziesz otrzymywać powiadomienia tylko o wiadomościach, w których o Tobie wspomniano." } }, "ps" : { "stringUnit" : { "state" : "translated", - "value" : "کاروونکی بلاک کړئ" + "value" : "کله چې فعال کړئ، تاسو به یوازې د هغه پیغامونو لپاره خبرتیاوې ترلاسه کړئ چې تاسو ته اشاره کوي." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Bloquear Usuário" + "value" : "Quando ativo, você só receberá notificações quando alguém mencionar você." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Bloquear utilizador" + "value" : "Após a sua ativação, apenas será notificado quando for mencionado numa mensagem." } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Blochează utilizator" + "value" : "În urma activării, vei primi notificări doar pentru mesajele care te menționează." } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Заблокировать пользователя" + "value" : "Когда включено, вы будете уведомлены только о сообщениях, упоминающих вас." } }, "sh" : { "stringUnit" : { "state" : "translated", - "value" : "Blokiraj korisnika" + "value" : "Kada je omogućena, dobićete samo obaveštenja za poruke koje vas pominju." } }, "si-LK" : { "stringUnit" : { "state" : "translated", - "value" : "අවහිර කරන්න" + "value" : "සබල කළ විට, ඔබ සඳහන් කරන පණිවිඩ සඳහා පමණක් ඔබට දැනුම් දෙනු ලැබේ." } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Zablokovať používateľa" + "value" : "Po povolení budete dostávať upozornenia len na správy, v ktorých ste spomenutí." } }, "sl" : { "stringUnit" : { "state" : "translated", - "value" : "Blokiraj uporabnika" + "value" : "Ko je omogočeno, boste dobili obvestila le za sporočila, ki vas omenijo." } }, "sq" : { "stringUnit" : { "state" : "translated", - "value" : "Bllokoni përdoruesin" + "value" : "Kur aktivizohet, do të njoftoheni vetëm për mesazhet që ju përmendin ju." } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : "Блокирај корисника" + "value" : "Када је омогућено, бићете обавештени само за поруке које вас помињу." } }, "sr-Latn" : { "stringUnit" : { "state" : "translated", - "value" : "Blokiraj korisnika" + "value" : "Kada je omogućeno, bićete obavešteni samo za poruke u kojima ste pomenuti." } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Blockera användare" + "value" : "När detta är aktiverat kommer du bara att meddelas för meddelanden som nämner dig." } }, "sw" : { "stringUnit" : { "state" : "translated", - "value" : "Zuia Mtumiaji" + "value" : "Ukiwezeshwa, utaarifiwa tu kwa ujumbe unaokutaja." } }, "ta" : { "stringUnit" : { "state" : "translated", - "value" : "பயனரைத் தடை செய்யவும்" + "value" : "இதை இயக்கினால், உங்களை குறிப்பிட்டுள்ள தகவல்களை மட்டும் அறிவிப்பீர்கள்." } }, "te" : { "stringUnit" : { "state" : "translated", - "value" : "వినియోగదారుని నిరోధించు" + "value" : "ఇది ప్రారంభించినప్పుడు, మీరు మాత్రమే చేర్పించిన సందేశాల కోసం నోటిఫికేషన్‌లు అందుకుంటారు." } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "บล็อกผู้ใช้" + "value" : "เมื่อเปิดใช้งาน คุณจะได้รับการแจ้งเตือนเฉพาะข้อความที่พูดถึงคุณ" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Kullanıcıyı Engelle" + "value" : "Etkinleştirildiğinde, yalnızca sizden bahseden iletiler için bilgilendirileceksiniz." } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Заблокувати користувача" + "value" : "Коли увімкнено, ви будете отримувати сповіщення лише про повідомлення, в яких згадують вас." } }, "ur-IN" : { "stringUnit" : { "state" : "translated", - "value" : "صارف کو بلاک کریں" + "value" : "جب فعال ہو جائے، آپ کو صرف ان پیغامات کے لیے مطلع کیا جائے گا جو آپ کا ذکر کرتے ہیں۔" } }, "uz" : { "stringUnit" : { "state" : "translated", - "value" : "Foydalanuvchini bloklash" + "value" : "Faollashtirilganda, faqat sizni tilga oladigan xabarlar haqida xabarlar olasiz." } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Chặn Người dùng này" + "value" : "Khi được bật, bạn sẽ chỉ nhận thông báo cho các tin nhắn đề cập đến bạn." } }, "xh" : { "stringUnit" : { "state" : "translated", - "value" : "Vimba Umsebenzisi" + "value" : "Xa ivuliwe, uza kuziswa kuphela ngemiyalezo ekukhankanywayo." } }, "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "屏蔽用户" + "value" : "启用后,您只会收到提及您的消息通知。" } }, "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "封鎖使用者" + "value" : "啟用後,你將只會收到提及你的訊息之通知。" } } } }, - "deleteAfterGroupPR1GroupSettings" : { + "deleteAfterGroupPR1MessageSound" : { "extractionState" : "manual", "localizations" : { "af" : { "stringUnit" : { "state" : "translated", - "value" : "Groep Instellings" + "value" : "Boodskap Klank" } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "إعدادات المجموعة" + "value" : "صوت الرسالة" } }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "Qrup ayarları" + "value" : "Mesaj səsi" } }, "bal" : { "stringUnit" : { "state" : "translated", - "value" : "گروپءِ ستینز" + "value" : "Message Sound" } }, "be" : { "stringUnit" : { "state" : "translated", - "value" : "Налады групы" + "value" : "Гук паведамлення" } }, "bg" : { "stringUnit" : { "state" : "translated", - "value" : "Настройки на групата" + "value" : "Тон на съобщенията" } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "গ্রুপ সেটিংস" + "value" : "ম্যাসেজের শব্দ" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Configuració del Grup" + "value" : "So de missatges" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Nastavení skupiny" + "value" : "Zvuk zprávy" } }, "cy" : { "stringUnit" : { "state" : "translated", - "value" : "Gosodiadau Grŵp" + "value" : "Sain Neges" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Gruppens indstillinger" + "value" : "Beskedlyd" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Gruppen-Einstellungen" + "value" : "Nachrichtenton" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Ρυθμίσεις Ομάδας" + "value" : "Ήχος Μηνύματος" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Group Settings" + "value" : "Message Sound" } }, "eo" : { "stringUnit" : { "state" : "translated", - "value" : "Grupaj Agordoj" + "value" : "Mesaĝa Sono" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Configuración del grupo" + "value" : "Mensajes con sonido" } }, "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Configuración del grupo" + "value" : "Notificaciones sonoras" } }, "et" : { "stringUnit" : { "state" : "translated", - "value" : "Grupi sätted" + "value" : "Sõnumiheli" } }, "eu" : { "stringUnit" : { "state" : "translated", - "value" : "Taldearen Ezarpenak" + "value" : "Mezu soinua" } }, "fa" : { "stringUnit" : { "state" : "translated", - "value" : "تنظیمات گروه" + "value" : "صدای پیام" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Ryhmäasetukset" + "value" : "Viestien ilmoitusääni" } }, "fil" : { "stringUnit" : { "state" : "translated", - "value" : "Mga Setting ng Grupo" + "value" : "Tunog ng Mensahe" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Paramètres de groupe" + "value" : "Son de message" } }, "gl" : { "stringUnit" : { "state" : "translated", - "value" : "Axustes do grupo" + "value" : "Son da mensaxe" } }, "ha" : { "stringUnit" : { "state" : "translated", - "value" : "Saitunan rukunin" + "value" : "Sautin saƙo" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "הגדרות קבוצה" + "value" : "צליל הודעה" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "समूह सेटिंग्स" + "value" : "संदेश ध्वनि" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Postavke grupe" + "value" : "Zvuk poruke" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Csoport beállítások" + "value" : "Üzenet hangja" } }, "hy-AM" : { "stringUnit" : { "state" : "translated", - "value" : "Խմբի կարգավորումներ" + "value" : "Հաղորդագրության ձայն" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Pengaturan grup" + "value" : "Suara Pesan" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Impostazioni gruppo" + "value" : "Suono messaggio" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "グループ設定" + "value" : "メッセージサウンド" } }, "ka" : { "stringUnit" : { "state" : "translated", - "value" : "ჯგუფის პარამეტრები" + "value" : "შეტყობინების ხმა" } }, "km" : { "stringUnit" : { "state" : "translated", - "value" : "ការកំណត់ក្រុម" + "value" : "សម្លេងប្រព័ន្ធផ្សព្វផ្សាយ" } }, "kn" : { "stringUnit" : { "state" : "translated", - "value" : "ಗುಂಪು ಸಂಯೋಜನೆಗಳು" + "value" : "ಸಂದೇಶ ಶಬ್ಧ" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "그룹 설정" + "value" : "메시지 소리" } }, "ku" : { "stringUnit" : { "state" : "translated", - "value" : "ڕێکەوتکردنی گروپ" + "value" : "دەنگی نامە" } }, "ku-TR" : { "stringUnit" : { "state" : "translated", - "value" : "Mîhengên Komê" + "value" : "Peyama Dengî" } }, "lg" : { "stringUnit" : { "state" : "translated", - "value" : "Setingi ze Kibinja" + "value" : "Amazzi agata fuka" } }, "lt" : { "stringUnit" : { "state" : "translated", - "value" : "Grupės nustatymai" + "value" : "Žinučių garsas" } }, "lv" : { "stringUnit" : { "state" : "translated", - "value" : "Grupas iestatījumi" + "value" : "Ziņojuma skaņa" } }, "mk" : { "stringUnit" : { "state" : "translated", - "value" : "Поставки за групата" + "value" : "Звук на пораки" } }, "mn" : { "stringUnit" : { "state" : "translated", - "value" : "Бүлгийн тохиргоо" + "value" : "Мессежийн Дуу" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Tetapan Kumpulan" + "value" : "Bunyi Mesej" } }, "my" : { "stringUnit" : { "state" : "translated", - "value" : "အုပ်စုဆက်တင်များ" + "value" : "မက်ဆေ့ခြင်းအသုံယွက်" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Gruppeinnstillinger" + "value" : "Meldingslyd" } }, "nb-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Gruppeinnstillinger" + "value" : "Melding lyd" } }, "ne-NP" : { "stringUnit" : { "state" : "translated", - "value" : "समूह सेटिङ्हरू" + "value" : "सन्देश ध्वनि" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Groepsinstellingen" + "value" : "Geluid van bericht" } }, "nn-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Gruppeinnstillinger" + "value" : "Melding lyd" } }, "ny" : { "stringUnit" : { "state" : "translated", - "value" : "Zikhazikiko za Gulu" + "value" : "Phokoso la uthenga" } }, "pa-IN" : { "stringUnit" : { "state" : "translated", - "value" : "ਗਰੁੱਪ ਸੈਟਿੰਗਾਂ" + "value" : "ਸੁਨੇਹਾ ਸਾਊਂਡ" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Ustawienia grupy" + "value" : "Dźwięk wiadomości" } }, "ps" : { "stringUnit" : { "state" : "translated", - "value" : "د ډلې تنظیمات" + "value" : "پیغام غږ" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Configurações do Grupo" + "value" : "Som de mensagem" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Definições do grupo" + "value" : "Som da Mensagem" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Setările grupului" + "value" : "Sunet notificare mesaj" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Настройки группы" + "value" : "Звук сообщения" } }, "sh" : { "stringUnit" : { "state" : "translated", - "value" : "Postavke grupe" + "value" : "Zvuk poruke" } }, "si-LK" : { "stringUnit" : { "state" : "translated", - "value" : "සමූහ සේටින්" + "value" : "පණිවිඩ ශබ්දය" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Nastavenia skupiny" + "value" : "Zvuk správy" } }, "sl" : { "stringUnit" : { "state" : "translated", - "value" : "Nastavitve skupine" + "value" : "Zvok sporočila" } }, "sq" : { "stringUnit" : { "state" : "translated", - "value" : "Cilësimet e grupit" + "value" : "Tingull mesazhi" } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : "Подешавања групе" + "value" : "Звук поруке" } }, "sr-Latn" : { "stringUnit" : { "state" : "translated", - "value" : "Podešavanja grupe" + "value" : "Zvuk poruke" } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Gruppinställningar" + "value" : "Meddelandeljud" } }, "sw" : { "stringUnit" : { "state" : "translated", - "value" : "Mipangilio ya Kikundi" + "value" : "Sauti ya Ujumbe" } }, "ta" : { "stringUnit" : { "state" : "translated", - "value" : "குழு அமைப்புகள்" + "value" : "Message Sound" } }, "te" : { "stringUnit" : { "state" : "translated", - "value" : "సమూహ సెట్టింగ్‌లు" + "value" : "సందేశ ధ్వని" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "การตั้งค่ากลุ่ม" + "value" : "เสียงข้อความ" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Grup Ayarları" + "value" : "İleti Sesi" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Налаштування групи" + "value" : "Звук повідомлення" } }, "ur-IN" : { "stringUnit" : { "state" : "translated", - "value" : "گروپ کی ترتیبات" + "value" : "پیغام کی آواز" } }, "uz" : { "stringUnit" : { "state" : "translated", - "value" : "Guruhning sozlamalari" + "value" : "Xabar ohangi" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Cài đặt nhóm" + "value" : "Âm báo" } }, "xh" : { "stringUnit" : { "state" : "translated", - "value" : "UsesiQela" + "value" : "Isandi soMyalezo" } }, "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "群组设置" + "value" : "消息提示音" } }, "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "群組設定" + "value" : "訊息音效" } } } }, - "deleteAfterGroupPR1MentionsOnly" : { + "deleteAfterGroupPR3DeleteMessagesConfirmation" : { "extractionState" : "manual", "localizations" : { "af" : { "stringUnit" : { "state" : "translated", - "value" : "Slegs waarsku vir vermeldings" + "value" : "Verwyder die boodskappe in hierdie gesprek permanent?" } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "إشعار للإشارات فقط" + "value" : "حذف المحادثة بصفة نهائية ؟" } }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "Yalnız adçəkmələr üçün bildir" + "value" : "Bu danışıqdakı mesajlar həmişəlik silinsin?" } }, "bal" : { "stringUnit" : { "state" : "translated", - "value" : "ساب گون ناچ" + "value" : "یہ گفتگوءِ پیاماں ہمیشہءِ خاطر حذف کئیں؟" } }, "be" : { "stringUnit" : { "state" : "translated", - "value" : "Апавяшчэнне толькі для згадак" + "value" : "Выдаліць паведамленні ў гэтай размове назаўсёды?" } }, "bg" : { "stringUnit" : { "state" : "translated", - "value" : "Известия само за споменавания" + "value" : "Перманентно изтриване на този разговор?" } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "Notify for Mentions Only" + "value" : "এই কথোপকথনের বার্তাগুলি স্থায়ীভাবে মুছবেন?" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Notify for Mentions Only" + "value" : "Voleu suprimir aquesta conversa de forma permanent?" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Upozorňovat pouze na zmínky" + "value" : "Trvale smazat tuto konverzaci?" } }, "cy" : { "stringUnit" : { "state" : "translated", - "value" : "Dim ond hysbysu am Gyfeiriadau" + "value" : "Dileu'r negeseuon yn y sgwrs hon yn barhaol?" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Underret kun når jeg bliver omtalt" + "value" : "Slet samtale permanent?" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Nur für Erwähnungen benachrichtigen" + "value" : "Soll diese Unterhaltung unwiderruflich gelöscht werden?" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Ειδοποίηση Μόνο για Αναφορές" + "value" : "Να διαγραφούν οριστικά τα μηνύματα σε αυτήν τη συνομιλία;" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Notify for Mentions Only" + "value" : "Permanently delete the messages in this conversation?" } }, "eo" : { "stringUnit" : { "state" : "translated", - "value" : "Notify for Mentions Only" + "value" : "Ĉu porĉiame forigi tiun ĉi tutan interparolon?" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Notificar Solo Menciones" + "value" : "¿Eliminar permanentemente los mensajes en esta conversación?" } }, "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Notificar Solo Menciones" + "value" : "¿Eliminar los mensajes de esta conversación permanentemente?" } }, "et" : { "stringUnit" : { "state" : "translated", - "value" : "Teavita vaid mainimistest" + "value" : "Kas kustutada see vestlus jäädavalt?" } }, "eu" : { "stringUnit" : { "state" : "translated", - "value" : "Jakinarazi Aipamenetarako bakarrik" + "value" : "Mezuak betiko ezabatu elkarrizketa honetan?" } }, "fa" : { "stringUnit" : { "state" : "translated", - "value" : "فقط برای ذکر شده ها اطلاع دهید" + "value" : "آیا می‌خواهید این گفتگو را برای همیشه حذف کنید؟" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Huomioi vain mainitut" + "value" : "Poistetaanko tämä keskustelu pysyvästi?" } }, "fil" : { "stringUnit" : { "state" : "translated", - "value" : "Notify for Mentions Only" + "value" : "Burahin ng tuluyan ang mga mensahe sa usapang ito?" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Activer les notifications que sur mention" + "value" : "Supprimer définitivement les messages dans cette conversation ?" } }, "gl" : { "stringUnit" : { "state" : "translated", - "value" : "Notificar só por mencións" + "value" : "Eliminar permanentemente as mensaxes nesta conversa?" } }, "ha" : { "stringUnit" : { "state" : "translated", - "value" : "Sanar Da Aka Kira Na Mentions Kadai" + "value" : "A kashe keɓewa cikin wannan tattaunawa?" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "Notify for Mentions Only" + "value" : "האם למחוק לצמיתות שיחה זו?" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "केवल उल्लेखों के लिए सूचित करें" + "value" : "इस वार्तालाप को स्थायी रूप से हटाएं?" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Obavijesti samo kod spominjanja" + "value" : "Trajno obrisati ovaj razgovor?" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Értesítés csak említések esetén" + "value" : "Véglegesen törlöd ezt a beszélgetést?" } }, "hy-AM" : { "stringUnit" : { "state" : "translated", - "value" : "Խոսել միայն հիշատակումների համար" + "value" : "Ընդմիշտ ջնջե՞լ այս խոսակցության հաղորդագրությունները:" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Beri Tahu Hanya untuk Sebutan" + "value" : "Hapus obrolan ini selamanya?" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Notifiche solo per le menzioni" + "value" : "Rimuovere definitivamente la chat?" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "メンションのみ" + "value" : "この会話のメッセージを永久に削除しますか?" } }, "ka" : { "stringUnit" : { "state" : "translated", - "value" : "მხოლოდ მოხსენებები გამომყენებელთათვის" + "value" : "გსურთ რომ სამუდამოდ წაშალოთ შეტყობინებები ამ სასაუბროში?" } }, "km" : { "stringUnit" : { "state" : "translated", - "value" : "Notify for Mentions Only" + "value" : "លុបការសន្ទនានេះចោលរហូត?" } }, "kn" : { "stringUnit" : { "state" : "translated", - "value" : "ಉಲ್ಲೇಖಗಳು ಮಾತ್ರ" + "value" : "ಈ ಸಂಭಾಷಣೆಯಲ್ಲಿನ ಸಂದೇಶಗಳನ್ನು ಶಾಶ್ವತವಾಗಿ ಅಳಾಯಿಸಿದಿವಾದೇ?" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "멘션만 알림 받기" + "value" : "대화에서 이 메세지를 영원히 지웁니까?" } }, "ku" : { "stringUnit" : { "state" : "translated", - "value" : "تەنها بۆ بیرکردنەوەکان ئاگادار بکەرەوە" + "value" : "پەیامەکان تەنها بەرێوەبەران لە پەیامەکەدا دەبێ پەیامەکان نادیدار بکەن." } }, "ku-TR" : { "stringUnit" : { "state" : "translated", - "value" : "Tenê tiya din ji bo peyamên şaneke edə par kendiakirinani wallah" + "value" : "Bila ev mesajên di vê sohbetê de daîmen were jêbirin?" } }, "lg" : { "stringUnit" : { "state" : "translated", - "value" : "Tegeera okuyitibwa nze kwokka" + "value" : "Buggya ddala oba ebiwandiiko mu kwogera kuno?" } }, "lt" : { "stringUnit" : { "state" : "translated", - "value" : "Notify for Mentions Only" + "value" : "Ar ištrinti šį pokalbį visiems laikams?" } }, "lv" : { "stringUnit" : { "state" : "translated", - "value" : "Paziņot tikai pieminēšanas gadījumā" + "value" : "Dzēst ziņojumus šajā sarunā uz visiem laikiem?" } }, "mk" : { "stringUnit" : { "state" : "translated", - "value" : "Извести само за спомнувања" + "value" : "Дали сакате трајно да ги избришете пораките во овој разговор?" } }, "mn" : { "stringUnit" : { "state" : "translated", - "value" : "Зөвхөн дурсамжийн талаар мэдэгдээрэй" + "value" : "Энэ яриан дахь мессежүүдийг бүрмөсөн устгах уу?" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Pemberitahuan hanya untuk Sebutan" + "value" : "Hapus mesej dalam perbualan ini secara kekal?" } }, "my" : { "stringUnit" : { "state" : "translated", - "value" : "အဖွဲ့၀င်များ၏ ဆိုင်းချက်အတွက်သာ အသိပေးချက်" + "value" : "ဤစကားပြောဆိုမှုမှ မက်ဆေ့ဂျ်များကို အပြီးတိုင်ဖျက်ပစ်မည်မှာ သေချာပါသလား?" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Varsle kun når jeg blir omtalt" + "value" : "Slett beskjedene permanent i denne samtalen?" } }, "nb-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Varsle kun når jeg blir omtalt" + "value" : "Vil du slette denne samtalen?" } }, "ne-NP" : { "stringUnit" : { "state" : "translated", - "value" : "केवल उल्लेखहरूको लागि सूचित गर्नुहोस्" + "value" : "यस कुराकानीका सन्देशहरू स्थायी रूपमा मेटाउनु हुन्छ?" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Alleen melding bij vermeldingen" + "value" : "De berichten in dit gesprek voorgoed wissen?" } }, "nn-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Varsle kun når jeg blir omtalt" + "value" : "Slett beskjedene permanent i denne samtalen?" } }, "ny" : { "stringUnit" : { "state" : "translated", - "value" : "Zidziwitso za ntchitoyi zimatumizidwa mukafuna kutsatira off - features." + "value" : "Kodi mukufuna kufufuta uthenga mu kukambirana uku mwamvuma?" } }, "pa-IN" : { "stringUnit" : { "state" : "translated", - "value" : "ਕੇਵਲ ਜ਼ਿਕਰ ਲਈ ਸੂਚਿਤ ਕਰੋ" + "value" : "ਕੀ ਤੁਸੀਂ ਇਸ ਗੱਲਬਾਤ ਦੇ ਸੁਨੇਹਿਆਂ ਨੂੰ ਸਥਾਈ ਤੌਰ 'ਤੇ ਮਿਟਾਉਣਾ ਹੈ?" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Powiadom tylko o wzmiankach" + "value" : "Trwale usunąć wiadomości w tej konwersacji?" } }, "ps" : { "stringUnit" : { "state" : "translated", - "value" : "یوازې یادونه لپاره خبرتیا" + "value" : "ایا غواړئ په دې خبرو کې پیغامونه تلپاتې حذف کړئ؟" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Notificar apenas menções" + "value" : "Você deseja apagar esta conversa definitivamente?" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Notificar apenas quando sou mencionado" + "value" : "Deseja apagar definitivamente esta conversa?" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Notifică doar pentru mențiuni" + "value" : "Ștergi permanent mesajele din acestă conversație?" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Уведомлять только об упоминаниях" + "value" : "Удалить эту беседу без возможности восстановления?" } }, "sh" : { "stringUnit" : { "state" : "translated", - "value" : "Samo obavijesti za spomene" + "value" : "Da li zaista želite trajno obrisati poruke iz ovog razgovora?" } }, "si-LK" : { "stringUnit" : { "state" : "translated", - "value" : "සඳහන් කිරීම් සඳහා පමණක් දැනුම් දෙන්න" + "value" : "මෙම සංවාදයේ ඇති පණිවිඩ ස්ථිරවම මකන්නද?" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Upozorniť Len ak Spomenutý" + "value" : "Natrvalo zmazať túto konverzáciu?" } }, "sl" : { "stringUnit" : { "state" : "translated", - "value" : "Obvesti le za omembe" + "value" : "Ali res želite nepovratno izbrisati ta pogovor?" } }, "sq" : { "stringUnit" : { "state" : "translated", - "value" : "Veç për Përmendjet" + "value" : "Të fshihet përgjithmonë kjo bisedë?" } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : "Обавештавај само за помене" + "value" : "Неопозиво уклонити преписку?" } }, "sr-Latn" : { "stringUnit" : { "state" : "translated", - "value" : "Obavesti samo kada se pominje" + "value" : "Neopozivo ukloniti poruke u ovoj konverzaciji?" } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Notifiera endast för omnämnanden" + "value" : "Vill du radera denna konversation för alltid?" } }, "sw" : { "stringUnit" : { "state" : "translated", - "value" : "Taarifa za bubu tu kwa kutajwa" + "value" : "Unataka kufuta meseji hizi kabisa kwenye mazungumzo haya?" } }, "ta" : { "stringUnit" : { "state" : "translated", - "value" : "குறிப்பில் மட்டும் அறிவிக்கவும்" + "value" : "இந்த உரையாடலில் உள்ள செய்திகளை நிரந்தரமாக நீக்கவா?" } }, "te" : { "stringUnit" : { "state" : "translated", - "value" : "కేవలం పేర్లు" + "value" : "ఈ సంభాషణలోని సందేశాలను శాశ్వతంగా తొలగించాలా?" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "แจ้งเฉพาะการกล่าวถึง" + "value" : "ลบการสนทนานี้โดยถาวรหรือไม่" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Yalnızca Bahsedildiğinde Bildir" + "value" : "Bu sohbeti kalıcı olarak sil?" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Сповіщати лише про згадки" + "value" : "Видалити повідомлення у цій розмові назавжди?" } }, "ur-IN" : { "stringUnit" : { "state" : "translated", - "value" : "صرف تذکرات کے لئے اطلاع دیں" + "value" : "اس گفتگو میں پیغامات کو مستقل طور پر حذف کریں؟" } }, "uz" : { "stringUnit" : { "state" : "translated", - "value" : "Faqat tilash haqida habar qilish" + "value" : "Bu suhbatni tag-tomiri bilan o'chiremi?" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Notify for Mentions Only" + "value" : "Xóa cuộc trò chuyện này vĩnh viễn?" } }, "xh" : { "stringUnit" : { "state" : "translated", - "value" : "Zaziso ZeMentions Kuphela" + "value" : "Cima imiyalezo kule ncoko unaphakade?" } }, "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "仅在提及时通知" + "value" : "永久删除此会话中的消息?" } }, "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "提及我時才通知" + "value" : "永久刪除對話中的訊息?" } } } }, - "deleteAfterGroupPR1MentionsOnlyDescription" : { + "deleteAfterGroupPR3GroupErrorLeave" : { "extractionState" : "manual", "localizations" : { "af" : { "stringUnit" : { "state" : "translated", - "value" : "Wanneer geaktiveer, sal jy net kennisgewings ontvang vir boodskappe wat jou noem." + "value" : "Kan nie verlaat terwyl ander lede gevoeg of verwyder word nie." } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "عند التمكين، سيتم إشعارك بالرسائل التي تشير إليك فقط." + "value" : "لا يمكن المغادرة أثناء إضافة أو إزالة أعضاء آخرين." } }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "Fəal olduqda, yalnız adınız çəkilən mesajlar barədə məlumatlandırılacaqsınız." + "value" : "Digər üzvləri əlavə edərkən və ya çıxardarkən tərk edilə bilməz." } }, "bal" : { "stringUnit" : { "state" : "translated", - "value" : "ہنرگنتہ، شمای چہگان پیغاماتی آ اینیگ ذکری بکود بیتنگ، گپ چندکندگ۔" + "value" : "دوسرے ممبروں کو شامل یا ہٹانے کے دوران آپ اس گروپ کو نہیں چھوڑ سکتے۔" } }, "be" : { "stringUnit" : { "state" : "translated", - "value" : "Калі гэта ўключана, вы будзеце атрымліваць апавяшчэнні толькі аб паведамленнях, у якіх вы згадваецеся." + "value" : "Нельга пакінуць пры даданні або выдаленні іншых удзельнікаў." } }, "bg" : { "stringUnit" : { "state" : "translated", - "value" : "Когато е включено, ще получавате известия само за съобщения, които ви споменават." + "value" : "Не може да напуснете докато добавяте или премахвате други членове." } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "When enabled, you'll only be notified for messages mentioning you." + "value" : "অন্যান্য সদস্য যোগ বা অপসারণ করার সময় ছেড়ে যাওয়া সম্ভব নয়।" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Quan estigui activat, només rebreu notificacions per missatges que us mencionin." + "value" : "No pots sortir mentre s'afegeixen o s'eliminen altres membres." } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Když je povoleno, budete upozorněni pouze na zprávy, které vás zmiňují." + "value" : "Nelze odejít při přidávání nebo odebírání dalších členů." } }, "cy" : { "stringUnit" : { "state" : "translated", - "value" : "Pan fydd ar gael, dim ond y negeseuon sy'n eich crybwyll byddwch chi'n cael eich hysbysu amdanynt." + "value" : "Methu gadael wrth ychwanegu neu dynnu aelodau eraill." } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Når aktiveret, vil du kun blive underrettet for beskeder, der nævner dig." + "value" : "Kan ikke forlade gruppen, mens du tilføjer eller fjerner andre medlemmer." } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Wenn aktiviert, wirst du nur über Nachrichten benachrichtigt, in denen du erwähnt wirst." + "value" : "Du kannst die Gruppe nicht verlassen, während andere Mitglieder hinzugefügt oder entfernt werden." } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Όταν ενεργοποιηθεί, θα ειδοποιηθείτε μόνο για μηνύματα που σας αναφέρουν." + "value" : "Δεν είναι δυνατή η αποχώρηση από την ομάδα κατά την προσθήκη ή αφαίρεση άλλων μελών." } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "When enabled, you'll only be notified for messages mentioning you." + "value" : "Can't leave while adding or removing other members." } }, "eo" : { "stringUnit" : { "state" : "translated", - "value" : "Kiam ebligita, vi ricevos notifikojn nur pri mesaĝoj kiuj mencios vin." + "value" : "Ne povas forlasi dum aldoni aŭ forigi aliajn membrojn." } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Cuando está activado, sólo se te notificará de mensajes que te mencionen." + "value" : "No se puede salir mientras se agregan o eliminan miembros." } }, "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Cuando está activado, sólo se te notificará de mensajes que te mencionen." + "value" : "No se puede salir mientras se agregan o eliminan miembros." } }, "et" : { "stringUnit" : { "state" : "translated", - "value" : "Kui lubatud, teavitatakse teid ainult sõnumitest, mis mainivad teid." + "value" : "Ei saa lahkuda, kui on teisi liikmeid lisatud või eemaldatud." } }, "eu" : { "stringUnit" : { "state" : "translated", - "value" : "Gaituta dagoenean, zu aipatzen dituzten mezuek soilik abisatuko zaituzte." + "value" : "Can't leave while adding or removing other members." } }, "fa" : { "stringUnit" : { "state" : "translated", - "value" : "وقتی فعال باشد، فقط برای پیام هایی که از شما نام می برند مطلع می شوید." + "value" : "هنگام افزودن یا حذف سایر اعضا، نمی‌توانید گروه را ترک کنید" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Kun toiminto on käytössä, saat ilmoituksia vain sinut mainitsevista viesteistä." + "value" : "Ei voida poistua lisättäessä tai poistettaessa muita jäseniä." } }, "fil" : { "stringUnit" : { "state" : "translated", - "value" : "Kapag naka-enable, ikaw ay makakatanggap lamang ng notifications para sa mga mensaheng tinutukoy ka." + "value" : "Hindi maaaring umalis habang nagdadagdag o nag-aalis ng ibang mga miyembro." } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Quand activé, vous recevrez les notifications uniquement pour les messages qui vous mentionnent." + "value" : "Impossible de quitter lors de l'ajout ou la suppression d'autres membres." } }, "gl" : { "stringUnit" : { "state" : "translated", - "value" : "Cando estea activado, só recibirás notificacións para mensaxes que te mencionen." + "value" : "Non se pode saír mentres se engaden ou eliminan outros membros." } }, "ha" : { "stringUnit" : { "state" : "translated", - "value" : "Lokacin da aka kunna, za a sanar da ku kawai don saƙonni da suke ambatarku." + "value" : "Ba za a iya barin lokacin ƙara ko cire membobin ba." } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "כאשר אופציה זו מופעלת, תקבל/י הודעות על הודעות בהן הוזכרת בלבד." + "value" : "לא ניתן לעזוב בעת הוספה או הסרה של משתתפים אחרים." } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "सक्षम होने पर, आपको केवल उन्हीं संदेशों के लिए सूचित किया जाएगा जो आपको उल्लेखित करते हैं।" + "value" : "अन्य सदस्यों को जोड़ते या हटाते समय छोड़ नहीं सकते।" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Kad je omogućeno, bit ćete obaviješteni samo o porukama koje vas spominju." + "value" : "Ne možete izaći dok dodajete ili uklanjate druge članove." } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Ha engedélyezve van, csak a téged említő üzenetekről kapsz értesítést." + "value" : "Nem tudsz kilépni addig, amíg hozzáadsz, vagy eltávolítasz csoporttagokat." } }, "hy-AM" : { "stringUnit" : { "state" : "translated", - "value" : "Միացնելու դեպքում, դուք ծանուցվելու եք միայն ձեզ նշող հաղորդագրությունների համար։" + "value" : "Չեք կարող դուրս գալ մինչ ավելացնում կամ հեռացնում եք այլ անդամներին" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Saat diaktifkan, Anda hanya akan diberi tahu untuk pesan yang menyebut Anda." + "value" : "Tidak dapat keluar saat menambahkan atau menghapus anggota lain." } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Se abilitato, verrai notificato solo per i messaggi in cui vieni menzionano." + "value" : "Non puoi abbandonare il gruppo durante l'aggiunta o la rimozione di altri membri." } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "有効にすると、あなたに言及するメッセージのみが通知されます。" + "value" : "他のメンバーを追加または削除中は退出できません" } }, "ka" : { "stringUnit" : { "state" : "translated", - "value" : "როდესაც ჩართულია, მხოლოდ იმ შეტყობინებებზე მიიღებთ შეტყობინებებს, რომლებიც თქვენ გეხებიან." + "value" : "ვერ დატოვებთ ჯგუფს, რადგან მიმდინარეობს წევრობის შეცვლა." } }, "km" : { "stringUnit" : { "state" : "translated", - "value" : "ជាប្រព្រឹត្តិការណ៍ អ្នកនឹងត្រូវបានជូនដំណឹងសម្រាប់សារដែលបានហៅ ឬកិច្ចសារ" + "value" : "មិនអាចចាកចេញនៅពេលបន្ថែមឬដកសមាជិកផ្សេងទៀត។" } }, "kn" : { "stringUnit" : { "state" : "translated", - "value" : "ಚಾಲನೆ ಮಾಡಿದಾಗ, ನಿಮಗೆ ಮಾತ್ರ ನಿಮ್ಮಲ್ಲಿ ಉಲ್ಲೇಖಿಸಿದ ಸಂದೇಶಗಳಿಗೆ ನೋಟಿಫಿಕೇಶನ್ ದೊರೆಯುತ್ತದೆ." + "value" : "ಇತರ ಸದಸ್ಯರನ್ನು ಸೇರಿಸುವ ಅಥವಾ ತೆಗೆದು ಹಾಕುವ ಸಂದರ್ಭದಲ್ಲಿ విడೀಕರಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ." } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "활성화되면 당신이 언급된 메시지만 알림을 받게 됩니다." + "value" : "다른 멤버를 추가하거나 제거하는 동안 나갈 수 없습니다." } }, "ku" : { "stringUnit" : { "state" : "translated", - "value" : "کاتێک داچوو، تۆ تەنها باسی دەکەیت بۆ نامەکانی پاراستنی تۆ." + "value" : ".ناتوانیت بەیەکتوانی کۆمەڵە ئەندام زیاد یان کەموەندام بکەیت" } }, "ku-TR" : { "stringUnit" : { "state" : "translated", - "value" : "Gava pêveka aktîv bike, hûn ten foraan bêjin mesajên yedi nexşanda te kirîna hûncarî." + "value" : "Tu nikarî gava îlawekirin an derxistina endamên din derkevî." } }, "lg" : { "stringUnit" : { "state" : "translated", - "value" : "Nga okolezebwa, onajanjaluka bubi ku bubakabwe bujanjaluko" + "value" : "Can't leave while adding or removing other members." + } + }, + "lo" : { + "stringUnit" : { + "state" : "translated", + "value" : "ບໍ່ສາມາດອອກໄດ້ໃນຂະນະນີ້ທ່ານກໍາລັງເພີ່ມຫຼຶລົບສະມາຊິກ." } }, "lt" : { "stringUnit" : { "state" : "translated", - "value" : "Kai įjungta, būsite informuoti tik apie žinutes, kuriose esate paminėti." + "value" : "Nepavyksta išeiti, kol pridedami arba pašalinami kiti nariai." } }, "lv" : { "stringUnit" : { "state" : "translated", - "value" : "Ja iespējots, tev tiks paziņots tikai par ziņojumiem, kuros esi pieminēts." + "value" : "Nevar iziet, kamēr pievieno vai noņem citus dalībniekus." } }, "mk" : { "stringUnit" : { "state" : "translated", - "value" : "Кога е овозможено, ќе добивате известувања само за пораките кои ве споменуваат." + "value" : "Не можете да го напуштите додека додавате или отстранувате други членови." } }, "mn" : { "stringUnit" : { "state" : "translated", - "value" : "Идэвхжүүлсэн үед зөвхөн таныг дурдсан мессежүүдэд л мэдэгдэл ирнэ." + "value" : "Бусад гишүүдийг нэмэх буюу хасахдаа энэ бүлгээс гарах боломжгүй." } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Apabila diaktifkan, anda hanya akan diberitahu untuk mesej yang menyebut anda." + "value" : "Tidak boleh meninggalkan semasa menambah atau mengeluarkan ahli lain." } }, "my" : { "stringUnit" : { "state" : "translated", - "value" : "When enabled, you'll only be notified for messages mentioning you." + "value" : "အခြားအဖွဲ့ဝင်များကို ထည့်ခြင်း သို့မဟုတ် ဖယ်ရှားခြင်းအနေဖြင့် ထွက်ခွာ၍မရပါ" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Når aktivert vil du bare bli varslet når meldinger omtaler deg." + "value" : "Kan ikke forlate mens du legger til eller fjerner andre medlemmer." } }, "nb-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Når aktivert vil du bare bli varslet når meldinger omtaler deg." + "value" : "Kan ikke forlate mens andre medlemmer legges til eller fjernes." } }, "ne-NP" : { "stringUnit" : { "state" : "translated", - "value" : "जब यो सक्षम गर्ने, तपाईंलाई केवल तपाईंको उल्लेख गर्ने सन्देशहरूको लागि सूचना दिइनेछ।" + "value" : "अन्य सदस्यहरू थप्दा वा हटाउँदा छोड्न सकिँदैन।" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Wanneer ingeschakeld, wordt u alleen op de hoogte gesteld voor berichten waarin u vermeld wordt." + "value" : "Kan niet verlaten terwijl andere leden worden toegevoegd of verwijderd." } }, "nn-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Når aktivert vil du bare bli varslet når meldinger omtaler deg." + "value" : "Kan ikkje forlata medan du legg til eller fjernar andre medlemmar." } }, "ny" : { "stringUnit" : { "state" : "translated", - "value" : "Mukayambitsa, mudzalengezedwa zokha kwa mauthenga omwe akukuthandizani." + "value" : "Can't leave while adding or removing other members." } }, "pa-IN" : { "stringUnit" : { "state" : "translated", - "value" : "ਜਦੋਂ ਇਹ ਸਬੰਧਿਤ ਹੁੰਦਾ ਹੈ, ਤੁਸੀਂ ਸਿਰਫ ਮੇਨਸ਼ਨ ਕੀਤੇ ਜਾਣ ਵਾਲੇ ਸੰਦੇਸ਼ਾਂ ਲਈ ਸੂਚਿਤ ਹੋਵੋਗੇ।" + "value" : "ਹੋਰ ਮੈਂਬਰਾਂ ਨੂੰ ਜੋੜਦੇ ਜਾਂ ਹਟਾਉਂਦੇ ਸਮੇਂ ਛੱਡ ਨਹੀਂ ਸਕਦੇ।" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Po włączeniu tej opcji będziesz otrzymywać powiadomienia tylko o wiadomościach, w których o Tobie wspomniano." + "value" : "Nie można opuścić podczas dodawania lub usuwania innych członków." } }, "ps" : { "stringUnit" : { "state" : "translated", - "value" : "کله چې فعال کړئ، تاسو به یوازې د هغه پیغامونو لپاره خبرتیاوې ترلاسه کړئ چې تاسو ته اشاره کوي." + "value" : "په داسې حال کې چې نور غړي اضافه یا لرې کوي، پریږدئ." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Quando ativo, você só receberá notificações quando alguém mencionar você." + "value" : "Não é possível sair enquanto adiciona ou remove outros membros." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Após a sua ativação, apenas será notificado quando for mencionado numa mensagem." + "value" : "Não pode sair enquanto não adicionar ou remover outros membros do grupo." } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "În urma activării, vei primi notificări doar pentru mesajele care te menționează." + "value" : "Nu se poate părăsi în timp ce adăugați sau eliminați alți membri." } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Когда включено, вы будете уведомлены только о сообщениях, упоминающих вас." + "value" : "Нельзя покинуть, пока добавляются или удаляются другие участники." } }, "sh" : { "stringUnit" : { "state" : "translated", - "value" : "Kada je omogućena, dobićete samo obaveštenja za poruke koje vas pominju." + "value" : "Ne možete napustiti grupu dok dodajete ili uklanjate druge članove." } }, "si-LK" : { "stringUnit" : { "state" : "translated", - "value" : "සබල කළ විට, ඔබ සඳහන් කරන පණිවිඩ සඳහා පමණක් ඔබට දැනුම් දෙනු ලැබේ." + "value" : "අන් අයට සෙසු සාමාජිකයන් එකතු කිරීම හෝ ඉවත් කිරීමේදී ප්‍රস্থান කළ නොහැක." } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Po povolení budete dostávať upozornenia len na správy, v ktorých ste spomenutí." + "value" : "Nemôžete odísť počas pridávania alebo odstraňovania iných členov." } }, "sl" : { "stringUnit" : { "state" : "translated", - "value" : "Ko je omogočeno, boste dobili obvestila le za sporočila, ki vas omenijo." + "value" : "Ne morete zapustiti, medtem ko dodajate ali odstranjujete druge člane." } }, "sq" : { "stringUnit" : { "state" : "translated", - "value" : "Kur aktivizohet, do të njoftoheni vetëm për mesazhet që ju përmendin ju." + "value" : "Nuk mund të largoheni ndërsa shtoni ose hiqni anëtarë të tjerë." } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : "Када је омогућено, бићете обавештени само за поруке које вас помињу." + "value" : "Не можете напустити док додајете или уклањате друге чланове." } }, "sr-Latn" : { "stringUnit" : { "state" : "translated", - "value" : "Kada je omogućeno, bićete obavešteni samo za poruke u kojima ste pomenuti." + "value" : "Ne možete napustiti dok dodajete ili uklanjate druge članove." } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "När detta är aktiverat kommer du bara att meddelas för meddelanden som nämner dig." + "value" : "Kan inte lämna medan andra medlemmar läggs till eller tas bort." } }, "sw" : { "stringUnit" : { "state" : "translated", - "value" : "Ukiwezeshwa, utaarifiwa tu kwa ujumbe unaokutaja." + "value" : "Huwezi kuondoka huku unaongeza au unapunguza wanachama wengine." } }, "ta" : { "stringUnit" : { "state" : "translated", - "value" : "இதை இயக்கினால், உங்களை குறிப்பிட்டுள்ள தகவல்களை மட்டும் அறிவிப்பீர்கள்." + "value" : "மற்ற உறுப்பினர்களை சேர்த்தவுடன் அல்லது அகத்தியவுடன் குழுவிலிருந்து வெளியேற முடியாது." } }, "te" : { "stringUnit" : { "state" : "translated", - "value" : "ఇది ప్రారంభించినప్పుడు, మీరు మాత్రమే చేర్పించిన సందేశాల కోసం నోటిఫికేషన్‌లు అందుకుంటారు." + "value" : "ఇతర సభ్యులను చేర్చడంలో లేదా తొలగించడంలో ఉన్నప్పుడు లీవ్ చేయలేరు." } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "เมื่อเปิดใช้งาน คุณจะได้รับการแจ้งเตือนเฉพาะข้อความที่พูดถึงคุณ" + "value" : "ไม่สามารถออกในขณะที่กำลังเพิ่มหรือลบสมาชิกอื่น" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Etkinleştirildiğinde, yalnızca sizden bahseden iletiler için bilgilendirileceksiniz." + "value" : "Diğer üyeleri eklerken veya çıkarırken çıkış yapılamaz." } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Коли увімкнено, ви будете отримувати сповіщення лише про повідомлення, в яких згадують вас." + "value" : "Не можна залишити під час додавання або видалення інших учасників." } }, "ur-IN" : { "stringUnit" : { "state" : "translated", - "value" : "جب فعال ہو جائے، آپ کو صرف ان پیغامات کے لیے مطلع کیا جائے گا جو آپ کا ذکر کرتے ہیں۔" + "value" : "دوسرے اراکین کو شامل یا ہٹاتے وقت آپ اس گروپ کو نہیں چھوڑ سکتے۔" } }, "uz" : { "stringUnit" : { "state" : "translated", - "value" : "Faollashtirilganda, faqat sizni tilga oladigan xabarlar haqida xabarlar olasiz." + "value" : "Boshqa a'zolarni qo'shish yoki olib tashlash vaqtida chiqib bo'lmaydi." } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Khi được bật, bạn sẽ chỉ nhận thông báo cho các tin nhắn đề cập đến bạn." + "value" : "Không thể rời đi trong khi đang thêm hoặc xóa các thành viên khác." } }, "xh" : { "stringUnit" : { "state" : "translated", - "value" : "Xa ivuliwe, uza kuziswa kuphela ngemiyalezo ekukhankanywayo." + "value" : "Awukwazi ukuhamba xa ungezelela okanye ususa amalungu amanye." } }, "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "启用后,您只会收到提及您的消息通知。" + "value" : "添加或移除其他成员时无法离开。" } }, "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "啟用後,你將只會收到提及你的訊息之通知。" + "value" : "無法在添加或移除其他成員時離開本群組。" } } } }, - "deleteAfterGroupPR1MessageSound" : { + "deleteAfterLegacyDisappearingMessagesLegacy" : { "extractionState" : "manual", "localizations" : { "af" : { "stringUnit" : { "state" : "translated", - "value" : "Boodskap Klank" + "value" : "Erfenis" } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "صوت الرسالة" + "value" : "قديم" } }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "Mesaj səsi" + "value" : "Köhnə" } }, "bal" : { "stringUnit" : { "state" : "translated", - "value" : "Message Sound" + "value" : "Legacy" } }, "be" : { "stringUnit" : { "state" : "translated", - "value" : "Гук паведамлення" + "value" : "Ранейшыя" } }, "bg" : { "stringUnit" : { "state" : "translated", - "value" : "Тон на съобщенията" + "value" : "Наследен" } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "ম্যাসেজের শব্দ" + "value" : "প্রাচীন" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "So de missatges" + "value" : "Herència" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Zvuk zprávy" + "value" : "Zastaralé" } }, "cy" : { "stringUnit" : { "state" : "translated", - "value" : "Sain Neges" + "value" : "Etifeddiaeth" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Beskedlyd" + "value" : "Legacy" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Nachrichtenton" + "value" : "Legacy" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Ήχος Μηνύματος" + "value" : "Legacy" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Message Sound" + "value" : "Legacy" } }, "eo" : { "stringUnit" : { "state" : "translated", - "value" : "Mesaĝa Sono" + "value" : "Heredaĵo" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Mensajes con sonido" + "value" : "Legado" } }, "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Notificaciones sonoras" + "value" : "Legacy" } }, "et" : { "stringUnit" : { "state" : "translated", - "value" : "Sõnumiheli" + "value" : "Pärand" } }, "eu" : { "stringUnit" : { "state" : "translated", - "value" : "Mezu soinua" + "value" : "Legacy" } }, "fa" : { "stringUnit" : { "state" : "translated", - "value" : "صدای پیام" + "value" : "قدیمی" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Viestien ilmoitusääni" + "value" : "Vanha" } }, "fil" : { "stringUnit" : { "state" : "translated", - "value" : "Tunog ng Mensahe" + "value" : "Legacy" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Son de message" + "value" : "Héritage" } }, "gl" : { "stringUnit" : { "state" : "translated", - "value" : "Son da mensaxe" + "value" : "Ancestral" } }, "ha" : { "stringUnit" : { "state" : "translated", - "value" : "Sautin saƙo" + "value" : "Tarihi" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "צליל הודעה" + "value" : "מורשת" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "संदेश ध्वनि" + "value" : "लिगेसी" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Zvuk poruke" + "value" : "Naslijeđeno" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Üzenet hangja" + "value" : "Legacy" } }, "hy-AM" : { "stringUnit" : { "state" : "translated", - "value" : "Հաղորդագրության ձայն" + "value" : "Legacy" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Suara Pesan" + "value" : "Legacy" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Suono messaggio" + "value" : "Originale" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "メッセージサウンド" + "value" : "レガシー" } }, "ka" : { "stringUnit" : { "state" : "translated", - "value" : "შეტყობინების ხმა" + "value" : "Legacy" } }, "km" : { "stringUnit" : { "state" : "translated", - "value" : "សម្លេងប្រព័ន្ធផ្សព្វផ្សាយ" + "value" : "អក្សនាន" } }, "kn" : { "stringUnit" : { "state" : "translated", - "value" : "ಸಂದೇಶ ಶಬ್ಧ" + "value" : "ಪರಂಪರೆ" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "메시지 소리" + "value" : "레거시" } }, "ku" : { "stringUnit" : { "state" : "translated", - "value" : "دەنگی نامە" + "value" : "لەگەڵی" } }, "ku-TR" : { "stringUnit" : { "state" : "translated", - "value" : "Peyama Dengî" + "value" : "Sîstema berê" } }, "lg" : { "stringUnit" : { "state" : "translated", - "value" : "Amazzi agata fuka" + "value" : "Legacy" } }, "lt" : { "stringUnit" : { "state" : "translated", - "value" : "Žinučių garsas" + "value" : "Senesnis" } }, "lv" : { "stringUnit" : { "state" : "translated", - "value" : "Ziņojuma skaņa" + "value" : "Mantojums" } }, "mk" : { "stringUnit" : { "state" : "translated", - "value" : "Звук на пораки" + "value" : "Наследство" } }, "mn" : { "stringUnit" : { "state" : "translated", - "value" : "Мессежийн Дуу" + "value" : "Уламжлалт" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Bunyi Mesej" + "value" : "Warisan" } }, "my" : { "stringUnit" : { "state" : "translated", - "value" : "မက်ဆေ့ခြင်းအသုံယွက်" + "value" : "မွေးဟောင်း" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Meldingslyd" + "value" : "Legacy" } }, "nb-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Melding lyd" + "value" : "Legacy" } }, "ne-NP" : { "stringUnit" : { "state" : "translated", - "value" : "सन्देश ध्वनि" + "value" : "विरासत" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Geluid van bericht" + "value" : "Legacy" } }, "nn-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Melding lyd" + "value" : "Legacy" } }, "ny" : { "stringUnit" : { "state" : "translated", - "value" : "Phokoso la uthenga" + "value" : "Legacy" } }, "pa-IN" : { "stringUnit" : { "state" : "translated", - "value" : "ਸੁਨੇਹਾ ਸਾਊਂਡ" + "value" : "ਉਰਾਂਵਿਧੀ" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Dźwięk wiadomości" + "value" : "Starsze" } }, "ps" : { "stringUnit" : { "state" : "translated", - "value" : "پیغام غږ" + "value" : "Legacy" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Som de mensagem" + "value" : "Legado" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Som da Mensagem" + "value" : "Legado" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Sunet notificare mesaj" + "value" : "Sistem învechit" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Звук сообщения" + "value" : "Устаревшее" } }, "sh" : { "stringUnit" : { "state" : "translated", - "value" : "Zvuk poruke" + "value" : "Legacy" } }, "si-LK" : { "stringUnit" : { "state" : "translated", - "value" : "පණිවිඩ ශබ්දය" + "value" : "පෙර" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Zvuk správy" + "value" : "Zastarané" } }, "sl" : { "stringUnit" : { "state" : "translated", - "value" : "Zvok sporočila" + "value" : "Legacy" } }, "sq" : { "stringUnit" : { "state" : "translated", - "value" : "Tingull mesazhi" + "value" : "Legacy" } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : "Звук поруке" + "value" : "Legacy" } }, "sr-Latn" : { "stringUnit" : { "state" : "translated", - "value" : "Zvuk poruke" + "value" : "Legacy" } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Meddelandeljud" + "value" : "Legacy" } }, "sw" : { "stringUnit" : { "state" : "translated", - "value" : "Sauti ya Ujumbe" + "value" : "Urithi" } }, "ta" : { "stringUnit" : { "state" : "translated", - "value" : "Message Sound" + "value" : "வரலாறு" } }, "te" : { "stringUnit" : { "state" : "translated", - "value" : "సందేశ ధ్వని" + "value" : "లెగసీ" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "เสียงข้อความ" + "value" : "รุ่นก่อน" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "İleti Sesi" + "value" : "Legacy" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Звук повідомлення" + "value" : "Застарілий" } }, "ur-IN" : { "stringUnit" : { "state" : "translated", - "value" : "پیغام کی آواز" + "value" : "پرانا" } }, "uz" : { "stringUnit" : { "state" : "translated", - "value" : "Xabar ohangi" + "value" : "Merosi" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Âm báo" + "value" : "Legacy" } }, "xh" : { "stringUnit" : { "state" : "translated", - "value" : "Isandi soMyalezo" + "value" : "Ilifa" } }, "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "消息提示音" + "value" : "旧版" } }, "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "訊息音效" + "value" : "舊版" } } } }, - "deleteAfterGroupPR3DeleteMessagesConfirmation" : { + "deleteAfterLegacyDisappearingMessagesOriginal" : { "extractionState" : "manual", "localizations" : { "af" : { "stringUnit" : { "state" : "translated", - "value" : "Verwyder die boodskappe in hierdie gesprek permanent?" + "value" : "Oorspronklike weergawe van verdwene boodskappe." } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "حذف المحادثة بصفة نهائية ؟" + "value" : "النسخة الأصلية من الرسائل المختفية." } }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "Bu danışıqdakı mesajlar həmişəlik silinsin?" + "value" : "Yox olan mesajların orijinal versiyası." } }, "bal" : { "stringUnit" : { "state" : "translated", - "value" : "یہ گفتگوءِ پیاماں ہمیشہءِ خاطر حذف کئیں؟" + "value" : "اصلانی ورژن په دستیں پیغاماں" } }, "be" : { "stringUnit" : { "state" : "translated", - "value" : "Выдаліць паведамленні ў гэтай размове назаўсёды?" + "value" : "Першапачатковы варыянт рэалізацыі знікнення паведамленняў." } }, "bg" : { "stringUnit" : { "state" : "translated", - "value" : "Перманентно изтриване на този разговор?" + "value" : "Оригинална версия на изчезващи съобщения." } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "এই কথোপকথনের বার্তাগুলি স্থায়ীভাবে মুছবেন?" + "value" : "অদৃশ্য হয়ে যাওয়া বার্তাগুলির মূল সংস্করণ।" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Voleu suprimir aquesta conversa de forma permanent?" + "value" : "Versió original dels missatges efímers." } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Trvale smazat tuto konverzaci?" + "value" : "Původní verze mizejících zpráv." } }, "cy" : { "stringUnit" : { "state" : "translated", - "value" : "Dileu'r negeseuon yn y sgwrs hon yn barhaol?" + "value" : "Fersiwn wreiddiol o negeseuon diflannu." } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Slet samtale permanent?" + "value" : "Original version af forsvindende beskeder." } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Soll diese Unterhaltung unwiderruflich gelöscht werden?" + "value" : "Originalversion von verschwindenden Nachrichten." } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Να διαγραφούν οριστικά τα μηνύματα σε αυτήν τη συνομιλία;" + "value" : "Αρχική έκδοση των μηνυμάτων που εξαφανίζονται." } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Permanently delete the messages in this conversation?" + "value" : "Original version of disappearing messages." } }, "eo" : { "stringUnit" : { "state" : "translated", - "value" : "Ĉu porĉiame forigi tiun ĉi tutan interparolon?" + "value" : "Originala versio de memviŝontataj mesaĝoj." } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "¿Eliminar permanentemente los mensajes en esta conversación?" + "value" : "Versión original de los mensajes desaparecidos." } }, "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "¿Eliminar los mensajes de esta conversación permanentemente?" + "value" : "Versión original de los mensajes desaparecidos." } }, "et" : { "stringUnit" : { "state" : "translated", - "value" : "Kas kustutada see vestlus jäädavalt?" + "value" : "Esialgne kaduvate sõnumite versioon." } }, "eu" : { "stringUnit" : { "state" : "translated", - "value" : "Mezuak betiko ezabatu elkarrizketa honetan?" + "value" : "Mezu desagertzaileen jatorrizko bertsioa." } }, "fa" : { "stringUnit" : { "state" : "translated", - "value" : "آیا می‌خواهید این گفتگو را برای همیشه حذف کنید؟" + "value" : "نسخه اصلی پیام‌های محوشونده." } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Poistetaanko tämä keskustelu pysyvästi?" + "value" : "Katoavien viestien alkuperäinen versio." } }, "fil" : { "stringUnit" : { "state" : "translated", - "value" : "Burahin ng tuluyan ang mga mensahe sa usapang ito?" + "value" : "Orihinal na bersyon ng mga naglalahong mensahe." } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Supprimer définitivement les messages dans cette conversation ?" + "value" : "Version originale des messages éphémères." } }, "gl" : { "stringUnit" : { "state" : "translated", - "value" : "Eliminar permanentemente as mensaxes nesta conversa?" + "value" : "Versión orixinal das mensaxes que desaparecen." } }, "ha" : { "stringUnit" : { "state" : "translated", - "value" : "A kashe keɓewa cikin wannan tattaunawa?" + "value" : "Nau'in sakonnin ɓacewa na asali." } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "האם למחוק לצמיתות שיחה זו?" + "value" : "גרסה מקורית של הודעות נעלמות." } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "इस वार्तालाप को स्थायी रूप से हटाएं?" + "value" : "गायब होने वाले संदेशों का मूल संस्करण।" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Trajno obrisati ovaj razgovor?" + "value" : "Izvorna verzija nestajućih poruka." } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Véglegesen törlöd ezt a beszélgetést?" + "value" : "Az eltűnő üzenetek eredeti verziója." } }, "hy-AM" : { "stringUnit" : { "state" : "translated", - "value" : "Ընդմիշտ ջնջե՞լ այս խոսակցության հաղորդագրությունները:" + "value" : "Անհետացող հաղորդագրությունների բնօրինակ տարբերակը:" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Hapus obrolan ini selamanya?" + "value" : "Versi asli dari pesan menghilang." } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Rimuovere definitivamente la chat?" + "value" : "La versione originale dei messaggi effimeri." } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "この会話のメッセージを永久に削除しますか?" + "value" : "消滅メッセージの元のバージョン。" } }, "ka" : { "stringUnit" : { "state" : "translated", - "value" : "გსურთ რომ სამუდამოდ წაშალოთ შეტყობინებები ამ სასაუბროში?" + "value" : "ქრება შეტყობინებების ორიგინალური ვერსია." } }, "km" : { "stringUnit" : { "state" : "translated", - "value" : "លុបការសន្ទនានេះចោលរហូត?" + "value" : "កំណែដើមនៃការបាត់ទៅរបស់សារ." } }, "kn" : { "stringUnit" : { "state" : "translated", - "value" : "ಈ ಸಂಭಾಷಣೆಯಲ್ಲಿನ ಸಂದೇಶಗಳನ್ನು ಶಾಶ್ವತವಾಗಿ ಅಳಾಯಿಸಿದಿವಾದೇ?" + "value" : "ಮೆಚ್ಚುಗೆ ಸಂದೇಶಗಳ ಮೂಲ ಆವೃತ್ತಿ." } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "대화에서 이 메세지를 영원히 지웁니까?" + "value" : "메시지 자동 삭제 원본 버전." } }, "ku" : { "stringUnit" : { "state" : "translated", - "value" : "پەیامەکان تەنها بەرێوەبەران لە پەیامەکەدا دەبێ پەیامەکان نادیدار بکەن." + "value" : "وەشانی بنەڕەتی پەیامەکانی نادیار ئەنجامەکان." } }, "ku-TR" : { "stringUnit" : { "state" : "translated", - "value" : "Bila ev mesajên di vê sohbetê de daîmen were jêbirin?" + "value" : "Wêrsiyona orijînala peyamên winda dibin." } }, "lg" : { "stringUnit" : { "state" : "translated", - "value" : "Buggya ddala oba ebiwandiiko mu kwogera kuno?" + "value" : "Versiyo eyasooka ya obubaka obubula." } }, "lt" : { "stringUnit" : { "state" : "translated", - "value" : "Ar ištrinti šį pokalbį visiems laikams?" + "value" : "Original version of disappearing messages." } }, "lv" : { "stringUnit" : { "state" : "translated", - "value" : "Dzēst ziņojumus šajā sarunā uz visiem laikiem?" + "value" : "Gaistošo ziņojumu oriģinālā versija." } }, "mk" : { "stringUnit" : { "state" : "translated", - "value" : "Дали сакате трајно да ги избришете пораките во овој разговор?" + "value" : "Оригинална верзија на исчезнати пораки." } }, "mn" : { "stringUnit" : { "state" : "translated", - "value" : "Энэ яриан дахь мессежүүдийг бүрмөсөн устгах уу?" + "value" : "Устгагдах мессежийн эх хувилбар." } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Hapus mesej dalam perbualan ini secara kekal?" + "value" : "Versi asal mesej hilang." } }, "my" : { "stringUnit" : { "state" : "translated", - "value" : "ဤစကားပြောဆိုမှုမှ မက်ဆေ့ဂျ်များကို အပြီးတိုင်ဖျက်ပစ်မည်မှာ သေချာပါသလား?" + "value" : "ပျောက်သွားမည့် မက်ဆေ့ဂျ် စနစ်မူကြမ်း" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Slett beskjedene permanent i denne samtalen?" + "value" : "Opprinnelig versjon av tidsbegrensede beskjeder." } }, "nb-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Vil du slette denne samtalen?" + "value" : "Originalversjon av tidsbegrensede beskjeder." } }, "ne-NP" : { "stringUnit" : { "state" : "translated", - "value" : "यस कुराकानीका सन्देशहरू स्थायी रूपमा मेटाउनु हुन्छ?" + "value" : "अदृश्य सन्देशहरूको मूल संस्करण।" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "De berichten in dit gesprek voorgoed wissen?" + "value" : "Oorspronkelijke versie van verdwijnende berichten." } }, "nn-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Slett beskjedene permanent i denne samtalen?" + "value" : "Opprinnelig versjon av forsvinnande meldingar." } }, "ny" : { "stringUnit" : { "state" : "translated", - "value" : "Kodi mukufuna kufufuta uthenga mu kukambirana uku mwamvuma?" + "value" : "Mtundu wapoyambirira wa uthenga wosatheka" } }, "pa-IN" : { "stringUnit" : { "state" : "translated", - "value" : "ਕੀ ਤੁਸੀਂ ਇਸ ਗੱਲਬਾਤ ਦੇ ਸੁਨੇਹਿਆਂ ਨੂੰ ਸਥਾਈ ਤੌਰ 'ਤੇ ਮਿਟਾਉਣਾ ਹੈ?" + "value" : "ਗੁੰਮ ਹੋਣ ਵਾਲੇ ਸੁਨੇਹਿਆਂ ਦਾ ਮੂਲ ਸੰਸਕਰਣ।" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Trwale usunąć wiadomości w tej konwersacji?" + "value" : "Oryginalna wersja znikających wiadomości." } }, "ps" : { "stringUnit" : { "state" : "translated", - "value" : "ایا غواړئ په دې خبرو کې پیغامونه تلپاتې حذف کړئ؟" + "value" : "د ورکیدونکو پیغامونو اصلی نسخه." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Você deseja apagar esta conversa definitivamente?" + "value" : "Versão original das mensagens temporárias." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Deseja apagar definitivamente esta conversa?" + "value" : "Versão original das mensagens que desaparecem." } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Ștergi permanent mesajele din acestă conversație?" + "value" : "Versiunea originală a mesajelor temporare." } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Удалить эту беседу без возможности восстановления?" + "value" : "Исходная версия исчезающих сообщений." } }, "sh" : { "stringUnit" : { "state" : "translated", - "value" : "Da li zaista želite trajno obrisati poruke iz ovog razgovora?" + "value" : "Originalna verzija nestajućih poruka." } }, "si-LK" : { "stringUnit" : { "state" : "translated", - "value" : "මෙම සංවාදයේ ඇති පණිවිඩ ස්ථිරවම මකන්නද?" + "value" : "Original version of disappearing messages." } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Natrvalo zmazať túto konverzáciu?" + "value" : "Pôvodná verzia miznúcich správ." } }, "sl" : { "stringUnit" : { "state" : "translated", - "value" : "Ali res želite nepovratno izbrisati ta pogovor?" + "value" : "Izvorna različica izginjajočih sporočil." } }, "sq" : { "stringUnit" : { "state" : "translated", - "value" : "Të fshihet përgjithmonë kjo bisedë?" + "value" : "Versioni origjinal i mesazheve që zhduken." } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : "Неопозиво уклонити преписку?" + "value" : "Оригинална верзија Disappearing Messages." } }, "sr-Latn" : { "stringUnit" : { "state" : "translated", - "value" : "Neopozivo ukloniti poruke u ovoj konverzaciji?" + "value" : "Originalna verzija nestajućih poruka." } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Vill du radera denna konversation för alltid?" + "value" : "Ursprunglig version av försvinnande meddelanden." } }, "sw" : { "stringUnit" : { "state" : "translated", - "value" : "Unataka kufuta meseji hizi kabisa kwenye mazungumzo haya?" + "value" : "Toleo la awali la meseji zinazopotea." } }, "ta" : { "stringUnit" : { "state" : "translated", - "value" : "இந்த உரையாடலில் உள்ள செய்திகளை நிரந்தரமாக நீக்கவா?" + "value" : "மாய்ஞாயிறு சீகிரம் அழிக்கும் செய்திகள் இப்போதும் மீட்டெடுக்க முடியாது." } }, "te" : { "stringUnit" : { "state" : "translated", - "value" : "ఈ సంభాషణలోని సందేశాలను శాశ్వతంగా తొలగించాలా?" + "value" : "కనుమరుగవుతున్న సందేశాల అసలు స్వరూపం." } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "ลบการสนทนานี้โดยถาวรหรือไม่" + "value" : "เวอร์ชันดั้งเดิมของข้อความที่ลบตัวเอง" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Bu sohbeti kalıcı olarak sil?" + "value" : "Kaybolan iletilerin orijinal sürümü." } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Видалити повідомлення у цій розмові назавжди?" + "value" : "Оригінальна версія зникнення повідомлень." } }, "ur-IN" : { "stringUnit" : { "state" : "translated", - "value" : "اس گفتگو میں پیغامات کو مستقل طور پر حذف کریں؟" + "value" : "غائب ہونے والے پیغامات کا اصل ورژن۔" } }, "uz" : { "stringUnit" : { "state" : "translated", - "value" : "Bu suhbatni tag-tomiri bilan o'chiremi?" + "value" : "Yoʻqolgan xabarlarning asl nusxasi." } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Xóa cuộc trò chuyện này vĩnh viễn?" + "value" : "Phiên bản gốc của tin nhắn tự huỷ." } }, "xh" : { "stringUnit" : { "state" : "translated", - "value" : "Cima imiyalezo kule ncoko unaphakade?" + "value" : "Uhlobo lwangaphambili lwemiyalezo edisela." } }, "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "永久删除此会话中的消息?" + "value" : "旧版阅后即焚。" } }, "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "永久刪除對話中的訊息?" + "value" : "舊自動銷毀訊息。" } } } }, - "deleteAfterGroupPR3GroupErrorLeave" : { + "deleteAfterLegacyDisappearingMessagesTheyChangedTimer" : { "extractionState" : "manual", "localizations" : { "af" : { "stringUnit" : { "state" : "translated", - "value" : "Kan nie verlaat terwyl ander lede gevoeg of verwyder word nie." + "value" : "{name} het die verdwynende boodskaptyd instel na {time}" } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "لا يمكن المغادرة أثناء إضافة أو إزالة أعضاء آخرين." + "value" : "{name} قام بتعيين مؤقت الرسائل المختفية إلى {time}" } }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "Digər üzvləri əlavə edərkən və ya çıxardarkən tərk edilə bilməz." + "value" : "{name} yox olan mesaj taymerini {time} olaraq ayarladı" } }, "bal" : { "stringUnit" : { "state" : "translated", - "value" : "دوسرے ممبروں کو شامل یا ہٹانے کے دوران آپ اس گروپ کو نہیں چھوڑ سکتے۔" + "value" : "{name} disappearing messages timer {time} pe di ke." } }, "be" : { "stringUnit" : { "state" : "translated", - "value" : "Нельга пакінуць пры даданні або выдаленні іншых удзельнікаў." + "value" : "{name} паставіў таймер знікнення паведамлення на {time}" } }, "bg" : { "stringUnit" : { "state" : "translated", - "value" : "Не може да напуснете докато добавяте или премахвате други членове." + "value" : "{name} зададе таймер за изчезващи съобщения на {time}" } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "অন্যান্য সদস্য যোগ বা অপসারণ করার সময় ছেড়ে যাওয়া সম্ভব নয়।" + "value" : "{name} অদৃশ্য মেসেজ টাইমার {time} এ সেট করেছেন।" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "No pots sortir mentre s'afegeixen o s'eliminen altres membres." + "value" : "{name} ha establert el temporitzador dels missatges efímers a {time}" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Nelze odejít při přidávání nebo odebírání dalších členů." + "value" : "{name} nastavil(a) časovač mizejících zpráv na {time}" } }, "cy" : { "stringUnit" : { "state" : "translated", - "value" : "Methu gadael wrth ychwanegu neu dynnu aelodau eraill." + "value" : "Gosododd {name} amserydd y neges diflannu i {time}" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Kan ikke forlade gruppen, mens du tilføjer eller fjerner andre medlemmer." + "value" : "{name} har indstillet timern for forsvindende beskeder til {time}" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Du kannst die Gruppe nicht verlassen, während andere Mitglieder hinzugefügt oder entfernt werden." + "value" : "{name} hat den Timer für verschwindende Nachrichten auf {time} eingestellt" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Δεν είναι δυνατή η αποχώρηση από την ομάδα κατά την προσθήκη ή αφαίρεση άλλων μελών." + "value" : "{name} όρισε το χρονοδιακόπτη των εξαφανιζόμενων μηνυμάτων σε {time}" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Can't leave while adding or removing other members." + "value" : "{name} set the disappearing message timer to {time}" } }, "eo" : { "stringUnit" : { "state" : "translated", - "value" : "Ne povas forlasi dum aldoni aŭ forigi aliajn membrojn." + "value" : "{name} agordis la memviŝontajn mesaĝojn al {time} " } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "No se puede salir mientras se agregan o eliminan miembros." + "value" : "{name} ha fijado la desaparición de mensajes en {time}." } }, "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "No se puede salir mientras se agregan o eliminan miembros." + "value" : "{name} ha fijado la desaparición de mensajes en {time}" } }, "et" : { "stringUnit" : { "state" : "translated", - "value" : "Ei saa lahkuda, kui on teisi liikmeid lisatud või eemaldatud." + "value" : "{name} määras kaduvate sõnumite taimeri väärtusele {time}" } }, "eu" : { "stringUnit" : { "state" : "translated", - "value" : "Can't leave while adding or removing other members." + "value" : "{name} (e)k mezu desagerkorraren denboragailua {time}-ra ezarri du" } }, "fa" : { "stringUnit" : { "state" : "translated", - "value" : "هنگام افزودن یا حذف سایر اعضا، نمی‌توانید گروه را ترک کنید" + "value" : "{name} تایمر پیام نابود شونده را روی {time} تنظیم کرد" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Ei voida poistua lisättäessä tai poistettaessa muita jäseniä." + "value" : "{name} asetti viestien katoamisajan: {time}" } }, "fil" : { "stringUnit" : { "state" : "translated", - "value" : "Hindi maaaring umalis habang nagdadagdag o nag-aalis ng ibang mga miyembro." + "value" : "Itinakda ni {name} ang timer ng naglalahong mensahe sa {time}" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Impossible de quitter lors de l'ajout ou la suppression d'autres membres." + "value" : "{name} a défini le minuteur des messages éphémères à {time}" } }, "gl" : { "stringUnit" : { "state" : "translated", - "value" : "Non se pode saír mentres se engaden ou eliminan outros membros." + "value" : "{name} estableceu o temporizador de desaparición das mensaxes a {time}" } }, "ha" : { "stringUnit" : { "state" : "translated", - "value" : "Ba za a iya barin lokacin ƙara ko cire membobin ba." + "value" : "{name} ya sa mai ƙidaya saƙon wucewa zuwa {time}." } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "לא ניתן לעזוב בעת הוספה או הסרה של משתתפים אחרים." + "value" : "{name}‏ הגדיר/ה את קוצב הזמן של ההודעות הנעלמות אל {time}‏" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "अन्य सदस्यों को जोड़ते या हटाते समय छोड़ नहीं सकते।" + "value" : "{name} ने गायब संदेश टाइमर को {time} पर सेट कर दिया" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Ne možete izaći dok dodajete ili uklanjate druge članove." + "value" : "{name} je postavio/la vrijeme za koliko će poruke nestati na {time}." } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Nem tudsz kilépni addig, amíg hozzáadsz, vagy eltávolítasz csoporttagokat." + "value" : "{name} beállította, hogy az üzenetek ennyi idő után tűnjenek el: {time}" } }, "hy-AM" : { "stringUnit" : { "state" : "translated", - "value" : "Չեք կարող դուրս գալ մինչ ավելացնում կամ հեռացնում եք այլ անդամներին" + "value" : "{name}֊ը անհետացող հաղորդագրության ժամաչափը դրեց {time}:" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Tidak dapat keluar saat menambahkan atau menghapus anggota lain." + "value" : "{name} mengatur pesan menghilang dalam {time}" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Non puoi abbandonare il gruppo durante l'aggiunta o la rimozione di altri membri." + "value" : "{name} ha impostato il timer dei messaggi effimeri a {time}" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "他のメンバーを追加または削除中は退出できません" + "value" : "{name}は消えるメッセージの消去時間を{time}に設定しました" } }, "ka" : { "stringUnit" : { "state" : "translated", - "value" : "ვერ დატოვებთ ჯგუფს, რადგან მიმდინარეობს წევრობის შეცვლა." + "value" : "{name}ს დაყენებულია ქრება შეტყობინებების თაიმერი {time}." } }, "km" : { "stringUnit" : { "state" : "translated", - "value" : "មិនអាចចាកចេញនៅពេលបន្ថែមឬដកសមាជិកផ្សេងទៀត។" + "value" : "{name}‍ បានកំណ់រយៈពល សាដរែលងាចបាត់ទៅវិញា {time}‍" } }, "kn" : { "stringUnit" : { "state" : "translated", - "value" : "ಇತರ ಸದಸ್ಯರನ್ನು ಸೇರಿಸುವ ಅಥವಾ ತೆಗೆದು ಹಾಕುವ ಸಂದರ್ಭದಲ್ಲಿ విడೀಕರಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ." + "value" : "{name} ಅವರು ಮಾಯವಾಗುವ ಸಂದೇಶದ ಟೈಮರ್ ಅನ್ನು {time} ಗೆ ಹೊಂದಿಸಿದ್ದಾರೆ." } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "다른 멤버를 추가하거나 제거하는 동안 나갈 수 없습니다." + "value" : "{name}님{time}로 메시지 삭제 타이머를 설정했습니다." } }, "ku" : { "stringUnit" : { "state" : "translated", - "value" : ".ناتوانیت بەیەکتوانی کۆمەڵە ئەندام زیاد یان کەموەندام بکەیت" + "value" : "{name} کاتە پەیام دەسڕێنەوەی دانا بە {time}" } }, "ku-TR" : { "stringUnit" : { "state" : "translated", - "value" : "Tu nikarî gava îlawekirin an derxistina endamên din derkevî." + "value" : "{name} saeta peyamên windaber kir {time}" } }, "lg" : { "stringUnit" : { "state" : "translated", - "value" : "Can't leave while adding or removing other members." + "value" : "{name} yakyusa ebiseera ebirere olw'okuggyawo message nga bwe ziseera ku {time}" } }, "lo" : { "stringUnit" : { "state" : "translated", - "value" : "ບໍ່ສາມາດອອກໄດ້ໃນຂະນະນີ້ທ່ານກໍາລັງເພີ່ມຫຼຶລົບສະມາຊິກ." + "value" : "{name}ໄດ້ກຳນົດລະຍະເວລາໃນການຂໍແກ້ຂອງຂໍ້ຄາບຊ່ວງທ່ານ{time}" } }, "lt" : { "stringUnit" : { "state" : "translated", - "value" : "Nepavyksta išeiti, kol pridedami arba pašalinami kiti nariai." + "value" : "{name} nustatė išnykstančių žinučių laikmatį į {time}" } }, "lv" : { "stringUnit" : { "state" : "translated", - "value" : "Nevar iziet, kamēr pievieno vai noņem citus dalībniekus." + "value" : "{name} iestatīja pazūdošo ziņu taimeri uz {time}" } }, "mk" : { "stringUnit" : { "state" : "translated", - "value" : "Не можете да го напуштите додека додавате или отстранувате други членови." + "value" : "{name} го постави тајмерот за исчезнување на пораките на {time}." } }, "mn" : { "stringUnit" : { "state" : "translated", - "value" : "Бусад гишүүдийг нэмэх буюу хасахдаа энэ бүлгээс гарах боломжгүй." + "value" : "{name} мессежийг арилгах таймерийг {time} тохируулсан" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Tidak boleh meninggalkan semasa menambah atau mengeluarkan ahli lain." + "value" : "{name} menetapkan pemasa disappearing message kepada {time}" } }, "my" : { "stringUnit" : { "state" : "translated", - "value" : "အခြားအဖွဲ့ဝင်များကို ထည့်ခြင်း သို့မဟုတ် ဖယ်ရှားခြင်းအနေဖြင့် ထွက်ခွာ၍မရပါ" + "value" : "{name} သည် ပျောက်မည့် မက်ဆေ့ချ်အတွက် အချိန်မှတ်စက်ကို {time} အဖြစ် သတ်မှတ်ထားသည်" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Kan ikke forlate mens du legger til eller fjerner andre medlemmer." + "value" : "{name} satt selvutslettende meldinger timer til {time}" } }, "nb-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Kan ikke forlate mens andre medlemmer legges til eller fjernes." + "value" : "{name} satte utløpstiden for meldinger til {time}" } }, "ne-NP" : { "stringUnit" : { "state" : "translated", - "value" : "अन्य सदस्यहरू थप्दा वा हटाउँदा छोड्न सकिँदैन।" + "value" : "{name}ले आफै मेटिने सन्देशको टाइमर {time}मा सेट गर्नुभयो" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Kan niet verlaten terwijl andere leden worden toegevoegd of verwijderd." + "value" : "{name} heeft de timer voor verdwijnende berichten ingesteld op {time}" } }, "nn-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Kan ikkje forlata medan du legg til eller fjernar andre medlemmar." + "value" : "{name} satte utløpstiden for beskjeder til {time}" } }, "ny" : { "stringUnit" : { "state" : "translated", - "value" : "Can't leave while adding or removing other members." + "value" : "{name} wakonza nthawi ya uthenga wochoka pa mauthenga otayika kukhala {time}" } }, "pa-IN" : { "stringUnit" : { "state" : "translated", - "value" : "ਹੋਰ ਮੈਂਬਰਾਂ ਨੂੰ ਜੋੜਦੇ ਜਾਂ ਹਟਾਉਂਦੇ ਸਮੇਂ ਛੱਡ ਨਹੀਂ ਸਕਦੇ।" + "value" : "{name}ਨੇ ਸੁਨੇਹੇ ਰੱਖਣ ਦਾ ਟਾਈਮਰ {time}ਤੇ ਸੈੱਟ ਕੀਤਾ।" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Nie można opuścić podczas dodawania lub usuwania innych członków." + "value" : "{name} ustawił(a) znikające wiadomości na {time}" } }, "ps" : { "stringUnit" : { "state" : "translated", - "value" : "په داسې حال کې چې نور غړي اضافه یا لرې کوي، پریږدئ." + "value" : "{name} د ورک شوي پیغام ټایمر {time} ته وټاکه" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Não é possível sair enquanto adiciona ou remove outros membros." + "value" : "{name} definiu o tempo de duração das mensagens temporárias como {time}" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Não pode sair enquanto não adicionar ou remover outros membros do grupo." + "value" : "{name} definiu o temporizador de mensagens que desaparecem para {time}" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Nu se poate părăsi în timp ce adăugați sau eliminați alți membri." + "value" : "{name} a setat timpul pentru mesajele temporare la {time}" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Нельзя покинуть, пока добавляются или удаляются другие участники." + "value" : "{name} установил(а) таймер исчезающих сообщений на {time}" } }, "sh" : { "stringUnit" : { "state" : "translated", - "value" : "Ne možete napustiti grupu dok dodajete ili uklanjate druge članove." + "value" : "{name} je postavio tajmer za nestajuće poruke na {time}" } }, "si-LK" : { "stringUnit" : { "state" : "translated", - "value" : "අන් අයට සෙසු සාමාජිකයන් එකතු කිරීම හෝ ඉවත් කිරීමේදී ප්‍රস্থান කළ නොහැක." + "value" : "{name} අතුරුදහන් වන පණිවිඩ ටයිමරය {time} කාලයට සකසා ඇත" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Nemôžete odísť počas pridávania alebo odstraňovania iných členov." + "value" : "{name} nastavil/a časovač miznúcich správ na {time}" } }, "sl" : { "stringUnit" : { "state" : "translated", - "value" : "Ne morete zapustiti, medtem ko dodajate ali odstranjujete druge člane." + "value" : "{name} je nastavil_a odštevanje za izginjajoča sporočila na {time}" } }, "sq" : { "stringUnit" : { "state" : "translated", - "value" : "Nuk mund të largoheni ndërsa shtoni ose hiqni anëtarë të tjerë." + "value" : "{name} caktoi kohëmatësin e zhdukjes së mesazheve në {time}" } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : "Не можете напустити док додајете или уклањате друге чланове." + "value" : "{name} је подесио тајмер за нестајуће поруке на {time}" } }, "sr-Latn" : { "stringUnit" : { "state" : "translated", - "value" : "Ne možete napustiti dok dodajete ili uklanjate druge članove." + "value" : "{name} je podesio/la tajmer za nestajuće poruke na {time}" } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Kan inte lämna medan andra medlemmar läggs till eller tas bort." + "value" : "{name} satt försvinnande meddelanden timern till {time}" } }, "sw" : { "stringUnit" : { "state" : "translated", - "value" : "Huwezi kuondoka huku unaongeza au unapunguza wanachama wengine." + "value" : "{name} ameseti kipima muda wa ujumbe unaopotea kwa {time}" } }, "ta" : { "stringUnit" : { "state" : "translated", - "value" : "மற்ற உறுப்பினர்களை சேர்த்தவுடன் அல்லது அகத்தியவுடன் குழுவிலிருந்து வெளியேற முடியாது." + "value" : "{name} மறைந்த தகவல்களுக்கான நேரம் அமைக்கப்படுகிறது {time}" } }, "te" : { "stringUnit" : { "state" : "translated", - "value" : "ఇతర సభ్యులను చేర్చడంలో లేదా తొలగించడంలో ఉన్నప్పుడు లీవ్ చేయలేరు." + "value" : "{name} కనుమరుగవుతున్న సందేశాన్ని టైమర్కు సెట్ చేశారు {time}" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "ไม่สามารถออกในขณะที่กำลังเพิ่มหรือลบสมาชิกอื่น" + "value" : "{name} ตั้งค่าตัวตั้งเวลาข้อความที่ลบตัวเองไว้ที่ {time}" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Diğer üyeleri eklerken veya çıkarırken çıkış yapılamaz." + "value" : "{name} kaybolan ileti zamanlayıcısını {time} olarak ayarladı." } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Не можна залишити під час додавання або видалення інших учасників." + "value" : "{name} встановив/ла таймер зникаючих повідомлень на {time}" } }, "ur-IN" : { "stringUnit" : { "state" : "translated", - "value" : "دوسرے اراکین کو شامل یا ہٹاتے وقت آپ اس گروپ کو نہیں چھوڑ سکتے۔" + "value" : "{name} نے غائب ہونے والے پیغامات کا ٹائمر {time} پر سیٹ کیا ہے" } }, "uz" : { "stringUnit" : { "state" : "translated", - "value" : "Boshqa a'zolarni qo'shish yoki olib tashlash vaqtida chiqib bo'lmaydi." + "value" : "{name} yo'qolgan xabar taymerini {time} ga o'rnatdi" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Không thể rời đi trong khi đang thêm hoặc xóa các thành viên khác." + "value" : "{name} đã đặt đồng hồ đếm ngược tin nhắn tự huỷ đến {time}" } }, "xh" : { "stringUnit" : { "state" : "translated", - "value" : "Awukwazi ukuhamba xa ungezelela okanye ususa amalungu amanye." + "value" : "{name} uyicala ixesha eliphumzayo lemyalezo ukuya ku {time}." } }, "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "添加或移除其他成员时无法离开。" + "value" : "{name}将阅后即焚时间设置为{time}" } }, "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "無法在添加或移除其他成員時離開本群組。" + "value" : "{name} 已將訊息自動銷毀時間設為 {time}" } } } }, - "deleteAfterLegacyDisappearingMessagesLegacy" : { + "deleteAfterLegacyGroupsGroupCreation" : { "extractionState" : "manual", "localizations" : { "af" : { "stringUnit" : { "state" : "translated", - "value" : "Erfenis" + "value" : "Wag asseblief terwyl die groep geskep word..." } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "قديم" + "value" : "يرجى الانتظار أثناء إنشاء المجموعة..." } }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "Köhnə" + "value" : "Qrup yaradılarkən lütfən gözləyin..." } }, "bal" : { "stringUnit" : { "state" : "translated", - "value" : "Legacy" + "value" : "گروپ اَپٹنٹ بگو انتظار کن..." } }, "be" : { "stringUnit" : { "state" : "translated", - "value" : "Ранейшыя" + "value" : "Калі ласка, пачакайце, пакуль група ствараецца..." } }, "bg" : { "stringUnit" : { "state" : "translated", - "value" : "Наследен" + "value" : "Моля, изчакайте, докато групата се създаде..." } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "প্রাচীন" + "value" : "অনুগ্রহ করে অপেক্ষা করুন, গ্রুপ তৈরি হচ্ছে..." } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Herència" + "value" : "Espereu mentre es crea el grup..." } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Zastaralé" + "value" : "Počkejte prosím, než se skupina vytvoří..." } }, "cy" : { "stringUnit" : { "state" : "translated", - "value" : "Etifeddiaeth" + "value" : "Arhoswch tra mae'r grŵp yn cael ei greu..." } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Legacy" + "value" : "Vent venligst mens gruppen oprettes..." } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Legacy" + "value" : "Bitte warte, während die Gruppe erstellt wird..." } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Legacy" + "value" : "Παρακαλώ περιμένετε όσο δημιουργείται η ομάδα..." } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Legacy" + "value" : "Please wait while the group is created..." } }, "eo" : { "stringUnit" : { "state" : "translated", - "value" : "Heredaĵo" + "value" : "Bonvolu atendi dum la grupo estas kreata..." } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Legado" + "value" : "Por favor, espera mientras se crea el grupo..." } }, "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Legacy" + "value" : "Por favor, espere mientras se crea el grupo..." } }, "et" : { "stringUnit" : { "state" : "translated", - "value" : "Pärand" + "value" : "Palun oota kuni grupp saab loodud..." } }, "eu" : { "stringUnit" : { "state" : "translated", - "value" : "Legacy" + "value" : "Mesedez itxaron taldea sortzen den bitartean..." } }, "fa" : { "stringUnit" : { "state" : "translated", - "value" : "قدیمی" + "value" : "تا زمانی که گروه ایجاد شود، لطفاً صبر کنید..." } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Vanha" + "value" : "Odota kunnes ryhmä on luotu..." } }, "fil" : { "stringUnit" : { "state" : "translated", - "value" : "Legacy" + "value" : "Pakiusap maghintay habang ginagawa ang grupo..." } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Héritage" - } - }, - "gl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ancestral" + "value" : "Veuillez patienter pendant la création du groupe..." } }, "ha" : { "stringUnit" : { "state" : "translated", - "value" : "Tarihi" + "value" : "Da fatan za a jira yayin da ake ƙirƙirar ƙungiyar..." } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "מורשת" + "value" : "אנא המתן בזמן שהקבוצה נוצרת..." } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "लिगेसी" + "value" : "कृपया समूह बनने तक प्रतीक्षा करें ..." } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Naslijeđeno" + "value" : "Molimo pričekajte dok se grupa kreira..." } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Legacy" + "value" : "Várj amíg a csoport elkészül..." } }, "hy-AM" : { "stringUnit" : { "state" : "translated", - "value" : "Legacy" + "value" : "Խնդրում ենք սպասել, մինչ խումբը կստեղծվի..." } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Legacy" + "value" : "Harap tunggu sementara grup sedang dibuat..." } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Originale" + "value" : "Attendi, la creazione del gruppo è in corso..." } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "レガシー" + "value" : "グループが作成されるまでお待ちください..." } }, "ka" : { "stringUnit" : { "state" : "translated", - "value" : "Legacy" + "value" : "გთხოვთ დაიცადოთ, სანამ ჯგუფი შეიქმნება..." } }, "km" : { "stringUnit" : { "state" : "translated", - "value" : "អក្សនាន" + "value" : "សូមរង់ចាំ ខណៈពេលដែលកំពុងតែបង្កើតក្រុម..." } }, "kn" : { "stringUnit" : { "state" : "translated", - "value" : "ಪರಂಪರೆ" + "value" : "ಗುಂಪನ್ನು ರಚಿಸಲಾಗುತ್ತಿದೆ, ದಯವಿಟ್ಟು ಕಾಯಿರಿ..." } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "레거시" + "value" : "그룹 생성 중..." } }, "ku" : { "stringUnit" : { "state" : "translated", - "value" : "لەگەڵی" + "value" : "تکایە چاوەڕێی چێنێکی گروپ دەکرێ..." } }, "ku-TR" : { "stringUnit" : { "state" : "translated", - "value" : "Sîstema berê" + "value" : "Ji kerema xwe li benda were dema ku komê vê bikin..." } }, "lg" : { "stringUnit" : { "state" : "translated", - "value" : "Legacy" + "value" : "Weerezaawo akaseera nga kibiina kikyajjibwa..." } }, "lt" : { "stringUnit" : { "state" : "translated", - "value" : "Senesnis" - } - }, - "lv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mantojums" + "value" : "Palaukite, kol yra kuriama grupė..." } }, "mk" : { "stringUnit" : { "state" : "translated", - "value" : "Наследство" + "value" : "Ве молиме почекајте додека се креира групата..." } }, "mn" : { "stringUnit" : { "state" : "translated", - "value" : "Уламжлалт" + "value" : "Бүлэг үүсгэгдэх хүртэл хүлээгээд байгаарай..." } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Warisan" + "value" : "Sila tunggu sementara kumpulan sedang dicipta..." } }, "my" : { "stringUnit" : { "state" : "translated", - "value" : "မွေးဟောင်း" + "value" : "အုပ်စုကို ဖန်တီးနေစဉ် ကျေးဇူးပြု၍ စောင့်ပါ..." } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Legacy" + "value" : "Vennligst vent mens gruppen opprettes..." } }, "nb-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Legacy" + "value" : "Vennligst vent mens gruppen opprettes..." } }, "ne-NP" : { "stringUnit" : { "state" : "translated", - "value" : "विरासत" + "value" : "कृपया समूह बनाउँदै गर्दा प्रतीक्षा गर्नुहोस्..." } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Legacy" + "value" : "Een moment geduld, de groep wordt aangemaakt..." } }, "nn-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Legacy" + "value" : "Please wait while the group is created..." } }, "ny" : { "stringUnit" : { "state" : "translated", - "value" : "Legacy" + "value" : "Chonde dikirani pamene gulu likupangidwa..." } }, "pa-IN" : { "stringUnit" : { "state" : "translated", - "value" : "ਉਰਾਂਵਿਧੀ" + "value" : "ਕਿਰਪਾ ਕਰਕੇ ਉਡੀਕ ਕਰੋ ਜਦੋਂ تک ਗਰੁੱਪ ਬਣਦਾ ਹੈ..." } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Starsze" + "value" : "Proszę czekać na utworzenie grupy…" } }, "ps" : { "stringUnit" : { "state" : "translated", - "value" : "Legacy" + "value" : "مهرباني وکړئ انتظار وکړئ تر څو ګروپ جوړ شي..." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Legado" + "value" : "Por favor, aguarde enquanto o grupo é criado..." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Legado" + "value" : "Por favor, espere enquanto o grupo é criado..." } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Sistem învechit" + "value" : "Te rugăm să aștepți până când grupul este creat..." } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Устаревшее" + "value" : "Пожалуйста, подождите, пока группа будет создана..." } }, "sh" : { "stringUnit" : { "state" : "translated", - "value" : "Legacy" + "value" : "Molimo pričekajte dok se grupa kreira..." } }, "si-LK" : { "stringUnit" : { "state" : "translated", - "value" : "පෙර" + "value" : "කරුණාකර රැදී සිටින්න, කණ්ඩායම නිර්මාණය වෙමු." } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Zastarané" + "value" : "Počkajte prosím, kým sa vytvorí skupina..." } }, "sl" : { "stringUnit" : { "state" : "translated", - "value" : "Legacy" + "value" : "Prosim, počakajte med ustvarjanjem skupine..." } }, "sq" : { "stringUnit" : { "state" : "translated", - "value" : "Legacy" + "value" : "Ju lutemi të prisni gjatë krijimit të grupit..." } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : "Legacy" + "value" : "Сачекајте док се група креира..." } }, "sr-Latn" : { "stringUnit" : { "state" : "translated", - "value" : "Legacy" + "value" : "Molimo sačekajte dok se grupa kreira..." } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Legacy" + "value" : "Vänligen vänta medans gruppen skapas..." } }, "sw" : { "stringUnit" : { "state" : "translated", - "value" : "Urithi" + "value" : "Tafadhali subiri kundi linapoundwa..." } }, "ta" : { "stringUnit" : { "state" : "translated", - "value" : "வரலாறு" + "value" : "குழு உருவாக்கப்படும் வரை காத்திருக்கவும்..." } }, "te" : { "stringUnit" : { "state" : "translated", - "value" : "లెగసీ" + "value" : "సమూహం సృష్టించబడుతున్నప్పటి వరకు దయచేసి వేచి ఉండండి..." } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "รุ่นก่อน" + "value" : "โปรดรอสักครู่ในขณะที่กำลังสร้างกลุ่ม..." } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Legacy" + "value" : "Grup oluşturulurken lütfen bekleyin" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Застарілий" + "value" : "Будь ласка, зачекайте поки створюється група..." } }, "ur-IN" : { "stringUnit" : { "state" : "translated", - "value" : "پرانا" + "value" : "براہ کرم انتظار کریں جب تک کہ گروپ بن جائے..." } }, "uz" : { "stringUnit" : { "state" : "translated", - "value" : "Merosi" + "value" : "Guruh yaratilyapti, kuting..." } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Legacy" + "value" : "Vui lòng chờ trong khi nhóm đang được tạo..." } }, "xh" : { "stringUnit" : { "state" : "translated", - "value" : "Ilifa" + "value" : "Nceda linda ngeli xesha iqela liyalungiswa..." } }, "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "旧版" + "value" : "正在创建群组,请稍候..." } }, "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "舊版" + "value" : "請稍候,正在建立群組……" } } } }, - "deleteAfterLegacyDisappearingMessagesOriginal" : { + "deleteAfterLegacyGroupsGroupUpdateErrorTitle" : { "extractionState" : "manual", "localizations" : { "af" : { "stringUnit" : { "state" : "translated", - "value" : "Oorspronklike weergawe van verdwene boodskappe." + "value" : "Kon nie die groep bywerk nie" } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "النسخة الأصلية من الرسائل المختفية." + "value" : "فشل في تحديث المجموعة" } }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "Yox olan mesajların orijinal versiyası." + "value" : "Qrup güncəlləmə uğursuz oldu" } }, "bal" : { "stringUnit" : { "state" : "translated", - "value" : "اصلانی ورژن په دستیں پیغاماں" + "value" : "گروپ اَپڈیٹ ناکام بوت" } }, "be" : { "stringUnit" : { "state" : "translated", - "value" : "Першапачатковы варыянт рэалізацыі знікнення паведамленняў." + "value" : "Не ўдалося абнавіць групу" } }, "bg" : { "stringUnit" : { "state" : "translated", - "value" : "Оригинална версия на изчезващи съобщения." + "value" : "Неуспешно обновяване на групата" } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "অদৃশ্য হয়ে যাওয়া বার্তাগুলির মূল সংস্করণ।" + "value" : "গ্রুপ আপডেট করতে ব্যর্থ হয়েছে" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Versió original dels missatges efímers." + "value" : "No s'ha pogut actualitzar el grup" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Původní verze mizejících zpráv." + "value" : "Aktualizace skupiny selhala" } }, "cy" : { "stringUnit" : { "state" : "translated", - "value" : "Fersiwn wreiddiol o negeseuon diflannu." + "value" : "Methwyd diweddaru'r grŵp" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Original version af forsvindende beskeder." + "value" : "Kunne ikke opdatere gruppe" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Originalversion von verschwindenden Nachrichten." + "value" : "Fehler beim Aktualisieren der Gruppe" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Αρχική έκδοση των μηνυμάτων που εξαφανίζονται." + "value" : "Αποτυχία Ενημέρωσης Ομάδας" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Original version of disappearing messages." + "value" : "Failed to Update Group" } }, "eo" : { "stringUnit" : { "state" : "translated", - "value" : "Originala versio de memviŝontataj mesaĝoj." + "value" : "Malsukcesis ĝisdatigi la grupon" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Versión original de los mensajes desaparecidos." + "value" : "Error al actualizar el grupo" } }, "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Versión original de los mensajes desaparecidos." + "value" : "Error al actualizar el grupo" } }, "et" : { "stringUnit" : { "state" : "translated", - "value" : "Esialgne kaduvate sõnumite versioon." + "value" : "Grupi uuendamine ebaõnnestus" } }, "eu" : { "stringUnit" : { "state" : "translated", - "value" : "Mezu desagertzaileen jatorrizko bertsioa." + "value" : "Hutsa izan da talde eguneratzean" } }, "fa" : { "stringUnit" : { "state" : "translated", - "value" : "نسخه اصلی پیام‌های محوشونده." + "value" : "خطا در به‌روزرسانی گروه" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Katoavien viestien alkuperäinen versio." + "value" : "Ryhmän päivitys epäonnistui" } }, "fil" : { "stringUnit" : { "state" : "translated", - "value" : "Orihinal na bersyon ng mga naglalahong mensahe." + "value" : "Nabigong I-update ang Grupo" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Version originale des messages éphémères." - } - }, - "gl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Versión orixinal das mensaxes que desaparecen." + "value" : "Échec de la mise à jour du groupe" } }, "ha" : { "stringUnit" : { "state" : "translated", - "value" : "Nau'in sakonnin ɓacewa na asali." + "value" : "An kasa Sabunta Ƙungiya" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "גרסה מקורית של הודעות נעלמות." + "value" : "נכשל בעדכון הקבוצה" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "गायब होने वाले संदेशों का मूल संस्करण।" + "value" : "समूह अपडेट करने में विफल" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Izvorna verzija nestajućih poruka." + "value" : "Neuspjelo ažuriranje grupe" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Az eltűnő üzenetek eredeti verziója." + "value" : "Nem sikerült frissíteni a csoportot" } }, "hy-AM" : { "stringUnit" : { "state" : "translated", - "value" : "Անհետացող հաղորդագրությունների բնօրինակ տարբերակը:" + "value" : "Չհաջողվեց թարմացնել խումբը" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Versi asli dari pesan menghilang." + "value" : "Gagal Memperbarui Grup" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "La versione originale dei messaggi effimeri." + "value" : "C'è stato un problema con l'aggiornamento del gruppo" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "消滅メッセージの元のバージョン。" + "value" : "グループの更新ができませんでした" } }, "ka" : { "stringUnit" : { "state" : "translated", - "value" : "ქრება შეტყობინებების ორიგინალური ვერსია." + "value" : "ჯგუფის განახლება ვერ მოხერხდა" } }, "km" : { "stringUnit" : { "state" : "translated", - "value" : "កំណែដើមនៃការបាត់ទៅរបស់សារ." + "value" : "បរាជ័យក្នុងការធ្វើបច្ចុប្បន្នភាពក្រុម" } }, "kn" : { "stringUnit" : { "state" : "translated", - "value" : "ಮೆಚ್ಚುಗೆ ಸಂದೇಶಗಳ ಮೂಲ ಆವೃತ್ತಿ." + "value" : "ಗುಂಪಿನ ನವೀಕರಣ ವಿಫಲವಾಗಿದೆ" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "메시지 자동 삭제 원본 버전." + "value" : "그룹 업데이트 실패" } }, "ku" : { "stringUnit" : { "state" : "translated", - "value" : "وەشانی بنەڕەتی پەیامەکانی نادیار ئەنجامەکان." + "value" : "شکستی گەڕانەوەی گروپ" } }, "ku-TR" : { "stringUnit" : { "state" : "translated", - "value" : "Wêrsiyona orijînala peyamên winda dibin." + "value" : "Bi ser neket ku komê biguherînin" } }, "lg" : { "stringUnit" : { "state" : "translated", - "value" : "Versiyo eyasooka ya obubaka obubula." + "value" : "Kibiina kya Update Kilememye" } }, "lt" : { "stringUnit" : { "state" : "translated", - "value" : "Original version of disappearing messages." - } - }, - "lv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gaistošo ziņojumu oriģinālā versija." + "value" : "Nepavyko atnaujinti grupės" } }, "mk" : { "stringUnit" : { "state" : "translated", - "value" : "Оригинална верзија на исчезнати пораки." + "value" : "Неуспешно ажурирање на групата" } }, "mn" : { "stringUnit" : { "state" : "translated", - "value" : "Устгагдах мессежийн эх хувилбар." + "value" : "Бүлгийг шинэчлэхэд алдаа гарлаа" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Versi asal mesej hilang." + "value" : "Gagal Mengemas Kini Kumpulan" } }, "my" : { "stringUnit" : { "state" : "translated", - "value" : "ပျောက်သွားမည့် မက်ဆေ့ဂျ် စနစ်မူကြမ်း" + "value" : "အုပ်စုအား အပ်ဒိတ်မလုပ်နိုင်ပါ" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Opprinnelig versjon av tidsbegrensede beskjeder." + "value" : "Kunne ikke oppdatere gruppen" } }, "nb-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Originalversjon av tidsbegrensede beskjeder." + "value" : "Kunne ikke oppdatere gruppen" } }, "ne-NP" : { "stringUnit" : { "state" : "translated", - "value" : "अदृश्य सन्देशहरूको मूल संस्करण।" + "value" : "समूह अद्यावधिक गर्न असफल भयो" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Oorspronkelijke versie van verdwijnende berichten." + "value" : "Het is mislukt om de groep bij te werken" } }, "nn-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Opprinnelig versjon av forsvinnande meldingar." + "value" : "Klarte ikkje å oppdatera gruppa" } }, "ny" : { "stringUnit" : { "state" : "translated", - "value" : "Mtundu wapoyambirira wa uthenga wosatheka" + "value" : "Zalephera Kusintha Gulu" } }, "pa-IN" : { "stringUnit" : { "state" : "translated", - "value" : "ਗੁੰਮ ਹੋਣ ਵਾਲੇ ਸੁਨੇਹਿਆਂ ਦਾ ਮੂਲ ਸੰਸਕਰਣ।" + "value" : "ਗਰੁੱਪ ਨੂੰ ਅੱਪਡੇਟ ਕਰਨ ਵਿੱਚ ਨਾਕਾਮ" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Oryginalna wersja znikających wiadomości." + "value" : "Nie udało się zaktualizować grupy" } }, "ps" : { "stringUnit" : { "state" : "translated", - "value" : "د ورکیدونکو پیغامونو اصلی نسخه." + "value" : "ګروپ تازه کولو کې پاتې راغی" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Versão original das mensagens temporárias." + "value" : "Falha ao atualizar o grupo" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Versão original das mensagens que desaparecem." + "value" : "Falha ao Atualizar Grupo" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Versiunea originală a mesajelor temporare." + "value" : "Eroare la actualizarea grupului" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Исходная версия исчезающих сообщений." + "value" : "Ошибка при обновлении группы" } }, "sh" : { "stringUnit" : { "state" : "translated", - "value" : "Originalna verzija nestajućih poruka." + "value" : "Nije moguće ažurirati grupu" } }, "si-LK" : { "stringUnit" : { "state" : "translated", - "value" : "Original version of disappearing messages." + "value" : "කණ්ඩායම යාවත්කාලීන කිරීමට අසමත් විය" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Pôvodná verzia miznúcich správ." + "value" : "Nepodarilo sa aktualizovať skupinu" } }, "sl" : { "stringUnit" : { "state" : "translated", - "value" : "Izvorna različica izginjajočih sporočil." + "value" : "Ni uspelo posodobiti skupine" } }, "sq" : { "stringUnit" : { "state" : "translated", - "value" : "Versioni origjinal i mesazheve që zhduken." + "value" : "Dështoi përditësimi i grupit" } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : "Оригинална верзија Disappearing Messages." + "value" : "Неуспешно ажурирање групе" } }, "sr-Latn" : { "stringUnit" : { "state" : "translated", - "value" : "Originalna verzija nestajućih poruka." + "value" : "Nije uspelo ažuriranje grupe" } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Ursprunglig version av försvinnande meddelanden." + "value" : "Misslyckades att uppdatera grupp" } }, "sw" : { "stringUnit" : { "state" : "translated", - "value" : "Toleo la awali la meseji zinazopotea." + "value" : "Imeshindikana Kusasisha Kundi" } }, "ta" : { "stringUnit" : { "state" : "translated", - "value" : "மாய்ஞாயிறு சீகிரம் அழிக்கும் செய்திகள் இப்போதும் மீட்டெடுக்க முடியாது." + "value" : "குழுவைப் புதுப்பிக்க முடியவில்லை" } }, "te" : { "stringUnit" : { "state" : "translated", - "value" : "కనుమరుగవుతున్న సందేశాల అసలు స్వరూపం." + "value" : "సమూహాన్ని అప్‌డేట్ చేయడంలో విఫలమైంది" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "เวอร์ชันดั้งเดิมของข้อความที่ลบตัวเอง" + "value" : "ไม่สามารถอัปเดตกลุ่มได้" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Kaybolan iletilerin orijinal sürümü." + "value" : "Grup güncellenemedi" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Оригінальна версія зникнення повідомлень." + "value" : "Не вдалося оновити групу" } }, "ur-IN" : { "stringUnit" : { "state" : "translated", - "value" : "غائب ہونے والے پیغامات کا اصل ورژن۔" + "value" : "گروپ اپ ڈیٹ کرنے میں ناکامی ہوئی" } }, "uz" : { "stringUnit" : { "state" : "translated", - "value" : "Yoʻqolgan xabarlarning asl nusxasi." + "value" : "Guruhni yangilashda xatolik" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Phiên bản gốc của tin nhắn tự huỷ." + "value" : "Không thể Cập nhật Nhóm" } }, "xh" : { "stringUnit" : { "state" : "translated", - "value" : "Uhlobo lwangaphambili lwemiyalezo edisela." + "value" : "Koyekile ukuhlaziya iqela" } }, "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "旧版阅后即焚。" + "value" : "更新群组失败" } }, "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "舊自動銷毀訊息。" + "value" : "無法更新群組" } } } }, - "deleteAfterLegacyDisappearingMessagesTheyChangedTimer" : { + "deleteAfterMessageDeletionStandardisationMessageDeletionForbidden" : { "extractionState" : "manual", "localizations" : { "af" : { "stringUnit" : { "state" : "translated", - "value" : "{name} het die verdwynende boodskaptyd instel na {time}" + "value" : "Jy het nie toestemming om ander se boodskappe te verwyder nie" } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "{name} قام بتعيين مؤقت الرسائل المختفية إلى {time}" + "value" : "ليس لديك صلاحيات حذف رسائل الاخرين" } }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "{name} yox olan mesaj taymerini {time} olaraq ayarladı" + "value" : "Başqalarının mesajlarını silmə icazəniz yoxdur" } }, "bal" : { "stringUnit" : { "state" : "translated", - "value" : "{name} disappearing messages timer {time} pe di ke." + "value" : "تہءِ اِسپی اجازت نَہ بوت کہ بروچنئے پیغامانی ماني ڈون" } }, "be" : { "stringUnit" : { "state" : "translated", - "value" : "{name} паставіў таймер знікнення паведамлення на {time}" + "value" : "У вас няма дазволу выдаляць чужыя паведамленні" } }, "bg" : { "stringUnit" : { "state" : "translated", - "value" : "{name} зададе таймер за изчезващи съобщения на {time}" + "value" : "Нямате право да изтривате съобщенията на другите" } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "{name} অদৃশ্য মেসেজ টাইমার {time} এ সেট করেছেন।" + "value" : "আপনি অন্যদের বার্তা মুছে ফেলার অনুমতি নেই" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "{name} ha establert el temporitzador dels missatges efímers a {time}" + "value" : "No teniu autorització per esborrar els missatges d'altres" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "{name} nastavil(a) časovač mizejících zpráv na {time}" + "value" : "Nemáte oprávnění k mazání zpráv ostatních" } }, "cy" : { "stringUnit" : { "state" : "translated", - "value" : "Gosododd {name} amserydd y neges diflannu i {time}" + "value" : "Nid oes gennych ganiatâd i ddileu negeseuon pobl eraill" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "{name} har indstillet timern for forsvindende beskeder til {time}" + "value" : "Du har ikke tilladelse til at slette andres beskeder" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "{name} hat den Timer für verschwindenen Nachrichten auf {time} eingestellt." + "value" : "Du hast nicht die Berechtigung, Nachrichten anderer Teilnehmer zu löschen" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "{name} όρισε το χρονοδιακόπτη των εξαφανιζόμενων μηνυμάτων σε {time}" + "value" : "Δεν έχετε άδεια να διαγράψετε τα μηνύματα άλλων" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "{name} set the disappearing message timer to {time}" + "value" : "You don’t have permission to delete others’ messages" } }, "eo" : { "stringUnit" : { "state" : "translated", - "value" : "{name} agordis la memviŝontajn mesaĝojn al {time} " + "value" : "Vi ne permesiĝas forigi la mesaĝojn de aliuloj" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "{name} ha fijado la desaparición de mensajes en {time}." + "value" : "No tienes permiso para borrar los mensajes de otros" } }, "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "{name} ha fijado la desaparición de mensajes en {time}" + "value" : "No tienes permiso de borrar mensajes de otros" } }, "et" : { "stringUnit" : { "state" : "translated", - "value" : "{name} määras kaduvate sõnumite taimeri väärtusele {time}" + "value" : "Sul ei ole õiguseid teiste sõnumeid kustutada" } }, "eu" : { "stringUnit" : { "state" : "translated", - "value" : "{name} (e)k mezu desagerkorraren denboragailua {time}-ra ezarri du" + "value" : "Ez duzu besteek bidalitako mezuak ezabatzeko baimenik" } }, "fa" : { "stringUnit" : { "state" : "translated", - "value" : "{name} تایمر پیام نابود شونده را روی {time} تنظیم کرد" + "value" : "شما اجازه حذف پیام‌های دیگران را ندارید" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "{name} asetti viestien katoamisajan: {time}" + "value" : "Sinulla ei ole oikeutta poistaa muiden viestejä" } }, "fil" : { "stringUnit" : { "state" : "translated", - "value" : "Itinakda ni {name} ang timer ng naglalahong mensahe sa {time}" + "value" : "Wala kang pahintulot para tangalin ang mensahe ng iba" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "{name} a défini le minuteur des messages éphémères à {time}" - } - }, - "gl" : { - "stringUnit" : { - "state" : "translated", - "value" : "{name} estableceu o temporizador de desaparición das mensaxes a {time}" + "value" : "Vous n'êtes pas autorisé à supprimer les messages des autres utilisateurs" } }, "ha" : { "stringUnit" : { "state" : "translated", - "value" : "{name} ya sa mai ƙidaya saƙon wucewa zuwa {time}." + "value" : "Ba ku da izinin share saƙonnin wasu" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "{name}‏ הגדיר/ה את קוצב הזמן של ההודעות הנעלמות אל {time}‏" + "value" : "אין לך הרשאה למחוק הודעות של אחרים" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "{name} ने गायब संदेश टाइमर को {time} पर सेट कर दिया" + "value" : "आपको दूसरों के संदेशों को हटाने की अनुमति नहीं है" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "{name} je postavio/la vrijeme za koliko će poruke nestati na {time}." + "value" : "Nemate dozvolu za brisanje poruka drugih korisnika" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "{name} beállította, hogy az üzenetek ennyi idő után tűnjenek el: {time}" + "value" : "Nincs jogod mások üzeneteit törölni" } }, "hy-AM" : { "stringUnit" : { "state" : "translated", - "value" : "{name}֊ը անհետացող հաղորդագրության ժամաչափը դրեց {time}:" + "value" : "Դուք ուրիշների հաղորդագրությունները ջնջելու թույլտվություն չունեք" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "{name} mengatur pesan menghilang dalam {time}" + "value" : "Anda tidak diizinkan untuk menghapus pesan orang lain" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "{name} ha impostato il timer dei messaggi effimeri a {time}" + "value" : "Non hai il permesso di eliminare i messaggi altrui" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "{name}は消えるメッセージの消去時間を{time}に設定しました" + "value" : "他のユーザーの投稿を削除する権限はありません" } }, "ka" : { "stringUnit" : { "state" : "translated", - "value" : "{name}ს დაყენებულია ქრება შეტყობინებების თაიმერი {time}." + "value" : "თქვენ არ გაქვთ იმის უფლება რომ წაშალოთ სხვისი შეტყობინებები" } }, "km" : { "stringUnit" : { "state" : "translated", - "value" : "{name}‍ បានកំណ់រយៈពល សាដរែលងាចបាត់ទៅវិញា {time}‍" + "value" : "អ្នកគ្មានសិទ្ធដើម្បីលុបសារអ្នកផ្សេងៗទេ" } }, "kn" : { "stringUnit" : { "state" : "translated", - "value" : "{name} ಅವರು ಮಾಯವಾಗುವ ಸಂದೇಶದ ಟೈಮರ್ ಅನ್ನು {time} ಗೆ ಹೊಂದಿಸಿದ್ದಾರೆ." + "value" : "ನಿಮಗೆ ಇತರರ ಸಂದೇಶಗಳನ್ನು ಅಳಿಸಲು ಅನುಮತಿ ಇಲ್ಲ" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "{name}님{time}로 메시지 삭제 타이머를 설정했습니다." + "value" : "다른 사람의 메시지를 삭제할 수 있는 권한이 없습니다" } }, "ku" : { "stringUnit" : { "state" : "translated", - "value" : "{name} کاتە پەیام دەسڕێنەوەی دانا بە {time}" + "value" : "بڕیارەکانت ناکرێ کە پەیامەکانی تر لەبێریت" } }, "ku-TR" : { "stringUnit" : { "state" : "translated", - "value" : "{name} saeta peyamên windaber kir {time}" + "value" : "Îzna te tine ye ku mesajên kesên din jê bibî" } }, "lg" : { "stringUnit" : { "state" : "translated", - "value" : "{name} yakyusa ebiseera ebirere olw'okuggyawo message nga bwe ziseera ku {time}" - } - }, - "lo" : { - "stringUnit" : { - "state" : "translated", - "value" : "{name}ໄດ້ກຳນົດລະຍະເວລາໃນການຂໍແກ້ຂອງຂໍ້ຄາບຊ່ວງທ່ານ{time}" + "value" : "Tolina busobozi kusazaamu obubaka bwa balala" } }, "lt" : { "stringUnit" : { "state" : "translated", - "value" : "{name} nustatė išnykstančių žinučių laikmatį į {time}" - } - }, - "lv" : { - "stringUnit" : { - "state" : "translated", - "value" : "{name} iestatīja pazūdošo ziņu taimeri uz {time}" + "value" : "Jūs neturite leidimo trinti kitų žmonių žinučių" } }, "mk" : { "stringUnit" : { "state" : "translated", - "value" : "{name} го постави тајмерот за исчезнување на пораките на {time}." + "value" : "Немате дозвола да ги бришете пораките на другите" } }, "mn" : { "stringUnit" : { "state" : "translated", - "value" : "{name} мессежийг арилгах таймерийг {time} тохируулсан" + "value" : "Та бусдын зурвас устгах эрхгүй байна" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "{name} menetapkan pemasa disappearing message kepada {time}" + "value" : "Anda tidak mempunyai kebenaran untuk memadam mesej orang lain" } }, "my" : { "stringUnit" : { "state" : "translated", - "value" : "{name} သည် ပျောက်မည့် မက်ဆေ့ချ်အတွက် အချိန်မှတ်စက်ကို {time} အဖြစ် သတ်မှတ်ထားသည်" + "value" : "သင်သည် အခြားလူများ၏ မက်ဆေ့ချ်များ ဖျက်ပစ်ရန် ခွင့်မရှိပါ" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "{name} satt selvutslettende meldinger timer til {time}" + "value" : "Du har ikke tillatelse til å slette andres beskjeder" } }, "nb-NO" : { "stringUnit" : { "state" : "translated", - "value" : "{name} satte utløpstiden for meldinger til {time}" + "value" : "Du har ikke tillatelse til å slette andres beskjeder" } }, "ne-NP" : { "stringUnit" : { "state" : "translated", - "value" : "{name}ले आफै मेटिने सन्देशको टाइमर {time}मा सेट गर्नुभयो" + "value" : "तपाईंलाई अरूसँग सन्देश मेट्नको अनुमति छैन।" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "{name} heeft de timer voor verdwijnende berichten ingesteld op {time}" + "value" : "Je hebt geen toestemming om andermans berichten te verwijderen" } }, "nn-NO" : { "stringUnit" : { "state" : "translated", - "value" : "{name} satte utløpstiden for beskjeder til {time}" + "value" : "Du har ikke tillatelse til å slette andres beskjeder" } }, "ny" : { "stringUnit" : { "state" : "translated", - "value" : "{name} wakonza nthawi ya uthenga wochoka pa mauthenga otayika kukhala {time}" + "value" : "Simuli ndi chilolezo chothetsa mauthenga ena" } }, "pa-IN" : { "stringUnit" : { "state" : "translated", - "value" : "{name}ਨੇ ਸੁਨੇਹੇ ਰੱਖਣ ਦਾ ਟਾਈਮਰ {time}ਤੇ ਸੈੱਟ ਕੀਤਾ।" + "value" : "ਤੁਹਾਨੂੰ ਦੂਜਿਆਂ ਦੇ ਸਨੇਹੇ ਮਿਟਾਉਣ ਦਾ ਅਧਿਕਾਰ ਨਹੀਂ ਹੈ।" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "{name} ustawił(a) znikające wiadomości na {time}" + "value" : "Nie masz uprawnień do usuwania wiadomości innych osób" } }, "ps" : { "stringUnit" : { "state" : "translated", - "value" : "{name} د ورک شوي پیغام ټایمر {time} ته وټاکه" + "value" : "تاسو د نورو پیغامونه ړنګولو اجازه نلرئ" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "{name} definiu o tempo de duração das mensagens temporárias como {time}" + "value" : "Você não tem permissão para excluir as mensagens de outros" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "{name} definiu o temporizador de mensagens que desaparecem para {time}" + "value" : "Você não tem permissão para eliminar mensagens de outros" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "{name} a setat timpul pentru mesajele temporare la {time}" + "value" : "Nu aveți permisiunea de a șterge mesajele altora" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "{name} установил(а) таймер исчезающих сообщений на {time}" + "value" : "У вас недостаточно прав для удаления других сообщений" } }, "sh" : { "stringUnit" : { "state" : "translated", - "value" : "{name} je postavio tajmer za nestajuće poruke na {time}" + "value" : "Nemaš dozvolu da brišeš tuđe poruke" } }, "si-LK" : { "stringUnit" : { "state" : "translated", - "value" : "{name} අතුරුදහන් වන පණිවිඩ ටයිමරය {time} කාලයට සකසා ඇත" + "value" : "ඔබට අන් අයගේ පණිවිඩ මැකීමට අවසර නැත" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "{name} nastavil/a časovač miznúcich správ na {time}" + "value" : "Nemáte právo vymazať správy ostatných" } }, "sl" : { "stringUnit" : { "state" : "translated", - "value" : "{name} je nastavil_a odštevanje za izginjajoča sporočila na {time}" + "value" : "Nimate dovoljenja za brisanje sporočil drugih" } }, "sq" : { "stringUnit" : { "state" : "translated", - "value" : "{name} caktoi kohëmatësin e zhdukjes së mesazheve në {time}" + "value" : "Ju nuk keni leje për të fshirë mesazhet e të tjerëve" } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : "{name} је подесио тајмер за нестајуће поруке на {time}" + "value" : "Немате дозволу да бришете туђе поруке" } }, "sr-Latn" : { "stringUnit" : { "state" : "translated", - "value" : "{name} je podesio/la tajmer za nestajuće poruke na {time}" + "value" : "Nemate dozvolu da brišete tuđe poruke" } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "{name} satt försvinnande meddelanden timern till {time}" + "value" : "Du har inte behörighet att ta bort andras meddelanden" } }, "sw" : { "stringUnit" : { "state" : "translated", - "value" : "{name} ameseti kipima muda wa ujumbe unaopotea kwa {time}" + "value" : "Huna ruhusa ya kufuta jumbe za wengine" } }, "ta" : { "stringUnit" : { "state" : "translated", - "value" : "{name} மறைந்த தகவல்களுக்கான நேரம் அமைக்கப்படுகிறது {time}" + "value" : "மற்றவர்களின் செய்திகளை நீக்க உங்களுக்கு அனுமதி இல்லை" } }, "te" : { "stringUnit" : { "state" : "translated", - "value" : "{name} కనుమరుగవుతున్న సందేశాన్ని టైమర్కు సెట్ చేశారు {time}" + "value" : "మీరు ఇతరుల సందేశాలను తొలగించే అనుమతి కలిగి లేరు" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "{name} ตั้งค่าตัวตั้งเวลาข้อความที่ลบตัวเองไว้ที่ {time}" + "value" : "คุณไม่มีสิทธิ์ในการลบข้อความของผู้อื่น" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "{name} kaybolan ileti zamanlayıcısını {time} olarak ayarladı." + "value" : "Başkalarının iletilerini silmek için yetkiniz bulunmamaktadır" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "{name} встановив/ла таймер зникаючих повідомлень на {time}" + "value" : "У вас немає прав на видалення інших повідомлень" } }, "ur-IN" : { "stringUnit" : { "state" : "translated", - "value" : "{name} نے غائب ہونے والے پیغامات کا ٹائمر {time} پر سیٹ کیا ہے" + "value" : "آپ کو دوسروں کے پیغامات حذف کرنے کی اجازت نہیں ہے" } }, "uz" : { "stringUnit" : { "state" : "translated", - "value" : "{name} yo'qolgan xabar taymerini {time} ga o'rnatdi" + "value" : "Siz boshqalarning xabarlarini oʻchira olmaysiz" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "{name} đã đặt đồng hồ đếm ngược tin nhắn tự huỷ đến {time}" + "value" : "Bạn không có quyền để xoá các tin nhắn của người khác" } }, "xh" : { "stringUnit" : { "state" : "translated", - "value" : "{name} uyicala ixesha eliphumzayo lemyalezo ukuya ku {time}." + "value" : "Awunamvume yokucima imiyalezo yabanye" } }, "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "{name}将阅后即焚时间设置为{time}" + "value" : "您无权删除他人的消息" } }, "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "{name} 已將訊息自動銷毀時間設為 {time}" + "value" : "您無權刪除他人訊息" } } } }, - "deleteAfterLegacyGroupsGroupCreation" : { + "deleteContactDescription" : { "extractionState" : "manual", "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wag asseblief terwyl die groep geskep word..." - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "يرجى الانتظار أثناء إنشاء المجموعة..." - } - }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "Qrup yaradılarkən lütfən gözləyin..." - } - }, - "bal" : { - "stringUnit" : { - "state" : "translated", - "value" : "گروپ اَپٹنٹ بگو انتظار کن..." - } - }, - "be" : { - "stringUnit" : { - "state" : "translated", - "value" : "Калі ласка, пачакайце, пакуль група ствараецца..." - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Моля, изчакайте, докато групата се създаде..." - } - }, - "bn" : { - "stringUnit" : { - "state" : "translated", - "value" : "অনুগ্রহ করে অপেক্ষা করুন, গ্রুপ তৈরি হচ্ছে..." + "value" : "{name} adlı şəxsi kontaktlarınızdan silmək istədiyinizə əminsinizmi?

Bu, bütün mesajlar və qoşmalar daxil olmaqla söhbətinizi siləcək. {name} ünvanından gələcək mesajlar mesaj sorğusu kimi görünəcək." } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Espereu mentre es crea el grup..." + "value" : "Estàs segur que vols suprimir {name} dels teus contactes?

Això suprimirà la teva conversa, inclosos tots els missatges i fitxers adjunts. Els missatges futurs de {name} apareixeran com una sol·licitud de missatge." } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Počkejte prosím, než se skupina vytvoří..." - } - }, - "cy" : { - "stringUnit" : { - "state" : "translated", - "value" : "Arhoswch tra mae'r grŵp yn cael ei greu..." + "value" : "Opravdu chcete smazat {name} z vašich kontaktů?

Tím smažete vaše konverzace, včetně všech zpráv a příloh. Budoucí zprávy od {name} se zobrazí jako žádost o komunikaci." } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Vent venligst mens gruppen oprettes..." + "value" : "Er du sikker på, at du vil slette {name} fra dine kontakter?

Dette vil slette din samtale, herunder alle beskeder og vedhæftede filer. Fremtidige beskeder fra {name} vises som en besked anmodning." } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Bitte warte, während die Gruppe erstellt wird..." - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Παρακαλώ περιμένετε όσο δημιουργείται η ομάδα..." + "value" : "Möchtest du {name} wirklich aus deinen Kontakten löschen?

Dies wird deine Unterhaltung einschließlich aller Nachrichten und Anhänge löschen. Zukünftige Nachrichten von {name} erscheinen als Nachrichtenanfrage." } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Please wait while the group is created..." - } - }, - "eo" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bonvolu atendi dum la grupo estas kreata..." + "value" : "Are you sure you want to delete {name} from your contacts?

This will delete your conversation, including all messages and attachments. Future messages from {name} will appear as a message request." } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Por favor, espera mientras se crea el grupo..." + "value" : "¿Estás seguro de que quieres eliminar a {name} de tus contactos?

Esto eliminará tu conversación, incluidos todos los mensajes y archivos adjuntos. Los mensajes futuros de {name} aparecerán como una solicitud de mensaje." } }, "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Por favor, espere mientras se crea el grupo..." - } - }, - "et" : { - "stringUnit" : { - "state" : "translated", - "value" : "Palun oota kuni grupp saab loodud..." - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mesedez itxaron taldea sortzen den bitartean..." - } - }, - "fa" : { - "stringUnit" : { - "state" : "translated", - "value" : "تا زمانی که گروه ایجاد شود، لطفاً صبر کنید..." - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Odota kunnes ryhmä on luotu..." - } - }, - "fil" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pakiusap maghintay habang ginagawa ang grupo..." + "value" : "¿Estás seguro de que quieres eliminar a {name} de tus contactos?

Esto eliminará tu conversación, incluidos todos los mensajes y archivos adjuntos. Los mensajes futuros de {name} aparecerán como una solicitud de mensaje." } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Veuillez patienter pendant la création du groupe..." - } - }, - "ha" : { - "stringUnit" : { - "state" : "translated", - "value" : "Da fatan za a jira yayin da ake ƙirƙirar ƙungiyar..." - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "אנא המתן בזמן שהקבוצה נוצרת..." + "value" : "Êtes-vous sûr de vouloir supprimer {name} de vos contacts ?

Cela supprimera votre conversation, y compris tous les messages et pièces jointes. Les futurs messages de {name} apparaîtront comme une demande de message." } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "कृपया समूह बनने तक प्रतीक्षा करें ..." - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Molimo pričekajte dok se grupa kreira..." + "value" : "क्या आप वाकई अपने संपर्कों से {name} को हटाना चाहते हैं?

यह आपके वार्तालाप को हटा देगा, जिसमें सभी संदेश और अटैचमेंट्स शामिल हैं। {name} से भविष्य के संदेश Message request के रूप में दिखाई देंगे।" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Várj amíg a csoport elkészül..." - } - }, - "hy-AM" : { - "stringUnit" : { - "state" : "translated", - "value" : "Խնդրում ենք սպասել, մինչ խումբը կստեղծվի..." - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "Harap tunggu sementara grup sedang dibuat..." + "value" : "Biztosan törli a névjegyek közül a következőt: {name}?

Ezzel törli a beszélgetést, beleértve az összes üzenetet és mellékletet. A jövőben a(z) {name} nevű partnerétől érkező üzenetek üzenetkérésként fognak megjelenni." } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Attendi, la creazione del gruppo è in corso..." + "value" : "Sei sicuro di voler eliminare {name} dai tuoi contatti?

Questo eliminerà la conversazione, inclusi tutti i messaggi e gli allegati. I messaggi futuri da parte di {name} verranno visualizzati come richiesta di messaggio." } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "グループが作成されるまでお待ちください..." - } - }, - "ka" : { - "stringUnit" : { - "state" : "translated", - "value" : "გთხოვთ დაიცადოთ, სანამ ჯგუფი შეიქმნება..." - } - }, - "km" : { - "stringUnit" : { - "state" : "translated", - "value" : "សូមរង់ចាំ ខណៈពេលដែលកំពុងតែបង្កើតក្រុម..." - } - }, - "kn" : { - "stringUnit" : { - "state" : "translated", - "value" : "ಗುಂಪನ್ನು ರಚಿಸಲಾಗುತ್ತಿದೆ, ದಯವಿಟ್ಟು ಕಾಯಿರಿ..." + "value" : "{name}を連絡先から削除してもよろしいですか?

この操作により、すべての会話(メッセージや添付ファイルを含む)が削除されます。{name}からの今後のメッセージはメッセージリクエストとして表示されます。" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "그룹 생성 중..." - } - }, - "ku" : { - "stringUnit" : { - "state" : "translated", - "value" : "تکایە چاوەڕێی چێنێکی گروپ دەکرێ..." - } - }, - "ku-TR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ji kerema xwe li benda were dema ku komê vê bikin..." - } - }, - "lg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Weerezaawo akaseera nga kibiina kikyajjibwa..." + "value" : "연락처에서 {name}을(를) 삭제하시겠습니까?

모든 대화 내역이 삭제되며, 이후에 오는 {name}의 메시지는 메시지 요청으로 오게 됩니다." } }, - "lt" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Palaukite, kol yra kuriama grupė..." + "value" : "Weet u zeker dat u {name} wilt verwijderen uit uw contacten?

Dit zal je gesprek, inclusief alle berichten en bijlagen, verwijderen. Toekomstige berichten van {name} worden weergegeven als een berichtverzoek." } }, - "mk" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Ве молиме почекајте додека се креира групата..." + "value" : "Czy na pewno chcesz usunąć {name} ze swoich kontaktów?

Spowoduje to usunięcie konwersacji, w tym wszystkich wiadomości i załączników. Przyszłe wiadomości od {name} będą wyświetlane jako prośba o wiadomość." } }, - "mn" : { + "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Бүлэг үүсгэгдэх хүртэл хүлээгээд байгаарай..." + "value" : "Tem a certeza de que pretende remover {name} dos seus contactos?

Isto eliminará a sua conversa, incluindo todas as mensagens e anexos. Mensagens futuras de {name} aparecerão como um pedido de mensagem." } }, - "ms" : { + "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Sila tunggu sementara kumpulan sedang dicipta..." + "value" : "Ești sigur/ă că dorești să ștergi {name} din contactele tale?

Aceasta va șterge conversația ta, inclusiv toate mesajele și atașamentele. Mesajele viitoare de la {name} vor apărea ca o solicitare de mesaj." } }, - "my" : { + "ru" : { "stringUnit" : { "state" : "translated", - "value" : "အုပ်စုကို ဖန်တီးနေစဉ် ကျေးဇူးပြု၍ စောင့်ပါ..." + "value" : "Вы уверены, что хотите удалить {name} из контактов?

Ваша переписка будет удалена, включая все сообщения и вложения. Последующие сообщения от {name} будут появляться в виде запроса на общение." } }, - "nb" : { + "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Vennligst vent mens gruppen opprettes..." + "value" : "Är du säker på att du vill ta bort {name} från dina kontakter?

Detta kommer att radera din konversation, inklusive alla meddelanden och bilagor. Framtida meddelanden från {name} kommer att visas som en meddelandeförfrågan." } }, - "nb-NO" : { + "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Vennligst vent mens gruppen opprettes..." + "value" : "{name} kişisini kişilerinizden silmek istediğinizden emin misiniz?

Bu işlem, tüm mesajlar ve ekler dahil olmak üzere sohbetinizi silecektir. {name} kişisinden gelen gelecekteki mesajlar, mesaj isteği olarak görünecektir." } }, - "ne-NP" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "कृपया समूह बनाउँदै गर्दा प्रतीक्षा गर्नुहोस्..." + "value" : "Ви впевнені, що хочете видалити {name} зі своїх контактів?

Це призведе до видалення вашої розмови, включно з усіма повідомленнями та вкладеннями. Майбутні повідомлення від {name} відображатимуться як запит на повідомлення." } }, - "nl" : { + "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "Een moment geduld, de groep wordt aangemaakt..." + "value" : "您确定要删除联系人{name}吗?

该操作将删除你们的会话,包括所有消息和附件。来自{name}的新消息将被视为消息请求。" } }, - "nn-NO" : { + "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "Please wait while the group is created..." + "value" : "您確定要從聯絡人中刪除 {name} 嗎?

這將刪除您的對話,包括所有訊息與附件。來自 {name} 的未來訊息將會顯示為 Message request。" } - }, - "ny" : { + } + } + }, + "deleteConversationDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "Chonde dikirani pamene gulu likupangidwa..." + "value" : "{name} ilə söhbətinizi silmək istədiyinizə əminsinizmi?
Bu, bütün mesajları və qoşmaları həmişəlik siləcək." } }, - "pa-IN" : { + "ca" : { "stringUnit" : { "state" : "translated", - "value" : "ਕਿਰਪਾ ਕਰਕੇ ਉਡੀਕ ਕਰੋ ਜਦੋਂ تک ਗਰੁੱਪ ਬਣਦਾ ਹੈ..." + "value" : "Estàs segur que vols suprimir la teva conversa amb {name} ?
Això eliminarà definitivament tots els missatges i fitxers adjunts." } }, - "pl" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Proszę czekać na utworzenie grupy…" + "value" : "Opracdu chcete smazat konverzaci s {name}?
Tím trvale smažete všechny zprávy a přílohy." } }, - "ps" : { + "da" : { "stringUnit" : { "state" : "translated", - "value" : "مهرباني وکړئ انتظار وکړئ تر څو ګروپ جوړ شي..." + "value" : "Er du sikker på, at du vil slette din samtale med {name}?
Dette vil permanent slette alle beskeder og vedhæftede filer." } }, - "pt-BR" : { + "de" : { "stringUnit" : { "state" : "translated", - "value" : "Por favor, aguarde enquanto o grupo é criado..." + "value" : "Möchtest du deine Unterhaltung mit {name} wirklich löschen?
Dies wird alle Nachrichten und Anhänge dauerhaft löschen." } }, - "pt-PT" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Por favor, espere enquanto o grupo é criado..." + "value" : "Are you sure you want to delete your conversation with {name}?
This will permanently delete all messages and attachments." } }, - "ro" : { + "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Te rugăm să aștepți până când grupul este creat..." + "value" : "¿Estás seguro de que quieres eliminar tu conversación con {name}?
Esto eliminará permanentemente todos los mensajes y archivos adjuntos." } }, - "ru" : { + "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Пожалуйста, подождите, пока группа будет создана..." + "value" : "¿Estás seguro de que quieres eliminar tu conversación con {name}?
Esto eliminará permanentemente todos los mensajes y archivos adjuntos." } }, - "sh" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Molimo pričekajte dok se grupa kreira..." + "value" : "Êtes-vous sûr de vouloir supprimer votre conversation avec {name} ?
Cela supprimera définitivement tous les messages et pièces jointes." } }, - "si-LK" : { + "hi" : { "stringUnit" : { "state" : "translated", - "value" : "කරුණාකර රැදී සිටින්න, කණ්ඩායම නිර්මාණය වෙමු." + "value" : "क्या आप वाकई {name} के साथ अपना वार्तालाप हटाना चाहते हैं?
यह सभी संदेशों और अटैचमेंट्स को स्थायी रूप से हटा देगा।" } }, - "sk" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Počkajte prosím, kým sa vytvorí skupina..." + "value" : "Biztosan törli a következő partnerével folytatott beszélgetést: {name}?
Ez véglegesen törli az összes üzenetet és mellékletet." } }, - "sl" : { + "it" : { "stringUnit" : { "state" : "translated", - "value" : "Prosim, počakajte med ustvarjanjem skupine..." + "value" : "Sei sicuro di voler eliminare la tua conversazione con {name}?
Questa azione eliminerà in modo permanente tutti i messaggi e gli allegati." } }, - "sq" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Ju lutemi të prisni gjatë krijimit të grupit..." + "value" : "{name}との会話を削除してよろしいですか?
この操作により、すべてのメッセージと添付ファイルが完全に削除されます。" } }, - "sr" : { + "ko" : { "stringUnit" : { "state" : "translated", - "value" : "Сачекајте док се група креира..." + "value" : "{name}간의 대화 내역을 삭제하시겠습니까?
이 결정은 영구적이며 모든 메시지와 첨부 파일이 삭제됩니다." } }, - "sr-Latn" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Molimo sačekajte dok se grupa kreira..." + "value" : "Weet u zeker dat u uw gesprek met {name}wilt verwijderen?
Alle berichten en bijlagen worden permanent verwijderd." } }, - "sv-SE" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Vänligen vänta medans gruppen skapas..." + "value" : "Czy na pewno chcesz usunąć swoją rozmowę z {name}?
Spowoduje to trwałe usunięcie wszystkich wiadomości i załączników." } }, - "sw" : { + "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Tafadhali subiri kundi linapoundwa..." + "value" : "Tem certeza de que deseja apagar sua conversa com {name}?
Isto irá eliminar permanentemente todas as mensagens e anexos." } }, - "ta" : { + "ro" : { "stringUnit" : { "state" : "translated", - "value" : "குழு உருவாக்கப்படும் வரை காத்திருக்கவும்..." + "value" : "Ești sigur/ă că dorești să ștergi conversația cu {name}?
Aceasta va șterge definitiv toate mesajele și fișierele atașate." } }, - "te" : { + "ru" : { "stringUnit" : { "state" : "translated", - "value" : "సమూహం సృష్టించబడుతున్నప్పటి వరకు దయచేసి వేచి ఉండండి..." + "value" : "Вы уверены, что хотите удалить свою беседу с {name}?
Это приведет к безвозвратному удалению всех сообщений и вложений." } }, - "th" : { + "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "โปรดรอสักครู่ในขณะที่กำลังสร้างกลุ่ม..." + "value" : "Är du säker på att du vill radera din konversation med {name}?
Detta kommer permanent radera alla meddelanden och bilagor." } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Grup oluşturulurken lütfen bekleyin" + "value" : "{name} ile olan sohbetinizi silmek istediğinizden emin misiniz?
Bu işlem, tüm mesajları ve ekleri kalıcı olarak silecektir." } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Будь ласка, зачекайте поки створюється група..." - } - }, - "ur-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "براہ کرم انتظار کریں جب تک کہ گروپ بن جائے..." - } - }, - "uz" : { - "stringUnit" : { - "state" : "translated", - "value" : "Guruh yaratilyapti, kuting..." - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vui lòng chờ trong khi nhóm đang được tạo..." - } - }, - "xh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nceda linda ngeli xesha iqela liyalungiswa..." + "value" : "Ви дійсно хочете видалити розмову з {name}?
Це назавжди видалить усі повідомлення та вкладення." } }, "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "正在创建群组,请稍候..." + "value" : "您确定要删除您与{name}的会话吗?
该操作将永久删除所有消息和附件。" } }, "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "請稍候,正在建立群組……" + "value" : "您確定要刪除與 {name} 的對話嗎?
這將永久刪除所有訊息和附件。" } } } }, - "deleteAfterLegacyGroupsGroupUpdateErrorTitle" : { + "deleteMessage" : { "extractionState" : "manual", "localizations" : { "af" : { "stringUnit" : { "state" : "translated", - "value" : "Kon nie die groep bywerk nie" + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skrap Boodskap" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skrap Boodskappe" + } + } + } + } + } } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "فشل في تحديث المجموعة" + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "حذف الرسائل" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "حذف الرسائل" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "حذف الرسالة" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "حذف الرسائل" + } + }, + "two" : { + "stringUnit" : { + "state" : "translated", + "value" : "حذف الرسائل" + } + }, + "zero" : { + "stringUnit" : { + "state" : "translated", + "value" : "حذف الرسائل" + } + } + } + } + } } }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "Qrup güncəlləmə uğursuz oldu" - } - }, - "bal" : { - "stringUnit" : { - "state" : "translated", - "value" : "گروپ اَپڈیٹ ناکام بوت" - } - }, - "be" : { - "stringUnit" : { + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesajı sil" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesajları sil" + } + } + } + } + } + } + }, + "bal" : { + "stringUnit" : { "state" : "translated", - "value" : "Не ўдалося абнавіць групу" + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delete Message" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delete Messages" + } + } + } + } + } + } + }, + "be" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Выдаліць паведамленні" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Выдаліць паведамленні" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Выдаліць паведамленне" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Выдаліць паведамленні" + } + } + } + } + } } }, "bg" : { "stringUnit" : { "state" : "translated", - "value" : "Неуспешно обновяване на групата" + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Изтрий съобщението" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Изтрий съобщенията" + } + } + } + } + } } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "গ্রুপ আপডেট করতে ব্যর্থ হয়েছে" + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "বার্তা মুছুন" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "বার্তাগুলি মুছুন" + } + } + } + } + } } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "No s'ha pogut actualitzar el grup" + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Suprimeix el missatge" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Suprimeix els missatges" + } + } + } + } + } } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Aktualizace skupiny selhala" + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Smazat zprávy" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Smazat zprávy" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Smazat zprávu" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Smazat zprávy" + } + } + } + } + } } }, "cy" : { "stringUnit" : { "state" : "translated", - "value" : "Methwyd diweddaru'r grŵp" + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dileu Negeseuon" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dileu Negeseuon" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dileu Neges" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dileu Negeseuon" + } + }, + "two" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dileu Negeseuon" + } + }, + "zero" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dileu Negeseuon" + } + } + } + } + } } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Kunne ikke opdatere gruppe" + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Slet besked" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Slet beskeder" + } + } + } + } + } } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Fehler beim Aktualisieren der Gruppe" + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nachricht löschen" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nachrichten löschen" + } + } + } + } + } } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Αποτυχία Ενημέρωσης Ομάδας" + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Διαγραφή Μηνύματος" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Διαγραφή Μηνυμάτων" + } + } + } + } + } } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Failed to Update Group" + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delete Message" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delete Messages" + } + } + } + } + } } }, "eo" : { "stringUnit" : { "state" : "translated", - "value" : "Malsukcesis ĝisdatigi la grupon" + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Forigi mesaĝon" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Forigi mesaĝojn" + } + } + } + } + } } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Error al actualizar el grupo" + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eliminar el mensaje" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eliminar el mensaje" + } + } + } + } + } } }, "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Error al actualizar el grupo" + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eliminar Mensaje" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eliminar Mensajes" + } + } + } + } + } } }, "et" : { "stringUnit" : { "state" : "translated", - "value" : "Grupi uuendamine ebaõnnestus" + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kustuta sõnum" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kustuta sõnumid" + } + } + } + } + } } }, "eu" : { "stringUnit" : { "state" : "translated", - "value" : "Hutsa izan da talde eguneratzean" + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mezua Ezabatu" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mezuak Ezabatu" + } + } + } + } + } } }, "fa" : { "stringUnit" : { "state" : "translated", - "value" : "خطا در به‌روزرسانی گروه" + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "پیام را پاک کنید" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "پیام ها را پاک کنید" + } + } + } + } + } } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Ryhmän päivitys epäonnistui" + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Poista viesti" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Poista viestit" + } + } + } + } + } } }, "fil" : { "stringUnit" : { "state" : "translated", - "value" : "Nabigong I-update ang Grupo" + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Burahin ang Mensahe" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Burahin ang mga Mensahe" + } + } + } + } + } } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Échec de la mise à jour du groupe" + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Supprimer le message" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Supprimer les messages" + } + } + } + } + } } }, "ha" : { "stringUnit" : { "state" : "translated", - "value" : "An kasa Sabunta Ƙungiya" + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Goge Saƙo" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Goge Saƙonni" + } + } + } + } + } } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "נכשל בעדכון הקבוצה" + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "מחק הודעות" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "מחק הודעה" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "מחק הודעות" + } + }, + "two" : { + "stringUnit" : { + "state" : "translated", + "value" : "מחק הודעות" + } + } + } + } + } } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "समूह अपडेट करने में विफल" + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "संदेश मिटाएं" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "संदेश मिटाएं" + } + } + } + } + } } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Neuspjelo ažuriranje grupe" + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Izbriši poruku" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Izbriši poruku" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Izbriši poruku" + } + } + } + } + } } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Nem sikerült frissíteni a csoportot" + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Üzenet törlése" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Üzenetek törlése" + } + } + } + } + } } }, "hy-AM" : { "stringUnit" : { "state" : "translated", - "value" : "Չհաջողվեց թարմացնել խումբը" + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ջնջել հաղորդագրությունը" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ջնջել հաղորդագրությունները" + } + } + } + } + } } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Gagal Memperbarui Grup" + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hapus Pesan" + } + } + } + } + } } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "C'è stato un problema con l'aggiornamento del gruppo" + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Elimina Messaggio" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Elimina Messaggi" + } + } + } + } + } } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "グループの更新ができませんでした" + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージを削除" + } + } + } + } + } } }, "ka" : { "stringUnit" : { "state" : "translated", - "value" : "ჯგუფის განახლება ვერ მოხერხდა" + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "შეტყობინების წაშლა" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "შეტყობინებების წაშლა" + } + } + } + } + } } }, "km" : { "stringUnit" : { "state" : "translated", - "value" : "បរាជ័យក្នុងការធ្វើបច្ចុប្បន្នភាពក្រុម" + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "លុបសារ" + } + } + } + } + } } }, "kn" : { "stringUnit" : { "state" : "translated", - "value" : "ಗುಂಪಿನ ನವೀಕರಣ ವಿಫಲವಾಗಿದೆ" + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "ಸಂದೇಶವನ್ನು ಅಳಿಸಿ" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "ಸಂದೇಶಗಳನ್ನು ಅಳಿಸಿ" + } + } + } + } + } } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "그룹 업데이트 실패" + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "메시지 삭제" + } + } + } + } + } } }, "ku" : { "stringUnit" : { "state" : "translated", - "value" : "شکستی گەڕانەوەی گروپ" + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "سڕینەوەی پەیام" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "سڕینەوەی پەیامەکان" + } + } + } + } + } } }, "ku-TR" : { "stringUnit" : { "state" : "translated", - "value" : "Bi ser neket ku komê biguherînin" + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Peyamê Jê Bibe" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Peyaman Jê Bibe" + } + } + } + } + } } }, "lg" : { "stringUnit" : { "state" : "translated", - "value" : "Kibiina kya Update Kilememye" + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Jjamu Olukome ngaleerake" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Jjamu Ente" + } + } + } + } + } } }, "lt" : { "stringUnit" : { "state" : "translated", - "value" : "Nepavyko atnaujinti grupės" + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ištrinti žinutes" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ištrinti žinutes" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ištrinti žinutę" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ištrinti žinutes" + } + } + } + } + } } }, "mk" : { "stringUnit" : { "state" : "translated", - "value" : "Неуспешно ажурирање на групата" + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Избриши порака" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Избриши пораки" + } + } + } + } + } } }, "mn" : { "stringUnit" : { "state" : "translated", - "value" : "Бүлгийг шинэчлэхэд алдаа гарлаа" + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Мессеж устгах" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Мессежүүдийг устгах" + } + } + } + } + } } }, "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gagal Mengemas Kini Kumpulan" - } - }, - "my" : { - "stringUnit" : { - "state" : "translated", - "value" : "အုပ်စုအား အပ်ဒိတ်မလုပ်နိုင်ပါ" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kunne ikke oppdatere gruppen" - } - }, - "nb-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kunne ikke oppdatere gruppen" - } - }, - "ne-NP" : { - "stringUnit" : { - "state" : "translated", - "value" : "समूह अद्यावधिक गर्न असफल भयो" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Het is mislukt om de groep bij te werken" - } - }, - "nn-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Klarte ikkje å oppdatera gruppa" - } - }, - "ny" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zalephera Kusintha Gulu" - } - }, - "pa-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "ਗਰੁੱਪ ਨੂੰ ਅੱਪਡੇਟ ਕਰਨ ਵਿੱਚ ਨਾਕਾਮ" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nie udało się zaktualizować grupy" - } - }, - "ps" : { - "stringUnit" : { - "state" : "translated", - "value" : "ګروپ تازه کولو کې پاتې راغی" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Falha ao atualizar o grupo" - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Falha ao Atualizar Grupo" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Eroare la actualizarea grupului" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ошибка при обновлении группы" - } - }, - "sh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nije moguće ažurirati grupu" - } - }, - "si-LK" : { - "stringUnit" : { - "state" : "translated", - "value" : "කණ්ඩායම යාවත්කාලීන කිරීමට අසමත් විය" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nepodarilo sa aktualizovať skupinu" - } - }, - "sl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ni uspelo posodobiti skupine" - } - }, - "sq" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dështoi përditësimi i grupit" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Неуспешно ажурирање групе" - } - }, - "sr-Latn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nije uspelo ažuriranje grupe" - } - }, - "sv-SE" : { - "stringUnit" : { - "state" : "translated", - "value" : "Misslyckades att uppdatera grupp" - } - }, - "sw" : { - "stringUnit" : { - "state" : "translated", - "value" : "Imeshindikana Kusasisha Kundi" - } - }, - "ta" : { - "stringUnit" : { - "state" : "translated", - "value" : "குழுவைப் புதுப்பிக்க முடியவில்லை" - } - }, - "te" : { - "stringUnit" : { - "state" : "translated", - "value" : "సమూహాన్ని అప్‌డేట్ చేయడంలో విఫలమైంది" - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "ไม่สามารถอัปเดตกลุ่มได้" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Grup güncellenemedi" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Не вдалося оновити групу" - } - }, - "ur-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "گروپ اپ ڈیٹ کرنے میں ناکامی ہوئی" - } - }, - "uz" : { - "stringUnit" : { - "state" : "translated", - "value" : "Guruhni yangilashda xatolik" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Không thể Cập nhật Nhóm" - } - }, - "xh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Koyekile ukuhlaziya iqela" - } - }, - "zh-CN" : { - "stringUnit" : { - "state" : "translated", - "value" : "更新群组失败" - } - }, - "zh-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "無法更新群組" - } - } - } - }, - "deleteAfterMessageDeletionStandardisationMessageDeletionForbidden" : { - "extractionState" : "manual", - "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jy het nie toestemming om ander se boodskappe te verwyder nie" - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "ليس لديك صلاحيات حذف رسائل الاخرين" - } - }, - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "Başqalarının mesajlarını silmə icazəniz yoxdur" - } - }, - "bal" : { - "stringUnit" : { - "state" : "translated", - "value" : "تہءِ اِسپی اجازت نَہ بوت کہ بروچنئے پیغامانی ماني ڈون" - } - }, - "be" : { - "stringUnit" : { - "state" : "translated", - "value" : "У вас няма дазволу выдаляць чужыя паведамленні" - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Нямате право да изтривате съобщенията на другите" - } - }, - "bn" : { - "stringUnit" : { - "state" : "translated", - "value" : "আপনি অন্যদের বার্তা মুছে ফেলার অনুমতি নেই" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "No teniu autorització per esborrar els missatges d'altres" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nemáte oprávnění k mazání zpráv ostatních" - } - }, - "cy" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nid oes gennych ganiatâd i ddileu negeseuon pobl eraill" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Du har ikke tilladelse til at slette andres beskeder" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Du hast nicht die Berechtigung, Nachrichten anderer Teilnehmer zu löschen" - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Δεν έχετε άδεια να διαγράψετε τα μηνύματα άλλων" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "You don’t have permission to delete others’ messages" - } - }, - "eo" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vi ne permesiĝas forigi la mesaĝojn de aliuloj" - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "No tienes permiso para borrar los mensajes de otros" - } - }, - "es-ES" : { - "stringUnit" : { - "state" : "translated", - "value" : "No tienes permiso de borrar mensajes de otros" - } - }, - "et" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sul ei ole õiguseid teiste sõnumeid kustutada" - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ez duzu besteek bidalitako mezuak ezabatzeko baimenik" - } - }, - "fa" : { - "stringUnit" : { - "state" : "translated", - "value" : "شما اجازه حذف پیام‌های دیگران را ندارید" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sinulla ei ole oikeutta poistaa muiden viestejä" - } - }, - "fil" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wala kang pahintulot para tangalin ang mensahe ng iba" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vous n'êtes pas autorisé à supprimer les messages des autres utilisateurs" - } - }, - "ha" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ba ku da izinin share saƙonnin wasu" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "אין לך הרשאה למחוק הודעות של אחרים" - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "आपको दूसरों के संदेशों को हटाने की अनुमति नहीं है" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nemate dozvolu za brisanje poruka drugih korisnika" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nincs jogod mások üzeneteit törölni" - } - }, - "hy-AM" : { - "stringUnit" : { - "state" : "translated", - "value" : "Դուք ուրիշների հաղորդագրությունները ջնջելու թույլտվություն չունեք" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "Anda tidak diizinkan untuk menghapus pesan orang lain" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Non hai il permesso di eliminare i messaggi altrui" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "他のユーザーの投稿を削除する権限はありません" - } - }, - "ka" : { - "stringUnit" : { - "state" : "translated", - "value" : "თქვენ არ გაქვთ იმის უფლება რომ წაშალოთ სხვისი შეტყობინებები" - } - }, - "km" : { - "stringUnit" : { - "state" : "translated", - "value" : "អ្នកគ្មានសិទ្ធដើម្បីលុបសារអ្នកផ្សេងៗទេ" - } - }, - "kn" : { - "stringUnit" : { - "state" : "translated", - "value" : "ನಿಮಗೆ ಇತರರ ಸಂದೇಶಗಳನ್ನು ಅಳಿಸಲು ಅನುಮತಿ ಇಲ್ಲ" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "다른 사람의 메시지를 삭제할 수 있는 권한이 없습니다" - } - }, - "ku" : { - "stringUnit" : { - "state" : "translated", - "value" : "بڕیارەکانت ناکرێ کە پەیامەکانی تر لەبێریت" - } - }, - "ku-TR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Îzna te tine ye ku mesajên kesên din jê bibî" - } - }, - "lg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tolina busobozi kusazaamu obubaka bwa balala" - } - }, - "lt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jūs neturite leidimo trinti kitų žmonių žinučių" - } - }, - "mk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Немате дозвола да ги бришете пораките на другите" - } - }, - "mn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Та бусдын зурвас устгах эрхгүй байна" - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "Anda tidak mempunyai kebenaran untuk memadam mesej orang lain" - } - }, - "my" : { - "stringUnit" : { - "state" : "translated", - "value" : "သင်သည် အခြားလူများ၏ မက်ဆေ့ချ်များ ဖျက်ပစ်ရန် ခွင့်မရှိပါ" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Du har ikke tillatelse til å slette andres beskjeder" - } - }, - "nb-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Du har ikke tillatelse til å slette andres beskjeder" - } - }, - "ne-NP" : { - "stringUnit" : { - "state" : "translated", - "value" : "तपाईंलाई अरूसँग सन्देश मेट्नको अनुमति छैन।" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Je hebt geen toestemming om andermans berichten te verwijderen" - } - }, - "nn-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Du har ikke tillatelse til å slette andres beskjeder" - } - }, - "ny" : { - "stringUnit" : { - "state" : "translated", - "value" : "Simuli ndi chilolezo chothetsa mauthenga ena" - } - }, - "pa-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "ਤੁਹਾਨੂੰ ਦੂਜਿਆਂ ਦੇ ਸਨੇਹੇ ਮਿਟਾਉਣ ਦਾ ਅਧਿਕਾਰ ਨਹੀਂ ਹੈ।" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nie masz uprawnień do usuwania wiadomości innych osób" - } - }, - "ps" : { - "stringUnit" : { - "state" : "translated", - "value" : "تاسو د نورو پیغامونه ړنګولو اجازه نلرئ" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Você não tem permissão para excluir as mensagens de outros" - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Você não tem permissão para eliminar mensagens de outros" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nu aveți permisiunea de a șterge mesajele altora" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "У вас недостаточно прав для удаления других сообщений" - } - }, - "sh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nemaš dozvolu da brišeš tuđe poruke" - } - }, - "si-LK" : { - "stringUnit" : { - "state" : "translated", - "value" : "ඔබට අන් අයගේ පණිවිඩ මැකීමට අවසර නැත" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nemáte právo vymazať správy ostatných" - } - }, - "sl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nimate dovoljenja za brisanje sporočil drugih" - } - }, - "sq" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ju nuk keni leje për të fshirë mesazhet e të tjerëve" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Немате дозволу да бришете туђе поруке" - } - }, - "sr-Latn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nemate dozvolu da brišete tuđe poruke" - } - }, - "sv-SE" : { - "stringUnit" : { - "state" : "translated", - "value" : "Du har inte behörighet att ta bort andras meddelanden" - } - }, - "sw" : { - "stringUnit" : { - "state" : "translated", - "value" : "Huna ruhusa ya kufuta jumbe za wengine" - } - }, - "ta" : { - "stringUnit" : { - "state" : "translated", - "value" : "மற்றவர்களின் செய்திகளை நீக்க உங்களுக்கு அனுமதி இல்லை" - } - }, - "te" : { - "stringUnit" : { - "state" : "translated", - "value" : "మీరు ఇతరుల సందేశాలను తొలగించే అనుమతి కలిగి లేరు" - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "คุณไม่มีสิทธิ์ในการลบข้อความของผู้อื่น" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Başkalarının iletilerini silmek için yetkiniz bulunmamaktadır" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "У вас немає прав на видалення інших повідомлень" - } - }, - "ur-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "آپ کو دوسروں کے پیغامات حذف کرنے کی اجازت نہیں ہے" - } - }, - "uz" : { - "stringUnit" : { - "state" : "translated", - "value" : "Siz boshqalarning xabarlarini oʻchira olmaysiz" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bạn không có quyền để xoá các tin nhắn của người khác" - } - }, - "xh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Awunamvume yokucima imiyalezo yabanye" - } - }, - "zh-CN" : { - "stringUnit" : { - "state" : "translated", - "value" : "您无权删除他人的消息" - } - }, - "zh-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "您無權刪除他人訊息" - } - } - } - }, - "deleteContactDescription" : { - "extractionState" : "manual", - "localizations" : { - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "{name} adlı şəxsi kontaktlarınızdan silmək istədiyinizə əminsinizmi?

Bu, bütün mesajlar və qoşmalar daxil olmaqla söhbətinizi siləcək. {name} ünvanından gələcək mesajlar mesaj sorğusu kimi görünəcək." - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Estàs segur que vols suprimir {name} dels teus contactes?

Això suprimirà la teva conversa, inclosos tots els missatges i fitxers adjunts. Els missatges futurs de {name} apareixeran com una sol·licitud de missatge." - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Opravdu chcete smazat {name} z vašich kontaktů?

Tím smažete vaše konverzace, včetně všech zpráv a příloh. Budoucí zprávy od {name} se zobrazí jako žádost o komunikaci." - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Er du sikker på, at du vil slette {name} fra dine kontakter?

Dette vil slette din samtale, herunder alle beskeder og vedhæftede filer. Fremtidige beskeder fra {name} vises som en besked anmodning." - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Are you sure you want to delete {name} from your contacts?

This will delete your conversation, including all messages and attachments. Future messages from {name} will appear as a message request." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Êtes-vous sûr de vouloir supprimer {name} de vos contacts ?

Cela supprimera votre conversation, y compris tous les messages et pièces jointes. Les futurs messages de {name} apparaîtront comme une demande de message." - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Biztosan törli a névjegyek közül a következőt: {name}?

Ezzel törli a beszélgetést, beleértve az összes üzenetet és mellékletet. A jövőben a(z) {name} nevű partnerétől érkező üzenetek üzenetkérésként fognak megjelenni." - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "연락처에서 {name}을(를) 삭제하시겠습니까?

모든 대화 내역이 삭제되며, 이후에 오는 {name}의 메시지는 메시지 요청으로 오게 됩니다." - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Weet u zeker dat u {name} wilt verwijderen uit uw contacten?

Dit zal je gesprek, inclusief alle berichten en bijlagen, verwijderen. Toekomstige berichten van {name} worden weergegeven als een berichtverzoek." - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Czy na pewno chcesz usunąć {name} ze swoich kontaktów?

Spowoduje to usunięcie konwersacji, w tym wszystkich wiadomości i załączników. Przyszłe wiadomości od {name} będą wyświetlane jako prośba o wiadomość." - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ви впевнені, що хочете видалити {name} зі своїх контактів?

Це призведе до видалення вашої розмови, включно з усіма повідомленнями та вкладеннями. Майбутні повідомлення від {name} відображатимуться як запит на повідомлення." - } - }, - "zh-CN" : { - "stringUnit" : { - "state" : "translated", - "value" : "您确定要删除联系人{name}吗?

该操作将删除你们的会话,包括所有消息和附件。来自{name}的新消息将被视为消息请求。" - } - } - } - }, - "deleteConversationDescription" : { - "extractionState" : "manual", - "localizations" : { - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "{name} ilə söhbətinizi silmək istədiyinizə əminsinizmi?
Bu, bütün mesajları və qoşmaları həmişəlik siləcək." - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Estàs segur que vols suprimir la teva conversa amb {name} ?
Això eliminarà definitivament tots els missatges i fitxers adjunts." - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Opracdu chcete smazat konverzaci s {name}?
Tím trvale smažete všechny zprávy a přílohy." - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Er du sikker på, at du vil slette din samtale med {name}?
Dette vil permanent slette alle beskeder og vedhæftede filer." - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Are you sure you want to delete your conversation with {name}?
This will permanently delete all messages and attachments." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Êtes-vous sûr de vouloir supprimer votre conversation avec {name} ?
Cela supprimera définitivement tous les messages et pièces jointes." - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Biztosan törli a következő partnerével folytatott beszélgetést: {name}?
Ez véglegesen törli az összes üzenetet és mellékletet." - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "{name}간의 대화 내역을 삭제하시겠습니까?
이 결정은 영구적이며 모든 메시지와 첨부 파일이 삭제됩니다." - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Weet u zeker dat u uw gesprek met {name}wilt verwijderen?
Alle berichten en bijlagen worden permanent verwijderd." - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Czy na pewno chcesz usunąć swoją rozmowę z {name}?
Spowoduje to trwałe usunięcie wszystkich wiadomości i załączników." - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ви дійсно хочете видалити розмову з {name}?
Це назавжди видалить усі повідомлення та вкладення." - } - }, - "zh-CN" : { - "stringUnit" : { - "state" : "translated", - "value" : "您确定要删除您与{name}的会话吗?
该操作将永久删除所有消息和附件。" - } - } - } - }, - "deleteMessage" : { - "extractionState" : "manual", - "localizations" : { - "af" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -136665,16 +139415,10 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Skrap Boodskap" - } - }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Skrap Boodskappe" + "value" : "Padam Mesej" } } } @@ -136682,7 +139426,7 @@ } } }, - "ar" : { + "my" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -136693,40 +139437,10 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { - "few" : { - "stringUnit" : { - "state" : "translated", - "value" : "حذف الرسائل" - } - }, - "many" : { - "stringUnit" : { - "state" : "translated", - "value" : "حذف الرسائل" - } - }, - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "حذف الرسالة" - } - }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "حذف الرسائل" - } - }, - "two" : { - "stringUnit" : { - "state" : "translated", - "value" : "حذف الرسائل" - } - }, - "zero" : { - "stringUnit" : { - "state" : "translated", - "value" : "حذف الرسائل" + "value" : "မက်ဆေ့ချ် ဖျက်မည်" } } } @@ -136734,7 +139448,7 @@ } } }, - "az" : { + "nb" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -136748,13 +139462,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Mesajı sil" + "value" : "Slett melding" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Mesajları sil" + "value" : "Slett meldinger" } } } @@ -136762,7 +139476,7 @@ } } }, - "bal" : { + "nb-NO" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -136776,13 +139490,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Delete Message" + "value" : "Slett melding" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Delete Messages" + "value" : "Slett meldinger" } } } @@ -136790,7 +139504,7 @@ } } }, - "be" : { + "ne-NP" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -136801,28 +139515,16 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { - "few" : { - "stringUnit" : { - "state" : "translated", - "value" : "Выдаліць паведамленні" - } - }, - "many" : { - "stringUnit" : { - "state" : "translated", - "value" : "Выдаліць паведамленні" - } - }, "one" : { "stringUnit" : { "state" : "translated", - "value" : "Выдаліць паведамленне" + "value" : "सन्देशहरू मेट्नुहोस्" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Выдаліць паведамленні" + "value" : "सन्देशहरू मेट्नुहोस्" } } } @@ -136830,7 +139532,7 @@ } } }, - "bg" : { + "nl" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -136844,13 +139546,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Изтрий съобщението" + "value" : "Verwijder bericht" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Изтрий съобщенията" + "value" : "Verwijder berichten" } } } @@ -136858,7 +139560,7 @@ } } }, - "bn" : { + "nn-NO" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -136872,13 +139574,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "বার্তা মুছুন" + "value" : "Slett beskjed" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "বার্তাগুলি মুছুন" + "value" : "Slett beskjeder" } } } @@ -136886,7 +139588,7 @@ } } }, - "ca" : { + "ny" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -136900,13 +139602,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Suprimeix el missatge" + "value" : "Chotsani Uthenga" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Suprimeix els missatges" + "value" : "Chotsani Mauthenga" } } } @@ -136914,7 +139616,7 @@ } } }, - "cs" : { + "pa-IN" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -136925,28 +139627,16 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { - "few" : { - "stringUnit" : { - "state" : "translated", - "value" : "Smazat zprávy" - } - }, - "many" : { - "stringUnit" : { - "state" : "translated", - "value" : "Smazat zprávy" - } - }, "one" : { "stringUnit" : { "state" : "translated", - "value" : "Smazat zprávu" + "value" : "ਸੁਨੇਹਾ ਮਿਟਾਓ" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Smazat zprávy" + "value" : "ਸੁਨੇਹੇ ਮਿਟਾਓ" } } } @@ -136954,7 +139644,7 @@ } } }, - "cy" : { + "pl" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -136968,37 +139658,25 @@ "few" : { "stringUnit" : { "state" : "translated", - "value" : "Dileu Negeseuon" + "value" : "Usuń wiadomości" } }, "many" : { "stringUnit" : { "state" : "translated", - "value" : "Dileu Negeseuon" + "value" : "Usuń wiadomości" } }, "one" : { "stringUnit" : { "state" : "translated", - "value" : "Dileu Neges" + "value" : "Usuń wiadomość" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Dileu Negeseuon" - } - }, - "two" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dileu Negeseuon" - } - }, - "zero" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dileu Negeseuon" + "value" : "Usuń wiadomości" } } } @@ -137006,7 +139684,7 @@ } } }, - "da" : { + "ps" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -137020,13 +139698,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Slet besked" + "value" : "پیغام ړنګول" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Slet beskeder" + "value" : "پیغامونه ړنګول" } } } @@ -137034,7 +139712,7 @@ } } }, - "de" : { + "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -137048,13 +139726,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Nachricht löschen" + "value" : "Excluir Mensagem" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Nachrichten löschen" + "value" : "Excluir mensagens" } } } @@ -137062,7 +139740,7 @@ } } }, - "el" : { + "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -137076,13 +139754,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Διαγραφή Μηνύματος" + "value" : "Eliminar Mensagem" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Διαγραφή Μηνυμάτων" + "value" : "Eliminar Mensagens" } } } @@ -137090,7 +139768,7 @@ } } }, - "en" : { + "ro" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -137101,16 +139779,22 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Șterge mesajele" + } + }, "one" : { "stringUnit" : { "state" : "translated", - "value" : "Delete Message" + "value" : "Șterge mesajul" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Delete Messages" + "value" : "Șterge mesajele" } } } @@ -137118,7 +139802,7 @@ } } }, - "eo" : { + "ru" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -137129,16 +139813,28 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Удалить сообщения" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Удалить сообщения" + } + }, "one" : { "stringUnit" : { "state" : "translated", - "value" : "Forigi mesaĝon" + "value" : "Удалить Сообщение" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Forigi mesaĝojn" + "value" : "Удалить сообщения" } } } @@ -137146,7 +139842,7 @@ } } }, - "es-419" : { + "sh" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -137157,24 +139853,36 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { - "one" : { + "few" : { "stringUnit" : { "state" : "translated", - "value" : "Eliminar el mensaje" + "value" : "Obriši poruke" } }, - "other" : { + "many" : { "stringUnit" : { "state" : "translated", - "value" : "Eliminar el mensaje" + "value" : "Obriši poruke" } - } - } - } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Obriši poruku" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Obriši poruke" + } + } + } + } } } }, - "es-ES" : { + "si-LK" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -137188,13 +139896,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Eliminar Mensaje" + "value" : "පණිවිඩය මකන්න" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Eliminar Mensajes" + "value" : "පණිවිඩ මකන්න" } } } @@ -137202,7 +139910,7 @@ } } }, - "et" : { + "sk" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -137213,16 +139921,28 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vymazať správy" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vymazať správy" + } + }, "one" : { "stringUnit" : { "state" : "translated", - "value" : "Kustuta sõnum" + "value" : "Vymazať správu" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Kustuta sõnumid" + "value" : "Vymazať správy" } } } @@ -137230,7 +139950,7 @@ } } }, - "eu" : { + "sl" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -137241,16 +139961,28 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Izbriši sporočila" + } + }, "one" : { "stringUnit" : { "state" : "translated", - "value" : "Mezua Ezabatu" + "value" : "Izbriši sporočilo" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Mezuak Ezabatu" + "value" : "Izbriši sporočila" + } + }, + "two" : { + "stringUnit" : { + "state" : "translated", + "value" : "Izbriši sporočili" } } } @@ -137258,7 +139990,7 @@ } } }, - "fa" : { + "sq" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -137272,13 +140004,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "پیام را پاک کنید" + "value" : "Fshije Mesazhin" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "پیام ها را پاک کنید" + "value" : "Fshini mesazhe" } } } @@ -137286,7 +140018,7 @@ } } }, - "fi" : { + "sr" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -137297,16 +140029,22 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Обриши поруке" + } + }, "one" : { "stringUnit" : { "state" : "translated", - "value" : "Poista viesti" + "value" : "Обриши поруку" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Poista viestit" + "value" : "Обриши поруке" } } } @@ -137314,7 +140052,7 @@ } } }, - "fil" : { + "sr-Latn" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -137325,16 +140063,22 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ukloni poruke" + } + }, "one" : { "stringUnit" : { "state" : "translated", - "value" : "Burahin ang Mensahe" + "value" : "Obriši poruku" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Burahin ang mga Mensahe" + "value" : "Ukloni poruke" } } } @@ -137342,7 +140086,7 @@ } } }, - "fr" : { + "sv-SE" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -137356,13 +140100,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Supprimer le message" + "value" : "Radera meddelande" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Supprimer les messages" + "value" : "Radera meddelanden" } } } @@ -137370,7 +140114,7 @@ } } }, - "ha" : { + "sw" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -137384,13 +140128,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Goge Saƙo" + "value" : "Futa Ujumbe" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Goge Saƙonni" + "value" : "Futa Jumbe" } } } @@ -137398,7 +140142,7 @@ } } }, - "he" : { + "ta" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -137409,28 +140153,66 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { - "many" : { + "one" : { "stringUnit" : { "state" : "translated", - "value" : "מחק הודעות" + "value" : "தகவலை நீக்கு" } }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "தகவலைகளை நீக்கு" + } + } + } + } + } + } + }, + "te" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { "one" : { "stringUnit" : { "state" : "translated", - "value" : "מחק הודעה" + "value" : "సందేశాన్ని తొలగించు" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "מחק הודעות" + "value" : "సందేశాలను తొలగించండి" } - }, - "two" : { + } + } + } + } + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "other" : { "stringUnit" : { "state" : "translated", - "value" : "מחק הודעות" + "value" : "ลบข้อความ" } } } @@ -137438,7 +140220,7 @@ } } }, - "hi" : { + "tr" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -137452,13 +140234,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "संदेश मिटाएं" + "value" : "İletiyi Sil" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "संदेश मिटाएं" + "value" : "İletileri Sil" } } } @@ -137466,7 +140248,7 @@ } } }, - "hr" : { + "uk" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -137480,19 +140262,25 @@ "few" : { "stringUnit" : { "state" : "translated", - "value" : "Izbriši poruku" + "value" : "Видалити повідомлення" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Видалити повідомлення" } }, "one" : { "stringUnit" : { "state" : "translated", - "value" : "Izbriši poruku" + "value" : "Видалити повідомлення" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Izbriši poruku" + "value" : "Видалити повідомлення" } } } @@ -137500,7 +140288,7 @@ } } }, - "hu" : { + "ur-IN" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -137514,13 +140302,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Üzenet törlése" + "value" : "پیغام حذف کریں" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Üzenetek törlése" + "value" : "پیغامات حذف کریں" } } } @@ -137528,7 +140316,7 @@ } } }, - "hy-AM" : { + "uz" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -137542,13 +140330,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Ջնջել հաղորդագրությունը" + "value" : "Xabarni o'chirish" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Ջնջել հաղորդագրությունները" + "value" : "Xabarlarni o'chirish" } } } @@ -137556,7 +140344,7 @@ } } }, - "id" : { + "vi" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -137570,7 +140358,7 @@ "other" : { "stringUnit" : { "state" : "translated", - "value" : "Hapus Pesan" + "value" : "Xóa tin nhắn" } } } @@ -137578,7 +140366,7 @@ } } }, - "it" : { + "xh" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -137592,13 +140380,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Elimina Messaggio" + "value" : "Sangula Umphumela" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Elimina Messaggi" + "value" : "Sangula Imiphumela" } } } @@ -137606,7 +140394,7 @@ } } }, - "ja" : { + "zh-CN" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -137620,7 +140408,7 @@ "other" : { "stringUnit" : { "state" : "translated", - "value" : "メッセージを削除" + "value" : "删除消息" } } } @@ -137628,7 +140416,34 @@ } } }, - "ka" : { + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "刪除訊息" + } + } + } + } + } + } + } + } + }, + "deleteMessageConfirm" : { + "extractionState" : "manual", + "localizations" : { + "ar" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -137639,16 +140454,40 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "هل أنت متأكد من حذف الرسائل؟" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "هل أنت متأكد من حذف الرسائل؟" + } + }, "one" : { "stringUnit" : { "state" : "translated", - "value" : "შეტყობინების წაშლა" + "value" : "هل أنت متأكد من حذف الرسالة؟" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "შეტყობინებების წაშლა" + "value" : "هل أنت متأكد من حذف الرسائل؟" + } + }, + "two" : { + "stringUnit" : { + "state" : "translated", + "value" : "هل أنت متأكد من حذف الرسائل؟" + } + }, + "zero" : { + "stringUnit" : { + "state" : "translated", + "value" : "هل أنت متأكد من حذف الرسائل؟" } } } @@ -137656,7 +140495,7 @@ } } }, - "km" : { + "az" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -137667,10 +140506,16 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu mesajı silmək istədiyinizə əminsiniz?" + } + }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "លុបសារ" + "value" : "Bu mesajları silmək istədiyinizə əminsiniz?" } } } @@ -137678,7 +140523,7 @@ } } }, - "kn" : { + "ca" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -137692,13 +140537,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "ಸಂದೇಶವನ್ನು ಅಳಿಸಿ" + "value" : "Estàs segur que vols suprimir aquest missatge?" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "ಸಂದೇಶಗಳನ್ನು ಅಳಿಸಿ" + "value" : "Estàs segur que vols suprimir aquests missatges?" } } } @@ -137706,7 +140551,7 @@ } } }, - "ko" : { + "cs" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -137717,10 +140562,28 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opravdu chcete smazat tyto zprávy?" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opravdu chcete smazat tyto zprávy?" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opravdu chcete smazat tuto zprávu?" + } + }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "메시지 삭제" + "value" : "Opravdu chcete smazat tyto zprávy?" } } } @@ -137728,7 +140591,7 @@ } } }, - "ku" : { + "da" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -137742,13 +140605,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "سڕینەوەی پەیام" + "value" : "Er du sikker på, at du vil slette denne besked?" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "سڕینەوەی پەیامەکان" + "value" : "Er du sikker på, at du vil slette disse beskeder?" } } } @@ -137756,7 +140619,7 @@ } } }, - "ku-TR" : { + "de" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -137770,13 +140633,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Peyamê Jê Bibe" + "value" : "Bist du dir sicher, dass du diese Nachricht löschen willst?" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Peyaman Jê Bibe" + "value" : "Bist du dir sicher, dass du diese Nachrichten löschen willst?" } } } @@ -137784,7 +140647,7 @@ } } }, - "lg" : { + "el" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -137798,13 +140661,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Jjamu Olukome ngaleerake" + "value" : "Είστε σίγουρος ότι θέλετε να διαγράψετε αυτό το μήνυμα;" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Jjamu Ente" + "value" : "Είστε σίγουρος ότι θέλετε να διαγράψετε αυτά τα μηνύματα;" } } } @@ -137812,7 +140675,7 @@ } } }, - "lt" : { + "en" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -137823,28 +140686,44 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { - "few" : { + "one" : { "stringUnit" : { "state" : "translated", - "value" : "Ištrinti žinutes" + "value" : "Are you sure you want to delete this message?" } }, - "many" : { + "other" : { "stringUnit" : { "state" : "translated", - "value" : "Ištrinti žinutes" + "value" : "Are you sure you want to delete these messages?" } - }, + } + } + } + } + } + }, + "eo" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { "one" : { "stringUnit" : { "state" : "translated", - "value" : "Ištrinti žinutę" + "value" : "Ĉu vi certas, ke vi volas forigi ĉi tiun mesaĝon?" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Ištrinti žinutes" + "value" : "Ĉu vi certas, ke vi volas forigi ĉi tiujn mesaĝojn?" } } } @@ -137852,7 +140731,7 @@ } } }, - "mk" : { + "es-419" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -137866,13 +140745,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Избриши порака" + "value" : "¿Seguro que quieres eliminar este mensaje?" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Избриши пораки" + "value" : "¿Seguro que quieres eliminar estos mensajes?" } } } @@ -137880,7 +140759,7 @@ } } }, - "mn" : { + "es-ES" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -137894,13 +140773,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Мессеж устгах" + "value" : "¿Seguro que quieres eliminar este mensaje?" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Мессежүүдийг устгах" + "value" : "¿Seguro que quieres eliminar estos mensajes?" } } } @@ -137908,7 +140787,7 @@ } } }, - "ms" : { + "et" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -137919,10 +140798,16 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Olete kindel, et soovite selle sõnumi kustutada?" + } + }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Padam Mesej" + "value" : "Olete kindel, et soovite need sõnumid kustutada?" } } } @@ -137930,7 +140815,7 @@ } } }, - "my" : { + "fr" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -137941,10 +140826,16 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Êtes-vous sûr de vouloir supprimer ce message ?" + } + }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "မက်ဆေ့ချ် ဖျက်မည်" + "value" : "Êtes-vous sûr de vouloir supprimer ces messages ?" } } } @@ -137952,7 +140843,7 @@ } } }, - "nb" : { + "hi" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -137966,13 +140857,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Slett melding" + "value" : "क्या आप वाकई इस संदेश को हटाना चाहते हैं?" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Slett meldinger" + "value" : "क्या आप वाकई इन संदेशों को हटाना चाहते हैं?" } } } @@ -137980,7 +140871,7 @@ } } }, - "nb-NO" : { + "hu" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -137994,13 +140885,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Slett melding" + "value" : "Biztosan törölni akarja ezt az üzenetet?" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Slett meldinger" + "value" : "Biztosan törölni akarja ezeket az üzeneteket?" } } } @@ -138008,7 +140899,7 @@ } } }, - "ne-NP" : { + "id" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -138019,16 +140910,10 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "सन्देशहरू मेट्नुहोस्" - } - }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "सन्देशहरू मेट्नुहोस्" + "value" : "Apakah Anda yakin ingin menghapus pesan ini?" } } } @@ -138036,7 +140921,7 @@ } } }, - "nl" : { + "it" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -138050,13 +140935,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Verwijder bericht" + "value" : "Sei sicuro di voler eliminare questo messaggio?" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Verwijder berichten" + "value" : "Sei sicuro di voler eliminare questi messaggi?" } } } @@ -138064,7 +140949,7 @@ } } }, - "nn-NO" : { + "ja" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -138075,16 +140960,10 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Slett beskjed" - } - }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Slett beskjeder" + "value" : "メッセージを削除します。本当によろしいいですか?" } } } @@ -138092,7 +140971,7 @@ } } }, - "ny" : { + "ka" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -138106,13 +140985,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Chotsani Uthenga" + "value" : "დარწმუნებული ხართ რომ ამ შეტყობინების წაშლა გსურთ?" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Chotsani Mauthenga" + "value" : "დარწმუნებული ხართ რომ ამ შეწყობინებების წაშლა გსურთ?" } } } @@ -138120,7 +140999,29 @@ } } }, - "pa-IN" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "정말 해당 메시지들을 삭제하시겠습니까?" + } + } + } + } + } + } + }, + "ku" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -138134,13 +141035,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "ਸੁਨੇਹਾ ਮਿਟਾਓ" + "value" : "دڵنیایت دەتەوێت ئەم پەیامە بسڕیتەوە؟" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "ਸੁਨੇਹੇ ਮਿਟਾਓ" + "value" : "دڵنیایت دەتەوێت ئەم پەیامانە بسڕیتەوە؟" } } } @@ -138148,7 +141049,7 @@ } } }, - "pl" : { + "ku-TR" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -138159,28 +141060,44 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { - "few" : { + "one" : { "stringUnit" : { "state" : "translated", - "value" : "Usuń wiadomości" + "value" : "Tu piştrast î ku tu dixwazî vê peyamê jê bibî?" } }, - "many" : { + "other" : { "stringUnit" : { "state" : "translated", - "value" : "Usuń wiadomości" + "value" : "Tu piştrast î ku tu dixwazî va peyaman jê bibî?" } - }, + } + } + } + } + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { "one" : { "stringUnit" : { "state" : "translated", - "value" : "Usuń wiadomość" + "value" : "Er du sikker på at du vil slette denne meldingen?" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Usuń wiadomości" + "value" : "Er du sikker på at du vil slette disse meldingene?" } } } @@ -138188,7 +141105,7 @@ } } }, - "ps" : { + "nl" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -138202,13 +141119,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "پیغام ړنګول" + "value" : "Weet u zeker dat u dit bericht wilt verwijderen?" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "پیغامونه ړنګول" + "value" : "Weet u zeker dat u deze berichten wilt verwijderen?" } } } @@ -138216,7 +141133,7 @@ } } }, - "pt-BR" : { + "pl" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -138227,16 +141144,28 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Czy na pewno chcesz usunąć te wiadomości?" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Czy na pewno chcesz usunąć te wiadomości?" + } + }, "one" : { "stringUnit" : { "state" : "translated", - "value" : "Excluir Mensagem" + "value" : "Czy na pewno chcesz usunąć tę wiadomość?" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Excluir mensagens" + "value" : "Czy na pewno chcesz usunąć te wiadomości?" } } } @@ -138258,13 +141187,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Eliminar Mensagem" + "value" : "Tem certeza de que deseja apagar esta mensagem?" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Eliminar Mensagens" + "value" : "Tem certeza de que deseja apagar essas mensagens?" } } } @@ -138286,19 +141215,19 @@ "few" : { "stringUnit" : { "state" : "translated", - "value" : "Șterge mesajele" + "value" : "Sunteți sigur că doriți să ștergeți aceste mesaje?" } }, "one" : { "stringUnit" : { "state" : "translated", - "value" : "Șterge mesajul" + "value" : "Sunteți sigur că doriți să ștergeți acest mesaj?" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Șterge mesajele" + "value" : "Sunteți sigur că doriți să ștergeți aceste mesaje?" } } } @@ -138320,25 +141249,25 @@ "few" : { "stringUnit" : { "state" : "translated", - "value" : "Удалить сообщения" + "value" : "Вы уверены, что хотите удалить эти сообщения?" } }, "many" : { "stringUnit" : { "state" : "translated", - "value" : "Удалить сообщения" + "value" : "Вы уверены, что хотите удалить эти сообщения?" } }, "one" : { "stringUnit" : { "state" : "translated", - "value" : "Удалить Сообщение" + "value" : "Вы уверены, что хотите удалить это сообщение?" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Удалить сообщения" + "value" : "Вы уверены, что хотите удалить эти сообщения?" } } } @@ -138346,7 +141275,7 @@ } } }, - "sh" : { + "sv-SE" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -138357,28 +141286,16 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { - "few" : { - "stringUnit" : { - "state" : "translated", - "value" : "Obriši poruke" - } - }, - "many" : { - "stringUnit" : { - "state" : "translated", - "value" : "Obriši poruke" - } - }, "one" : { "stringUnit" : { "state" : "translated", - "value" : "Obriši poruku" + "value" : "Är du säker på att du vill radera detta meddelande?" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Obriši poruke" + "value" : "Är du säker på att du vill radera dessa meddelanden?" } } } @@ -138386,7 +141303,7 @@ } } }, - "si-LK" : { + "tr" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -138400,13 +141317,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "පණිවිඩය මකන්න" + "value" : "Bu mesajı silmek istediğinizden emin misiniz?" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "පණිවිඩ මකන්න" + "value" : "Bu mesajları silmek istediğinizden emin misiniz?" } } } @@ -138414,7 +141331,7 @@ } } }, - "sk" : { + "uk" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -138428,25 +141345,25 @@ "few" : { "stringUnit" : { "state" : "translated", - "value" : "Vymazať správy" + "value" : "Ви дійсно хочете видалити ці повідомлення?" } }, "many" : { "stringUnit" : { "state" : "translated", - "value" : "Vymazať správy" + "value" : "Ви дійсно хочете видалити ці повідомлення?" } }, "one" : { "stringUnit" : { "state" : "translated", - "value" : "Vymazať správu" + "value" : "Ви дійсно хочете видалити це повідомлення?" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Vymazať správy" + "value" : "Ви дійсно хочете видалити ці повідомлення?" } } } @@ -138454,7 +141371,7 @@ } } }, - "sl" : { + "vi" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -138465,28 +141382,10 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { - "few" : { - "stringUnit" : { - "state" : "translated", - "value" : "Izbriši sporočila" - } - }, - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Izbriši sporočilo" - } - }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Izbriši sporočila" - } - }, - "two" : { - "stringUnit" : { - "state" : "translated", - "value" : "Izbriši sporočili" + "value" : "Bạn có chắc chắn rằng bạn muốn xoá các tin nhắn này không?" } } } @@ -138494,7 +141393,7 @@ } } }, - "sq" : { + "zh-CN" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -138505,16 +141404,10 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fshije Mesazhin" - } - }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Fshini mesazhe" + "value" : "您确定要删除这些信息吗?" } } } @@ -138522,7 +141415,7 @@ } } }, - "sr" : { + "zh-TW" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -138533,22 +141426,43 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { - "few" : { + "other" : { "stringUnit" : { "state" : "translated", - "value" : "Обриши поруке" + "value" : "您確定要刪除這些訊息嗎?" } - }, + } + } + } + } + } + } + } + }, + "deleteMessageDeleted" : { + "extractionState" : "manual", + "localizations" : { + "af" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { "one" : { "stringUnit" : { "state" : "translated", - "value" : "Обриши поруку" + "value" : "Boodskap verwyder" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Обриши поруке" + "value" : "Boodskappe verwyder" } } } @@ -138556,7 +141470,7 @@ } } }, - "sr-Latn" : { + "ar" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -138570,19 +141484,37 @@ "few" : { "stringUnit" : { "state" : "translated", - "value" : "Ukloni poruke" + "value" : "تم حذف الرسائل" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "تم حذف الرسائل" } }, "one" : { "stringUnit" : { "state" : "translated", - "value" : "Obriši poruku" + "value" : "تم حذف الرسالة" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Ukloni poruke" + "value" : "تم حذف الرسائل" + } + }, + "two" : { + "stringUnit" : { + "state" : "translated", + "value" : "تم حذف الرسائل" + } + }, + "zero" : { + "stringUnit" : { + "state" : "translated", + "value" : "تم حذف الرسائل" } } } @@ -138590,7 +141522,7 @@ } } }, - "sv-SE" : { + "az" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -138604,13 +141536,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Radera meddelande" + "value" : "Mesaj silindi" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Radera meddelanden" + "value" : "Mesajlar silindi" } } } @@ -138618,7 +141550,7 @@ } } }, - "sw" : { + "bal" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -138632,13 +141564,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Futa Ujumbe" + "value" : "Message deleted" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Futa Jumbe" + "value" : "Messages deleted" } } } @@ -138646,7 +141578,7 @@ } } }, - "ta" : { + "be" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -138657,16 +141589,28 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Паведамленні выдалены" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Паведамленні выдалены" + } + }, "one" : { "stringUnit" : { "state" : "translated", - "value" : "தகவலை நீக்கு" + "value" : "Паведамленне выдалена" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "தகவலைகளை நீக்கு" + "value" : "Паведамленні выдалены" } } } @@ -138674,7 +141618,7 @@ } } }, - "te" : { + "bg" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -138688,13 +141632,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "సందేశాన్ని తొలగించు" + "value" : "Съобщението е изтрито" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "సందేశాలను తొలగించండి" + "value" : "Съобщенията са изтрити" } } } @@ -138702,7 +141646,7 @@ } } }, - "th" : { + "bn" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -138713,10 +141657,16 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "বার্তা মুছে ফেলা হয়েছে" + } + }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "ลบข้อความ" + "value" : "বার্তাগুলি মুছে ফেলা হয়েছে" } } } @@ -138724,7 +141674,7 @@ } } }, - "tr" : { + "ca" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -138738,13 +141688,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "İletiyi Sil" + "value" : "Missatge suprimit" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "İletileri Sil" + "value" : "Missatges suprimits" } } } @@ -138752,7 +141702,7 @@ } } }, - "uk" : { + "cs" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -138766,25 +141716,25 @@ "few" : { "stringUnit" : { "state" : "translated", - "value" : "Видалити повідомлення" + "value" : "Zprávy smazány" } }, "many" : { "stringUnit" : { "state" : "translated", - "value" : "Видалити повідомлення" + "value" : "Zprávy smazány" } }, "one" : { "stringUnit" : { "state" : "translated", - "value" : "Видалити повідомлення" + "value" : "Zpráva smazána" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Видалити повідомлення" + "value" : "Zprávy smazány" } } } @@ -138792,7 +141742,7 @@ } } }, - "ur-IN" : { + "cy" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -138803,66 +141753,40 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { - "one" : { + "few" : { "stringUnit" : { "state" : "translated", - "value" : "پیغام حذف کریں" + "value" : "Negeseuon wedi'u dileu" } }, - "other" : { + "many" : { "stringUnit" : { "state" : "translated", - "value" : "پیغامات حذف کریں" + "value" : "Negeseuon wedi'u dileu" } - } - } - } - } - } - }, - "uz" : { - "stringUnit" : { - "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { + }, "one" : { "stringUnit" : { "state" : "translated", - "value" : "Xabarni o'chirish" + "value" : "Neges wedi'i dileu" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Xabarlarni o'chirish" + "value" : "Negeseuon wedi'u dileu" } - } - } - } - } - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "other" : { + }, + "two" : { "stringUnit" : { "state" : "translated", - "value" : "Xóa tin nhắn" + "value" : "Negeseuon wedi'u dileu" + } + }, + "zero" : { + "stringUnit" : { + "state" : "translated", + "value" : "Negeseuon wedi'u dileu" } } } @@ -138870,7 +141794,7 @@ } } }, - "xh" : { + "da" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -138884,13 +141808,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Sangula Umphumela" + "value" : "Besked slettet" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Sangula Imiphumela" + "value" : "Beskeder slettet" } } } @@ -138898,7 +141822,7 @@ } } }, - "zh-CN" : { + "de" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -138909,45 +141833,24 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { - "other" : { + "one" : { "stringUnit" : { "state" : "translated", - "value" : "删除消息" + "value" : "Nachricht gelöscht" } - } - } - } - } - } - }, - "zh-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { + }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "刪除訊息" + "value" : "Nachrichten gelöscht" } } } } } } - } - } - }, - "deleteMessageConfirm" : { - "extractionState" : "manual", - "localizations" : { - "ar" : { + }, + "el" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -138958,40 +141861,16 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { - "few" : { - "stringUnit" : { - "state" : "translated", - "value" : "هل أنت متأكد من حذف الرسائل؟" - } - }, - "many" : { - "stringUnit" : { - "state" : "translated", - "value" : "هل أنت متأكد من حذف الرسائل؟" - } - }, "one" : { "stringUnit" : { "state" : "translated", - "value" : "هل أنت متأكد من حذف الرسالة؟" + "value" : "Το μήνυμα διαγράφηκε" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "هل أنت متأكد من حذف الرسائل؟" - } - }, - "two" : { - "stringUnit" : { - "state" : "translated", - "value" : "هل أنت متأكد من حذف الرسائل؟" - } - }, - "zero" : { - "stringUnit" : { - "state" : "translated", - "value" : "هل أنت متأكد من حذف الرسائل؟" + "value" : "Τα μηνύματα διαγράφηκαν" } } } @@ -138999,7 +141878,7 @@ } } }, - "az" : { + "en" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -139013,13 +141892,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Bu mesajı silmək istədiyinizə əminsiniz?" + "value" : "Message deleted" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Bu mesajları silmək istədiyinizə əminsiniz?" + "value" : "Messages deleted" } } } @@ -139027,7 +141906,7 @@ } } }, - "ca" : { + "eo" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -139041,13 +141920,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Estàs segur que vols suprimir aquest missatge?" + "value" : "Mesaĝo forigita" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Estàs segur que vols suprimir aquests missatges?" + "value" : "Mesaĝoj forigitaj" } } } @@ -139055,7 +141934,7 @@ } } }, - "cs" : { + "es-419" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -139066,28 +141945,16 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { - "few" : { - "stringUnit" : { - "state" : "translated", - "value" : "Opravdu chcete smazat tyto zprávy?" - } - }, - "many" : { - "stringUnit" : { - "state" : "translated", - "value" : "Opravdu chcete smazat tyto zprávy?" - } - }, "one" : { "stringUnit" : { "state" : "translated", - "value" : "Opravdu chcete smazat tuto zprávu?" + "value" : "Mensaje eliminado" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Opravdu chcete smazat tyto zprávy?" + "value" : "Mensajes eliminados" } } } @@ -139095,7 +141962,7 @@ } } }, - "da" : { + "es-ES" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -139109,13 +141976,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Er du sikker på, at du vil slette denne besked?" + "value" : "Mensaje borrado" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Er du sikker på, at du vil slette disse beskeder?" + "value" : "Mensajes borrados" } } } @@ -139123,7 +141990,7 @@ } } }, - "de" : { + "et" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -139137,13 +142004,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Bist du dir sicher, dass du diese Nachricht löschen willst?" + "value" : "Sõnum kustutatud" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Bist du dir sicher, dass du diese Nachrichten löschen willst?" + "value" : "Sõnumid kustutatud" } } } @@ -139151,7 +142018,7 @@ } } }, - "en" : { + "eu" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -139165,13 +142032,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Are you sure you want to delete this message?" + "value" : "Mezua ezabatuta" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Are you sure you want to delete these messages?" + "value" : "Mezuak ezabatuta" } } } @@ -139179,7 +142046,7 @@ } } }, - "eo" : { + "fa" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -139193,13 +142060,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Ĉu vi certas, ke vi volas forigi ĉi tiun mesaĝon?" + "value" : "پیام پاک شد" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Ĉu vi certas, ke vi volas forigi ĉi tiujn mesaĝojn?" + "value" : "پبام ها حذف شدند" } } } @@ -139207,7 +142074,7 @@ } } }, - "es-419" : { + "fi" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -139221,13 +142088,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "¿Seguro que quieres eliminar este mensaje?" + "value" : "Viesti poistettu" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "¿Seguro que quieres eliminar estos mensajes?" + "value" : "Viestit poistettu" } } } @@ -139235,7 +142102,7 @@ } } }, - "es-ES" : { + "fil" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -139249,13 +142116,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "¿Seguro que quieres eliminar este mensaje?" + "value" : "Mensahe nabura" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "¿Seguro que quieres eliminar estos mensajes?" + "value" : "Mga mensahe nabura" } } } @@ -139277,13 +142144,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Êtes-vous sûr de vouloir supprimer ce message ?" + "value" : "Message supprimé" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Êtes-vous sûr de vouloir supprimer ces messages ?" + "value" : "Messages supprimés" } } } @@ -139291,7 +142158,7 @@ } } }, - "hi" : { + "ha" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -139305,13 +142172,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "क्या आप वाकई इस संदेश को हटाना चाहते हैं?" + "value" : "An goge saƙo" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "क्या आप वाकई इन संदेशों को हटाना चाहते हैं?" + "value" : "An goge Saƙonni" } } } @@ -139319,7 +142186,7 @@ } } }, - "hu" : { + "he" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -139330,88 +142197,28 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { - "one" : { + "many" : { "stringUnit" : { "state" : "translated", - "value" : "Biztosan törölni akarja ezt az üzenetet?" + "value" : "הודעות נמחקו" } }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Biztosan törölni akarja ezeket az üzeneteket?" - } - } - } - } - } - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Apakah Anda yakin ingin menghapus pesan ini?" - } - } - } - } - } - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { "one" : { "stringUnit" : { "state" : "translated", - "value" : "Sei sicuro di voler eliminare questo messaggio?" + "value" : "הודעה נמחקה" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Sei sicuro di voler eliminare questi messaggi?" + "value" : "הודעות נמחקו" } - } - } - } - } - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "other" : { + }, + "two" : { "stringUnit" : { "state" : "translated", - "value" : "メッセージを削除します。本当によろしいいですか?" + "value" : "הודעות נמחקו" } } } @@ -139419,7 +142226,7 @@ } } }, - "ka" : { + "hi" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -139433,13 +142240,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "დარწმუნებული ხართ რომ ამ შეტყობინების წაშლა გსურთ?" + "value" : "संदेश मिटाया गया" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "დარწმუნებული ხართ რომ ამ შეწყობინებების წაშლა გსურთ?" + "value" : "संदेश मिटाये गए" } } } @@ -139447,7 +142254,7 @@ } } }, - "ko" : { + "hr" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -139458,38 +142265,22 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { - "other" : { + "few" : { "stringUnit" : { "state" : "translated", - "value" : "정말 해당 메시지들을 삭제하시겠습니까?" + "value" : "Poruke obrisane" } - } - } - } - } - } - }, - "ku-TR" : { - "stringUnit" : { - "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { + }, "one" : { "stringUnit" : { "state" : "translated", - "value" : "Tu piştrast î ku tu dixwazî vê peyamê jê bibî?" + "value" : "Poruka izbrisana" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Tu piştrast î ku tu dixwazî va peyaman jê bibî?" + "value" : "Poruke obrisane" } } } @@ -139497,7 +142288,7 @@ } } }, - "nl" : { + "hu" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -139511,13 +142302,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Weet u zeker dat u dit bericht wilt verwijderen?" + "value" : "Üzenet törölve" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Weet u zeker dat u deze berichten wilt verwijderen?" + "value" : "Üzenetek törölve" } } } @@ -139525,7 +142316,7 @@ } } }, - "pl" : { + "hy-AM" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -139536,28 +142327,16 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { - "few" : { - "stringUnit" : { - "state" : "translated", - "value" : "Czy na pewno chcesz usunąć te wiadomości?" - } - }, - "many" : { - "stringUnit" : { - "state" : "translated", - "value" : "Czy na pewno chcesz usunąć te wiadomości?" - } - }, "one" : { "stringUnit" : { "state" : "translated", - "value" : "Czy na pewno chcesz usunąć tę wiadomość?" + "value" : "Հաղորդագրությունը ջնջված է" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Czy na pewno chcesz usunąć te wiadomości?" + "value" : "Հաղորդագրությունները ջնջված են" } } } @@ -139565,7 +142344,7 @@ } } }, - "ru" : { + "id" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -139576,28 +142355,10 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { - "few" : { - "stringUnit" : { - "state" : "translated", - "value" : "Вы уверены, что хотите удалить эти сообщения?" - } - }, - "many" : { - "stringUnit" : { - "state" : "translated", - "value" : "Вы уверены, что хотите удалить эти сообщения?" - } - }, - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Вы уверены, что хотите удалить это сообщение?" - } - }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Вы уверены, что хотите удалить эти сообщения?" + "value" : "Pesan dihapus" } } } @@ -139605,7 +142366,7 @@ } } }, - "sv-SE" : { + "it" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -139619,13 +142380,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Är du säker på att du vill radera detta meddelande?" + "value" : "Messaggio eliminato" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Är du säker på att du vill radera dessa meddelanden?" + "value" : "Messaggi eliminati" } } } @@ -139633,7 +142394,7 @@ } } }, - "tr" : { + "ja" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -139644,16 +142405,10 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bu mesajı silmek istediğinizden emin misiniz?" - } - }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Bu mesajları silmek istediğinizden emin misiniz?" + "value" : "メッセージが削除されました" } } } @@ -139661,7 +142416,7 @@ } } }, - "uk" : { + "ka" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -139672,28 +142427,16 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { - "few" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ви дійсно хочете видалити ці повідомлення?" - } - }, - "many" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ви дійсно хочете видалити ці повідомлення?" - } - }, "one" : { "stringUnit" : { "state" : "translated", - "value" : "Ви дійсно хочете видалити це повідомлення?" + "value" : "შეტყობინება წაშლილია" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Ви дійсно хочете видалити ці повідомлення?" + "value" : "შეტყობინებები წაშლილია" } } } @@ -139701,7 +142444,7 @@ } } }, - "vi" : { + "km" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -139715,7 +142458,7 @@ "other" : { "stringUnit" : { "state" : "translated", - "value" : "Bạn có chắc chắn rằng bạn muốn xoá các tin nhắn này không?" + "value" : "សារត្រូវបានលុបហើយ" } } } @@ -139723,7 +142466,7 @@ } } }, - "zh-CN" : { + "kn" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -139734,23 +142477,24 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "ಸಂದೇಶವನ್ನು ಅಳಿಸಲಾಗಿದೆ" + } + }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "您确定要删除这些信息吗?" + "value" : "ಸಂದೇಶಗಳನ್ನು ಅಳಿಸಲಾಗಿದೆ" } } } } } } - } - } - }, - "deleteMessageDeleted" : { - "extractionState" : "manual", - "localizations" : { - "af" : { + }, + "ko" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -139761,16 +142505,10 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Boodskap verwyder" - } - }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Boodskappe verwyder" + "value" : "메시지가 삭제되었습니다" } } } @@ -139778,7 +142516,7 @@ } } }, - "ar" : { + "ku" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -139789,40 +142527,16 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { - "few" : { - "stringUnit" : { - "state" : "translated", - "value" : "تم حذف الرسائل" - } - }, - "many" : { - "stringUnit" : { - "state" : "translated", - "value" : "تم حذف الرسائل" - } - }, "one" : { "stringUnit" : { "state" : "translated", - "value" : "تم حذف الرسالة" + "value" : "نامە سڕا" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "تم حذف الرسائل" - } - }, - "two" : { - "stringUnit" : { - "state" : "translated", - "value" : "تم حذف الرسائل" - } - }, - "zero" : { - "stringUnit" : { - "state" : "translated", - "value" : "تم حذف الرسائل" + "value" : "نامەکان بسڕانەوە" } } } @@ -139830,7 +142544,7 @@ } } }, - "az" : { + "ku-TR" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -139844,13 +142558,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Mesaj silindi" + "value" : "Peyam hate rakirin" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Mesajlar silindi" + "value" : "Peyamên hate rakirin" } } } @@ -139858,7 +142572,7 @@ } } }, - "bal" : { + "lg" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -139872,13 +142586,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Message deleted" + "value" : "Obubaka bukyusiddwako" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Messages deleted" + "value" : "Obubaka obwokutorera obukyusiddwako" } } } @@ -139886,7 +142600,7 @@ } } }, - "be" : { + "lt" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -139900,25 +142614,25 @@ "few" : { "stringUnit" : { "state" : "translated", - "value" : "Паведамленні выдалены" + "value" : "Žinutės ištrintos" } }, "many" : { "stringUnit" : { "state" : "translated", - "value" : "Паведамленні выдалены" + "value" : "Žinutės ištrintos" } }, "one" : { "stringUnit" : { "state" : "translated", - "value" : "Паведамленне выдалена" + "value" : "Žinutė ištrinta" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Паведамленні выдалены" + "value" : "Žinutės ištrintos" } } } @@ -139926,7 +142640,7 @@ } } }, - "bg" : { + "mk" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -139940,13 +142654,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Съобщението е изтрито" + "value" : "Пораката е избришана" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Съобщенията са изтрити" + "value" : "Пораките се избришани" } } } @@ -139954,7 +142668,7 @@ } } }, - "bn" : { + "mn" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -139968,13 +142682,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "বার্তা মুছে ফেলা হয়েছে" + "value" : "Мессеж устгагдсан" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "বার্তাগুলি মুছে ফেলা হয়েছে" + "value" : "Мессежүүд устгагдсан" } } } @@ -139982,7 +142696,7 @@ } } }, - "ca" : { + "ms" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -139993,16 +142707,10 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Missatge suprimit" - } - }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Missatges suprimits" + "value" : "Mesej dipadam" } } } @@ -140010,7 +142718,7 @@ } } }, - "cs" : { + "my" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -140021,28 +142729,10 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { - "few" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zprávy smazány" - } - }, - "many" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zprávy smazány" - } - }, - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zpráva smazána" - } - }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Zprávy smazány" + "value" : "မက်ဆေ့ချ်များ ဖျက်ထားသည်" } } } @@ -140050,7 +142740,7 @@ } } }, - "cy" : { + "nb" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -140061,40 +142751,16 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { - "few" : { - "stringUnit" : { - "state" : "translated", - "value" : "Negeseuon wedi'u dileu" - } - }, - "many" : { - "stringUnit" : { - "state" : "translated", - "value" : "Negeseuon wedi'u dileu" - } - }, "one" : { "stringUnit" : { "state" : "translated", - "value" : "Neges wedi'i dileu" + "value" : "Melding slettet" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Negeseuon wedi'u dileu" - } - }, - "two" : { - "stringUnit" : { - "state" : "translated", - "value" : "Negeseuon wedi'u dileu" - } - }, - "zero" : { - "stringUnit" : { - "state" : "translated", - "value" : "Negeseuon wedi'u dileu" + "value" : "Meldinger slettet" } } } @@ -140102,7 +142768,7 @@ } } }, - "da" : { + "nb-NO" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -140116,13 +142782,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Besked slettet" + "value" : "Beskjed slettet" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Beskeder slettet" + "value" : "Beskjeder slettet" } } } @@ -140130,7 +142796,7 @@ } } }, - "de" : { + "nl" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -140144,13 +142810,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Nachricht gelöscht" + "value" : "Bericht verwijderd" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Nachrichten gelöscht" + "value" : "Berichten verwijderd" } } } @@ -140158,7 +142824,7 @@ } } }, - "el" : { + "nn-NO" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -140172,13 +142838,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Το μήνυμα διαγράφηκε" + "value" : "Beskjed slettet" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Τα μηνύματα διαγράφηκαν" + "value" : "Beskjeder slettet" } } } @@ -140186,7 +142852,7 @@ } } }, - "en" : { + "ny" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -140200,13 +142866,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Message deleted" + "value" : "Uthenga wachotsedwa" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Messages deleted" + "value" : "Mauthenga omwe achotsedwa" } } } @@ -140214,7 +142880,7 @@ } } }, - "eo" : { + "pa-IN" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -140228,13 +142894,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Mesaĝo forigita" + "value" : "ਸੁਨੇਹਾ ਹਟਾਇਆ ਗਿਆ" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Mesaĝoj forigitaj" + "value" : "ਸੁਨੇਹੇ ਹਟਾਏ ਗਏ" } } } @@ -140242,7 +142908,7 @@ } } }, - "es-419" : { + "pl" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -140253,44 +142919,28 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { - "one" : { + "few" : { "stringUnit" : { "state" : "translated", - "value" : "Mensaje eliminado" + "value" : "Usunięto wiadomości" } }, - "other" : { + "many" : { "stringUnit" : { "state" : "translated", - "value" : "Mensajes eliminados" + "value" : "Usunięto wiadomości" } - } - } - } - } - } - }, - "es-ES" : { - "stringUnit" : { - "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { + }, "one" : { "stringUnit" : { "state" : "translated", - "value" : "Mensaje borrado" + "value" : "Wiadomość usunięta" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Mensajes borrados" + "value" : "Usunięto wiadomości" } } } @@ -140298,7 +142948,7 @@ } } }, - "et" : { + "ps" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -140312,13 +142962,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Sõnum kustutatud" + "value" : "پیغام حذف شوی" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Sõnumid kustutatud" + "value" : "پیغامونه حذف شوي" } } } @@ -140326,7 +142976,7 @@ } } }, - "eu" : { + "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -140340,13 +142990,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Mezua ezabatuta" + "value" : "Mensagem excluída" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Mezuak ezabatuta" + "value" : "Mensagens excluídas" } } } @@ -140354,7 +143004,7 @@ } } }, - "fa" : { + "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -140368,13 +143018,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "پیام پاک شد" + "value" : "Mensagem eliminada" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "پبام ها حذف شدند" + "value" : "Mensagens eliminadas" } } } @@ -140382,7 +143032,7 @@ } } }, - "fi" : { + "ro" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -140393,44 +143043,22 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { - "one" : { + "few" : { "stringUnit" : { "state" : "translated", - "value" : "Viesti poistettu" + "value" : "Mesaje șterse" } }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Viestit poistettu" - } - } - } - } - } - } - }, - "fil" : { - "stringUnit" : { - "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { "one" : { "stringUnit" : { "state" : "translated", - "value" : "Mensahe nabura" + "value" : "Mesaj șters" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Mga mensahe nabura" + "value" : "Mesaje șterse" } } } @@ -140438,7 +143066,7 @@ } } }, - "fr" : { + "ru" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -140449,44 +143077,28 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { - "one" : { + "few" : { "stringUnit" : { "state" : "translated", - "value" : "Message supprimé" + "value" : "Сообщения удалены" } }, - "other" : { + "many" : { "stringUnit" : { "state" : "translated", - "value" : "Messages supprimés" + "value" : "Сообщения удалены" } - } - } - } - } - } - }, - "ha" : { - "stringUnit" : { - "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { + }, "one" : { "stringUnit" : { "state" : "translated", - "value" : "An goge saƙo" + "value" : "Сообщение удалено" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "An goge Saƙonni" + "value" : "Сообщения удалены" } } } @@ -140494,7 +143106,7 @@ } } }, - "he" : { + "sh" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -140505,28 +143117,28 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { - "many" : { + "few" : { "stringUnit" : { "state" : "translated", - "value" : "הודעות נמחקו" + "value" : "Poruke obrisane" } }, - "one" : { + "many" : { "stringUnit" : { "state" : "translated", - "value" : "הודעה נמחקה" + "value" : "Poruke obrisane" } }, - "other" : { + "one" : { "stringUnit" : { "state" : "translated", - "value" : "הודעות נמחקו" + "value" : "Poruka obrisana" } }, - "two" : { + "other" : { "stringUnit" : { "state" : "translated", - "value" : "הודעות נמחקו" + "value" : "Poruke obrisane" } } } @@ -140534,7 +143146,7 @@ } } }, - "hi" : { + "si-LK" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -140548,13 +143160,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "संदेश मिटाया गया" + "value" : "පණිවිඩය මැකිණි" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "संदेश मिटाये गए" + "value" : "පණිවිඩ මැකිණි" } } } @@ -140562,7 +143174,7 @@ } } }, - "hr" : { + "sk" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -140576,19 +143188,25 @@ "few" : { "stringUnit" : { "state" : "translated", - "value" : "Poruke obrisane" + "value" : "Správy vymazané" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Správy vymazané" } }, "one" : { "stringUnit" : { "state" : "translated", - "value" : "Poruka izbrisana" + "value" : "Správa vymazaná" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Poruke obrisane" + "value" : "Správy vymazané" } } } @@ -140596,7 +143214,7 @@ } } }, - "hu" : { + "sl" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -140607,16 +143225,28 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sporočila so izbrisana" + } + }, "one" : { "stringUnit" : { "state" : "translated", - "value" : "Üzenet törölve" + "value" : "Sporočilo izbrisano" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Üzenetek törölve" + "value" : "Sporočila so izbrisana" + } + }, + "two" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sporočili sta izbrisani" } } } @@ -140624,7 +143254,7 @@ } } }, - "hy-AM" : { + "sq" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -140638,13 +143268,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Հաղորդագրությունը ջնջված է" + "value" : "Mesazhi u fshi" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Հաղորդագրությունները ջնջված են" + "value" : "Messages deleted" } } } @@ -140652,7 +143282,7 @@ } } }, - "id" : { + "sr" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -140663,38 +143293,22 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { - "other" : { + "few" : { "stringUnit" : { "state" : "translated", - "value" : "Pesan dihapus" + "value" : "Поруке су обрисане" } - } - } - } - } - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { + }, "one" : { "stringUnit" : { "state" : "translated", - "value" : "Messaggio eliminato" + "value" : "Порука је обрисана" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Messaggi eliminati" + "value" : "Поруке су обрисане" } } } @@ -140702,7 +143316,7 @@ } } }, - "ja" : { + "sr-Latn" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -140713,38 +143327,22 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { - "other" : { + "few" : { "stringUnit" : { "state" : "translated", - "value" : "メッセージが削除されました" + "value" : "Poruke obrisane" } - } - } - } - } - } - }, - "ka" : { - "stringUnit" : { - "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { + }, "one" : { "stringUnit" : { "state" : "translated", - "value" : "შეტყობინება წაშლილია" + "value" : "Poruka obrisana" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "შეტყობინებები წაშლილია" + "value" : "Poruke obrisane" } } } @@ -140752,7 +143350,7 @@ } } }, - "km" : { + "sv-SE" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -140763,10 +143361,16 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meddelande raderat" + } + }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "សារត្រូវបានលុបហើយ" + "value" : "Meddelanden raderade" } } } @@ -140774,7 +143378,7 @@ } } }, - "kn" : { + "sw" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -140788,13 +143392,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "ಸಂದೇಶವನ್ನು ಅಳಿಸಲಾಗಿದೆ" + "value" : "Ujumbe umefutwa" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "ಸಂದೇಶಗಳನ್ನು ಅಳಿಸಲಾಗಿದೆ" + "value" : "Jumbe zimefutwa" } } } @@ -140802,7 +143406,7 @@ } } }, - "ko" : { + "ta" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -140813,10 +143417,16 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "செய்தி நீக்கப்பட்டது" + } + }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "메시지가 삭제되었습니다" + "value" : "செய்திகள் அகற்றப்பட்டது" } } } @@ -140824,7 +143434,7 @@ } } }, - "ku" : { + "te" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -140838,13 +143448,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "نامە سڕا" + "value" : "సందేశం తొలగించబడింది" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "نامەکان بسڕانەوە" + "value" : "సందేశాలు తొలగించబడ్డాయి" } } } @@ -140852,7 +143462,7 @@ } } }, - "ku-TR" : { + "th" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -140863,16 +143473,10 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Peyam hate rakirin" - } - }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Peyamên hate rakirin" + "value" : "ข้อความถูกลบ" } } } @@ -140880,7 +143484,7 @@ } } }, - "lg" : { + "tr" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -140894,13 +143498,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Obubaka bukyusiddwako" + "value" : "İleti silindi" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Obubaka obwokutorera obukyusiddwako" + "value" : "İletiler silindi" } } } @@ -140908,7 +143512,7 @@ } } }, - "lt" : { + "uk" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -140922,25 +143526,25 @@ "few" : { "stringUnit" : { "state" : "translated", - "value" : "Žinutės ištrintos" + "value" : "Повідомлення видалені" } }, "many" : { "stringUnit" : { "state" : "translated", - "value" : "Žinutės ištrintos" + "value" : "Повідомлення видалені" } }, "one" : { "stringUnit" : { "state" : "translated", - "value" : "Žinutė ištrinta" + "value" : "Повідомлення видалено" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Žinutės ištrintos" + "value" : "Повідомлення видалені" } } } @@ -140948,7 +143552,7 @@ } } }, - "mk" : { + "ur-IN" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -140962,13 +143566,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Пораката е избришана" + "value" : "پیغام حذف ہو گیا" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Пораките се избришани" + "value" : "پیغامات حذف ہو گئے" } } } @@ -140976,7 +143580,7 @@ } } }, - "mn" : { + "uz" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -140990,35 +143594,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Мессеж устгагдсан" + "value" : "Xabar o'chirildi" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Мессежүүд устгагдсан" - } - } - } - } - } - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mesej dipadam" + "value" : "Xabarlar oʻchirildi" } } } @@ -141026,7 +143608,7 @@ } } }, - "my" : { + "vi" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -141040,7 +143622,7 @@ "other" : { "stringUnit" : { "state" : "translated", - "value" : "မက်ဆေ့ချ်များ ဖျက်ထားသည်" + "value" : "Các tin nhắn đã bị xoá" } } } @@ -141048,7 +143630,7 @@ } } }, - "nb" : { + "xh" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -141062,13 +143644,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Melding slettet" + "value" : "Umyalezo ucinyiwe" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Meldinger slettet" + "value" : "Imiyalezo icinyiwe" } } } @@ -141076,7 +143658,7 @@ } } }, - "nb-NO" : { + "zh-CN" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -141087,16 +143669,10 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Beskjed slettet" - } - }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Beskjeder slettet" + "value" : "消息已删除" } } } @@ -141104,7 +143680,7 @@ } } }, - "nl" : { + "zh-TW" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -141115,967 +143691,83 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bericht verwijderd" - } - }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Berichten verwijderd" + "value" : "訊息已刪除" } } } } } } + } + } + }, + "deleteMessageDeletedGlobally" : { + "extractionState" : "manual", + "localizations" : { + "af" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hierdie boodskap is verwyder" + } }, - "nn-NO" : { + "ar" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Beskjed slettet" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Beskjeder slettet" - } - } - } - } - } + "value" : "تم حذف هذه الرسالة" } }, - "ny" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Uthenga wachotsedwa" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mauthenga omwe achotsedwa" - } - } - } - } - } + "value" : "Bu mesaj silindi" } }, - "pa-IN" : { + "bal" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "ਸੁਨੇਹਾ ਹਟਾਇਆ ਗਿਆ" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "ਸੁਨੇਹੇ ਹਟਾਏ ਗਏ" - } - } - } - } - } + "value" : "یہ پیغام حذف کر دی گئی" } }, - "pl" : { + "be" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "few" : { - "stringUnit" : { - "state" : "translated", - "value" : "Usunięto wiadomości" - } - }, - "many" : { - "stringUnit" : { - "state" : "translated", - "value" : "Usunięto wiadomości" - } - }, - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wiadomość usunięta" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Usunięto wiadomości" - } - } - } - } - } + "value" : "Гэта паведамленне было выдалена" } }, - "ps" : { + "bg" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "پیغام حذف شوی" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "پیغامونه حذف شوي" - } - } - } - } - } + "value" : "Това съобщение беше изтрито" } }, - "pt-BR" : { + "bn" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mensagem excluída" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mensagens excluídas" - } - } - } - } - } + "value" : "এই মেসেজটি মুছে ফেলা হয়েছে" } }, - "pt-PT" : { + "ca" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mensagem eliminada" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mensagens eliminadas" - } - } - } - } - } + "value" : "Aquest missatge s'ha suprimit" } }, - "ro" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "few" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mesaje șterse" - } - }, - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mesaj șters" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mesaje șterse" - } - } - } - } - } + "value" : "Tato zpráva byla odstraněna." } }, - "ru" : { + "cy" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "few" : { - "stringUnit" : { - "state" : "translated", - "value" : "Сообщения удалены" - } - }, - "many" : { - "stringUnit" : { - "state" : "translated", - "value" : "Сообщения удалены" - } - }, - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Сообщение удалено" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Сообщения удалены" - } - } - } - } - } + "value" : "Dilewyd y neges hon" } }, - "sh" : { - "stringUnit" : { - "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "few" : { - "stringUnit" : { - "state" : "translated", - "value" : "Poruke obrisane" - } - }, - "many" : { - "stringUnit" : { - "state" : "translated", - "value" : "Poruke obrisane" - } - }, - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Poruka obrisana" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Poruke obrisane" - } - } - } - } - } - } - }, - "si-LK" : { - "stringUnit" : { - "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "පණිවිඩය මැකිණි" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "පණිවිඩ මැකිණි" - } - } - } - } - } - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "few" : { - "stringUnit" : { - "state" : "translated", - "value" : "Správy vymazané" - } - }, - "many" : { - "stringUnit" : { - "state" : "translated", - "value" : "Správy vymazané" - } - }, - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Správa vymazaná" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Správy vymazané" - } - } - } - } - } - } - }, - "sl" : { - "stringUnit" : { - "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "few" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sporočila so izbrisana" - } - }, - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sporočilo izbrisano" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sporočila so izbrisana" - } - }, - "two" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sporočili sta izbrisani" - } - } - } - } - } - } - }, - "sq" : { - "stringUnit" : { - "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mesazhi u fshi" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Messages deleted" - } - } - } - } - } - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "few" : { - "stringUnit" : { - "state" : "translated", - "value" : "Поруке су обрисане" - } - }, - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Порука је обрисана" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Поруке су обрисане" - } - } - } - } - } - } - }, - "sr-Latn" : { - "stringUnit" : { - "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "few" : { - "stringUnit" : { - "state" : "translated", - "value" : "Poruke obrisane" - } - }, - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Poruka obrisana" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Poruke obrisane" - } - } - } - } - } - } - }, - "sv-SE" : { - "stringUnit" : { - "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Meddelande raderat" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Meddelanden raderade" - } - } - } - } - } - } - }, - "sw" : { - "stringUnit" : { - "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ujumbe umefutwa" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jumbe zimefutwa" - } - } - } - } - } - } - }, - "ta" : { - "stringUnit" : { - "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "செய்தி நீக்கப்பட்டது" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "செய்திகள் அகற்றப்பட்டது" - } - } - } - } - } - } - }, - "te" : { - "stringUnit" : { - "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "సందేశం తొలగించబడింది" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "సందేశాలు తొలగించబడ్డాయి" - } - } - } - } - } - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "ข้อความถูกลบ" - } - } - } - } - } - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "İleti silindi" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "İletiler silindi" - } - } - } - } - } - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "few" : { - "stringUnit" : { - "state" : "translated", - "value" : "Повідомлення видалені" - } - }, - "many" : { - "stringUnit" : { - "state" : "translated", - "value" : "Повідомлення видалені" - } - }, - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Повідомлення видалено" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Повідомлення видалені" - } - } - } - } - } - } - }, - "ur-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "پیغام حذف ہو گیا" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "پیغامات حذف ہو گئے" - } - } - } - } - } - } - }, - "uz" : { - "stringUnit" : { - "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Xabar o'chirildi" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Xabarlar oʻchirildi" - } - } - } - } - } - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Các tin nhắn đã bị xoá" - } - } - } - } - } - } - }, - "xh" : { - "stringUnit" : { - "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Umyalezo ucinyiwe" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Imiyalezo icinyiwe" - } - } - } - } - } - } - }, - "zh-CN" : { - "stringUnit" : { - "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "消息已删除" - } - } - } - } - } - } - }, - "zh-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "訊息已刪除" - } - } - } - } - } - } - } - } - }, - "deleteMessageDeletedGlobally" : { - "extractionState" : "manual", - "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hierdie boodskap is verwyder" - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "تم حذف هذه الرسالة" - } - }, - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bu mesaj silindi" - } - }, - "bal" : { - "stringUnit" : { - "state" : "translated", - "value" : "یہ پیغام حذف کر دی گئی" - } - }, - "be" : { - "stringUnit" : { - "state" : "translated", - "value" : "Гэта паведамленне было выдалена" - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Това съобщение беше изтрито" - } - }, - "bn" : { - "stringUnit" : { - "state" : "translated", - "value" : "এই মেসেজটি মুছে ফেলা হয়েছে" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aquest missatge s'ha suprimit" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tato zpráva byla odstraněna." - } - }, - "cy" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dilewyd y neges hon" - } - }, - "da" : { + "da" : { "stringUnit" : { "state" : "translated", "value" : "Denne besked er slettet" @@ -143177,6 +144869,34 @@ } } }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Είστε σίγουρος ότι θέλετε να διαγράψετε αυτό το μήνυμα μόνο από αυτή τη συσκευή;" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Είστε σίγουρος ότι θέλετε να διαγράψετε αυτά τα μηνύματα μόνο από αυτή τη συσκευή;" + } + } + } + } + } + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -143289,6 +145009,34 @@ } } }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kas olete kindel, et soovite selle sõnumi ainult sellest seadmest kustutada?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kas olete kindel, et soovite need sõnumid ainult sellest seadmetest kustutada?" + } + } + } + } + } + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -143495,6 +145243,34 @@ } } }, + "ku" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "ئایا دڵنیای کە دەتەوێت ئەم پەیامە تەنها لەم ئامێرە بسڕیتەوە؟" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "ئایا دڵنیای کە دەتەوێت ئەم پەیامە تەنها لەم ئامێرە بسڕیتەوە؟" + } + } + } + } + } + } + }, "ku-TR" : { "stringUnit" : { "state" : "translated", @@ -143523,6 +145299,34 @@ } } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Er du sikker på du vil slette denne beskjeden fra denne enheten bare?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Er du sikker du vil slette dem her beskjedene fra denne enheten bare?" + } + } + } + } + } + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -143591,6 +145395,34 @@ } } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tem certeza de que deseja apagar esta mensagem apenas deste dispositivo?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tem a certeza que pretende eliminar estas mensagens apenas deste dispositivo?" + } + } + } + } + } + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -143804,6 +145636,28 @@ } } } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "您確定只從此設備上刪除這些訊息嗎?" + } + } + } + } + } + } } } }, @@ -143825,7 +145679,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Bu mesajı hamı üçün silmək istədiyinizə əminsiniz?" + "value" : "Bu mesajı hər kəs üçün silmək istədiyinizə əminsiniz?" } }, "bal" : { @@ -148501,6 +150355,62 @@ } } }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seda sõnumit ei saa kõikidest teie seadmetest kustutada" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mõnda valitud sõnumit ei saa kõikidest teie seadmetest kustutada" + } + } + } + } + } + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "پرشین" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "برخی از پیام‌هایی که انتخاب کرده‌اید، نمی‌توانند از همهٔ دستگاه‌های شما پاک شوند" + } + } + } + } + } + } + }, "fi" : { "stringUnit" : { "state" : "translated", @@ -148735,6 +150645,34 @@ } } }, + "ku" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "ئەم پەیامە ناتوانرێت لە هەموو ئامێرەکانت بسڕدرێتەوە" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "هەندێک لەو نامانەی کە هەڵتبژاردووە ناتوانرێت لە هەموو ئامێرەکانت بسڕدرێنەوە" + } + } + } + } + } + } + }, "ku-TR" : { "stringUnit" : { "state" : "translated", @@ -148813,6 +150751,34 @@ } } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Denne meldingen kan ikke bli slettet fra alle dine enheter" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Noen av meldingene du valgte kan ikke bli slettet fra alle dine enheter" + } + } + } + } + } + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -150087,6 +152053,34 @@ } } }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seda sõnumit ei saa kõigi jaoks kustutada" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mõned sõnumid mille oled valinud ei saa kõigi jaoks kustutada" + } + } + } + } + } + } + }, "fa" : { "stringUnit" : { "state" : "translated", @@ -150349,6 +152343,34 @@ } } }, + "ku" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "ئەم پەیامە بۆ هەموو کەسێک ناسڕدرێتەوە" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "هەندێک لەو نامانەی کە هەڵتبژاردووە ناتوانرێت بۆ هەموو کەسێک بسڕدرێتەوە" + } + } + } + } + } + } + }, "ku-TR" : { "stringUnit" : { "state" : "translated", @@ -150441,13 +152463,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Diese Nachricht kann nicht gelöscht werden" + "value" : "Denne meldingen kan ikke bli slettet for alle" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Ein paar Nachrichten könnten nicht gelöscht werden" + "value" : "Noen av meldingene du valgte kan ikke bli slettet for alle" } } } @@ -152333,7 +154355,7 @@ "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "开始听写..." + "value" : "开始语音输入..." } }, "zh-TW" : { @@ -168660,6 +170682,65 @@ } } }, + "display" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nümayiş" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zobrazení" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Display" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Display" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Affichage" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Weergave" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Дисплей" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skärm" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Зовнішній вигляд" + } + } + } + }, "displayNameDescription" : { "extractionState" : "manual", "localizations" : { @@ -172552,7 +174633,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Your Display Name is visible to users, groups and communities you interact with." + "value" : "Your Display Name is visible to users, groups, and communities you interact with." } }, "eo" : { @@ -173201,6 +175282,12 @@ "donate" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "İanə ver" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -173213,6 +175300,12 @@ "value" : "Darovat" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Spenden" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -173225,29 +175318,113 @@ "value" : "Donaci" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Donar" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Donar" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Faire un don" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "दान करें" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Adományozás" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dona" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "寄付" + } + }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "후원하기" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Doneer" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wspomóż" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fazer uma doação" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Donează" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Донат" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Donera" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bağış yap" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Підтримати" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "捐赠" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "捐款" + } } } }, @@ -186821,46 +188998,683 @@ } } }, + "enableNotifications" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yeni mesaj aldığınız zaman bildirişlər göstərilsin." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zobrazit upozornění při přijetí nových zpráv." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Benachrichtigungen anzeigen, wenn du neue Nachrichten erhältst." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Show notifications when you receive new messages." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Afficher les notifications lorsque vous recevez de nouveaux messages." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toon meldingen wanneer je nieuwe berichten ontvangt." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wysyłaj powiadomienia o nowych wiadomościach." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Показывать уведомления при получении новых сообщений." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Visa aviseringar när du får nya meddelanden." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Показувати сповіщення, коли ви отримуєте нові повідомлення." + } + } + } + }, "enjoyingSession" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name}-dan zövq alırsınız?" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Líbí se vám {app_name}?" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gefällt dir {app_name}?" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Enjoying {app_name}?" } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Te está gustando {app_name}?" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Te está gustando {app_name}?" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vous aimez {app_name} ?" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "क्या आप {app_name} का आनंद ले रहे हैं?" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ti piace {app_name}?" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name}を楽しんでいますか?" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geniet je van {app_name}?" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Podoba Ci się {app_name}?" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Está a gostar do {app_name}?" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Îți place {app_name}?" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Нравится {app_name}?" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gillar du {app_name}?" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name}'i beğendiniz mi?" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подобається {app_name}?" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "喜欢使用 {app_name} 吗?" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "喜歡使用 {app_name} 嗎?" + } } } }, "enjoyingSessionButtonNegative" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Təkmilləşməlidir {emoji}" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Necessita feina {emoji}" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Potřebuje vylepšit {emoji}" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verbesserungswürdig {emoji}" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Needs Work {emoji}" } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Necesita mejoras {emoji}" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Necesita mejoras {emoji}" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Des améliorations seraient utiles {emoji}" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "सुधार की आवश्यकता है {emoji}" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Da migliorare {emoji}" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "改善が必要です {emoji}" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Moet beter {emoji}" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wymaga poprawek {emoji}" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Precisa de melhorias {emoji}" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mai e de lucru {emoji}" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Требуется доработка {emoji}" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Behöver förbättras {emoji}" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geliştirilmesi Gerekiyor {emoji}" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Потребує доопрацювання {emoji}" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "需要改进 {emoji}" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "有待改進 {emoji}" + } } } }, "enjoyingSessionButtonPositive" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Əladır {emoji}" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "\"És fantàstic {emoji}" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skvělé {emoji}" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Großartig {emoji}" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "It's Great {emoji}" } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Está genial {emoji}" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Está genial {emoji}" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "C’est génial {emoji}" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "बहुत बढ़िया {emoji}" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "È fantastica {emoji}" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "すばらしいです {emoji}" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geweldig {emoji}" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Jest świetnie {emoji}" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Está ótimo {emoji}" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Este grozav {emoji}" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Отлично {emoji}" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Det är fantastiskt {emoji}" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Harika {emoji}" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Крутяк {emoji}" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "很棒 {emoji}" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "很棒 {emoji}" + } } } }, "enjoyingSessionDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} tətbiqini bir müddətdir istifadə edirsiniz, necə gedir? Fikirlərinizi eşitmək bizim üçün çox dəyərli olardı." + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Has estat utilitzant {app_name} durant una estona, com va? Ens agradaria escoltar els teus pensaments." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Používáte {app_name}
a rádi bychom znali váš názor." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Du nutzt {app_name} jetzt schon eine Weile – wie läuft’s? Wir würden uns sehr über dein Feedback freuen." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "You've been using {app_name} for a little while, how’s it going? We’d really appreciate hearing your thoughts." + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Has estado usando {app_name} por un tiempo, ¿cómo va todo? Agradeceríamos mucho saber tu opinión." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Has estado usando {app_name} por un tiempo, ¿cómo va todo? Agradeceríamos mucho saber tu opinión." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vous utilisez {app_name} depuis un petit moment, comment ça se passe ? Nous aimerions beaucoup connaître votre avis." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "आप कुछ समय से {app_name} का उपयोग कर रहे हैं, सब कैसा चल रहा है? हम आपके विचार जानकर बहुत आभारी होंगे।" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Stai usando {app_name} da un po', come ti trovi? Ci farebbe piacere conoscere la tua opinione." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name}をご利用いただいてしばらく経ちましたね。調子はいかがですか?ご意見をお聞かせいただけると嬉しいです。" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je gebruikt {app_name} al een tijdje, hoe gaat het? We horen graag je mening." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Korzystasz z {app_name} już od jakiegoś czasu, jak Ci się podoba? Będziemy bardzo wdzięczni za Twoją opinię." + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Já usa o {app_name} há algum tempo, como tem corrido? Gostaríamos muito de ouvir a sua opinião." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Folosești {app_name} de ceva timp, cum ți se pare? Ne-ar face mare plăcere să aflăm părerea ta." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вы уже некоторое время пользуетесь {app_name}, как оно? Нам действительно интересно узнать ваше мнение." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Du har använt {app_name} ett tag, hur går det? Vi skulle uppskatta om du delar dina tankar." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ви вже деякий час користуєтесь {app_name}, які у вас враження? Нам би дуже хотілося дізнатися вашу думку." + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "您已使用 {app_name} 一段时间了,感觉如何?非常希望能听到您的反馈。" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "您已使用 {app_name} 一段時間,使用體驗如何?我們非常希望能聽到您的想法。" + } + } + } + }, + "enter" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Daxil ol" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vstoupit" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bestätigen" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enter" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Entrer" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verder" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Войти" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enter" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Увійти" + } + } + } + }, + "enterPasswordDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enter the password you set for {app_name}" + } + } + } + }, + "enterPasswordTooltip" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enter the password you use to unlock Session \r\non startup, not your Recovery Password" + } + } + } + }, + "errorCheckingProStatus" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} statusunu yoxlama xətası." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chyba kontroly stavu {pro}." + } + }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "You've been using {app_name} for a little
while, how’s it going? We’d really
appreciate hearing your thoughts." + "value" : "Error checking {pro} status." } } } @@ -187847,7 +190661,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Databaza xətası" + "value" : "Veri bazası xətası" } }, "bal" : { @@ -188317,6 +191131,12 @@ "errorGeneric" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nəsə səhv getdi. Lütfən daha sonra yenidən sınayın." + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -188329,6 +191149,12 @@ "value" : "Něco se pokazilo. Zkuste to prosím později." } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Etwas ist schiefgelaufen. Bitte versuche es später erneut." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -188341,18 +191167,48 @@ "value" : "Io misfunkciis. Bonvolu reprovi poste." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Algo salió mal. Por favor, inténtalo de nuevo más tarde." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Algo salió mal. Por favor, inténtalo de nuevo más tarde." + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Une erreur s'est produite. Veuillez réessayer plus tard." } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "कुछ गलत हो गया। कृपया बाद में पुनः प्रयास करें।" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Valami hiba történt. Próbálja meg később újra." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Si è verificato un errore. Riprova più tardi." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "問題が発生しました。後でもう一度お試しください。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -188371,11 +191227,64 @@ "value" : "Coś poszło nie tak. Spróbuj ponownie później." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ocorreu um erro. Por favor, tente novamente mais tarde." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ceva nu a mers bine. Te rugăm să încerci din nou mai târziu." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Что-то пошло не так. Попробуйте ещё раз позже." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Något gick fel. Försök igen senare." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bir hata oluştu. Lütfen daha sonra tekrar deneyin." + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Щось пішло не так. Будь ласка, спробуйте пізніше." } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "出现问题。请稍后再试。" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "發生錯誤。請稍後再試。" + } + } + } + }, + "errorLoadingProPlan" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Error loading {pro} plan" + } } } }, @@ -188915,6 +191824,18 @@ "value" : "Elŝutado fiaskis" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Descarga fallida" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Descarga fallida" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -188939,6 +191860,18 @@ "value" : "Gagal mengunduh" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Download non riuscito" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ダウンロードに失敗しました" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -188957,6 +191890,18 @@ "value" : "Nie udało się pobrać" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Falha ao transferir" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Descărcarea a eșuat" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -188969,6 +191914,12 @@ "value" : "Nedladdningen misslyckades" } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İndirme başarısız" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -188986,6 +191937,12 @@ "state" : "translated", "value" : "下载失败" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "下載失敗" + } } } }, @@ -189468,6 +192425,136 @@ } } }, + "feedback" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Əks-əlaqə" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zpětná vazba" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Feedback" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Feedback" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Donner votre avis" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Feedback" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Feedback" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Отзыв" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Feedback" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Відгук" + } + } + } + }, + "feedbackDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Qısa anketi dolduraraq {app_name} ilə təcrübənizi paylaşın." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Podělte se o své zkušenosti s {app_name} vyplněním krátkého dotazníku." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Teile deine Erfahrungen mit {app_name}, indem du eine kurze Umfrage ausfüllst." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Share your experience with {app_name} by completing a short survey." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Partagez votre expérience avec {app_name} en répondant à un court sondage." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deel je ervaring met {app_name} door een korte enquête in te vullen." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Podziel się wrażeniami o {app_name} wypełniając krótką ankietę." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Поделитесь своим опытом использования {app_name}, пройдя короткий опрос." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dela med dig av din upplevelse med {app_name} genom att fylla i en kort undersökning." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Поділіться вашим досвідом використання {app_name} пройшовши коротке опитування." + } + } + } + }, "file" : { "extractionState" : "manual", "localizations" : { @@ -190429,478 +193516,70 @@ "followSystemSettings" : { "extractionState" : "manual", "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Volg stelselinstellings" - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "طابق إعدادات النظام" - } - }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "Sistem ayarlarını izlə" - } - }, - "bal" : { - "stringUnit" : { - "state" : "translated", - "value" : "آگاھی سسیتم پرات وٹ" - } - }, - "be" : { - "stringUnit" : { - "state" : "translated", - "value" : "Выкарыстоўвайце налады сістэмны" - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Следвай системните настройки" - } - }, - "bn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Follow system settings" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Seguir configuracions del sistema" + "value" : "Sistem ayarlarını izlə." } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Použít nastavení systému" - } - }, - "cy" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dilyn gosodiadau'r system" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Følg systemindstillinger" + "value" : "Použít nastavení systému." } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Systemeinstellungen folgen" - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Αντιστοίχιση ρυθμίσεων συστήματος" + "value" : "Systemeinstellungen übernehmen." } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Follow system settings" - } - }, - "eo" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sekvi sistemajn agordojn" - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Seguir configuración del sistema" - } - }, - "es-ES" : { - "stringUnit" : { - "state" : "translated", - "value" : "Coincidir ajustes del sistema" - } - }, - "et" : { - "stringUnit" : { - "state" : "translated", - "value" : "Järgi süsteemi sätteid" - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jarraitu sistema ezarpenak" - } - }, - "fa" : { - "stringUnit" : { - "state" : "translated", - "value" : "مطابق با تنظیمات سیستم" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Seuraa järjestelmän asetusta" - } - }, - "fil" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sundin ang mga setting ng system" + "value" : "Follow system settings." } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Faire correspondre aux paramètres systèmes" - } - }, - "gl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Seguir a configuración do sistema" - } - }, - "ha" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bi saitunan tsarin" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "עקוב אחרי הגדרות המערכת" - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "सिस्टम सेटिंग्स का पालन करें" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Slijedite sistemske postavke" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rendszerbeállítások követése" - } - }, - "hy-AM" : { - "stringUnit" : { - "state" : "translated", - "value" : "Հետևել համակարգի կարգավորումներին" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sesuaikan dengan pengaturan sistem" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Utilizza le impostazioni di sistema" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "システム設定に合わせる" - } - }, - "ka" : { - "stringUnit" : { - "state" : "translated", - "value" : "დაემორჩილეთ სისტემის კონფიგურაციას" - } - }, - "km" : { - "stringUnit" : { - "state" : "translated", - "value" : "ផ្គូផ្គងការកំណត់ប្រព័ន្ធ" - } - }, - "kn" : { - "stringUnit" : { - "state" : "translated", - "value" : "ಅಂಕಣೋತ್ಸವವು ಪಾಲಿಸಲಾಗುವುದು" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "시스템 설정 따르기" - } - }, - "ku" : { - "stringUnit" : { - "state" : "translated", - "value" : "شیاوی سیستەمەکان" - } - }, - "ku-TR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mîhengên sîstemê taqîb bike" - } - }, - "lg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Goberera ssitula z'empuliziganya" - } - }, - "lt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sekti sistemos nustatymus" - } - }, - "lv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sekot sistēmas iestatījumiem" - } - }, - "mk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Следи системски подесувања" - } - }, - "mn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Системийн тохиргоонуудыг дагах" - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ikut tetapan sistem" - } - }, - "my" : { - "stringUnit" : { - "state" : "translated", - "value" : "စက်ရုပ်ဆီstem ဆက်တင်များကိုလိုက်နာပါ" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Følg systeminnstillinger" - } - }, - "nb-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Følg systeminnstillinger" - } - }, - "ne-NP" : { - "stringUnit" : { - "state" : "translated", - "value" : "सहयोग पृष्ठमा जानुहोस्" + "value" : "Faire correspondre aux paramètres systèmes." } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Systeeminstellingen volgen" - } - }, - "nn-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Følg systeminnstillinger" - } - }, - "ny" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tsatira makonda a dongosolo" - } - }, - "pa-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "ਸਿਸਟਮ ਸੈਟਿੰਗਾਂ ਦੀ ਪਾਲਣਾ ਕਰੋ" + "value" : "Systeeminstellingen volgen." } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Dopasuj do ustawień systemu" - } - }, - "ps" : { - "stringUnit" : { - "state" : "translated", - "value" : "د سیسټم تنظیمات تعقیب کړئ" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Acompanhar as configurações do sistema" - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Alinhar com definições do sistema" + "value" : "Dopasuj do ustawień systemu." } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Urmează setările sistemului" + "value" : "Urmărește setările sistemului." } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Использовать настройки системы" - } - }, - "sh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Prati sistemske postavke" - } - }, - "si-LK" : { - "stringUnit" : { - "state" : "translated", - "value" : "සැකසුමට ගළපන්න" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rovnaké ako nastavenia systému" - } - }, - "sl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sledi sistemskim nastavitvam" - } - }, - "sq" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ndiq cilësimet e sistemit" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Пратити системска подешавања" - } - }, - "sr-Latn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Prati sistemska podešavanja" + "value" : "Использовать настройки системы." } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Följ systeminställningar" - } - }, - "sw" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fuata mipangilio ya mfumo" - } - }, - "ta" : { - "stringUnit" : { - "state" : "translated", - "value" : "கணினி அமைப்புகளை பின்பற்றுக" - } - }, - "te" : { - "stringUnit" : { - "state" : "translated", - "value" : "సిస్టమ్ సెట్టింగ్‌లను అనుసరించండి" - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "ตามการตั้งค่าระบบ" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sistem ayarlarını takip et" + "value" : "Följ systeminställningen." } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Використовувати системні налаштування" - } - }, - "ur-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "سسٹم کی ترتیبات کو فالو کریں" - } - }, - "uz" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tizim sozlamalariga ergashing" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Theo cài đặt hệ thống" - } - }, - "xh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Landela usetiwe lwenkqubo" - } - }, - "zh-CN" : { - "stringUnit" : { - "state" : "translated", - "value" : "匹配系统设置" - } - }, - "zh-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "遵循系統設定" + "value" : "Використовувати системні налаштування." } } } @@ -190932,6 +193611,12 @@ "value" : "For altid" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Für immer" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -190944,12 +193629,30 @@ "value" : "Por ĉiam" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Para siempre" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Para siempre" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Définitivement" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "हमेशा के लिए" + } + }, "hu" : { "stringUnit" : { "state" : "translated", @@ -190962,6 +193665,18 @@ "value" : "Selamanya" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Per sempre" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "常に" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -190980,6 +193695,36 @@ "value" : "Zawsze" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Para sempre" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pentru totdeauna" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Навсегда" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "För alltid" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sonsuza dek" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -190997,6 +193742,12 @@ "state" : "translated", "value" : "永久" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "永遠" + } } } }, @@ -193404,21 +196155,249 @@ "giveFeedback" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Əks-əlaqə vermək istəyirsiniz?" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dóna comentaris?" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Poskytnout zpětnou vazbu?" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Feedback geben?" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Give Feedback?" } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Enviar comentarios?" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Enviar comentarios?" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Donner votre avis ?" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "प्रतिक्रिया देना चाहते हैं?" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lascia un feedback?" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "フィードバックを送りますか?" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Feedback geven?" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Przekazać opinię?" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dar opinião?" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Oferi feedback?" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Оставите отзыв?" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ge feedback?" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Залишити відгук?" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "愿意提供反馈吗?" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "提供回饋?" + } } } }, "giveFeedbackDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} təcrübənizin yaxşı olmadığını eşitmək bizi məyus etdi. Fikirlərinizi qısa anket üzərindən bizimlə paylaşsanız minnətdar olarıq" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Disculpa escoltar la teva experiència {app_name} no ha estat ideal. Ens agrairem si podries prendre un moment per compartir els teus pensaments en una breu enquesta" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je nám líto, že vaše zkušenost se {app_name} nebyla ideální. Ocenili bychom, kdybyste věnovali chvíli vyplnění krátkého dotazníku" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Es tut uns leid zu hören, dass deine Erfahrung mit {app_name} nicht ideal war. Wir würden uns freuen, wenn du dir einen Moment Zeit nimmst und deine Gedanken in einer kurzen Umfrage mit uns teilst." + } + }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Sorry to hear your {app_name} experience
hasn’t been ideal. We'd be grateful if you
could take a moment to share your
thoughts in a brief survey" + "value" : "Sorry to hear your {app_name} experience hasn’t been ideal. We'd be grateful if you could take a moment to share your thoughts in a brief survey" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lamentamos saber que tu experiencia con {app_name} no ha sido ideal. Estaríamos agradecidos si pudieras tomarte un momento para compartir tus opiniones en una breve encuesta." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lamentamos saber que tu experiencia con {app_name} no ha sido ideal. Estaríamos agradecidos si pudieras tomarte un momento para compartir tus opiniones en una breve encuesta." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nous sommes désolés d’apprendre que votre expérience avec {app_name} n’a pas été idéale. Nous vous serions reconnaissants si vous pouviez prendre un instant pour répondre à une brève enquête." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "हमें खेद है कि आपका {app_name} अनुभव आदर्श नहीं रहा। यदि आप एक क्षण निकाल सकें और एक छोटे सर्वेक्षण में अपने विचार साझा करें, तो हम आभारी होंगे" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ci dispiace sapere che la tua esperienza con {app_name} non è stata ideale. Ti saremmo grati se prendessi un momento per condividere i tuoi pensieri in un breve sondaggio" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} のご利用が理想的でなかったとのことで残念です。お時間があれば、簡単なアンケートでご意見をお聞かせください。" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Jammer om te horen dat je ervaring met {app_name} niet ideaal was. We zouden het waarderen als je een momentje neemt om je mening te delen in een korte enquête" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Przykro nam, że Twoje doświadczenie z {app_name} nie było idealne. Bylibyśmy wdzięczni, gdybyś poświęcił chwilę na podzielenie się swoją opinią w krótkiej ankiecie" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lamentamos saber que a sua experiência com o {app_name} não foi ideal. Agradecíamos se pudesse partilhar a sua opinião através de um breve questionário" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ne pare rău că experiența ta cu {app_name} nu a fost ideală. Am aprecia dacă ne-ai putea împărtăși părerea ta într-un scurt sondaj" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сожалеем, что ваш опыт использования {app_name} не был идеальным. Будем признательны, если вы уделите немного времени и поделитесь своими мыслями в кратком опросе" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tråkigt att höra att din upplevelse med {app_name} inte varit optimal. Vi skulle uppskatta om du kunde ta ett ögonblick för att dela dina tankar i en kort undersökning." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Шкодуємо, що ваш досвід користування {app_name} був неідеальним. Ми були б вдячні, якби ви змогли поділитися своїми думками у короткому опитуванні" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "很遗憾听到您在 {app_name} 上的使用体验不佳。希望您抽空填写简短问卷,分享您的看法。" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "很遺憾聽到您在使用 {app_name} 時的體驗不盡理想。如果您能花點時間在簡短問卷中分享您的想法,我們將不勝感激" } } } @@ -195378,6 +198357,18 @@ "value" : "Er du sikker på, at du vil slette {group_name}?

Dette vil fjerne alle medlemmer og slette alt gruppe-indhold." } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bist du sicher, dass du {group_name} verlassen möchtest?

Dadurch werden alle Mitglieder entfernt und alle Gruppendaten gelöscht." + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Είστε βέβαιοι ότι θέλετε να διαγράψετε το {group_name}?

Αυτό θα αφαιρέσει όλα τα μέλη και θα διαγράψει όλο το περιεχόμενο της ομάδας." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -195414,12 +198405,24 @@ "value" : "क्या आप वाकई {group_name} को हटाना चाहते हैं?

इससे सभी सदस्य हट जाएंगे और समूह की सारी सामग्री भी मिट जाएगी।" } }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Biztosan ki akar lépni a(z) {group_name} csoportból?

Ez az összes tag eltávolításával és a csoport teljes tartalmának törlésével jár." + } + }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Apakah Anda yakin ingin menghapus {group_name}?

Ini akan mengeluarkan semua anggota dan menghapus semua konten grup." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Confermi di voler eliminare {group_name}?

Questo eliminerà tutti i membri e cancellerà tutto il contenuto del gruppo." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -195438,6 +198441,12 @@ "value" : "정말로 {group_name}을 제거하시겠습니까?

모든 멤버가 제거되고 모든 그룹 컨텐츠가 삭제됩니다." } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Er du sikker på at du vil slette {group_name}?

Dette vil fjerne alle medlemmer og slette alt av innholdet i gruppen." + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -195450,12 +198459,30 @@ "value" : "Czy na pewno chcesz usunąć {group_name}?

Spowoduje to usunięcie wszystkich członków i całej zawartości grupy." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tem a certeza de que pretende eliminar {group_name}?

Isto irá remover todos os membros e eliminar todo o conteúdo do grupo." + } + }, "ro" : { "stringUnit" : { "state" : "translated", "value" : "Sunteți sigur că vreți să ștergeți {group_name}?

Aceasta va elimina toți membrii și va șterge tot conținutul grupului." } }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вы уверены, что хотите удалить {group_name}?

Это приведет к удалению всех участников и всего содержимого группы." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Är du säker på att du vill radera {group_name}?

Detta kommer att ta bort alla medlemmar och radera allt gruppinnehåll." + } + }, "tr" : { "stringUnit" : { "state" : "translated", @@ -195473,6 +198500,12 @@ "state" : "translated", "value" : "您确定要删除{group_name}吗?

这将移除所有成员并删除所有群组内容。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "您確定要刪除 {group_name} 嗎?

這將移除所有成員並刪除所有群組內容。" + } } } }, @@ -195593,6 +198626,18 @@ "value" : "Czy jesteś pewny, że chcesz usunąć {group_name}?" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tem a certeza de que pretende eliminar {group_name}?" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ești sigur/ă că vrei să ștergi {group_name}?" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -195628,6 +198673,12 @@ "state" : "translated", "value" : "你确定要删除群组 {group_name}吗?" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "您確定要刪除 {group_name} 嗎?" + } } } }, @@ -195748,6 +198799,18 @@ "value" : "{group_name} została usunięta przez administratora grupy. Nie będzie można wysyłać więcej wiadomości." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "{group_name} foi eliminado por um administrador do grupo. Não poderá enviar mais mensagens." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "{group_name} a fost șters de către un administrator de grup. Nu veți mai putea trimite mesaje." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -195783,6 +198846,12 @@ "state" : "translated", "value" : "{group_name} 已被群组管理员删除。您将无法再发送任何信息。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "{group_name} 已被群組管理員刪除。您將無法再傳送任何訊息。" + } } } }, @@ -201675,6 +204744,12 @@ "value" : "Invito non inviato" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "招待が送信されていません" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -201693,6 +204768,18 @@ "value" : "Zaproszenie niewysłane" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Convite não enviado" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Invitația nu a fost trimisă" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -201728,6 +204815,12 @@ "state" : "translated", "value" : "邀请未发送" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "邀請未傳送" + } } } }, @@ -202368,6 +205461,34 @@ } } }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Στέλνεται πρόσκληση" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Στέλνονται προσκλήσεις" + } + } + } + } + } + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -202480,6 +205601,34 @@ } } }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Saadan kutse" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Saadan kutsed" + } + } + } + } + } + } + }, "fa" : { "stringUnit" : { "state" : "translated", @@ -202670,6 +205819,28 @@ } } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "招待状を送信中" + } + } + } + } + } + } + }, "ka" : { "stringUnit" : { "state" : "translated", @@ -202720,6 +205891,34 @@ } } }, + "ku" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "ناردنی بانگ" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "ناردنی بانگه کان" + } + } + } + } + } + } + }, "ku-TR" : { "stringUnit" : { "state" : "translated", @@ -202748,6 +205947,34 @@ } } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sender invitasjon" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sender invitasjoner" + } + } + } + } + } + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -202844,6 +206071,34 @@ } } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enviando convite" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enviando convites" + } + } + } + } + } + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -203057,6 +206312,28 @@ } } } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在傳送邀請" + } + } + } + } + } + } } } }, @@ -203632,12 +206909,24 @@ "value" : "Stato invito sconosciuto" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "招待状のステータスが不明です" + } + }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "초대 상태를 알 수 없습니다" } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Status på invitasjonen er ukjent" + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -203650,6 +206939,18 @@ "value" : "Status zaproszenia nieznany" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Estado do convite desconhecido" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Statusul invitației necunoscut" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -203685,6 +206986,12 @@ "state" : "translated", "value" : "邀请状态未知" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "邀請狀態未知" + } } } }, @@ -208693,7 +212000,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "{name}{count} başqaları qrupu tərk etdi." + "value" : "{name}başqa {count} nəfər qrupu tərk etdi." } }, "bal" : { @@ -213001,6 +216308,18 @@ "value" : "Vi kaj {other_name} estis invititaj por aliĝi al la grupo. Historio de la babilejo estis diskonigita." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : " y {other_name} fueron invitados a unirse al grupo. Se ha compartido el historial del chat." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : " y {other_name} fueron invitados a unirse al grupo. Se ha compartido el historial del chat." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -213037,6 +216356,12 @@ "value" : "あなた{other_name} はグループに招待されました。チャット履歴が共有されました。" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "당신{other_name}이 그룹에 초대 되었습니다. 대화 내용이 공유 되었습니다." + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -213049,6 +216374,12 @@ "value" : "Ty i {other_name} zostaliście zaproszeni do grupy. Historia czatu została udostępniona." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Você e {other_name} foram convidados a juntar-se ao grupo. O histórico da conversa foi partilhado." + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -213090,6 +216421,12 @@ "state" : "translated", "value" : "{other_name}被邀请加入了群组。 聊天记录已共享。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "{other_name} 加入了群組。聊天記錄已分享。" + } } } }, @@ -218143,6 +221480,12 @@ "value" : "Denne gruppe er ikke blevet opdateret i over 30 dage. Du kan opleve problemer med at sende beskeder eller se gruppeoplysninger." } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Diese Gruppe wurde seit über 30 Tagen nicht aktualisiert. Beim Senden von Nachrichten oder beim Anzeigen der Gruppeninformationen können Probleme auftreten." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -218185,6 +221528,18 @@ "value" : "A csoportot több mint 30 napja nem frissítették. Előfordulhat, hogy problémák merülnek fel az üzenetek küldésével vagy a csoportinformációk megtekintésével kapcsolatban." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Questo gruppo non è stato aggiornato da oltre 30 giorni. Potresti riscontrare problemi nell'invio dei messaggi o nella visualizzazione delle informazioni del gruppo." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このグループは30日以上更新されていません。メッセージの送信やグループ情報の表示に問題が発生する可能性があります。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -218203,6 +221558,30 @@ "value" : "Ta grupa nie była aktualizowana od ponad 30 dni. Mogą wystąpić problemy z wysyłaniem wiadomości lub wyświetlaniem informacji o grupie." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Este grupo não foi atualizado nos últimos 30 dias. Pode ter problemas ao enviar mensagens ou ao visualizar informações do grupo." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Acest grup nu a fost actualizat de peste 30 de zile. Este posibil să întâmpinați probleme la trimiterea mesajelor sau vizualizarea informațiilor grupului." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Эта группа не обновлялась более 30 дней. У вас могут возникнуть проблемы с отправкой сообщений или просмотром информации о группе." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Denna grupp har inte uppdaterats på över 30 dagar. Du kan uppleva problem med att skicka meddelanden eller visa gruppinformation." + } + }, "tr" : { "stringUnit" : { "state" : "translated", @@ -218220,6 +221599,12 @@ "state" : "translated", "value" : "此群组已超过 30 天未更新。您可能在发送消息或查看群组信息时遇到问题。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "此群組已超過 30 天未更新。您可能在傳送訊息或查看群組資訊時遇到問題。" + } } } }, @@ -218789,6 +222174,12 @@ "value" : "Rimozione in corso" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "削除保留中" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -218807,6 +222198,18 @@ "value" : "Oczekuje na usunięcie" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Remoção pendente" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "În curs de eliminare" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -218842,6 +222245,12 @@ "state" : "translated", "value" : "待移除" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "等待移除" + } } } }, @@ -219914,6 +223323,12 @@ "value" : "Ty i {other_name} zostaliście administratorami." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Você e {other_name} foram promovidos a Admin." + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -219955,6 +223370,12 @@ "state" : "translated", "value" : "{other_name}已被授权为管理员。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "{other_name} 被設置為管理員。" + } } } }, @@ -230598,18 +234019,48 @@ "value" : "कनेक्शन उम्मीदवारों को संभाला जा रहा है" } }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kapcsolat jelöltek kezelése" + } + }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Menangani Kandidat Sambungan" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gestione dei candidati alla connessione" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "接続候補を処理中" + } + }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "연결 후보 처리 중" } }, + "ku" : { + "stringUnit" : { + "state" : "translated", + "value" : "مامەڵەکردن لەگەڵ کاندیدەکانی پەیوەندی" + } + }, + "ku-TR" : { + "stringUnit" : { + "state" : "translated", + "value" : "مامەڵەکردن لەگەڵ کاندیدەکانی پەیوەندی" + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -230622,6 +234073,18 @@ "value" : "Obsługa kandydatów do połączenia" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "A processar candidatos de ligação" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se gestionează candidații pentru conexiune" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -230634,6 +234097,12 @@ "value" : "Hantera kontakt kandidater" } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bağlantı Adayları İşleniyor" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -230645,6 +234114,18 @@ "state" : "translated", "value" : "Đang xử lý thông tin các kết nối khả dĩ" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "处理连接候选人" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在處理連線候選項目" + } } } }, @@ -231127,6 +234608,71 @@ } } }, + "helpFAQDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ümumi suallara cavab tapmaq üçün {app_name} TVS-yə baxın." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odpovědi na časté otázky najdete v sekci FAQ {app_name}." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sieh dir die {app_name}-FAQ an, um Antworten auf häufig gestellte Fragen zu erhalten." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Check the {app_name} FAQ for answers to common questions." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Consultez la FAQ de {app_name} pour obtenir des réponses aux questions fréquentes." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bekijk de {app_name} FAQ voor antwoorden op veelgestelde vragen." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sprawdź FAQ {app_name} by poznać odpowiedzi na często zadawane pytania." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ознакомьтесь с часто задаваемыми вопросами {app_name}, чтобы найти ответы на распространённые вопросы." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kolla in FAQ på {app_name} för svar på vanliga frågor." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Перегляд ЧЗП {app_name} для перегляду відповідей на часті запитання." + } + } + } + }, "helpHelpUsTranslateSession" : { "extractionState" : "manual", "localizations" : { @@ -231603,52 +235149,10 @@ "helpReportABug" : { "extractionState" : "manual", "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rapporteer 'n fout" - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "الإبلاغ عن خطأ" - } - }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "Bir xətanı bildir" - } - }, - "bal" : { - "stringUnit" : { - "state" : "translated", - "value" : "بگ رپورٹ کنت" - } - }, - "be" : { - "stringUnit" : { - "state" : "translated", - "value" : "Паведаміць аб памылцы" - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Докладвай за проблем" - } - }, - "bn" : { - "stringUnit" : { - "state" : "translated", - "value" : "বাগ রিপোর্ট করুন" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Informeu d'un error" + "value" : "Bir xəta bildir" } }, "cs" : { @@ -231657,82 +235161,16 @@ "value" : "Nahlásit chybu" } }, - "cy" : { - "stringUnit" : { - "state" : "translated", - "value" : "Adrodd nam" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rapporter en fejl" - } - }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Melde einen Fehler" - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Αναφορά Σφάλματος" + "value" : "Einen Fehler melden" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Report a bug" - } - }, - "eo" : { - "stringUnit" : { - "state" : "translated", - "value" : "Raporti eraron" - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Reportar un error" - } - }, - "es-ES" : { - "stringUnit" : { - "state" : "translated", - "value" : "Reportar Error" - } - }, - "et" : { - "stringUnit" : { - "state" : "translated", - "value" : "Teata veast" - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Akats bat salatu" - } - }, - "fa" : { - "stringUnit" : { - "state" : "translated", - "value" : "گزارش خرابی" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ilmoita virheestä" - } - }, - "fil" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mag-ulat ng bug" + "value" : "Report a Bug" } }, "fr" : { @@ -231741,210 +235179,18 @@ "value" : "Signaler un bug" } }, - "gl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Informar dun erro" - } - }, - "ha" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bayyana bug" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "דווח על תקלה" - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "बग सूचित करें" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Prijavi bug" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hiba jelentése" - } - }, - "hy-AM" : { - "stringUnit" : { - "state" : "translated", - "value" : "Հաղորդել սխալի մասին" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "Laporkan Bug" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Segnala un bug" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "バグを報告" - } - }, - "ka" : { - "stringUnit" : { - "state" : "translated", - "value" : "შეცდომის შესახებ" - } - }, - "km" : { - "stringUnit" : { - "state" : "translated", - "value" : "រាយការណ៍ពីកំហុសមួយ" - } - }, - "kn" : { - "stringUnit" : { - "state" : "translated", - "value" : "ದೋಷವನ್ನು ವರದಿ ಮಾಡಿ" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "버그 제보" - } - }, - "ku" : { - "stringUnit" : { - "state" : "translated", - "value" : "راپۆرتی هەڵە" - } - }, - "ku-TR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Çewtiyekê ragihîne" - } - }, - "lg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Alaga ekivvuuni" - } - }, - "lt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pranešti apie klaidą" - } - }, - "lv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ziņot par kļūdu" - } - }, - "mk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Пријави грешка" - } - }, - "mn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Алдааг мэдээлэх" - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "Laporkan pepijat" - } - }, - "my" : { - "stringUnit" : { - "state" : "translated", - "value" : "ပြဿနာတစ်ခုကို အစီရင်ခံပါ" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rapporter en feil" - } - }, - "nb-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rapporter en bug" - } - }, - "ne-NP" : { - "stringUnit" : { - "state" : "translated", - "value" : "बग रिपोर्ट गर्नुहोस्" - } - }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Meld een bug" } }, - "nn-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rapporter en bug" - } - }, - "ny" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fotokozerani Chifukwa cholakwikacho" - } - }, - "pa-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "ਬੱਗ ਦੀ ਰਿਪੋਰਟ ਕਰੋ।" - } - }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zgłoś błąd" } }, - "ps" : { - "stringUnit" : { - "state" : "translated", - "value" : "یو غلطي راپور کړئ" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Reportar um erro" - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Reportar um Erro" - } - }, "ro" : { "stringUnit" : { "state" : "translated", @@ -231957,82 +235203,16 @@ "value" : "Сообщить об ошибке" } }, - "sh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Prijavi grešku" - } - }, - "si-LK" : { - "stringUnit" : { - "state" : "translated", - "value" : "දෝෂයක් වාර්තා කරන්න" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nahlásiť chybu" - } - }, - "sl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Prijavite napako" - } - }, - "sq" : { - "stringUnit" : { - "state" : "translated", - "value" : "Raporto një difekt" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Пријави грешку" - } - }, - "sr-Latn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Prijavi grešku" - } - }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Rapportera en bugg" - } - }, - "sw" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ripoti hitilafu" - } - }, - "ta" : { - "stringUnit" : { - "state" : "translated", - "value" : "முடையைச் செய்யல்" - } - }, - "te" : { - "stringUnit" : { - "state" : "translated", - "value" : "బగ్‌ను నివేదించండి" - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "รายงานบั๊ก" + "value" : "Rapportera ett fel" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Bir hata bildir" + "value" : "Hata Bildir" } }, "uk" : { @@ -232040,42 +235220,6 @@ "state" : "translated", "value" : "Повідомити про помилку" } - }, - "ur-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "بگ کی اطلاع دیں" - } - }, - "uz" : { - "stringUnit" : { - "state" : "translated", - "value" : "Xato haqida habar bering" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Báo cáo lỗi" - } - }, - "xh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Xela impazamo" - } - }, - "zh-CN" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bug反馈" - } - }, - "zh-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "回報錯誤" - } } } }, @@ -234010,478 +237154,70 @@ "helpReportABugExportLogsSaveToDesktopDescription" : { "extractionState" : "manual", "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Stoor hierdie lêer na jou lessenaar, en deel dit dan met die {app_name} ontwikkelaars." - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "احفظ هذا الملف على سطح المكتب، ثم شاركه مع مطوري {app_name}." - } - }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "Bu faylı masaüstündə saxlayıb {app_name} tərtibatçıları ilə paylaşın." - } - }, - "bal" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} کاردارانءَ ہَچّ اِی فائیل انت دسک ٹاپیْ، پَس اِے کِھ شیئر کَپُت." - } - }, - "be" : { - "stringUnit" : { - "state" : "translated", - "value" : "Захавайце гэты файл на сваім працоўным стале, затым падзяліцеся ім з распрацоўшчыкамі {app_name}." - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Запазете този файл на работния плот и го споделете с разработчиците на {app_name}." - } - }, - "bn" : { - "stringUnit" : { - "state" : "translated", - "value" : "এই ফাইলটি আপনার ডেস্কটপে সংরক্ষণ করুন, তারপর এটি {app_name} ডেভেলপারদের সাথে শেয়ার করুন।" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Deseu aquest fitxer a l'escriptori, i després compartiu-lo amb els desenvolupadors de {app_name}." + "value" : "Bu faylı saxlayın, sonra onu {app_name} gəlişdiriciləri ilə paylaşın." } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Uložte tento soubor na plochu, poté jej sdílejte s vývojáři aplikace {app_name}." - } - }, - "cy" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cadw'r ffeil hon i'ch bwrdd gwaith, yna ei rhannu gyda datblygwyr {app_name}." - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gemt denne fil til din desktop, og del den derefter med {app_name} udviklere." + "value" : "Uložte tento soubor, poté jej sdílejte s vývojáři aplikace {app_name}." } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Speichere diese Datei auf deinem Desktop und teile sie dann mit den Entwicklern von {app_name}." - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Αποθηκεύστε αυτό το αρχείο στην επιφάνεια εργασίας σας, και μετά κοινοποιήστε το στους προγραμματιστές του {app_name}." + "value" : "Speichere diese Datei und teile sie dann mit den {app_name} Entwicklern." } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Save this file to your desktop, then share it with {app_name} developers." - } - }, - "eo" : { - "stringUnit" : { - "state" : "translated", - "value" : "Konservu ĉi tiun dosieron al via labortablo, poste kunhavigu ĝin kun la zhviligistoj de {app_name}." - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Guarde este archivo en su escritorio, luego compártalo con los desarrolladores de {app_name}." - } - }, - "es-ES" : { - "stringUnit" : { - "state" : "translated", - "value" : "Guarde este archivo en su escritorio y luego compártalo con los desarrolladores de {app_name}." - } - }, - "et" : { - "stringUnit" : { - "state" : "translated", - "value" : "Salvesta see fail oma arvutisse ja jaga seda {app_name} arendajatega." - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gorde fitxategi hau zure mahaigainean, eta ondoren partekatu {app_name} garatzaileekin." - } - }, - "fa" : { - "stringUnit" : { - "state" : "translated", - "value" : "این فایل را در دسکتاپ خود ذخیره کنید، سپس آن را با توسعه‌دهندگان {app_name} به اشتراک بگذارید." - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tallenna tämä tiedosto työpöydällesi ja jaa se sitten {app_name}in kehittäjille." - } - }, - "fil" : { - "stringUnit" : { - "state" : "translated", - "value" : "Save this file to your desktop, then share it with {app_name} developers." + "value" : "Save this file, then share it with {app_name} developers." } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Enregistrez ce fichier sur votre bureau, puis partagez-le avec les développeurs de {app_name}." - } - }, - "gl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Garda este ficheiro no teu escritorio, logo compárteo cos desenvolvedores de {app_name}." - } - }, - "ha" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ajiye wannan fayil zuwa teburin kwamfutarka, sannan raba shi tare da masu haɓaka {app_name}." - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "שמור את הקובץ הזה לשולחן העבודה שלך, ואז שתף אותו עם המפתחים של {app_name}." - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "इस फ़ाइल को अपने डेस्कटॉप पर सहेजें, फिर इसे {app_name} डेवलपर्स के साथ साझा करें।" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Spremi ovu datoteku na radnu površinu, zatim je podijeli s razvojnim timom {app_name}." - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mentse ezt a fájlt az asztalra, majd ossza meg a {app_name} fejlesztőivel." - } - }, - "hy-AM" : { - "stringUnit" : { - "state" : "translated", - "value" : "Պահեք այս ֆայլը ձեր աշխատասեղանին, ապա կիսվեք այն {app_name} ծրագրավորողների հետ։" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "Simpan file ini ke desktop Anda, lalu bagikan dengan pengembang {app_name}." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Salva questo file sul tuo desktop, poi condividilo con gli sviluppatori di {app_name}." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "このファイルをデスクトップに保存し、{app_name}開発者に共有してください" - } - }, - "ka" : { - "stringUnit" : { - "state" : "translated", - "value" : "შეინახე ეს ფაილი დესკტოპზე, შემდეგ გაუზიარე მას {app_name}-ის დეველოპერებს." - } - }, - "km" : { - "stringUnit" : { - "state" : "translated", - "value" : "រក្សាទុកឯកសារនេះទៅក្នុងកុំព្យូទ័ររបស់អ្នក រួចចែករំលែកទៅអ្នកអភិវឌ្ឍ​ {app_name}" - } - }, - "kn" : { - "stringUnit" : { - "state" : "translated", - "value" : "ಈ ಕಡತವನ್ನು ನಿಮ್ಮ ಡೆಸ್ಕ್‌ಟಾಪ್‌ಗೆ ಉಳಿಸಿ, ನಂತರ ಅದನ್ನು {app_name} ಡೆವಲಪರ್‌ಗಳಿಗೆ ಹಂಚಿಕೊಳ್ಳಿ." - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "이 파일을 데스크탑에 저장한 후, {app_name} 개발자와 공유하세요." - } - }, - "ku" : { - "stringUnit" : { - "state" : "translated", - "value" : "ئەم فایلە پاشەکەوت بکە لە سەر ئۆفیسەکەت، پاشان بهێنە گیاندن بە پەرەودەکارانی {app_name}." - } - }, - "ku-TR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vê pelê li sermaseyê xwe qeydkirin, piştî wê bi pêşkeftinên {app_name} ve parastin." - } - }, - "lg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kuuma fayiro eno ku desktop yo oluvannyuma ogigabane ne bakozi ba {app_name}." - } - }, - "lt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Įrašykite šį failą ant savo darbalaukio, tada pasidalinkite juo su {app_name} kūrėjais." - } - }, - "lv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Saglabājiet šo failu savā darbvirsmā, pēc tam dalieties ar to {app_name} izstrādātājiem." - } - }, - "mk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Зачувај ја оваа датотека на твоето работно место, потоа сподели ја со развивачите на {app_name}." - } - }, - "mn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Энэ файлыг таны ширээний компьютер дээр хадгалж, дараа нь {app_name} хөгжүүлэгчидтэй хуваалцаарай." - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "Simpan fail ini ke desktop anda, kemudian kongsikannya dengan pembangun {app_name}." - } - }, - "my" : { - "stringUnit" : { - "state" : "translated", - "value" : "ဤဖိုင်ကို သင့်ကွန်ပျူတာဖော်ပြရာနေရာတွင် သိမ်း၊ ထို့နောက် {app_name} ဖွံ့ဖြိုးသူများနဲ့ ဝေမျှပါ။" + "value" : "Enregistrez ce fichier, puis partagez-le avec les développeurs de {app_name}." } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Lagre denne filen til skrivebordet, deretter del den med {app_name}-utviklerne." - } - }, - "nb-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lagre denne filen til skrivebordet, og del den deretter med {app_name} utviklere." - } - }, - "ne-NP" : { - "stringUnit" : { - "state" : "translated", - "value" : "यो फाइल तपाईंको डेस्कटपमा बचत गर्नुहोस्, त्यसपछि यसलाई {app_name} विकासकर्ताहरूसँग साझा गर्नुहोस्।" + "value" : "Lagre denne filen, så del den med {app_name}-utviklerne." } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Sla dit bestand op uw bureaublad op en deel het dan met {app_name} ontwikkelaars." - } - }, - "nn-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lagre denne fila til skrivebordet, og del ho med utviklarane av {app_name}." - } - }, - "ny" : { - "stringUnit" : { - "state" : "translated", - "value" : "Save this file to your desktop, then share it with {app_name} developers." - } - }, - "pa-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "ਇਸ ਫਾਈਲ ਨੂੰ ਆਪਣੇ ਡੈਸਕਟਾਪ 'ਤੇ ਸੇਵ ਕਰੋ, ਫਿਰ ਇਸ ਨੂੰ {app_name} ਡਿਵੈਲਪਰਾਂ ਨਾਲ ਸਾਂਝੀ ਕਰੋ ਜੀ." + "value" : "Sla dit bestand op en deel het vervolgens met de {app_name} ontwikkelaars." } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Zapisz plik na pulpicie, a następnie udostępnij go programistom aplikacji {app_name}." - } - }, - "ps" : { - "stringUnit" : { - "state" : "translated", - "value" : "دا دوسیه خپل ډیسټاپ ته وساتئ ، بیا یې د {app_name} پراختیا کونکي سره شریکه کړئ." - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Salve este arquivo no seu desktop, então compartilhe-o com os desenvolvedores do {app_name}." - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Guarde este ficheiro no seu desktop e partilhe-o com os desenvolvedores do {app_name}." - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Salvează acest fișier pe desktop-ul tău, apoi partajează-l cu dezvoltatorii {app_name}." + "value" : "Zapisz ten plik i wyślij go do deweloperów {app_name}." } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Сохраните этот файл на рабочий стол, затем поделитесь им с разработчиками {app_name}." - } - }, - "sh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sačuvajte ovu datoteku na svom desktopu, a zatim je podijelite s {app_name} programerima." - } - }, - "si-LK" : { - "stringUnit" : { - "state" : "translated", - "value" : "මේ ගොනුව ඔබේ ඩෙස්ක්ටොප් එකට සුරකින්න, අවසානයේ එය {app_name} සංවර්ධකයන් සමඟ බෙදාගන්න." - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Uložiť tento súbor na pracovnú plochu, potom ho zdieľajte s vývojármi aplikácie {app_name}." - } - }, - "sl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Shrani to datoteko na namizje in jo nato deli z razvijalci {app_name}." - } - }, - "sq" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ruaje këtë skedar në desktop-in tuaj, pastaj ndaje me zhvilluesit e {app_name}." - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Сачувајте овај фајл на вашој радној површини, затим поделите са {app_name} програмерима." - } - }, - "sr-Latn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sačuvajte ovaj fajl na svom desktopu, zatim ga podelite sa programerima {app_name}." + "value" : "Сохраните этот файл, затем поделитесь им с разработчиками {app_name}." } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Spara denna fil till ditt skrivbord och dela den sedan med {app_name} utvecklarna." - } - }, - "sw" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hifadhi faili hili kwenye desktop yako, kisha shirikisha na watengenezaji wa {app_name}." - } - }, - "ta" : { - "stringUnit" : { - "state" : "translated", - "value" : "இந்த கோப்பை உங்கள் டெஸ்க்டாப் கணினியில் சேமிக்கவும், பின்னர் ஏதும் பொருந்தும் {app_name} அபிவிருத்தியாளர்களுக்கு பகிரவும்." - } - }, - "te" : { - "stringUnit" : { - "state" : "translated", - "value" : "ఈ దస్త్రాన్ని మీ డెస్క్‌టాప్‌లో సేవ్ చేసి, తరువాత దాన్ని {app_name} డెవలపర్‌లకు షేర్ చేయండి." - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "บันทึกไฟล์นี้ลงในเดสก์ท็อปของคุณ จากนั้นแชร์กับนักพัฒนาของ {app_name}" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bu dosyayı masaüstüne kaydedin, ardından {app_name}'in geliştiricileriyle paylaşın." + "value" : "Spara denna fil, dela den sedan med {app_name} utvecklarna." } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Збережіть цей файл на робочому столі, а потім розділіть його з розробниками {app_name}." - } - }, - "ur-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "اس فائل کو اپنے ڈیسک ٹاپ پر محفوظ کریں، پھر اسے {app_name} کے ڈویلپرز کے ساتھ شیئر کریں۔" - } - }, - "uz" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ushbu faylni ish stolingizga saqlang, so'ngra {app_name} ishlab chiquvchilari bilan ulashing." - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lưu tệp này vào máy tính để bàn của bạn, rồi chia sẻ nó với các nhà phát triển {app_name}." - } - }, - "xh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gcina le fayile kwi desktop yakho, uze uyibonise abaphuhlisi be-{app_name}." - } - }, - "zh-CN" : { - "stringUnit" : { - "state" : "translated", - "value" : "将此文件保存到您的桌面,然后与{app_name}开发者分享" - } - }, - "zh-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "將次文檔存儲至您的桌面,並分享與 {app_name} 的開發者。" + "value" : "Збережіть цей файл, а потім надішліть його розробникам {app_name}." } } } @@ -234965,6 +237701,71 @@ } } }, + "helpTranslateSessionDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} tətbiqini 80-dən çox dildə tərcümə etməyə kömək edin!" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pomozte přeložit {app_name} do více než 80 jazyků!" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hilf mit, {app_name} in über 80 Sprachen zu übersetzen!" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Help translate {app_name} into over 80 languages!" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aidez à traduire {app_name} en plus de 80 langues !" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Help met het vertalen van {app_name} in meer dan 80 talen!" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pomóż przetłumaczyć {app_name} na ponad 80 języków!" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Помогите перевести {app_name} на более чем 80 языков!" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hjälp till att översätta {app_name} till över 80 språk!" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Допоможіть перекласти {app_name} на більше ніж 80 мов!" + } + } + } + }, "helpWedLoveYourFeedback" : { "extractionState" : "manual", "localizations" : { @@ -235926,537 +238727,207 @@ "hideMenuBarDescription" : { "extractionState" : "manual", "localizations" : { - "af" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "Wissel lêerstelsel werkbalk sigbaarheid" + "value" : "Sistem menyu çubuğunun görünməsini dəyişdir." } }, - "ar" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "تبديل رؤية شريط قائمة النظام" + "value" : "Přepínač viditelnosti lišty systémového menu." } }, - "az" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Sistem menyu çubuğunun görünməsini dəyişdir" + "value" : "Toggle system menu bar visibility." } }, - "bal" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "System menu bar čārāw visibility" + "value" : "Activer/désactiver la visibilité de la barre de menu système." } }, - "be" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Рэгуляваць бачнасць сістэмнага меню" + "value" : "Zichtbaarheid systeem-menubalk in-/uitschakelen." } }, - "bg" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Превключване на видимостта на системната лента с менюта" + "value" : "Pokaż/ukryj pasek menu systemowego." } }, - "bn" : { + "ru" : { "stringUnit" : { "state" : "translated", - "value" : "সিস্টেম মেনুবার দৃশ্যমানতা টগল করুন" + "value" : "Спрятать или показать системное меню." } }, - "ca" : { + "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Commuta la visibilitat de la barra de menú del sistema" + "value" : "Växla synlighet för systemmenyraden." } }, - "cs" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Přepínač viditelnosti lišty systémového menu" + "value" : "Видимість панелі меню." } - }, - "cy" : { + } + } + }, + "hideNoteToSelfDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "Toglo gwelededd bar dewislen system" + "value" : "Özünə Qeydi söhbət siyahınızdan gizlətmək istədiyinizə əminsinizmi?" } }, - "da" : { + "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Slå synlighed for systemmenulinjen til/fra" + "value" : "Estàs segur que vols amagar Nota a Si Mateix de la teva llista de converses?" } }, - "de" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Schalte die Sichtbarkeit der Menüleiste um" + "value" : "Opravdu chcete skrýt Poznámku sobě ze svého seznamu konverzací?" } }, - "el" : { + "da" : { "stringUnit" : { "state" : "translated", - "value" : "Εναλλαγή προβολής γραμμής μενού συστήματος" + "value" : "Er du sikker på, at du vil skjule Egen note fra din samtaleliste?" } }, - "en" : { + "de" : { "stringUnit" : { "state" : "translated", - "value" : "Toggle system menu bar visibility" + "value" : "Möchtest du Notiz an mich wirklich aus deiner Unterhaltungsliste ausblenden?" } }, - "eo" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Ŝalti la videblecon de la sistemmenuo" + "value" : "Are you sure you want to hide Note to Self from your conversation list?" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Cambiar visibilidad de la barra de menú del sistema" + "value" : "¿Estás seguro de que quieres ocultar Nota Personal de tu lista de conversaciones?" } }, "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Cambiar visibilidad de la barra de menú del sistema" - } - }, - "et" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vaheta süsteemi menüüriba nähtavust" - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sistemaren menu barraren ikusgarritasuna piztu/itzali" - } - }, - "fa" : { - "stringUnit" : { - "state" : "translated", - "value" : "تغییر نمایش نوار منوی سیستم" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Valitse järjestelmän valikkopalkin näkyvyys" - } - }, - "fil" : { - "stringUnit" : { - "state" : "translated", - "value" : "I-toggle ang pagkakakita ng menu bar ng system" + "value" : "¿Estás seguro de que quieres ocultar Nota Personal de tu lista de conversaciones?" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Afficher ou masquer la barre du menu système" - } - }, - "ha" : { - "stringUnit" : { - "state" : "translated", - "value" : "Canza hasken menu ɗin tsarin" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "החלף את מצב הסתרת סרגל התפריט של המערכת" + "value" : "Êtes-vous sûr de vouloir masquer Note pour soi-même de votre liste de conversations ?" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "सिस्टम मेनू बार दृश्यता टॉगल करें" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Uključivanje vidljivosti trake izbornika sustava" + "value" : "क्या आप वाकई अपनी बातचीत सूची से अपने लिए नोट छुपाना चाहते हैं?" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "A rendszer menüsor láthatóságának be-/kikapcsolása" - } - }, - "hy-AM" : { - "stringUnit" : { - "state" : "translated", - "value" : "Միացնել համակարգի ընտրացանկի տեսանելիությունը" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "Alihkan visibilitas bilah menu sistem" + "value" : "Biztosan el akarja rejteni a Jegyzet magamnak jegyzetet a beszélgetési listából?" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Mostra/Nascondi la barra delle impostazioni" + "value" : "Sei sicuro di voler nascondere Note to Self dalla tua lista di conversazioni?" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "メニューバーの表示を切り替える" - } - }, - "ka" : { - "stringUnit" : { - "state" : "translated", - "value" : "სისტემის მენიუს ხილვადობის გადართვა" - } - }, - "km" : { - "stringUnit" : { - "state" : "translated", - "value" : "បិទបើកប្រព័ន្ធរបារម៉ឺនុយដែលអាចមើលឃើញ។" - } - }, - "kn" : { - "stringUnit" : { - "state" : "translated", - "value" : "ಸಿಸ್ಟಮ್ ಮೆನು ಬಾರ್ ದೃಶ್ಯಮಾನತೆಯನ್ನು ಷೇಮರಿಸಲು ಟೊರ್ನ್ ಕ್ರಿಯಲೆಗೆ" + "value" : "自分用メモを会話リストから非表示にしますか?" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "시스템 메뉴 바를 끄거나 켜기" - } - }, - "ku" : { - "stringUnit" : { - "state" : "translated", - "value" : "چالاکی بینینی پەڕەی سیستەم" - } - }, - "ku-TR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Xuyabûna darikê menuyê ya sîstemê veke/bigire" - } - }, - "lg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ganda ebbanga ly’amakubo ery’ebikozesebwa ebyenfuna" - } - }, - "lt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Perjungti sistemos meniu juostos matomumą" - } - }, - "mk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Вклучување на менито на системот" - } - }, - "mn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Системийн цэсийн мөрний харагдах байдлыг тохируулах" - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "Togol kebolehlihatan bar menu sistem" - } - }, - "my" : { - "stringUnit" : { - "state" : "translated", - "value" : "စနစ် မီနှူးဘား၏ မြင်ကွင်းကို အတိအကျပြပါ" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vis eller skjul menylinjen" - } - }, - "nb-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vis eller skjul menylinjen" - } - }, - "ne-NP" : { - "stringUnit" : { - "state" : "translated", - "value" : "तन्त्र प्रणाली मेनु बार दृश्यता टगल गर्नुहोस्" + "value" : "정말로 대화 목록에서 개인용 메모를 숨기시겠습니까?" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Zichtbaarheid systeemmenubalk in-/uitschakelen" - } - }, - "nn-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vis eller skjul menylinjen" - } - }, - "ny" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sintha kuonekera kwa system menu bar" - } - }, - "pa-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "ਸਿਸਟਮ ਮੀਨੂ ਬਾਰ ਵਿਖਾਈ ਦੇਣ ਦੀ ਸਥਿਤੀ ਟੌਗਲ ਕਰੋ" + "value" : "Ben je zeker dat je Bericht aan Jezelf in je conversatie lijst wilt verbergen?" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Przełącz widoczność systemowego paska menu" - } - }, - "ps" : { - "stringUnit" : { - "state" : "translated", - "value" : "د سیستم مینو بار لیدنه وښایاست" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Alternar visibilidade da barra de menu do sistema" + "value" : "Czy na pewno chcesz ukryć Moje notatki na liście konwersacji?" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Alternar a visibilidade da barra de menu do sistema" + "value" : "Tem a certeza de que pretende ocultar a Nota Pessoal da sua lista de conversas?" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Comută vizibilitatea barei de meniu a sistemului" + "value" : "Ești sigur/ă că dorești să ascunzi Notă personală din lista ta de conversații?" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Переключить видимость панели системного меню" - } - }, - "sh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Prebacivanje vidljivosti sistemske trake s izbornicima" - } - }, - "si-LK" : { - "stringUnit" : { - "state" : "translated", - "value" : "පද්ධති මෙනු තීරු දෘශ්‍යතාව ටොගල් කරන්න" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Prepnúť viditeľnosť systémového panela ponuky" - } - }, - "sl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Preklopite vidnost sistemske menijske vrstice" - } - }, - "sq" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ndryshoni dukshmërinë e shiritit të menusë së sistemit" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Пребаци видљивост системске траке са менијем" - } - }, - "sr-Latn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Uključi / isključi vidljivost sistemskog menija" + "value" : "Вы уверены, что хотите скрыть Заметки для Себя из списка бесед?" } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Växla synlighet för systemmenyraden" - } - }, - "sw" : { - "stringUnit" : { - "state" : "translated", - "value" : "Badilisha muonekano wa upau wa menyu ya mfumo" - } - }, - "ta" : { - "stringUnit" : { - "state" : "translated", - "value" : "சிஸ்டம் மெனு பட்டியின் காட்சியைக் காட்டு" - } - }, - "te" : { - "stringUnit" : { - "state" : "translated", - "value" : "సిస్టమ్ మెనూ బార్ విజిబిలిటిని టాగిల్ చేయండి" - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "สลับการแสดงผลแถบเมนูระบบ" + "value" : "Är du säker på att du vill gömma Notera till mig själv från din konversationslista?" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Sistem menü çubuğu görünürlüğünü değiştirin" + "value" : "Kendime Not'u sohbet listenizden gizlemek istediğinizden emin misiniz?" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Увімкнути або вимкнути видимість панелі меню" - } - }, - "ur-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "سسٹم مینو بار کی مرئیت کو ٹوگل کریں" - } - }, - "uz" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tizim menyusi paneli koʻrinishini oʻzgartirish" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Chuyển đổi sự hiển thị thanh trình đơn hệ thống" - } - }, - "xh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Guqula ukubonakalisa kwebar yeendlela zenkqubo" + "value" : "Ви впевнені, що хочете приховати Нотатку для себе зі свого списку розмов?" } }, "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "切换系统菜单栏可见性" + "value" : "你确定要从对话列表中隐藏Note to Self吗?" } }, "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "切換系統選單列可見性" - } - } - } - }, - "hideNoteToSelfDescription" : { - "extractionState" : "manual", - "localizations" : { - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "Özünə Qeydi söhbət siyahınızdan gizlətmək istədiyinizə əminsinizmi?" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Estàs segur que vols amagar Nota a Si Mateix de la teva llista de converses?" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Opravdu chcete skrýt Poznámku sobě ze svého seznamu konverzací?" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Er du sikker på, at du vil skjule Egen note fra din samtaleliste?" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Are you sure you want to hide Note to Self from your conversation list?" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Êtes-vous sûr de vouloir masquer Note pour soi-même de votre liste de conversations ?" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Biztosan el akarja rejteni a Jegyzet magamnak jegyzetet a beszélgetési listából?" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "정말로 대화 목록에서 개인용 메모를 숨기시겠습니까?" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ben je zeker dat je Bericht aan Jezelf in je conversatie lijst wilt verbergen?" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Czy na pewno chcesz ukryć Moje notatki na liście konwersacji?" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ви впевнені, що хочете приховати Нотатку для себе зі свого списку розмов?" + "value" : "您確定要將 小筆記 從您的對話清單中隱藏嗎?" } } } @@ -237470,6 +239941,18 @@ "value" : "bildoj" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "imágenes" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "imágenes" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -237494,6 +239977,18 @@ "value" : "gambar" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "immagini" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "画像" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -237512,6 +240007,18 @@ "value" : "obrazy" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "imagens" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "imagini" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -237524,6 +240031,12 @@ "value" : "bilder" } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "resimler" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -237541,6 +240054,59 @@ "state" : "translated", "value" : "图片" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "圖片" + } + } + } + }, + "important" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vacib" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Důležité" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Important" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Important" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Belangrijk" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ważne" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Важливо" + } } } }, @@ -239615,6 +242181,34 @@ } } }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Η πρόσκληση απέτυχε" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Οι προσκλήσεις απέτυχαν" + } + } + } + } + } + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -239727,6 +242321,34 @@ } } }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kutse saatmine ebaõnnestus" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kutsete saatmine ebaõnnestus" + } + } + } + } + } + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -239861,6 +242483,28 @@ } } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "招待に失敗しました" + } + } + } + } + } + } + }, "ka" : { "stringUnit" : { "state" : "translated", @@ -239911,6 +242555,34 @@ } } }, + "ku" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "بانگهێشتکردن شکستی هێنا" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "بانگهێشتەکان شکستی هێنا" + } + } + } + } + } + } + }, "ku-TR" : { "stringUnit" : { "state" : "translated", @@ -239939,6 +242611,34 @@ } } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Invitasjon mislykket" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Invitasjoner mislykket" + } + } + } + } + } + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -240007,6 +242707,34 @@ } } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "O convite falhou" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Os convites falharam" + } + } + } + } + } + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -240220,6 +242948,28 @@ } } } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "邀請失敗" + } + } + } + } + } + } } } }, @@ -240378,7 +243128,7 @@ } } }, - "en" : { + "el" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -240392,13 +243142,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "The invite could not be sent. Would you like to try again?" + "value" : "Η πρόσκληση δεν μπορούσε να σταλθεί. Θέλετε να προσπαθήσετε ξανά;" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "The invites could not be sent. Would you like to try again?" + "value" : "Οι προσκλήσεις δεν μπορούσαν να σταλθούν. Θέλετε να προσπαθήσετε ξανά;" } } } @@ -240406,7 +243156,7 @@ } } }, - "eo" : { + "en" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -240420,13 +243170,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "La invitilo ne povas esti sendita. Ĉu vi volas reprovi?" + "value" : "The invite could not be sent. Would you like to try again?" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "La invitiloj ne povas esti senditaj. Ĉu vi volas reprovi?" + "value" : "The invites could not be sent. Would you like to try again?" } } } @@ -240434,7 +243184,7 @@ } } }, - "es-419" : { + "eo" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -240448,13 +243198,13 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "No se ha podido enviar la invitación. ¿Quieres volver a intentarlo?" + "value" : "La invitilo ne povas esti sendita. Ĉu vi volas reprovi?" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "No se han podido enviar las invitaciones. ¿Quieres volver a intentarlo?" + "value" : "La invitiloj ne povas esti senditaj. Ĉu vi volas reprovi?" } } } @@ -240462,7 +243212,7 @@ } } }, - "es-ES" : { + "es-419" : { "stringUnit" : { "state" : "translated", "value" : "%#@arg1@" @@ -240490,6 +243240,62 @@ } } }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "No se ha podido enviar la invitación. ¿Quieres volver a intentarlo?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "No se han podido enviar las invitaciones. ¿Quieres volver a intentarlo?" + } + } + } + } + } + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kutset ei õnnestunud saata. Kas soovite uuesti proovida?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kutseid ei õnnestunud saata. Kas soovite uuesti proovida?" + } + } + } + } + } + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -240624,6 +243430,28 @@ } } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "招待を送信できませんでした。再試行しますか?" + } + } + } + } + } + } + }, "ka" : { "stringUnit" : { "state" : "translated", @@ -240674,6 +243502,34 @@ } } }, + "ku" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "بانگهێشتنامەکە نەتوانرا بنێردرێت. حەز دەکەیت هەوڵی دووبارە بدەیتەوە؟" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "بانگهێشتنامەکان نەتوانرا بنێردرێت. حەز دەکەیت هەوڵی دووبارە بدەیتەوە؟" + } + } + } + } + } + } + }, "ku-TR" : { "stringUnit" : { "state" : "translated", @@ -240702,6 +243558,34 @@ } } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Invitasjon kunne ikke bli sent. Vil du prøve på nytt?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Invitasjoner kunne ikke bli sent. Vil du prøve på nytt?" + } + } + } + } + } + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -240770,6 +243654,34 @@ } } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "O convite não pôde ser enviado. Gostaria de tentar novamente?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Os convites não puderam ser enviados. Gostaria de tentar novamente?" + } + } + } + } + } + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -240983,6 +243895,28 @@ } } } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法傳送邀請,您要再試一次嗎?" + } + } + } + } + } + } } } }, @@ -241944,6 +244878,111 @@ } } }, + "launchOnStartDescriptionDesktop" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kompüteriniz açıldığı zaman {app_name}-u avtomatik başlat." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Spustit {app_name} automaticky při spuštění počítače." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Launch {app_name} automatically when your computer starts up." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lancer automatiquement {app_name} au démarrage de votre ordinateur." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Автоматично запускати {app_name} під час увімкнення компʼютера." + } + } + } + }, + "launchOnStartDesktop" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Açılışda başlat" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Spuštění při startu systému" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Launch on Startup" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lancer au démarrage" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Автозапуск при старті системи" + } + } + } + }, + "launchOnStartupDisabledDesktop" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu ayar, Linux-dakı sisteminiz tərəfindən idarə olunur. Avtomatik açılışı fəallaşdırmaq üçün sistem ayarlarında {app_name} tətbiqini açılış tətbiqlərinizə əlavə edin." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toto nastavení spravuje váš systém s Linuxem. Chcete-li povolit automatické spuštění, přidejte {app_name} do spouštěných aplikací v nastavení systému." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "This setting is managed by your system on Linux. To enable automatic startup, add {app_name} to your startup applications in system settings." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ce paramètre est géré par votre système sous Linux. Pour activer le démarrage automatique, ajoutez {app_name} à vos applications de démarrage dans les paramètres système." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Цим параметром у Linux керує ваша система. Щоб увімкнути автоматичний запуск, додайте {app_name} до програм автозапуску в системних параметрах." + } + } + } + }, "learnMore" : { "extractionState" : "manual", "localizations" : { @@ -243432,6 +246471,18 @@ "value" : "Tiu ĉi grupo nun estas nurlega. Rekreu tiun ĉi grupon por daŭrigi babiladon." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Este grupo ahora es de solo lectura. Vuelve a crear este grupo para seguir chateando." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Este grupo ahora es de solo lectura. Vuelve a crear este grupo para seguir chateando." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -243486,6 +246537,18 @@ "value" : "Ta grupa jest teraz tylko do odczytu. Odtwórz grupę, żeby dalej rozmawiać." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Este grupo está agora apenas em leitura. Recrie este grupo para continuar a conversar." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Acest grup este acum doar pentru citire. Recreează grupul pentru a continua conversația." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -243527,6 +246590,12 @@ "state" : "translated", "value" : "此群组目前为只读状态。请重新创建此群组以继续聊天。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "此群組目前為唯讀。請重新建立群組以繼續聊天。" + } } } }, @@ -243581,6 +246650,18 @@ "value" : "Tiu ĉi grupo nun estas nurlega. Petu administranton de la grupo rekrei tiun ĉi grupon por daŭrigi babiladon." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Este grupo ahora es de solo lectura. Pide al administrador del grupo que vuelva a crear este grupo para seguir chateando." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Este grupo ahora es de solo lectura. Pide al administrador del grupo que vuelva a crear este grupo para seguir chateando." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -243635,6 +246716,18 @@ "value" : "Ta grupa jest teraz tylko do odczytu. Zapytaj administratora grupy, aby odtworzył grupę, żeby dalej rozmawiać." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Este grupo está agora apenas em leitura. Peça ao administrador do grupo para recriar este grupo e continuar a conversar." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Acest grup este acum doar pentru citire. Cere administratorului grupului să recreeze acest grup pentru a continua conversația." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -243676,6 +246769,12 @@ "state" : "translated", "value" : "此群组目前为只读状态。请要求管理员重新创建此群组以继续聊天。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "此群組目前為唯讀。請要求群組管理員重新建立此群組以繼續聊天。" + } } } }, @@ -243724,6 +246823,18 @@ "value" : "Grupoj estis plibonigitaj! Rekreu tiun ĉi grupon por plibonigi la fidindecon. Tiu ĉi grupo fariĝos nurlega je {date}." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "¡Los grupos se han actualizado! Vuelve a crear este grupo para mejorar la fiabilidad. Este grupo pasará a ser de solo lectura el {date}." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "¡Los grupos se han actualizado! Vuelve a crear este grupo para mejorar la fiabilidad. Este grupo pasará a ser de solo lectura el {date}." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -243778,6 +246889,18 @@ "value" : "Grupy zostały ulepszone! Odtwórz tę grupę dla większej niezawodności. Ta grupa będzie tylko do odczytu od {date}." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Os grupos foram atualizados! Recrie este grupo para melhorar a fiabilidade. Este grupo passará a estar apenas em leitura em {date}." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Grupurile au fost actualizate! Recreează acest grup pentru o fiabilitate îmbunătățită. Acest grup va deveni doar pentru citire la data de {date}." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -243813,6 +246936,12 @@ "state" : "translated", "value" : "群组已升级!请重新创建此群组以提高可靠性。此群组将于{date}变为只读状态。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "群組已升級!請重新建立此群組以提升穩定性。本群組將於 {date} 起變為唯讀。" + } } } }, @@ -243861,6 +246990,18 @@ "value" : "Grupoj estis plibonigitaj! Petu administranton de la grupo rekrei la grupon por plibonigi la fidindecon. Tiu ĉi grupo fariĝos nurlega je {date}." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "¡Los grupos se han actualizado! Pide al administrador del grupo que vuelva a crear este grupo para mejorar la fiabilidad. Este grupo pasará a ser de solo lectura el {date}." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "¡Los grupos se han actualizado! Pide al administrador del grupo que vuelva a crear este grupo para mejorar la fiabilidad. Este grupo pasará a ser de solo lectura el {date}." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -243915,6 +247056,18 @@ "value" : "Grupy zostały ulepszone! Zapytaj administratora grupy, aby odtworzył tę grupę dla większej niezawodności. Ta grupa będzie tylko do odczytu od {date}." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Os grupos foram atualizados! Peça ao administrador do grupo para recriar este grupo e melhorar a fiabilidade. Este grupo passará a estar apenas em leitura em {date}." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Grupurile au fost actualizate! Cere administratorului să recreeze acest grup pentru o fiabilitate îmbunătățită. Acest grup va deveni doar pentru citire la data de {date}." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -243950,6 +247103,12 @@ "state" : "translated", "value" : "群组已升级!请要求群组管理员重新创建此群组以提高可靠性。此群组将于{date}变为只读状态。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "群組已升級!請請求群組管理員重新建立此群組以提升穩定性。本群組將於 {date} 起變為唯讀。" + } } } }, @@ -244004,6 +247163,18 @@ "value" : "Historio de babilejo ne estos transportita al la nova grupo. Vi ankoraŭ povas vidi tutan historion de la babilado en via malnova grupo." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "El historial de chat no se transferirá al nuevo grupo. Aún puedes ver todo el historial de chat en tu antiguo grupo." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "El historial de chat no se transferirá al nuevo grupo. Todavía puedes ver todo el historial de chat en tu grupo antiguo." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -244058,6 +247229,18 @@ "value" : "Historia czatu nie będzie przeniesiona do nowej grupy. Możesz wciąż zobaczyć całą historię czatu w swojej starej grupie." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "O histórico da conversa não será transferido para o novo grupo. Ainda poderá ver todo o histórico no seu grupo antigo." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Istoricul conversațiilor nu va fi transferat în noul grup. Totuși, poți vizualiza în continuare întreg istoricul în grupul tău vechi." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -244099,6 +247282,12 @@ "state" : "translated", "value" : "聊天记录被不会转移到新群组。您仍然可以查看旧群组中的所有聊天记录。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "聊天記錄不會轉移到新群組,您仍可在舊群組中查看所有聊天記錄。" + } } } }, @@ -250323,7 +253512,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Keçid önizləmələri göndərərkən tam metadata qorumasına sahib olmayacaqsınız." + "value" : "Keçid önizləmələri göndərərkən tam meta veri qorumasına sahib olmayacaqsınız." } }, "bal" : { @@ -251748,6 +254937,65 @@ } } }, + "links" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keçidlər" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odkazy" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verknüpfungen" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Links" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Liens" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Koppelingen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Linki" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Länkar" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Посилання" + } + } + } + }, "loadAccount" : { "extractionState" : "manual", "localizations" : { @@ -257508,6 +260756,65 @@ } } }, + "logs" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Log-lar" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Logy" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Protokolle" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Logs" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Journaux" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Logboeken" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dzienniki" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loggar" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Журнали" + } + } + } + }, "manageMembers" : { "extractionState" : "manual", "localizations" : { @@ -257535,6 +260842,12 @@ "value" : "Administrer medlemmer" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mitglieder verwalten" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -257547,12 +260860,30 @@ "value" : "Administri membrojn" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Administrar miembros" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Administrar miembros" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Gérer Membres" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "सदस्यों का प्रबंधन करें" + } + }, "hu" : { "stringUnit" : { "state" : "translated", @@ -257565,6 +260896,18 @@ "value" : "Atur Anggota" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gestisci membri" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メンバーの管理" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -257583,6 +260926,36 @@ "value" : "Zarządzaj członkami" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gerir membros" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gestionează membri" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Управление участниками" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hantera medlemmar" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Üyeleri Yönet" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -257594,6 +260967,59 @@ "state" : "translated", "value" : "管理成员" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "管理成員" + } + } + } + }, + "managePro" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} - idarə et" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Spravovat {pro}" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Manage {pro}" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gérer {pro}" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} beheren" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zarządzaj {pro}" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Налаштування {pro}" + } } } }, @@ -263512,7 +266938,7 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Meghívás küldése" + "value" : "Meghívó küldése" } }, "other" : { @@ -267816,6 +271242,71 @@ } } }, + "menuBar" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Menyu çubuğu" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Panel menu" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Menüleiste" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Menu Bar" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Barre de menu" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Menubalk" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pasek Menu" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Панель меню" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Menyrad" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Панель меню" + } + } + } + }, "message" : { "extractionState" : "manual", "localizations" : { @@ -268298,6 +271789,12 @@ "messageBubbleReadMore" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Daha çox oxu" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -268310,23 +271807,184 @@ "value" : "Více" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Weiterlesen" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Read more" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Leer más" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Leer más" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lire plus" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "और पढ़ें" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Tudjon meg többet" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Leggi di più" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "続きを読む" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lees meer" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Przeczytaj więcej" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ler mais" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Citește mai mult" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Читать далее" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Läs mer" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Devamını oku" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Читати далі" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "了解更多" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "閱讀更多" + } + } + } + }, + "messageCopy" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesajı kopyala" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kopírovat zprávu" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nachricht kopieren" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copy Message" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copier le message" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bericht kopiëren" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kopiuj wiadomość" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Копировать текст сообщения" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kopiera meddelande" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Копіювати повідомлення" + } } } }, @@ -268881,7 +272539,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Nachrichtenübermittlung gescheitert" + "value" : "Diese Nachricht konnte nicht zugestellt werden" } }, "el" : { @@ -277089,6 +280747,24 @@ } } }, + "el" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Έχετε νέο μήνυμα στο {group_name}." + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Έχετε %lld νέα μηνύματα στο {group_name}." + } + } + } + } + }, "en" : { "variations" : { "plural" : { @@ -277161,6 +280837,24 @@ } } }, + "et" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sul on uus sõnum {group_name}." + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sul on %lld uut sõnumit {group_name}." + } + } + } + } + }, "fr" : { "variations" : { "plural" : { @@ -277305,6 +280999,24 @@ } } }, + "nb" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Du har fått en ny melding i {group_name}." + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Du har fått %lld nye meldinger i {group_name}." + } + } + } + } + }, "nl" : { "variations" : { "plural" : { @@ -277353,6 +281065,24 @@ } } }, + "pt-PT" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tem uma nova mensagem em {group_name}." + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tem %lld novas mensagens em {group_name}." + } + } + } + } + }, "ro" : { "variations" : { "plural" : { @@ -277496,6 +281226,18 @@ } } } + }, + "zh-TW" : { + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "您在 {group_name} 中收到 %lld 則新訊息。" + } + } + } + } } } }, @@ -277981,6 +281723,12 @@ "messageRequestDisabledToastAttachments" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesaj Tələbiniz qəbul edilənə qədər qoşma göndərə bilməzsiniz" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -277993,24 +281741,60 @@ "value" : "Dokud není vaše žádost o komunikaci přijata, nemůžete posílat přílohy" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Du kannst keine Anhänge versenden, bis deine Nachrichtanfrage akzeptiert wurde" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "You cannot send attachments until your Message Request is accepted" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "No puedes enviar archivos adjuntos hasta que se acepte tu solicitud de mensaje" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "No puedes enviar archivos adjuntos hasta que se acepte tu solicitud de mensaje" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Vous ne pouvez pas envoyer de pièces jointes tant que votre demande de message n'est pas acceptée" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "जब तक आपका संदेश अनुरोध स्वीकार नहीं किया जाता, आप अटैचमेंट नहीं भेज सकते" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Addig nem küldhet mellékleteket, amíg az üzenetkérelmét el nem fogadják" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Non è possibile inviare allegati finché la richiesta di messaggio non sarà accettata" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージリクエストが承認されるまで添付ファイルを送信できません" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -278029,6 +281813,36 @@ "value" : "Nie można wysyłać załączników, dopóki prośba o wiadomość nie zostanie zaakceptowana" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Não é possível enviar anexos até que o seu Pedido de Mensagem seja aceite" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nu poți trimite atașamente până când cererea de mesaj nu este acceptată" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вы не можете отправлять вложения, пока ваш запрос на сообщение не будет принят" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Du kan inte skicka bilagor förrän din meddelandeförfrågan har godkänts" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesaj İsteğiniz kabul edilene kadar ek gönderemezsiniz" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -278040,6 +281854,18 @@ "state" : "translated", "value" : "Bạn không thể gửi tệp đính kèm cho đến khi tin nhắn chờ của bạn được chấp nhận" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "在您的消息请求被接受之前,你无法发送附件" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "在您的訊息請求被接受之前,您無法傳送附件" + } } } }, @@ -278052,6 +281878,12 @@ "value" : "لا يمكنك إرسال رسائل صوتية حتى يتم قَبُول طلب الرسالة الخاص بك" } }, + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesaj Tələbiniz qəbul edilənə qədər səsli mesaj göndərə bilməzsiniz" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -278064,24 +281896,60 @@ "value" : "Dokud není vaše žádost o komunikaci přijata, nemůžete posílat hlasové zprávy" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Du kannst keine Sprachnachrichten senden, bis deine Nachrichtanfrage akzeptiert wurde" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "You cannot send voice messages until your Message Request is accepted" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "No puedes enviar mensajes de voz hasta que se acepte tu solicitud de mensaje" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "No puedes enviar mensajes de voz hasta que se acepte tu solicitud de mensaje" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Vous ne pouvez pas envoyer de messages vocaux tant que votre demande de message n'est pas acceptée" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "जब तक आपका संदेश अनुरोध स्वीकार नहीं किया जाता, आप वॉयस संदेश नहीं भेज सकते" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Addig nem küldhet hangüzeneteket, amíg az üzenetkérelmét el nem fogadják" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Non è possibile inviare messaggi vocali finché la richiesta di messaggio non sarà accettata" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージリクエストが承認されるまで音声メッセージを送信できません" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -278100,6 +281968,36 @@ "value" : "Nie można wysyłać wiadomości głosowych, dopóki prośba o wiadomość nie zostanie zaakceptowana" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Não é possível enviar mensagens de voz até que o seu Pedido de Mensagem seja aceite" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nu poți trimite mesaje vocale până când cererea de mesaj nu este acceptată" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вы не можете отправлять голосовые сообщения, пока ваш запрос на сообщение не будет принят" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Du kan inte skicka röstmeddelanden förrän din meddelandeförfrågan har godkänts" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesaj İsteğiniz kabul edilene kadar sesli mesaj gönderemezsiniz" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -278111,6 +282009,18 @@ "state" : "translated", "value" : "Bạn không thể gửi tin nhắn thoại cho đến khi tin nhắn chờ của bạn được chấp nhận" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "在您的消息请求被接受之前,您无法发送语音消息" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "在您的訊息請求被接受之前,您無法傳送語音訊息" + } } } }, @@ -281485,7 +285395,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Cəmiyyət Mesaj Tələbləri" + "value" : "İcma mesaj tələbləri" } }, "bal" : { @@ -282440,6 +286350,12 @@ "messageRequestsContactDelete" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu mesaj tələbini və əlaqəli kontaktı silmək istədiyinizə əminsiniz?" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -282452,6 +286368,12 @@ "value" : "Opravdu chcete smazat tuto žádost o komunikaci a s ní spojený kontakt?" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bist du sicher, dass du diese Nachrichtenanfrage und den zugehörigen Kontakt löschen möchtest?" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -282464,29 +286386,113 @@ "value" : "Ĉu vi certas, ke vi volas forigi ĉi tiun mesaĝpeton kaj la asociitan kontakton?" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Estás seguro de que deseas eliminar esta solicitud de mensaje y el contacto asociado?" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Estás seguro de que deseas eliminar esta solicitud de mensaje y el contacto asociado?" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Êtes-vous sûr de vouloir supprimer cette demande de message ainsi que le contact associé ?" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "क्या आप वाकई इस संदेश अनुरोध और संबंधित संपर्क को हटाना चाहते हैं?" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Biztosan törölni szeretné ezt az üzenetkérést és a hozzá tartozó kapcsolatot?" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sei sicuro di voler eliminare questa richiesta di messaggio e il contatto associato?" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このメッセージリクエストおよび関連する連絡先を本当に削除してもよろしいですか?" + } + }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "정말로 이 메시지 요청과 연결된 연락처를 삭제하시겠습니까?" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Weet je zeker dat je dit berichtverzoek en de bijbehorende contactpersoon wilt verwijderen?" + } + }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Czy na pewno chcesz usunąć to żądanie wiadomości i powiązany z nim kontakt?" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tem a certeza que pretende eliminar este pedido de mensagem e o contacto associado?" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sigur doriți să ștergeți această solicitare de mesaj și contactul asociat?" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вы уверены, что хотите удалить этот запрос на сообщение и связанный с ним контакт?" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Är du säker på att du vill ta bort denna message request och tillhörande kontakt?" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu mesaj isteğini ve ilişkili kişiyi silmek istediğinizden emin misiniz?" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Ви дійсно хочете видалити цей запит на надіслання повідомлення та пов'язаний з ним контакт?" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "你确定要删除此消息请求和关联的联系人吗?" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "您確定要刪除此訊息請求以及相關聯的聯絡人嗎?" + } } } }, @@ -291091,6 +295097,24 @@ "modalMessageCharacterDisplayDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesajların {limit} xarakter limiti var. %lld xarakteriniz qaldı." + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesajların {limit} xarakter limiti var. %lld xarakteriniz qaldı." + } + } + } + } + }, "ca" : { "variations" : { "plural" : { @@ -291139,6 +295163,42 @@ } } }, + "de" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nachrichten haben ein Zeichenlimit von {limit} Zeichen. Du hast noch %lld Zeichen." + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nachrichten haben ein Zeichenlimit von {limit} Zeichen. Du hast noch %lld Zeichen." + } + } + } + } + }, + "el" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Τα μηνύματα έχουν όριο {limit} χαρακτήρων. Σας απομένει %lld χαρακτήρας." + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Τα μηνύματα έχουν όριο {limit} χαρακτήρων. Σας απομένουν %lld χαρακτήρες." + } + } + } + } + }, "en" : { "variations" : { "plural" : { @@ -291157,6 +295217,96 @@ } } }, + "es-419" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los mensajes tienen un límite de {limit} caracteres. Te queda %lld carácter." + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los mensajes tienen un límite de {limit} caracteres. Te quedan %lld caracteres." + } + } + } + } + }, + "es-ES" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los mensajes tienen un límite de {limit} caracteres. Te queda %lld carácter." + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los mensajes tienen un límite de {limit} caracteres. Te quedan %lld caracteres." + } + } + } + } + }, + "et" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sõnumitel on tähemärgipiirang {limit} tähemärki. Teil on %lld tähemärki alles." + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sõnumitel on tähemärgipiirang {limit} tähemärki. Teil on %lld tähemärki alles." + } + } + } + } + }, + "fr" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Les messages ont une limite de {limit} caractères. Il vous reste encore %lld caractères" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Les messages ont une limite de {limit} caractères. Il vous reste %lld caractères." + } + } + } + } + }, + "hi" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "संदेशों की अक्षर सीमा {limit} वर्ण है। आपके पास %lld वर्ण शेष हैं।" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "संदेशों की अक्षर सीमा {limit} वर्ण है। आपके पास %lld वर्ण शेष हैं।" + } + } + } + } + }, "hu" : { "variations" : { "plural" : { @@ -291175,6 +295325,24 @@ } } }, + "it" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "I messaggi hanno un limite di {limit} caratteri. Hai ancora %lld carattere." + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "I messaggi hanno un limite di {limit} caratteri. Hai ancora %lld caratteri." + } + } + } + } + }, "ja" : { "variations" : { "plural" : { @@ -291186,12 +295354,246 @@ } } } + }, + "nb" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meldinger har en tegngrense på {limit} tegn. Du har %lld tegn igjen." + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meldinger har en tegngrense på {limit} tegn. Du har %lld tegn igjen." + } + } + } + } + }, + "nl" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Berichten hebben een limiet van {limit} tekens. Je hebt nog %lld teken over." + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Berichten hebben een limiet van {limit} tekens. Je hebt nog %lld tekens over." + } + } + } + } + }, + "pl" : { + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wiadomości mogą mieć maksymalnie {limit} znaki. Pozostały %lld znaki." + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wiadomości mogą mieć maksymalnie {limit} znaków. Pozostało %lld znaków." + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wiadomości mogą mieć maksymalnie {limit} znak. Pozostał %lld znak." + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wiadomości mogą mieć maksymalnie {limit} znaków. Pozostało %lld znaków." + } + } + } + } + }, + "pt-PT" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "As mensagens têm um limite de {limit} caracteres. Resta %lld caractere." + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "As mensagens têm um limite de {limit} caracteres. Restam %lld caracteres." + } + } + } + } + }, + "ro" : { + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesajele au o limită de {limit} caractere. Mai ai %lld caractere disponibile." + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesajele au o limită de {limit} caractere. Mai ai %lld caracter disponibil." + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesajele au o limită de {limit} caractere. Mai ai %lld de caractere disponibile." + } + } + } + } + }, + "ru" : { + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Максимальная длина у сообщений {limit} символов. Осталось %lld символа." + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Максимальная длина у сообщений {limit} символов. Осталось %lld символов." + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Максимальная длина у сообщений {limit} символов. Остался %lld символ." + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Максимальная длина у сообщений {limit} символов. Осталось %lld символов." + } + } + } + } + }, + "sv-SE" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meddelanden har en teckengräns på {limit} tecken. Du har %lld tecken kvar." + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meddelanden har en gräns på {limit} tecken. Du har %lld tecken kvar." + } + } + } + } + }, + "tr" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesajlar {limit} karakter ile sınırlıdır. %lld karakteriniz kaldı." + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesajlar {limit} karakter ile sınırlıdır. %lld karakteriniz kaldı." + } + } + } + } + }, + "uk" : { + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Повідомлення має обмеження кількості символів — {limit}. Залишилось %lld символів" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Повідомлення має обмеження кількості символів — {limit}. Залишилось %lld символів" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Повідомлення має обмеження кількості символів — {limit}. Залишився %lld символ" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Повідомлення має обмеження кількості символів — {limit}. Залишилось %lld символів" + } + } + } + } + }, + "zh-CN" : { + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "消息的字符限制为 {limit} 个字符。您还剩 %lld 个字符。" + } + } + } + } + }, + "zh-TW" : { + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "訊息最多限制 {limit} 個字元。您還剩下 %lld 個字元可以使用。" + } + } + } + } } } }, "modalMessageCharacterDisplayTitle" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesaj uzunluğu" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -291204,736 +295606,1346 @@ "value" : "Délka zprávy" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nachrichtenlänge" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Message Length" } }, - "hu" : { + "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Üzenet hossza" + "value" : "Longitud del mensaje" } }, - "uk" : { + "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Довжина повідомлення" + "value" : "Longitud del mensaje" } - } - } - }, - "modalMessageCharacterTooLongDescription" : { - "extractionState" : "manual", - "localizations" : { - "ca" : { + }, + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Heu superat el límit de caràcters per a aquest missatge. Si us plau, escurceu el vostre missatge a {limit} caràcters o menys." + "value" : "Longueur du message" } }, - "cs" : { + "hi" : { "stringUnit" : { "state" : "translated", - "value" : "Překročili jste limit počtu znaků pro tuto zprávu. Zkraťte prosím svou zprávu na {limit} znaků nebo méně." + "value" : "संदेश की लंबाई" } }, - "en" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "You have exceeded the character limit for this message. Please shorten your message to {limit} characters or less." + "value" : "Üzenet hossza" } }, - "hu" : { + "it" : { "stringUnit" : { "state" : "translated", - "value" : "Az üzenet karakterszáma túllépte a megadott. Rövidítse le az üzenetet {limit} karakterekre vagy kevesebbre." + "value" : "Lunghezza del messaggio" } }, - "uk" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Ви перевищили максимальну кількість символів для цього повідомлення. Будь ласка, скоротіть ваше повідомлення до {limit} символів або менше." + "value" : "メッセージの長さ" } - } - } - }, - "modalMessageCharacterTooLongTitle" : { - "extractionState" : "manual", - "localizations" : { - "ca" : { + }, + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Missatge massa llarg" + "value" : "Berichtlengte" } }, - "cs" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Zpráva je příliš dlouhá" + "value" : "Długość wiadomości" } }, - "en" : { + "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Message Too Long" + "value" : "Comprimento da Mensagem" } }, - "hu" : { + "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Az üzenet túl hosszú" + "value" : "Lungimea mesajului" } }, - "uk" : { + "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Задовге повідомлення" + "value" : "Длина Сообщения" } - } - } - }, - "modalMessageTooLongDescription" : { - "extractionState" : "manual", - "localizations" : { - "ca" : { + }, + "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Si us plau, escurceu el vostre missatge als {limit} caràcters o menys." + "value" : "Meddelandelängd" } }, - "cs" : { + "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Zkraťte prosím svou zprávu na {limit} znaků nebo méně." + "value" : "Mesaj Uzunluğu" } }, - "en" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Please shorten your message to {limit} characters or less." + "value" : "Довжина повідомлення" } }, - "hu" : { + "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "Rövidítse le az üzenetét {limit} karakterekre vagy kevesebbre." + "value" : "消息长度" } }, - "uk" : { + "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "Будь ласка, скоротіть повідомлення до {limit} символів або менше." + "value" : "訊息長度" } } } }, - "modalMessageTooLongTitle" : { + "modalMessageCharacterTooLongDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu mesaj üçün xarakter limitini aşmısınız. Lütfən mesajınızı {limit} xarakter və ya daha az qədər qısaldın." + } + }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Missatge massa llarg" + "value" : "Heu superat el límit de caràcters per a aquest missatge. Si us plau, escurceu el vostre missatge a {limit} caràcters o menys." } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Zpráva je příliš dlouhá" + "value" : "Překročili jste limit počtu znaků pro tuto zprávu. Zkraťte prosím svou zprávu na {limit} znaků nebo méně." } }, - "en" : { + "de" : { "stringUnit" : { "state" : "translated", - "value" : "Message Too Long" + "value" : "Du hast das Zeichenlimit für diese Nachricht überschritten. Bitte kürze deine Nachricht auf {limit} Zeichen oder weniger." } }, - "hu" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Az üzenet túl hosszú" + "value" : "You have exceeded the character limit for this message. Please shorten your message to {limit} characters or less." } }, - "uk" : { + "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Задовге повідомлення" + "value" : "Has superado el límite de caracteres para este mensaje. Por favor, acorta tu mensaje a {limit} caracteres o menos." } - } - } - }, - "next" : { - "extractionState" : "manual", - "localizations" : { - "af" : { + }, + "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Volgende" + "value" : "Has superado el límite de caracteres para este mensaje. Por favor, acorta tu mensaje a {limit} caracteres o menos." } }, - "ar" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "التالي" + "value" : "Vous avez dépassé la limite pour ce message. Merci de raccourcir votre message à {limit} caractères ou moins." } }, - "az" : { + "hi" : { "stringUnit" : { "state" : "translated", - "value" : "Növbəti" + "value" : "आपने इस संदेश के लिए वर्ण सीमा को पार कर लिया है। कृपया अपने संदेश को {limit} वर्णों या कम में छोटा करें।" } }, - "bal" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "گُدام" + "value" : "Az üzenet karakterszáma túllépte a megadott. Rövidítse le az üzenetet {limit} karakterekre vagy kevesebbre." } }, - "be" : { + "it" : { "stringUnit" : { "state" : "translated", - "value" : "Далей" + "value" : "Hai superato il limite di caratteri per questo messaggio. Riduci il tuo messaggio a {limit} caratteri o meno." } }, - "bg" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Следващ" + "value" : "このメッセージは文字数制限を超えています。{limit}文字以内に短くしてください。" } }, - "bn" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "পরবর্তী" + "value" : "Je hebt de tekenlimiet voor dit bericht overschreden. Verkort je bericht tot {limit} tekens of minder." } }, - "ca" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Següent" + "value" : "Przekroczono limit znaków dla tej wiadomości. Skróć wiadomość do {limit} znaków lub mniej." } }, - "cs" : { + "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Další" + "value" : "Excedeu o limite de caracteres para esta mensagem. Por favor, reduza a sua mensagem para {limit} caracteres ou menos." } }, - "cy" : { + "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Nesaf" + "value" : "Ai depășit limita de caractere pentru acest mesaj. Te rugăm să scurtezi mesajul la {limit} caractere sau mai puțin." } }, - "da" : { + "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Næste" + "value" : "Вы превысили лимит символов в сообщении. Пожалуйста, сократите сообщение до {limit} символов." } }, - "de" : { + "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Weiter" + "value" : "Du har överskridit teckengränsen för detta meddelande. Förkorta ditt meddelande till {limit} tecken eller färre." } }, - "el" : { + "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Επόμενο" + "value" : "Bu mesaj için karakter sınırını aştınız. Lütfen mesajınızı {limit} karakter veya daha az olacak şekilde kısaltın." } }, - "en" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Next" + "value" : "Ви перевищили максимальну кількість символів для цього повідомлення. Будь ласка, скоротіть ваше повідомлення до {limit} символів або менше." } }, - "eo" : { + "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "Sekva" + "value" : "你已超出此消息的字符限制。请将消息缩短至 {limit} 个字符或更少。" } }, - "es-419" : { + "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "Siguiente" + "value" : "您已超過此訊息的字元限制。請將您的訊息縮短至 {limit} 個字元或更少。" + } + } + } + }, + "modalMessageCharacterTooLongTitle" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesaj çox uzundur" } }, - "es-ES" : { + "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Siguiente" + "value" : "Missatge massa llarg" } }, - "et" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Järgmine" + "value" : "Zpráva je příliš dlouhá" } }, - "eu" : { + "de" : { "stringUnit" : { "state" : "translated", - "value" : "Hurrengoa" + "value" : "Nachricht zu lang" } }, - "fa" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "بعدی" + "value" : "Message Too Long" } }, - "fi" : { + "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Seuraava" + "value" : "Mensaje demasiado largo" } }, - "fil" : { + "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Susunod" + "value" : "Mensaje demasiado largo" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Suivant" + "value" : "Le message est trop long" } }, - "gl" : { + "hi" : { "stringUnit" : { "state" : "translated", - "value" : "Seguinte" + "value" : "संदेश बहुत लंबा है" } }, - "ha" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Na Gaba" + "value" : "Az üzenet túl hosszú" } }, - "he" : { + "it" : { "stringUnit" : { "state" : "translated", - "value" : "הבא" + "value" : "Messaggio troppo lungo" } }, - "hi" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "अगला" + "value" : "メッセージが長すぎます" } }, - "hr" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Sljedeće" + "value" : "Bericht te lang" } }, - "hu" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Tovább" + "value" : "Wiadomość jest za długa" } }, - "hy-AM" : { + "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Հաջորդը" + "value" : "Mensagem muito longa" } }, - "id" : { + "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Selanjutnya" + "value" : "Mesaj prea lung" } }, - "it" : { + "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Avanti" + "value" : "Сообщение слишком длинное" } }, - "ja" : { + "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "次" + "value" : "Meddelandet är för långt" } }, - "ka" : { + "tr" : { "stringUnit" : { "state" : "translated", - "value" : "შემდეგი" + "value" : "Mesaj çok uzun" } }, - "km" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "បន្ទាប់" + "value" : "Задовге повідомлення" } }, - "kn" : { + "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "ಮುಂದಿನ" + "value" : "消息太长" } }, - "ko" : { + "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "다음" + "value" : "訊息過長" } - }, - "ku" : { + } + } + }, + "modalMessageTooLongDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "دواتر" + "value" : "Lütfən mesajınızı {limit} xarakter və ya daha az qədər qısaldın." } }, - "ku-TR" : { + "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Yê piştî" + "value" : "Si us plau, escurceu el vostre missatge als {limit} caràcters o menys." } }, - "lg" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Ojikulembaza" + "value" : "Zkraťte prosím svou zprávu na {limit} znaků nebo méně." } }, - "lt" : { + "de" : { "stringUnit" : { "state" : "translated", - "value" : "Kitas" + "value" : "Bitte kürze deine Nachricht auf {limit} Zeichen oder weniger." } }, - "lv" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Nākamais" + "value" : "Please shorten your message to {limit} characters or less." } }, - "mk" : { + "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Следно" + "value" : "Por favor, acorta tu mensaje a {limit} caracteres o menos." } }, - "mn" : { + "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Дараагийн" + "value" : "Por favor, acorta tu mensaje a {limit} caracteres o menos." } }, - "ms" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Seterusnya" + "value" : "Veuillez raccourcir votre message a {limit} caractères ou moins." } }, - "my" : { + "hi" : { "stringUnit" : { "state" : "translated", - "value" : "ရှေ့သို့" + "value" : "कृपया अपने संदेश को {limit} वर्णों या कम में छोटा करें।" } }, - "nb" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Neste" + "value" : "Rövidítse le az üzenetét {limit} karakterekre vagy kevesebbre." } }, - "nb-NO" : { + "it" : { "stringUnit" : { "state" : "translated", - "value" : "Neste" + "value" : "Riduci il tuo messaggio a {limit} caratteri o meno." } }, - "ne-NP" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "अर्को" + "value" : "メッセージを{limit}文字以内に短くしてください。" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Volgende" + "value" : "Verkort je bericht tot {limit} tekens of minder." } }, - "nn-NO" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Neste" + "value" : "Skróć wiadomość do {limit} znaków lub mniej." } }, - "ny" : { + "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Ena" + "value" : "Por favor, reduza a sua mensagem para {limit} caracteres ou menos." } }, - "pa-IN" : { + "ro" : { "stringUnit" : { "state" : "translated", - "value" : "ਅਗਲਾ" + "value" : "Te rugăm să scurtezi mesajul la {limit} caractere sau mai puțin." } }, - "pl" : { + "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Następne" + "value" : "Пожалуйста, сократите свое сообщение до {limit} символов." } }, - "ps" : { + "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "بل" + "value" : "Förkorta ditt meddelande till {limit} tecken eller färre." } }, - "pt-BR" : { + "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Avançar" + "value" : "Lütfen mesajınızı {limit} karakter veya daha az olacak şekilde kısaltın." } }, - "pt-PT" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Seguinte" + "value" : "Будь ласка, скоротіть повідомлення до {limit} символів або менше." } }, - "ro" : { + "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "Următorul" + "value" : "请将消息缩短至 {limit} 个字符或更少。" } }, - "ru" : { + "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "Далее" + "value" : "請將您的訊息縮短至 {limit} 個字元或更少。" + } + } + } + }, + "modalMessageTooLongTitle" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesaj çox uzundur" } }, - "sh" : { + "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Sledeći" + "value" : "Missatge massa llarg" } }, - "si-LK" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "ඊළඟ" + "value" : "Zpráva je příliš dlouhá" } }, - "sk" : { + "de" : { "stringUnit" : { "state" : "translated", - "value" : "Ďalej" + "value" : "Nachricht zu lang" } }, - "sl" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Naslednje" + "value" : "Message Too Long" } }, - "sq" : { + "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Tutje" + "value" : "Mensaje demasiado largo" } }, - "sr" : { + "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Следеће" + "value" : "Mensaje demasiado largo" } }, - "sr-Latn" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Dalje" + "value" : "Le message est trop long" } }, - "sv-SE" : { + "hi" : { "stringUnit" : { "state" : "translated", - "value" : "Nästa" + "value" : "संदेश बहुत लंबा है" } }, - "sw" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Inayofuata" + "value" : "Az üzenet túl hosszú" } }, - "ta" : { + "it" : { "stringUnit" : { "state" : "translated", - "value" : "அடுத்தது" + "value" : "Messaggio troppo lungo" } }, - "te" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "తర్వాత" + "value" : "メッセージが長すぎます" } }, - "th" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "ถัดไป" + "value" : "Bericht te lang" } }, - "tr" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "İleri" + "value" : "Wiadomość jest za długa" } }, - "uk" : { + "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Далі" + "value" : "Mensagem muito longa" } }, - "ur-IN" : { + "ro" : { "stringUnit" : { "state" : "translated", - "value" : "اگلا" + "value" : "Mesaj prea lung" } }, - "uz" : { + "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Keyingi" + "value" : "Сообщение слишком длинное" } }, - "vi" : { + "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Tiếp" + "value" : "Meddelandet är för långt" } }, - "xh" : { + "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Okulandelayo" + "value" : "Mesaj çok uzun" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Задовге повідомлення" } }, "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "下一步" + "value" : "消息太长" } }, "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "下一步" + "value" : "訊息過長" } } } }, - "nicknameDescription" : { + "newPassword" : { "extractionState" : "manual", "localizations" : { - "af" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "Kies 'n bynaam vir {name}. Dit sal vir jou verskyn in jou een-tot-een en groepe gesprekke." + "value" : "Yeni parol" } }, - "ar" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "اختر اسم مستعار لـ {name}. سيظهر لك في محادثاتك الفردية والجماعية." + "value" : "Nové heslo" } }, - "az" : { + "de" : { "stringUnit" : { "state" : "translated", - "value" : "{name} üçün bir ləqəb seçin. Bu, təkbətək və qrup danışıqlarınızda sizə görünəcək." + "value" : "Neues Passwort" } }, - "bal" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "برای {name} یک کُرنا نام انتخاب کنے. ایں شما میں آپ ءَ یک به یک ءَ گروهیتی گفتگوئیں ظاہر ببت." + "value" : "New Password" } }, - "be" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Абярыце мянушку для {name}. Гэтую мянушку будзеце бачыць толькі вы ў асабістых і групавых размовах." + "value" : "Nouveau mot de passe" } }, - "bg" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Изберете прякор за {name}. Това ще се появи при вас в личните и груповите разговори." + "value" : "Nieuw wachtwoord" } }, - "bn" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "{name} এর জন্য একটি ডাকনাম নির্বাচন করুন। এটি আপনাকে আপনার এক-তার-এক এবং গ্রুপ কথোপকথনে প্রদর্শিত হবে।" + "value" : "Nowe hasło" } }, - "ca" : { + "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Trieu un sobrenom per a {name}. Això us apareixerà a les vostres converses individuals i de grup." + "value" : "Nytt Lösenord" } }, - "cs" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Vyberte přezdívku pro {name}. Zobrazí se ve vašich konverzacích jeden na jednoho a ve skupinových." + "value" : "Новий пароль" } - }, - "cy" : { + } + } + }, + "next" : { + "extractionState" : "manual", + "localizations" : { + "af" : { "stringUnit" : { "state" : "translated", - "value" : "Dewiswch lysenw ar gyfer {name}. Bydd hyn yn ymddangos ichi yn eich sgyrsiau un-i-un a grŵp." + "value" : "Volgende" } }, - "da" : { + "ar" : { "stringUnit" : { "state" : "translated", - "value" : "Vælg et kælenavn for {name}. Dette vil blive vist for dig i din en-til-en samtaler og gruppekonversationer." + "value" : "التالي" } }, - "de" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "Wähle einen Spitznamen für {name}. Dieser wird dir in deinen Einzel- und Gruppengesprächen angezeigt." + "value" : "Növbəti" } }, - "el" : { + "bal" : { "stringUnit" : { "state" : "translated", - "value" : "Επιλέξτε ένα ψευδώνυμο για {name}. Αυτό θα εμφανιστεί σε εσάς στις προσωπικές και ομαδικές συνομιλίες σας." + "value" : "گُدام" } }, - "en" : { + "be" : { "stringUnit" : { "state" : "translated", - "value" : "Choose a nickname for {name}. This will appear to you in your one-to-one and group conversations." + "value" : "Далей" } }, - "eo" : { + "bg" : { "stringUnit" : { "state" : "translated", - "value" : "Elektu kromnomon por {name}. Ĉi tio aperos al vi en viaj unu-kontraŭ-unu kaj grupaj konversacioj." + "value" : "Следващ" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "পরবর্তী" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Següent" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Další" + } + }, + "cy" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nesaf" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Næste" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Weiter" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Επόμενο" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Next" + } + }, + "eo" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sekva" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Siguiente" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Siguiente" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Järgmine" + } + }, + "eu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hurrengoa" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "بعدی" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seuraava" + } + }, + "fil" : { + "stringUnit" : { + "state" : "translated", + "value" : "Susunod" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Suivant" + } + }, + "gl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seguinte" + } + }, + "ha" : { + "stringUnit" : { + "state" : "translated", + "value" : "Na Gaba" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "הבא" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "अगला" + } + }, + "hr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sljedeće" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tovább" + } + }, + "hy-AM" : { + "stringUnit" : { + "state" : "translated", + "value" : "Հաջորդը" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Selanjutnya" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avanti" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "次" + } + }, + "ka" : { + "stringUnit" : { + "state" : "translated", + "value" : "შემდეგი" + } + }, + "km" : { + "stringUnit" : { + "state" : "translated", + "value" : "បន្ទាប់" + } + }, + "kn" : { + "stringUnit" : { + "state" : "translated", + "value" : "ಮುಂದಿನ" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "다음" + } + }, + "ku" : { + "stringUnit" : { + "state" : "translated", + "value" : "دواتر" + } + }, + "ku-TR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yê piştî" + } + }, + "lg" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ojikulembaza" + } + }, + "lt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kitas" + } + }, + "lv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nākamais" + } + }, + "mk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Следно" + } + }, + "mn" : { + "stringUnit" : { + "state" : "translated", + "value" : "Дараагийн" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seterusnya" + } + }, + "my" : { + "stringUnit" : { + "state" : "translated", + "value" : "ရှေ့သို့" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Neste" + } + }, + "nb-NO" : { + "stringUnit" : { + "state" : "translated", + "value" : "Neste" + } + }, + "ne-NP" : { + "stringUnit" : { + "state" : "translated", + "value" : "अर्को" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Volgende" + } + }, + "nn-NO" : { + "stringUnit" : { + "state" : "translated", + "value" : "Neste" + } + }, + "ny" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ena" + } + }, + "pa-IN" : { + "stringUnit" : { + "state" : "translated", + "value" : "ਅਗਲਾ" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Następne" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "بل" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avançar" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seguinte" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Următorul" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Далее" + } + }, + "sh" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sledeći" + } + }, + "si-LK" : { + "stringUnit" : { + "state" : "translated", + "value" : "ඊළඟ" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ďalej" + } + }, + "sl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Naslednje" + } + }, + "sq" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tutje" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Следеће" + } + }, + "sr-Latn" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dalje" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nästa" + } + }, + "sw" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inayofuata" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "அடுத்தது" + } + }, + "te" : { + "stringUnit" : { + "state" : "translated", + "value" : "తర్వాత" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "ถัดไป" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İleri" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Далі" + } + }, + "ur-IN" : { + "stringUnit" : { + "state" : "translated", + "value" : "اگلا" + } + }, + "uz" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keyingi" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tiếp" + } + }, + "xh" : { + "stringUnit" : { + "state" : "translated", + "value" : "Okulandelayo" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "下一步" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "下一步" + } + } + } + }, + "nextSteps" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Növbəti addımlar" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Další kroky" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Next Steps" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Étapes suivantes" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Volgende stappen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Następne kroki" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подальші кроки" + } + } + } + }, + "nicknameDescription" : { + "extractionState" : "manual", + "localizations" : { + "af" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kies 'n bynaam vir {name}. Dit sal vir jou verskyn in jou een-tot-een en groepe gesprekke." + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "اختر اسم مستعار لـ {name}. سيظهر لك في محادثاتك الفردية والجماعية." + } + }, + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{name} üçün bir ləqəb seçin. Bu, təkbətək və qrup danışıqlarınızda sizə görünəcək." + } + }, + "bal" : { + "stringUnit" : { + "state" : "translated", + "value" : "برای {name} یک کُرنا نام انتخاب کنے. ایں شما میں آپ ءَ یک به یک ءَ گروهیتی گفتگوئیں ظاہر ببت." + } + }, + "be" : { + "stringUnit" : { + "state" : "translated", + "value" : "Абярыце мянушку для {name}. Гэтую мянушку будзеце бачыць толькі вы ў асабістых і групавых размовах." + } + }, + "bg" : { + "stringUnit" : { + "state" : "translated", + "value" : "Изберете прякор за {name}. Това ще се появи при вас в личните и груповите разговори." + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "{name} এর জন্য একটি ডাকনাম নির্বাচন করুন। এটি আপনাকে আপনার এক-তার-এক এবং গ্রুপ কথোপকথনে প্রদর্শিত হবে।" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trieu un sobrenom per a {name}. Això us apareixerà a les vostres converses individuals i de grup." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vyberte přezdívku pro {name}. Zobrazí se vám v konverzacích jeden na jednoho a ve skupinách." + } + }, + "cy" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dewiswch lysenw ar gyfer {name}. Bydd hyn yn ymddangos ichi yn eich sgyrsiau un-i-un a grŵp." + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vælg et kælenavn for {name}. Dette vil blive vist for dig i din en-til-en samtaler og gruppekonversationer." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wähle einen Spitznamen für {name}. Dieser wird dir in deinen Einzel- und Gruppengesprächen angezeigt." + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Επιλέξτε ένα ψευδώνυμο για {name}. Αυτό θα εμφανιστεί σε εσάς στις προσωπικές και ομαδικές συνομιλίες σας." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Choose a nickname for {name}. This will appear to you in your one-to-one and group conversations." + } + }, + "eo" : { + "stringUnit" : { + "state" : "translated", + "value" : "Elektu kromnomon por {name}. Ĉi tio aperos al vi en viaj unu-kontraŭ-unu kaj grupaj konversacioj." } }, "es-419" : { @@ -297339,6 +302351,77 @@ } } }, + "notificationDisplay" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bildiriş nümayişi" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zobrazení upozornění" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Benachrichtigungsanzeige" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Notification Display" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Affichage des notifications" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Notificatie weergave" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wyświetlanie powiadomień" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vizualizare notificări" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Отображение уведомлений" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aviseringsvisning" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сповіщення" + } + } + } + }, "notificationsAllMessages" : { "extractionState" : "manual", "localizations" : { @@ -299729,7 +304812,7 @@ "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "仅显示发送者" + "value" : "仅显示发送者名称" } }, "zh-TW" : { @@ -300219,6 +305302,136 @@ } } }, + "notificationSenderNameAndPreview" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesajın göndərənin adı və mesaj məzmununun bir önizləməsi nümayiş olunsun." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zobrazit jméno odesílatele a náhled obsahu zprávy." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zeigt den Namen des Absenders und eine Vorschau des Nachrichteninhalts an." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Display the sender's name and a preview of the message content." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Afficher le nom de l'expéditeur et un aperçu du contenu du message." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toon de naam van de afzender en een voorbeeld van de berichtinhoud." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wyświetlaj nazwę nadawcy i podgląd wiadomości." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Показывать имя отправителя и предварительный просмотр содержимого сообщения." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Visa avsändarens namn och en förhandsvisning av meddelandets innehåll." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Показувати ім’я відправника та стислий вміст повідомлення." + } + } + } + }, + "notificationSenderNameOnly" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Heç bir mesaj məzmunu olmadan yalnız mesajı göndərənin adı nümayiş olunsun." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zobrazit pouze jméno odesílatele bez obsahu zprávy." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nur den Namen des Absenders ohne Nachrichteninhalt anzeigen." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Display only the sender's name without any message content." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Afficher uniquement le nom de l'expéditeur sans aucun contenu du message." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toon alleen de naam van de afzender zonder enige berichtinhoud." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wyświetlaj tylko nazwę nadawcy, bez podglądu wiadomości." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Отображать только имя отправителя без содержимого сообщения." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Visa endast avsändarens namn utan något meddelandeinnehåll." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Показувати лише ім'я відправника без вмісту повідомлення." + } + } + } + }, "notificationsFastMode" : { "extractionState" : "manual", "localizations" : { @@ -301222,6 +306435,18 @@ "value" : "You'll be notified of new messages reliably and immediately using Huawei’s notification servers." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se le notificará de los nuevos mensajes de forma fiable e inmediata usando los servidores de notificaciones de Huawei." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se le notificará de los nuevos mensajes de forma fiable e inmediata usando los servidores de notificaciones de Huawei." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -301240,6 +306465,18 @@ "value" : "A Huawei értesítési kiszolgálóinak segítségével megbízhatóan és azonnal értesítést fog kapni az új üzenetekről." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Riceverai notifiche di nuovi messaggi in modo affidabile e immediato utilizzando i server di notifica di Huawei." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Huaweiの通知サーバーを使用することで、新しいメッセージの通知を即時かつ確実に受け取ることができます。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -301258,6 +306495,18 @@ "value" : "Będziesz otrzymywać powiadomienia o nowych wiadomościach niezawodnie i natychmiastowo, korzystając z serwerów powiadomień Huawei." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Será notificado de novas mensagens de forma fiável e imediata usando os servidores de notificação da Huawei." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vei fi notificat în legătură cu noile mesaje imediat și în mod fiabil folosind serverele de notificări Huawei." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -301270,6 +306519,12 @@ "value" : "Du kommer att meddelas om nya meddelanden på ett tillförlitligt sätt och omedelbart genom att använda Huawei’s aviseringsservrar." } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Huawei'nin bildirim sunucuları kullanılarak yeni mesajlardan güvenilir bir şekilde ve anında haberdar edileceksiniz." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -301281,6 +306536,12 @@ "state" : "translated", "value" : "您将会收到由华为的通知服务器发出的即时可靠的新消息通知。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "您將會透過華為的通知伺服器即時且可靠地收到新訊息通知。" + } } } }, @@ -301763,6 +307024,71 @@ } } }, + "notificationsGenericOnly" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesajı göndərənin adı və ya mesajın məzmunu olmadan ümumi {app_name} bildirişi nümayiş olunsun." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zobrazit obecné oznámení {app_name} bez jména odesílatele a obsahu zprávy." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zeige eine allgemeine {app_name}-Benachrichtigung ohne Namen des Absenders oder Nachrichteninhalt an." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Display a generic {app_name} notification without the sender's name or message content." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Afficher une notification générique de {app_name} sans le nom de l'expéditeur ni le contenu du message." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toon een algemene {app_name} melding zonder de naam van de afzender of de inhoud van het bericht." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wyświetlaj tylko powiadomienie {app_name}, bez podglądu wiadomości ani nazwy nadawcy." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Показывать общие уведомления {app_name} без имени отправителя и содержимого сообщения." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Visa en generell {app_name}-avisering utan avsändarens namn eller meddelandets innehåll." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Показувати типове сповіщення {app_name} без імені відправника або вмісту повідомлення." + } + } + } + }, "notificationsGoToDevice" : { "extractionState" : "manual", "localizations" : { @@ -303661,7 +308987,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "{name} {conversation_name} üçün" + "value" : "{name} > {conversation_name}" } }, "bal" : { @@ -305086,6 +310412,71 @@ } } }, + "notificationsMakeSound" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yeni mesaj aldığınız zaman bir səs oxudulsun." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Přehrát zvuk při přijetí nových zpráv." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Einen Ton abspielen, wenn neue Nachrichten empfangen werden." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Play a sound when you receive receive new messages." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Jouer un son lorsque vous recevez de nouveaux messages." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Speel een geluid af wanneer je nieuwe berichten ontvangt." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odtwórz dźwięk, kiedy otrzymasz nową wiadomość." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Воспроизводить звук при получении новых сообщений." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Spela upp ett ljud när du får nya meddelanden." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Відтворювати звук, коли ви отримуєте нові повідомлення." + } + } + } + }, "notificationsMentionsOnly" : { "extractionState" : "manual", "localizations" : { @@ -307508,6 +312899,12 @@ "value" : "Notifikationer på pause i {time_large}" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Stummgeschaltet für {time_large}" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -307520,12 +312917,30 @@ "value" : "Silentigita por {time_large}" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Silenciado por {time_large}" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Silenciado por {time_large}" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Muet pour {time_large}" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "{time_large} के लिए म्यूट किया गया" + } + }, "hu" : { "stringUnit" : { "state" : "translated", @@ -307538,6 +312953,18 @@ "value" : "Senyapkan selama {time_large}" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Silenzia per {time_large}" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "{time_large} 間ミュート" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -307556,6 +312983,36 @@ "value" : "Wyciszony przez {time_large}" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Silenciado por {time_large}" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Silențios pentru {time_large}" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Отключено на {time_large}" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tysta i {time_large}" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "{time_large} süresince sessize alındı" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -307573,6 +313030,12 @@ "state" : "translated", "value" : "已设置免打扰{time_large}" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "已靜音 {time_large}" + } } } }, @@ -307585,6 +313048,12 @@ "value" : "كتم حتى {date_time}" } }, + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{date_time} qədər səssizdə" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -307597,6 +313066,12 @@ "value" : "Ztlumeno do {date_time}" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Stummgeschaltet bis {date_time}" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -307609,12 +313084,30 @@ "value" : "Silentigita ĝis {date_time}" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Silenciado hasta {date_time}" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Silenciado hasta {date_time}" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Muet jusqu'à {date_time}" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "{date_time} तक मौन किया गया" + } + }, "hu" : { "stringUnit" : { "state" : "translated", @@ -307627,6 +313120,18 @@ "value" : "Senyapkan sampai {date_time}" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Silenzioso fino alle {date_time}" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "{date_time}までミュート" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -307645,11 +313150,53 @@ "value" : "Wyciszony do {date_time}" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Silenciado até {date_time}" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Amuțit până la {date_time}" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Звук отключен {date_time}" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tystades till {date_time}" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "{date_time} tarihine kadar sessize alındı" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Стишено до {date_time}" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "已禁言至 {date_time}" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "靜音通知,直到 {date_time}" + } } } }, @@ -322029,6 +327576,82 @@ } } }, + "onDevice" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{device_type} cihazınızda" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Na vašem zařízení {device_type}" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "On your {device_type} device" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sur votre appareil {device_type}" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Op je {device_type} apparaat" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "На вашому пристрої {device_type}" + } + } + } + }, + "onDeviceDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Başda qeydiyyatdan keçdiyiniz {platform_account} hesabına giriş etdiyiniz {device_type} cihazından bu {app_name} hesabını açın. Sonra planınızı {app_pro} ayarları vasitəsilə dəyişdirin." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Otevřete tento účet {app_name} na zařízení {device_type}, které je přihlášeno do účtu {platform_account}, pomocí kterého jste se původně zaregistrovali. Poté změňte svůj tarif v nastavení {app_pro}." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Open this {app_name} account on an {device_type} device logged into the {platform_account} you originally signed up with. Then, change your plan via the {app_pro} settings." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ouvrez ce compte {app_name} sur un appareil {device_type} connecté au compte {platform_account} avec lequel vous vous êtes inscrit à l'origine. Ensuite, modifiez votre abonnement via les paramètres {app_pro}." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Open dit {app_name} account op een {device_type} apparaat waarop je bent aangemeld met het {platform_account} waarmee je je oorspronkelijk hebt geregistreerd. Wijzig vervolgens je abonnement via de instellingen van {app_pro}." + } + } + } + }, "onionRoutingPath" : { "extractionState" : "manual", "localizations" : { @@ -324921,6 +330544,28 @@ } } }, + "onPlatformStoreWebsite" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "On the {platform_store} website" + } + } + } + }, + "onPlatformWebsite" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "On the {platform} website" + } + } + } + }, "onsErrorNotRecognized" : { "extractionState" : "manual", "localizations" : { @@ -326358,14 +332003,150 @@ } } }, + "openPlatformStoreWebsite" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Open {platform_store} Website" + } + } + } + }, + "openPlatformWebsite" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Open {platform} Website" + } + } + } + }, "openSurvey" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Anketi aç" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enquesta oberta" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Otevřít dotazník" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Umfrage starten" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Open Survey" } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abrir encuesta" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abrir encuesta" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ouvrir le questionnaire" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "सर्वेक्षण खोलें" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apri sondaggio" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アンケートを開く" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enquête openen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Otwórz ankietę" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abrir questionário" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deschide sondajul" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Открытый опрос" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Öppna undersökning" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пройти опитування" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "打开调查问卷" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "開啟問卷" + } } } }, @@ -326848,6 +332629,77 @@ } } }, + "password" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Parol" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Heslo" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Passwort" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Password" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mot de passe" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wachtwoord" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hasło" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Parolă" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пароль" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lösenord" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пароль" + } + } + } + }, "passwordChange" : { "extractionState" : "manual", "localizations" : { @@ -327333,73 +333185,19 @@ } } }, - "passwordChangedDescription" : { + "passwordChangedDescriptionToast" : { "extractionState" : "manual", "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jou wagwoord is verander. Hou dit asseblief veilig." - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "تم تغيير كلمة المرور الخاصة بك. احفظها في مامن." - } - }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "Parolunuz dəyişdirildi. Lütfən, onu güvəndə saxlayın." - } - }, - "bal" : { - "stringUnit" : { - "state" : "translated", - "value" : "ما گپ درخواست قبول کردی بیک اپلیکیشن پاسکوڈ ناقض کردی. براہپس محفوظے کہ." - } - }, - "be" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ваш пароль быў зменены. Захавайце яго ў бяспецы." - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Вашата парола беше променена. Моля, пазете я безопасно." - } - }, - "bn" : { - "stringUnit" : { - "state" : "translated", - "value" : "আপনার পাসওয়ার্ড পরিবর্তন করা হয়েছে। দয়া করে এটি নিরাপদ রাখুন।" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "La vostra contrasenya s'ha definit. Mantingueu-la segura." + "value" : "Parolunuz dəyişdirilib. Lütfən onu güvəndə saxlayın." } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Tvé heslo bylo změněno. Pečlivě si ho odlož." - } - }, - "cy" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mae eich cyfrinair wedi'i newid. Cadwch ef yn ddiogel." - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Din adgangskode er blevet ændret. Venligst hold den sikker." + "value" : "Your password has been changed. Please keep it safe." } }, "de" : { @@ -327408,64 +333206,22 @@ "value" : "Dein Passwort wurde geändert. Bitte bewahre es sicher auf." } }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ο κωδικός πρόσβασής σας έχει αλλάξει. Παρακαλώ κρατήστε τον ασφαλή." - } - }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Your password has been changed. Please keep it safe." } }, - "eo" : { - "stringUnit" : { - "state" : "translated", - "value" : "Via pasvorto estas ŝanĝita. Bonvolu konservi ĝin sekura." - } - }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Tu contraseña ha sido cambiada. Por favor, manténla segura." + "value" : "Tu contraseña ha sido cambiada. Por favor, guárdala en un lugar seguro." } }, "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Tu contraseña ha sido cambiada. Por favor, manténla segura." - } - }, - "et" : { - "stringUnit" : { - "state" : "translated", - "value" : "Teie parool on muudetud. Hoidke seda turvaliselt." - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zure pasahitza aldatu da. Gorde seguru batean." - } - }, - "fa" : { - "stringUnit" : { - "state" : "translated", - "value" : "رمز عبور شما تغییر کرد. لطفا آن را در جای امنی نگهداری کنید." - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Salasanasi on vaihdettu. Pidä se turvassa." - } - }, - "fil" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nabago na ang iyong password. Pakisuyong itago ito." + "value" : "Tu contraseña ha sido cambiada. Por favor, guárdala en un lugar seguro." } }, "fr" : { @@ -327474,214 +333230,22 @@ "value" : "Votre mot de passe a été changé. Veuillez le conserver en sécurité." } }, - "gl" : { - "stringUnit" : { - "state" : "translated", - "value" : "O teu contrasinal foi cambiado. Por favor, mantéñeo seguro." - } - }, - "ha" : { - "stringUnit" : { - "state" : "translated", - "value" : "An canza kalmar sirrinku. Da fatan za a kiyaye shi lafiya." - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "הסיסמה שלך השתנתה. שמור עליה בבטחה." - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "आपका पासवर्ड बदल दिया गया है। कृपया इसे सुरक्षित रखें।" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vaša lozinka je promijenjena. Molimo, čuvajte je na sigurnom." - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "A jelszó megváltozott. Tartsd biztonságos helyen!" - } - }, - "hy-AM" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ձեր գաղտնաբառը փոխվել է։ Խնդրում ենք անվտանգ պահել։" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kata sandi anda telah diubah. Harap untuk menjaganya." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "La tua password è stata modificata. Per favore tienila al sicuro." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "パスワードが変更されました。安全に保管してください。" - } - }, - "ka" : { - "stringUnit" : { - "state" : "translated", - "value" : "თქვენი პაროლი შეცვლილია. გთხოვთ, შეინახეთ იგი უსაფრთხოდ." - } - }, - "km" : { - "stringUnit" : { - "state" : "translated", - "value" : "ពាក្យសម្ងាត់ របស់អ្នកត្រូវ​បាន​ប្តូរ។ សូមរក្សា​វា​ឲ្យ​មាន​សុវត្ថិភាព។" - } - }, - "kn" : { - "stringUnit" : { - "state" : "translated", - "value" : "ನಿಮ್ಮ ಗುಪ್ತಪದವನ್ನು ಬದಲಾಯಿಸಲಾಗಿದೆ. ಅದು ಸುರಕ್ಷಿತವಾಗಿರಿಸಿ." - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "비밀번호 변경이 완료되었습니다. 안전히 관리하시기 바랍니다." - } - }, - "ku" : { - "stringUnit" : { - "state" : "translated", - "value" : "وشەی پەرەسەدت گۆڕدرا. تکایە ئەوە بەندەن پارێزەر بێت." - } - }, - "ku-TR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Te jîrêbandeya we yê danîn Muhafize mane sihîn bike." - } - }, - "lg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Password yo ekabatiddwa. Kaakasa nti bagutemye mu kifo ekitalemerera." - } - }, - "lt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jūsų slaptažodis buvo pakeistas. Prašome saugoti jį saugiai." - } - }, - "lv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jūsu parole tika nomainīta. Lūdzu, saglabājiet to drošībā." - } - }, - "mk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Вашата лозинка е променета. Ве молиме чувајте ја безбедно." - } - }, - "mn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Таны нууц үг солигдож байна. Нууц үгээ хамгаалж байгаарай." - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kata laluan anda telah ditukar. Sila simpan dengan selamat." - } - }, - "my" : { - "stringUnit" : { - "state" : "translated", - "value" : "သင်၏ စကားဝှက် ပြောင်းလဲ ပြီးပါပြီ။ ထိန်းသိမ်းပါ။" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Passordet ditt er endret. Vennligst oppbevar det trygt." - } - }, - "nb-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Passordet ditt er endret. Vennligst oppbevar det trygt." - } - }, - "ne-NP" : { - "stringUnit" : { - "state" : "translated", - "value" : "तपाईँको पासवर्ड परिवर्तन भयो। कृपया यसलाई सुरक्षित राख्नुहोस्।" - } - }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Uw wachtwoord is gewijzigd. Hou het veilig." } }, - "nn-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Passordet ditt er blitt endra. Vennligst oppbevar det trygt." - } - }, - "ny" : { - "stringUnit" : { - "state" : "translated", - "value" : "Password yanu yasinthidwa. Chonde sungani mosamala." - } - }, - "pa-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "ਤੁਹਾਡਾ ਪਾਸਵਰਡ ਬਦਲਿਆ ਗਿਆ ਹੈ। ਕਿਰਪਾ ਕਰਕੇ ਇਸਨੂੰ ਸੁਰੱਖਿਅਤ ਰੱਖੋ।" - } - }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Zmieniono hasło. Zachowaj je w bezpiecznym miejscu." - } - }, - "ps" : { - "stringUnit" : { - "state" : "translated", - "value" : "ستاسو پاسورډ بدل شوی. مهرباني وکړۍ، دا خوندي وساتئ." - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sua senha foi alterada. Por favor, mantenha-a segura." - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "A sua palavra-passe foi alterada. Por favor, mantenha-a segura." + "value" : "Twoje hasło zostało zmienione. Zapisz je w bezpiecznym miejscu." } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Parola ta a fost schimbată. Te rugăm să o păstrezi în siguranță." + "value" : "Parola ta a fost modificata. Securizați-va parola." } }, "ru" : { @@ -327690,207 +333254,33 @@ "value" : "Ваш пароль был изменен. Пожалуйста, храните его в безопасном месте." } }, - "sh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tvoja šifra je promijenjena. Molimo, čuvaj je na sigurnom." - } - }, - "si-LK" : { - "stringUnit" : { - "state" : "translated", - "value" : "ඔබගේ මුරපදය වෙනස් කර ඇත. කරුණාකර එය ආරක්ෂිතව තබා ගන්න." - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vaše heslo bolo zmenené. Uchovajte ho prosím v bezpečí." - } - }, - "sl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vaše geslo je bilo spremenjeno. Prosim, hranite ga na varnem mestu." - } - }, - "sq" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fjalëkalimi juaj është ndryshuar. Ju lutemi ta mbani të sigurt." - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ваша лозинка је промењена. Молимо вас да је сачувате." - } - }, - "sr-Latn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vaša lozinka je promenjena. Čuvajte je na sigurnom mestu." - } - }, "sv-SE" : { "stringUnit" : { "state" : "translated", "value" : "Ditt lösenord har ändrats. Håll det säkert." } }, - "sw" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nenosiri lako limebadilishwa. Tafadhali lihifadhi salama." - } - }, - "ta" : { - "stringUnit" : { - "state" : "translated", - "value" : "உங்களின் கடவுச்சொல் மாற்றப்பட்டுள்ளது. தயவுசெய்து அதை பாதுகாப்பாக வைத்திருங்கள்." - } - }, - "te" : { - "stringUnit" : { - "state" : "translated", - "value" : "మీ పాస్‌వర్డ్ మార్పు జరిగింది. దయచేసి దాన్ని సురక్షితంగా ఉంచండి." - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "รหัสผ่านของคุณได้รับการเปลี่ยนแปลงแล้ว กรุณารักษาเอาไว้ให้ปลอดภัย" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Şifreniz değiştirildi. Lütfen güvende tutunuz." - } - }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Ваш пароль змінено. Будь ласка, збережіть його в безпеці." - } - }, - "ur-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "آپ کا پاس ورڈ تبدیل ہو گیا ہے۔ براہ کرم اسے محفوظ رکھیں۔" - } - }, - "uz" : { - "stringUnit" : { - "state" : "translated", - "value" : "Xabar so'rovingiz hozirda kutilmoqda." - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mật khẩu của bạn đã được đổi. Hãy giữ nó cẩn thận." - } - }, - "xh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Iphasiwedi yakho itshintshiwe. Nceda uyigcine ikhuselekile." - } - }, - "zh-CN" : { - "stringUnit" : { - "state" : "translated", - "value" : "您的密码已经设定。请妥善保管。" - } - }, - "zh-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "您的密碼變更完成。請注意保管。" + "value" : "Ваш пароль змінено. Будь ласка, зберігайте його надійно." } } } }, - "passwordChangeDescription" : { + "passwordChangeShortDescription" : { "extractionState" : "manual", "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Verander die wagwoord wat benodig word om {app_name} te ontsluit." - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "تغيير كلمة السر المطلوبة لفتح {app_name}." - } - }, "az" : { "stringUnit" : { "state" : "translated", "value" : "{app_name} kilidini açmaq üçün tələb olunan parolu dəyişdir." } }, - "bal" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} کو ان لاک کرنے کے لئے درکار پاس ورڈ تبدیل کریں۔" - } - }, - "be" : { - "stringUnit" : { - "state" : "translated", - "value" : "Змяніць пароль для разблакоўкі {app_name}." - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Сменете паролата, изисквана за отключване на {app_name}." - } - }, - "bn" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} আনলক করতে প্রয়োজনীয় পাসওয়ার্ড পরিবর্তন করুন।" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Canvia la contrasenya requerida per desblocar {app_name}." - } - }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Změňte heslo pro odemykání {app_name}." - } - }, - "cy" : { - "stringUnit" : { - "state" : "translated", - "value" : "Newid y cyfrinair sy'n angenrheidiol i ddatgloi {app_name}." - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Skift adgangskoden, der kræves for at låse {app_name} op." - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Das Passwort zum Entsperren von {app_name} ändern." - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Αλλαγή του κωδικού πρόσβασης που απαιτείται για το ξεκλείδωμα του {app_name}." + "value" : "Změnit heslo pro odemykání {app_name}." } }, "en" : { @@ -327899,220 +333289,10 @@ "value" : "Change the password required to unlock {app_name}." } }, - "eo" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ŝanĝi la pasvorton, kiu necesas por malŝlosi {app_name}." - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cambiar la contraseña necesaria para desbloquear {app_name}." - } - }, - "es-ES" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cambiar la contraseña requerida para desbloquear {app_name}." - } - }, - "et" : { - "stringUnit" : { - "state" : "translated", - "value" : "Muuda parooli, mida on vaja {app_name} avamiseks." - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Change the password required to unlock {app_name}." - } - }, - "fa" : { - "stringUnit" : { - "state" : "translated", - "value" : "رمز عبور مورد نیاز برای باز کردن {app_name} را تغییر بده." - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vaihda {app_name} in avaukseen käytettävä salasana." - } - }, - "fil" : { - "stringUnit" : { - "state" : "translated", - "value" : "Palitan ang password na kinakailangan para i-unlock ang {app_name}." - } - }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Modifier le mot de passe requis pour déverrouiller {app_name}" - } - }, - "gl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cambia o contrasinal necesario para desbloquear {app_name}." - } - }, - "ha" : { - "stringUnit" : { - "state" : "translated", - "value" : "Canza kalmar sirrin da ake bukata don buɗe {app_name}." - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "שנה את הסיסמה הנדרשת לפתיחת {app_name}." - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} को अनलॉक करने के लिए आवश्यक पासवर्ड बदलें।" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Promijenite lozinku potrebnu za otključavanje {app_name}." - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} alkalmazás jelszavának megváltoztatása." - } - }, - "hy-AM" : { - "stringUnit" : { - "state" : "translated", - "value" : "Փոխեք {app_name}-ն ապակողպելու համար պահանջվող գաղտնաբառը:" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ubah kata sandi yang diperlukan untuk membuka kunci {app_name}." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cambia la password richiesta per sbloccare {app_name}." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name}のロック解除に必要なパスワードを変更します" - } - }, - "ka" : { - "stringUnit" : { - "state" : "translated", - "value" : "პაროლის შეცვლა აუცილებელია {app_name}-ის გახსნისთვის." - } - }, - "km" : { - "stringUnit" : { - "state" : "translated", - "value" : "ប្ដូរពាក្យសម្ងាត់ដែលបានតម្រូវឲ្យមានដើម្បីឈប់ទប់ស្កាត់ {app_name}។" - } - }, - "kn" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} ತೆಗೆಯಲು ಬೇಕಾದ ಪಾಸ್ವರ್ಡ್ ಬದಲಾಯಿಸಿ." - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} 잠금 해제 시 사용되는 비밀번호를 변경합니다." - } - }, - "ku" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} وشە نهێنی بگۆڕە بۆ کردنەوەی" - } - }, - "ku-TR" : { - "stringUnit" : { - "state" : "translated", - "value" : "şîfreyê ku ji bo vekirina {app_name} lazim e biguherîne." - } - }, - "lg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Change the password required to unlock {app_name}." - } - }, - "lo" : { - "stringUnit" : { - "state" : "translated", - "value" : "ປ່ຽນລະຫັດຕົກທາງທີ່ຈະເຜີດ {app_name}." - } - }, - "lt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pakeisti slaptažodį, reikalingą atrakinti {app_name}." - } - }, - "lv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mainīt paroli, kas nepieciešama {app_name} atbloķēšanai." - } - }, - "mk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Смени ја лозинката што е потребна за отклучување {app_name}." - } - }, - "mn" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} -г нээхийн тулд шаардлагатай нууц үгийг өөрчлөх." - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tukar kata laluan yang diperlukan untuk membuka kunci {app_name}." - } - }, - "my" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} ဖြင့် လော့ခ်ဖွင့်ရန် လျှို့ဝှက် စကားဝှက် ပြောင်းပါ" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Endre passordet som kreves for å låse opp {app_name}." - } - }, - "nb-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Endre passordet som kreves for å låse opp {app_name}." - } - }, - "ne-NP" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} अनलक गर्न आवश्यक पासवर्ड परिवर्तन गर्नुहोस्।" + "value" : "Obligation de changer le mot de passe pour déverrouiller {app_name}." } }, "nl" : { @@ -328121,52 +333301,10 @@ "value" : "Wijzig het wachtwoord dat nodig is om {app_name} te ontgrendelen." } }, - "nn-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Endre passordet som krevst for å låsa opp {app_name}." - } - }, - "ny" : { - "stringUnit" : { - "state" : "translated", - "value" : "Change the password required to unlock {app_name}." - } - }, - "pa-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} ਨੂੰ ਅਨਲੌਕ ਕਰਨ ਲਈ ਲੋੜੀਂਦੇ ਪਾਸਵਰਡ ਨੂੰ ਬਦਲੋ।" - } - }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Zmień hasło wymagane do odblokowania aplikacji {app_name}." - } - }, - "ps" : { - "stringUnit" : { - "state" : "translated", - "value" : "د {app_name} خلاصولو لپاره اړین پاسورډ بدل کړئ." - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Altere a senha necessária para desbloquear {app_name}." - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Altere a palavra-passe, necessária para desbloquear {app_name}." - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Schimbați parola necesară pentru a debloca {app_name}." + "value" : "Zmień hasło wymagane do odblokowania {app_name}." } }, "ru" : { @@ -328175,82 +333313,10 @@ "value" : "Измените пароль, необходимый для разблокировки {app_name}." } }, - "sh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Promeni lozinku potrebnu za otključavanje {app_name}." - } - }, - "si-LK" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} අගුළු විවෘත කිරීමට අවශ්‍ය මුරපදය වෙනස් කරන්න." - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zmeňte heslo potrebné na odomknutie {app_name}." - } - }, - "sl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Spremeni geslo potrebno za odklepanje {app_name}." - } - }, - "sq" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ndryshoni fjalëkalimin e kërkuar për të zhbllokuar {app_name}." - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Измените лозинку потребну за откључавање {app_name}." - } - }, - "sr-Latn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Promenite lozinku koja je potrebna za otključavanje {app_name}." - } - }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Ändra lösenordet som krävs för att låsa upp {app_name}." - } - }, - "sw" : { - "stringUnit" : { - "state" : "translated", - "value" : "Badilisha nywila inayohitajika kufungua {app_name}." - } - }, - "ta" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} ேத்தUnlock ச்சபட செய்ய வேண்டிய கடவுச்சொல்லை மாற்றவும்." - } - }, - "te" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} ని అన్‌లాక్ చేయడానికి అవసరమైన పాస్‌వర్డ్ మార్చండి." - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "เปลี่ยนรหัสผ่านที่ใช้ปลดล็อก {app_name}" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} kilidini açmak için gereken parolayı değiştirin." + "value" : "Ändra lösenordet som krävs att låsa upp {app_name}." } }, "uk" : { @@ -328258,42 +333324,6 @@ "state" : "translated", "value" : "Змінити пароль, необхідний для розблокування {app_name}." } - }, - "ur-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} کو ان لاک کرنے کے لیے مطلوبہ پاس ورڈ تبدیل کریں۔" - } - }, - "uz" : { - "stringUnit" : { - "state" : "translated", - "value" : "O'zingizga {app_name}ni ochish uchun zarur parolni o'zgartiring." - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Đổi mật khẩu cần thiết để mở khóa {app_name}." - } - }, - "xh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tshintsha i-password efunekayo ukusikhulula {app_name}." - } - }, - "zh-CN" : { - "stringUnit" : { - "state" : "translated", - "value" : "更改{app_name}的解锁密码" - } - }, - "zh-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "更改解鎖 {app_name} 所需的密碼。" - } } } }, @@ -328785,70 +333815,16 @@ "passwordCreate" : { "extractionState" : "manual", "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Skep jou wagwoord" - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "إنشاء كلمة سر" - } - }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "Parolunuzu yaradın" - } - }, - "bal" : { - "stringUnit" : { - "state" : "translated", - "value" : "اپنی رمز بناؤ" - } - }, - "be" : { - "stringUnit" : { - "state" : "translated", - "value" : "Стварыць пароль" - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Създай своя парола" - } - }, - "bn" : { - "stringUnit" : { - "state" : "translated", - "value" : "আপনার পাসওয়ার্ড তৈরি করুন" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Crea la teva contrasenya" + "value" : "Parol yarat" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Vytvořte si heslo" - } - }, - "cy" : { - "stringUnit" : { - "state" : "translated", - "value" : "Creu eich cyfrinair" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Opret din adgangskode" + "value" : "Vytvořit heslo" } }, "de" : { @@ -328857,256 +333833,22 @@ "value" : "Passwort erstellen" } }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Δημιουργήστε τον κωδικό σας πρόσβασης" - } - }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Create your password" - } - }, - "eo" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kreu vian pasvorton" - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Crea tu contraseña" - } - }, - "es-ES" : { - "stringUnit" : { - "state" : "translated", - "value" : "Crea tu contraseña" - } - }, - "et" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loo oma parool" - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sortu zure pasahitza" - } - }, - "fa" : { - "stringUnit" : { - "state" : "translated", - "value" : "تغییر گذرواژه" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Luo salasana" - } - }, - "fil" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lumikha ng password mo" + "value" : "Create Password" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Créez votre mot de passe" - } - }, - "gl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Crear contrasinal" - } - }, - "ha" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ƙirƙiri kalmar sirrinka" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "צור סיסמה" - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "अपना पासवर्ड बनाएं" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Izradite lozinku" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jelszó létrehozása" - } - }, - "hy-AM" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ստեղծել գաղտնաբառ" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "Buat kata sandi Anda" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Crea password" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "パスワードを作成してください" - } - }, - "ka" : { - "stringUnit" : { - "state" : "translated", - "value" : "შექმენით თქვენი პაროლი" - } - }, - "km" : { - "stringUnit" : { - "state" : "translated", - "value" : "បង្កើតពាក្យសម្ងាត់របស់អ្នក" - } - }, - "kn" : { - "stringUnit" : { - "state" : "translated", - "value" : "ನಿಮ್ಮ ಗುಪ್ತಪದವನ್ನು ರಚಿಸಿ" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "비밀번호 만들기" - } - }, - "ku" : { - "stringUnit" : { - "state" : "translated", - "value" : "تێپەڕوشەکەت دروست بکە" - } - }, - "ku-TR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Şîfreya xwe çêke" - } - }, - "lg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kilira akakufulu ko" - } - }, - "lo" : { - "stringUnit" : { - "state" : "translated", - "value" : "ເກີງແທ" - } - }, - "lt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sukurti slaptažodį" - } - }, - "lv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Izveidot savu paroli" - } - }, - "mk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Креирај ја твојата лозинка" - } - }, - "mn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Нууц үгээ оруулах" - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cipta kata laluan anda" - } - }, - "my" : { - "stringUnit" : { - "state" : "translated", - "value" : "စကားဝှက်ကိုဖန်တီးကာ ပြုလုပ်ပါ။" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Opprett passordet ditt" - } - }, - "nb-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Opprett passordet" - } - }, - "ne-NP" : { - "stringUnit" : { - "state" : "translated", - "value" : "तपाईंको पासवर्ड बनाउनुहोस्" + "value" : "Créer un mot de passe" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Maak je wachtwoord aan" - } - }, - "nn-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Opprett passordet ditt" - } - }, - "ny" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pangani mawu achinsinsi anu" - } - }, - "pa-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "ਆਪਣਾ ਪਾਸਵਰਡ ਬਨਾਓ" + "value" : "Wachtwoord aanmaken" } }, "pl" : { @@ -329115,154 +333857,28 @@ "value" : "Utwórz hasło" } }, - "ps" : { - "stringUnit" : { - "state" : "translated", - "value" : "حساب جوړول سمدستي، وړیا او بې نومه دی" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Crie a sua senha" - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Crie a sua palavra-passe" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Creează-ți parola" - } - }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Создать пароль" } }, - "sh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kreiraj lozinku" - } - }, - "si-LK" : { - "stringUnit" : { - "state" : "translated", - "value" : "ඔබගේ මුරපදය සාදන්න" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vytvorte si heslo" - } - }, - "sl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ustvari svoje geslo" - } - }, - "sq" : { - "stringUnit" : { - "state" : "translated", - "value" : "Krijo fjalëkalimin tënd" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Креирајте вашу лозинку" - } - }, - "sr-Latn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kreiraj svoju lozinku" - } - }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Skapa ditt lösenord" - } - }, - "sw" : { - "stringUnit" : { - "state" : "translated", - "value" : "Unda nenosiri lako" - } - }, - "ta" : { - "stringUnit" : { - "state" : "translated", - "value" : "உங்கள் கடவுச்சொல்லை உருவாக்குக" - } - }, - "te" : { - "stringUnit" : { - "state" : "translated", - "value" : "మీ పాస్‌వర్డ్ సృష్టించండి" - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "Create your password" + "value" : "Skapa lösenord" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Parolanızı oluşturun" + "value" : "Şifre Oluştur" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Створіть пароль" - } - }, - "ur-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "اپنا پاس ورڈ بنائیں" - } - }, - "uz" : { - "stringUnit" : { - "state" : "translated", - "value" : "Parolni yarating" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tạo mật khẩu của bạn" - } - }, - "xh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Yenza iphasiwedi yakho" - } - }, - "zh-CN" : { - "stringUnit" : { - "state" : "translated", - "value" : "创建您的密码" - } - }, - "zh-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "建立你的密碼" + "value" : "Створити пароль" } } } @@ -329746,485 +334362,6 @@ } } }, - "passwordDescription" : { - "extractionState" : "manual", - "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vereis wagwoord om {app_name} oop te maak." - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "يتطلب كلمة السر لفتح {app_name}." - } - }, - "az" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} kilidini açmaq üçün parol tələb et." - } - }, - "bal" : { - "stringUnit" : { - "state" : "translated", - "value" : "پاسورڈ درکار و بند را {app_name}." - } - }, - "be" : { - "stringUnit" : { - "state" : "translated", - "value" : "Неабходны пароль для разблакіроўкі {app_name}." - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Изисквайте парола за отключване на {app_name}." - } - }, - "bn" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} আনলক করতে পাসওয়ার্ড প্রয়োজন।" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Requereix contrasenya per a desbloquejar {app_name}." - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vyžadovat heslo k odemknutí {app_name}." - } - }, - "cy" : { - "stringUnit" : { - "state" : "translated", - "value" : "Angen cyfrinair i ddatgloi {app_name}." - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kræv adgangskode for at låse {app_name} op." - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zum Entsperren von {app_name} ist Passwort erforderlich." - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Να απαιτείται κωδικός πρόσβασης για το ξεκλείδωμα του {app_name}." - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Require password to unlock {app_name}." - } - }, - "eo" : { - "stringUnit" : { - "state" : "translated", - "value" : "Postuli pasvorton por malŝlosi {app_name}." - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Necesita contraseña para desbloquear {app_name}." - } - }, - "es-ES" : { - "stringUnit" : { - "state" : "translated", - "value" : "Se requiere contraseña para desbloquear {app_name}." - } - }, - "et" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nõutav parool {app_name} avamiseks." - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Eskatu pasahitza {app_name} desblokeatzeko." - } - }, - "fa" : { - "stringUnit" : { - "state" : "translated", - "value" : "برای بازشدن قفل {app_name} به رمز نیاز است." - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vaadi {app_name}in avaukseen salasana." - } - }, - "fil" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nangangailangan ng password para i-unlock ang {app_name}." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mot de passe requis pour déverrouiller {app_name}." - } - }, - "gl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Requerir contrasinal para desbloquear {app_name}." - } - }, - "ha" : { - "stringUnit" : { - "state" : "translated", - "value" : "Buƙatar kalmar sirri don buɗe {app_name}." - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "דרוש סיסמה לביטול נעילת {app_name}." - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} को अनलॉक करने के लिए पासवर्ड की आवश्यकता है।" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zahtijevaj lozinku za otključavanje {app_name}." - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jelszó szükséges {app_name} feloldásához." - } - }, - "hy-AM" : { - "stringUnit" : { - "state" : "translated", - "value" : "Պահանջվում է գաղտնաբառ՝ {app_name}-ը ապակողպելու համար:" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "Memerlukan kata sandi untuk membuka kunci {app_name}." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Richiede la password per sbloccare {app_name}." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} のロックを解除するにはパスワードが必要です" - } - }, - "ka" : { - "stringUnit" : { - "state" : "translated", - "value" : "მოთხოვნათა პაროლის აპლიკაციის განბლოკვისათვის {app_name}." - } - }, - "km" : { - "stringUnit" : { - "state" : "translated", - "value" : "តម្រូវឲ្យមានពាក្យសម្ងាត់ដើម្បីឈប់ទប់ស្កាត់ {app_name}។" - } - }, - "kn" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} ಅನ್ನು ಅನ್ಲಾಕ್ ಮಾಡಲು ಪಾಸ್ವರ್ಡ್ ಅಗತ್ಯವಿದೆ." - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} 잠금 해제에 비밀번호가 필요합니다." - } - }, - "ku" : { - "stringUnit" : { - "state" : "translated", - "value" : "پێویستە تێپەڕەوشە بکرێتە کار {app_name}." - } - }, - "ku-TR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Şîfreya ku vekirinîna {app_name} lazim bike." - } - }, - "lg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tegeka okulaba nte ebeera na password eri okukuta {app_name}." - } - }, - "lt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Reikalingas slaptažodis, kad atrakintumėte {app_name}." - } - }, - "lv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nepieciešama parole, lai atbloķētu {app_name}." - } - }, - "mk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Бара лозинка за отклучување на {app_name}." - } - }, - "mn" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name}-ыг нээхийн тулд нууц үг шаардана." - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "Memerlukan kata laluan untuk membuka kunci {app_name}." - } - }, - "my" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name}ကို လော့ခ်ဖွင့်ရန် စကားဝှက် လိုအပ်သည်။" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Krev passord for å låse opp {app_name}." - } - }, - "nb-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Krev passord for å låse opp {app_name}." - } - }, - "ne-NP" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} अनलक गर्न पासवर्ड आवश्यक छ।" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wachtwoord vereisen om {app_name} te ontgrendelen." - } - }, - "nn-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Krev passord for å låsa opp {app_name}." - } - }, - "ny" : { - "stringUnit" : { - "state" : "translated", - "value" : "Funsani achinsinsi kuti mutsegule {app_name}." - } - }, - "pa-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} ਨੂੰ ਅਣਲੌਕ ਕਰਨ ਲਈ ਪਾਸਵਰਡ ਦੀ ਲੋੜ ਹੁੰਦੀ ਹੈ ਜੀ." - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wymagaj hasła, aby odblokować aplikację {app_name}." - } - }, - "ps" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} خلاصول لپاره پاسورډ لازمي دی." - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Exigir senha para desbloquear {app_name}." - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Solicitar palavra-passe para desbloquear {app_name}." - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Necesită parolă pentru a debloca {app_name}." - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Требовать пароль для разблокировки {app_name}." - } - }, - "sh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zahtevaj lozinku za otključavanje {app_name}." - } - }, - "si-LK" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} අගුළු විවෘත කිරීම සඳහා මුරපදයක් අවශ්‍ය වේ." - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vyžadovať heslo na odomknutie {app_name}." - } - }, - "sl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zahtevaj geslo za odklepanje {app_name}." - } - }, - "sq" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kërko fjalëkalimin për të zhbllokuar {app_name}-in." - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Захтевај лозинку за откључавање {app_name}." - } - }, - "sr-Latn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Potrebna je lozinka da otključate {app_name}." - } - }, - "sv-SE" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kräv lösenord för att låsa upp {app_name}." - } - }, - "sw" : { - "stringUnit" : { - "state" : "translated", - "value" : "omba nywila kufungua {app_name}." - } - }, - "ta" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} இல்லாமல் திறக்க கடவுச்சொல்லைஉடன் வேண்டுகின்றேன்." - } - }, - "te" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name}ని అన్లాక్ చేయడానికి పాస్వర్డ్ అవసరం." - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "จำเป็นต้องใช้รหัสผ่านในการปลดล็อก {app_name}" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} uygulamasının kilidini açmak için şifre iste." - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Вимагати пароль для розблокування {app_name}." - } - }, - "ur-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} کو ان لاک کرنے کے لئے پاس ورڈ درکار ہے۔" - } - }, - "uz" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} ni ochish uchun parolni talab qilish." - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Yêu cầu mật khẩu để mở khóa {app_name}." - } - }, - "xh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ifuna iphaswedi ukuvula {app_name}." - } - }, - "zh-CN" : { - "stringUnit" : { - "state" : "translated", - "value" : "需要设置密码以解锁{app_name}。" - } - }, - "zh-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "需要密碼才能解鎖 {app_name}。" - } - } - } - }, "passwordEnter" : { "extractionState" : "manual", "localizations" : { @@ -332144,478 +336281,124 @@ "passwordErrorLength" : { "extractionState" : "manual", "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wagwoord moet tussen 6 en 64 karakters lank wees" - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "كلمة المرور يجب ان تكون بين 6 و 64 عنصر" - } - }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "Parol 6-64 simvol uzunluğunda olmalıdır" - } - }, - "bal" : { - "stringUnit" : { - "state" : "translated", - "value" : "پاسورڈ 6 تا 64 حرفء درمیان بوت" - } - }, - "be" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ваш пароль павінен мець даўжыню ў межах ад 6 да 64 сімвалаў" - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Паролата трябва да е между 6 и 64 символа" - } - }, - "bn" : { - "stringUnit" : { - "state" : "translated", - "value" : "পাসওয়ার্ড ৬ থেকে ৬৪ অক্ষরের মধ্যে হতে হবে" + "value" : "Parol, {min} ilə {max} xarakter uzunluğunda olmalıdır" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "La contrasenya ha de ser d'entre 6 i 64 caràcters" + "value" : "La contrasenya ha d'estar entre {min} i {max} caràcters" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Heslo musí mít od 6 do 64 znaků" - } - }, - "cy" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rhaid i gyfrinair fod rhwng 6 a 64 nod o hyd" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Adgangskoden skal være mellem 6 til 64 tegn" + "value" : "Heslo musí mít od {min} do {max} znaků" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Das Passwort muss zwischen 6 und 64 Zeichen lang sein" - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ο κωδικός πρόσβασης πρέπει να αποτελείται από 6 έως 64 χαρακτήρες" + "value" : "Das Passwort muss zwischen {min} und {max} Zeichen lang sein" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Password must be between 6 and 64 characters long" - } - }, - "eo" : { - "stringUnit" : { - "state" : "translated", - "value" : "Necesas, ke pasvorto estu inter 6 kaj 64 longe" + "value" : "Password must be between {min} and {max} characters long" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "La contraseña debe tener entre 6 y 64 caracteres" + "value" : "La contraseña debe tener entre {min} y {max} caracteres de longitud" } }, "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "La contraseña debe tener entre 6 y 64 caracteres de longitud" - } - }, - "et" : { - "stringUnit" : { - "state" : "translated", - "value" : "Parool peab olema 6 kuni 64 tähemärki pikk" - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pasahitzak 6 eta 64 karaktere bitartekoa izan behar du" - } - }, - "fa" : { - "stringUnit" : { - "state" : "translated", - "value" : "طول رمز عبور باید بین ۶ تا ۶۴ کاراکتر باشد" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Salasanan tulee olla 6-64 merkkiä pitkä" - } - }, - "fil" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dapat may haba na 6 hanggang 20 titik ang 'yong password" + "value" : "La contraseña debe tener entre {min} y {max} caracteres de longitud" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Le mot de passe doit avoir une longueur comprise entre 6 et 64 caractères" - } - }, - "gl" : { - "stringUnit" : { - "state" : "translated", - "value" : "O contrasinal debe ter entre 6 e 64 caracteres" - } - }, - "ha" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kalmar sirri dole ta kasance tsakanin haruffa 6 zuwa 64" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "הסיסמא חייבת להיות באורך של בין 6 ל-64 תווים" + "value" : "Le mot de passe doit contenir entre {min} et {max} caractères" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "पासवर्ड 6 से 64 वर्णों के बीच होना चाहिए" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lozinka mora imati između 6 i 64 znakova" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "A jelszónak minimum 6 és maximum 64 karakter hosszúságúnak kell lennie" - } - }, - "hy-AM" : { - "stringUnit" : { - "state" : "translated", - "value" : "Գաղտնաբառը պետք է լինի 6-ից 64 նիշ" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "Panjang kata sandi anda harus diantara 6 dan 64 karakter" + "value" : "पासवर्ड की लंबाई {min} से {max} वर्णों के बीच होनी चाहिए" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "La password deve essere lunga tra i 6 e i 64 caratteri" + "value" : "La password deve essere lunga tra {min} e {max} caratteri" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "パスワードの長さを6文字から64文字にしてください" - } - }, - "ka" : { - "stringUnit" : { - "state" : "translated", - "value" : "პაროლი უნდა იყოს 6-64 სიმბოლოს სიგრძის" - } - }, - "km" : { - "stringUnit" : { - "state" : "translated", - "value" : "ពាក្យ​សម្ងាត់​ត្រូវ​តែ​មាន​ចន្លោះ​ពី 6 ទៅ 64 តួអក្សរ" - } - }, - "kn" : { - "stringUnit" : { - "state" : "translated", - "value" : "ಪಾಸ್ವರ್ಡ್ 6.೦ರಿಂದ 64 ಅಕ್ಷರಗಳಷ್ಟು ಉದ್ದವು ಇರಬೇಕು" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "비밀번호는 반드시 6자에서 12자 사이어야 합니다." - } - }, - "ku" : { - "stringUnit" : { - "state" : "translated", - "value" : "تێپەڕبووی تێپەڕاندەکانی تێپەڕەبە دووبارە بکەوە دەگل ناچێ." - } - }, - "ku-TR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Divê şîfre di navbera dirêjiya 6 û 64 karakteran de be" - } - }, - "lg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Akasumulizo kalina kubeera wakati wa ennyukuta 6 ne 64." - } - }, - "lt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Password must be between 6 and 64 characters long" - } - }, - "lv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Parolei jābūt no 6 līdz 64 rakstzīmēm garai" - } - }, - "mk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Лозинката мора да содржи помеѓу 6 и 64 карактери" - } - }, - "mn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Нууц үг 6-аас 64 тэмдэгттэй байх ёстой" - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kata laluan mestilah antara 6 hingga 64 aksara panjang" - } - }, - "my" : { - "stringUnit" : { - "state" : "translated", - "value" : "စကားဝှက်သည်စာလုံးများအကြား ၆ မှ ၆၄ ရှိရမည်" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Passordet må være mellom 6 og 64 tegn langt" - } - }, - "nb-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Passordet må være mellom 6 og 64 tegn langt" - } - }, - "ne-NP" : { - "stringUnit" : { - "state" : "translated", - "value" : "पासवर्ड ६ देखि ६४ वर्णको बीचमा हुनुपर्छ" + "value" : "パスワードの長さを{min}文字から{max}文字にしてください" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Wachtwoord moet tussen de 6 en 64 tekens lang zijn" - } - }, - "nn-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Passordet må være mellom 6 og 64 tegn langt" - } - }, - "ny" : { - "stringUnit" : { - "state" : "translated", - "value" : "Chinsinsi chiyenera kutalika pakati pa zilembo 6 ndi 64" - } - }, - "pa-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "ਪਾਸਵਰਡ 6 ਤੋਂ 64 ਅੱਖਰ ਲੰਮਾ ਹੋਣਾ ਚਾਹੀਦਾ ਹੈ" + "value" : "Wachtwoord moet tussen de {min} en {max} tekens lang zijn" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Hasło musi zawierać od 6 do 64 znaków" - } - }, - "ps" : { - "stringUnit" : { - "state" : "translated", - "value" : "پاسورډ باید د 6 او 64 تورو ترمنځ وي" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "A senha deve ter entre 6 e 64 caracteres" + "value" : "Hasło musi zawierać od {min} do {max} znaków" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "A palavra-passe deve ter entre 6 e 64 carateres" + "value" : "A palavra-passe deve ter entre {min} e {max} carateres" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Parola trebuie să aibă între 6 și 64 de caractere lungime" + "value" : "Parola trebuie să aibă între {min} și {max} caractere." } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Пароль должен содержать от 6 до 64 символов" - } - }, - "sh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lozinka mora imati između 6 i 64 znakova" - } - }, - "si-LK" : { - "stringUnit" : { - "state" : "translated", - "value" : "මුරපදය අක්ෂර 6 සහ 64 අතර දිග විය යුතුය" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Heslo musí mať dĺžku 6 až 64 znakov" - } - }, - "sl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Geslo mora biti dolgo med 6 in 64 znakov" - } - }, - "sq" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fjalëkalimi duhet të jetë midis 6 dhe 64 karaktere të gjata" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Лозинка мора имати између 6 и 64 карактера" - } - }, - "sr-Latn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lozinka mora imati između 6 i 64 karaktera" + "value" : "Пароль должен содержать от {min} до {max} символов" } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Lösenordet måste vara mellan 6 och 64 tecken långt" - } - }, - "sw" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nywila lazima iwe kati ya herufi 6 na 64" - } - }, - "ta" : { - "stringUnit" : { - "state" : "translated", - "value" : "கடவுச்சொல் 6 முதல் 64 எழுத்துக்களினிடையே இருக்க வேண்டும்" - } - }, - "te" : { - "stringUnit" : { - "state" : "translated", - "value" : "పాస్‌వర్డ్ 6 మరియు 64 అక్షరాల మధ్య ఉండాలి" - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "รหัสผ่านต้องมีความยาวตั้งแต่ 6 ถึง 64 ตัวอักษร" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Parolanız 6 ila 64 karakter uzunluğu aralığında olmalıdır" + "value" : "Lösenordet måste vara mellan {min} och {max} tecken långt" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Пароль має бути довжиною від 6 до 64 символів" - } - }, - "ur-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "پاس ورڈ 6 اور 64 حروف کے درمیان ہونا چاہیے" - } - }, - "uz" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sizning parolingiz uzunligi 6 va 64 belgidan iborat bo'lishi mumkin" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mật khẩu phải dài từ 6 đến 64 ký tự" - } - }, - "xh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Iphasiwedi mayibe phakathi kwe-6 ne-64 iimpawu ngokubude" + "value" : "Пароль має містити від {min} до {max} символів" } }, "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "密码长度必须在6到64个字符之间" + "value" : "密码长度必须在{min}到{max}个字符之间" } }, "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "密碼必須介於6到64個字元之間。" + "value" : "密碼必須介於 {min} 到 {max} 個字元之間" } } } @@ -334057,6 +337840,77 @@ } } }, + "passwordNewConfirm" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yeni parolu təsdiqlə" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Potvrďte nové heslo" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Neues Passwort wiederholen" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Confirm New Password" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Confirmer le nouveau mot de passe" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bevestig nieuwe wachtwoord" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Potwierdź nowe hasło" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Confirmați noua parolă" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подтвердите новый пароль" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bekräfta nytt lösenord" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Підтвердити новий пароль" + } + } + } + }, "passwordRemove" : { "extractionState" : "manual", "localizations" : { @@ -334536,73 +338390,19 @@ } } }, - "passwordRemovedDescription" : { + "passwordRemovedDescriptionToast" : { "extractionState" : "manual", "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jou wagwoord is verwyder." - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "تمت إزالة كلمة السر الخاصة بك." - } - }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "Parolunuz silindi." - } - }, - "bal" : { - "stringUnit" : { - "state" : "translated", - "value" : "ما گپ درخواست قبول کردی بیک پاسکوڈ ہٹاٹی." - } - }, - "be" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ваш пароль быў выдалены." - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Вашата парола беше премахната." - } - }, - "bn" : { - "stringUnit" : { - "state" : "translated", - "value" : "আপনার পাসওয়ার্ড সরানো হয়েছে।" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "La vostra contrasenya s'ha eliminat." + "value" : "Parolunuz silinib." } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Vaše heslo bylo odstraněno." - } - }, - "cy" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mae eich cyfrinair wedi'i dynnu." - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Din adgangskode er blevet fjernet." + "value" : "Vaše heslo bylo odebráno." } }, "de" : { @@ -334611,328 +338411,40 @@ "value" : "Dein Passwort wurde entfernt." } }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ο κωδικός πρόσβασής σας έχει αφαιρεθεί." - } - }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Your password has been removed." } }, - "eo" : { - "stringUnit" : { - "state" : "translated", - "value" : "Via pasvorto estas forigita." - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tu contraseña ha sido eliminada." - } - }, - "es-ES" : { - "stringUnit" : { - "state" : "translated", - "value" : "Has eliminado tu contraseña." - } - }, - "et" : { - "stringUnit" : { - "state" : "translated", - "value" : "Teie parool on eemaldatud." - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zure pasahitza kendu da." - } - }, - "fa" : { - "stringUnit" : { - "state" : "translated", - "value" : "گذرواژه شما حذف شده است." - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Salasanasi on on poistettu." - } - }, - "fil" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ang iyong password ay naalis na." - } - }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Votre mot de passe a été supprimé." } }, - "gl" : { - "stringUnit" : { - "state" : "translated", - "value" : "O teu contrasinal foi eliminado." - } - }, - "ha" : { - "stringUnit" : { - "state" : "translated", - "value" : "An cire kalmar sirrinku." - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "הסיסמה שלך הוסרה." - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "आपका पासवर्ड हटा दिया गया है।" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vaša lozinka je uklonjena." - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "A jelszavadat eltávolítottuk." - } - }, - "hy-AM" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ձեր գաղտնաբառը հեռացվել է։" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kata sandi Anda telah dihapus." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "La tua password è stata rimossa." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "パスワードを削除しました。" - } - }, - "ka" : { - "stringUnit" : { - "state" : "translated", - "value" : "თქვენი პაროლი წაშლილია." - } - }, - "km" : { - "stringUnit" : { - "state" : "translated", - "value" : "ពាក្យសម្ងាត់ របស់អ្នកត្រូវបានលុបចេញ។" - } - }, - "kn" : { - "stringUnit" : { - "state" : "translated", - "value" : "ನಿಮ್ಮ ಗುಪ್ತಪದವನ್ನು ತೆಗೆದುಹಾಕಲಾಗಿದೆ." - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "당신의 비밀번호가 제거되었습니다." - } - }, - "ku" : { - "stringUnit" : { - "state" : "translated", - "value" : "وشەی پەرەسەدت وەکبێژاند." - } - }, - "ku-TR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zoom" - } - }, - "lg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Password yo ekatutuzzibwa." - } - }, - "lt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jūsų slaptažodis buvo pašalintas." - } - }, - "lv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jūsu parole tika noņemta." - } - }, - "mk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Вашата лозинка е отстранета." - } - }, - "mn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Таны нууц үг устгагдсан." - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kata laluan anda telah dibuang." - } - }, - "my" : { - "stringUnit" : { - "state" : "translated", - "value" : "သင်၏ စကားဝှက် ဖယ်ရှားပြီးပါပြီ။" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Passordet ditt er fjernet." - } - }, - "nb-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Passordet ditt har blitt fjernet." - } - }, - "ne-NP" : { - "stringUnit" : { - "state" : "translated", - "value" : "तपाईँको पासवर्ड हटाइएको छ।" - } - }, "nl" : { "stringUnit" : { "state" : "translated", "value" : "Uw wachtwoord is verwijderd." } }, - "nn-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Passordet ditt er blitt fjerna." - } - }, - "ny" : { - "stringUnit" : { - "state" : "translated", - "value" : "Password yanu yachotsedwa." - } - }, - "pa-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "ਤੁਹਾਡਾ ਪਾਸਵਰਡ ਹਟਾ ਦਿੱਤਾ ਗਿਆ ਹੈ।" - } - }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Usunięto hasło" - } - }, - "ps" : { - "stringUnit" : { - "state" : "translated", - "value" : "ستاسو پاسورډ لرې شوی دی." - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sua senha foi removida." - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "A sua palavra-passe foi removida." + "value" : "Twoje hasło zostało usunięte." } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Parola ta a fost eliminată." + "value" : "Parola ta a fost ștearsă." } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Ваш пароль удален." - } - }, - "sh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tvoja šifra je uklonjena." - } - }, - "si-LK" : { - "stringUnit" : { - "state" : "translated", - "value" : "ඔබගේ මුරපදය ඉවත් කර ඇත." - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vaše heslo bolo odstránené." - } - }, - "sl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vaše geslo je bilo odstranjeno." - } - }, - "sq" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fjalëkalimi juaj është hequr." - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ваша лозинка је уклоњена." - } - }, - "sr-Latn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vaša lozinka je uklonjena." + "value" : "Ваш пароль удалён." } }, "sv-SE" : { @@ -334941,555 +338453,146 @@ "value" : "Ditt lösenord har tagits bort." } }, - "sw" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nenosiri lako limeondolewa." - } - }, - "ta" : { - "stringUnit" : { - "state" : "translated", - "value" : "உங்களின் கடவுச்சொல் நீக்கப்பட்டது." - } - }, - "te" : { - "stringUnit" : { - "state" : "translated", - "value" : "మీ పాస్‌వర్డ్ తొలగించబడింది." - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "รหัสผ่านของคุณถูกลบแล้ว" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Parolanız kaldırıldı." - } - }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Ваш пароль був видалений." - } - }, - "ur-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "آپ کا پاس ورڈ ہٹا دیا گیا ہے۔" - } - }, - "uz" : { - "stringUnit" : { - "state" : "translated", - "value" : "Parolingiz saqlandi. Iltimos, uni xavfsiz joyda saqlang." - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mật khẩu của bạn đã được gỡ bỏ." - } - }, - "xh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Iphasiwedi yakho isusiwe." - } - }, - "zh-CN" : { - "stringUnit" : { - "state" : "translated", - "value" : "您的密码已被移除。" - } - }, - "zh-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "已移除密碼。" + "value" : "Ваш пароль видалено." } } } }, - "passwordRemoveDescription" : { + "passwordRemoveShortDescription" : { "extractionState" : "manual", "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Verwyder die wagwoord wat nodig is om {app_name} oop te maak." - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "إزالة كلمة السر المطلوبة لفتح {app_name}." - } - }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "{app_name} kilidini açmaq üçün tələb olunan parolu sil." - } - }, - "bal" : { - "stringUnit" : { - "state" : "translated", - "value" : "پاسورڈ برس ک و بندروی {app_name} لایا وانتگ." - } - }, - "be" : { - "stringUnit" : { - "state" : "translated", - "value" : "Выдаліце пароль, неабходны для разблакіроўкі {app_name}." - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Премахнете паролата, необходима за отключване на {app_name}." - } - }, - "bn" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} আনলক করতে প্রয়োজনীয় পাসওয়ার্ড সরান।" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Elimina la contrasenya necessària per desbloquejar {app_name}." + "value" : "{app_name} kilidini açmaq üçün tələb olunan parolu sil" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Odebrat heslo pro odemykání {app_name}." - } - }, - "cy" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tynnu'r cyfrinair sydd ei angen i ddatgloi {app_name}." - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fjern adgangskoden, der kræves for at låse {app_name} op." + "value" : "Odebrat heslo pro odemykání {app_name}" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Das Passwort zum Entsperren von {app_name} entfernen." - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Αφαίρεση του κωδικού πρόσβασης που απαιτείται για το ξεκλείδωμα του {app_name}." + "value" : "Entfernung des Passwortes erforderlich um {app_name} zu entsperren" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Remove the password required to unlock {app_name}." - } - }, - "eo" : { - "stringUnit" : { - "state" : "translated", - "value" : "Forigi la pasvorton necesan por malŝlosi {app_name}." - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Eliminar la contraseña necesaria para desbloquear {app_name}." - } - }, - "es-ES" : { - "stringUnit" : { - "state" : "translated", - "value" : "Eliminar la contraseña requerida para desbloquear {app_name}." - } - }, - "et" : { - "stringUnit" : { - "state" : "translated", - "value" : "Eemalda parool, mis on vajalik {app_name} avamiseks." - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pasahitza kendu {app_name} desblokeatzeko beharrezkoa dena." - } - }, - "fa" : { - "stringUnit" : { - "state" : "translated", - "value" : "رمز عبور مورد نیاز برای باز کردن {app_name} را حذف کن." - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Poista {app_name} avaukseen tarvittava salasana." - } - }, - "fil" : { - "stringUnit" : { - "state" : "translated", - "value" : "Alisin ang password na kinakailangan para i-unlock ang {app_name}." + "value" : "Remove the password required to unlock {app_name}" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Retirer le mot de passe requis pour déverrouiller {app_name}." + "value" : "Retirer le mot de passe requis pour déverrouiller {app_name}" } }, - "gl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Elimina o contrasinal necesario para desbloquear {app_name}." - } - }, - "ha" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cire kalmar sirrin da ake buƙata don buɗe {app_name}." - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "הסר את הסיסמה הנדרשת לביטול נעילת {app_name}." - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "पासवर्ड हटाएं जो {app_name} को अनलॉक करने के लिए आवश्यक है।" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Uklonite lozinku potrebnu za otključavanje {app_name}." - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Távolítsd el a {app_name} alkalmazás jelszavát." - } - }, - "hy-AM" : { - "stringUnit" : { - "state" : "translated", - "value" : "Փոխեք {app_name}-ն ապակողպելու համար պահանջվող գաղտնաբառը:" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hapus kata sandi yang diperlukan untuk membuka kunci {app_name}." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rimuovi la password richiesta per sbloccare {app_name}." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} のロックを解除するために必要なパスワードを削除します" - } - }, - "ka" : { - "stringUnit" : { - "state" : "translated", - "value" : "პაროლის მოხსნა {app_name}'ის განბლოკვისათვის." - } - }, - "km" : { - "stringUnit" : { - "state" : "translated", - "value" : "ប្ដូរពាក្យសម្ងាត់ដែលបានតម្រូវឲ្យមានដើម្បីឈប់ទប់ស្កាត់ {app_name}។" - } - }, - "kn" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} ಅನ್ನು ಅನ್ಲಾಕ್ ಮಾಡಲು ಅಗತ್ಯವಿರುವ ಪಾಸ್ವರ್ಡ್ ತೆಗೆದುಹಾಕಿ." - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} 잠금 해제 시 필요한 비밀번호를 제거합니다." - } - }, - "ku" : { - "stringUnit" : { - "state" : "translated", - "value" : "لابردنی تێپەڕەوشەی پێویست بۆ کردنەوەی {app_name}." - } - }, - "ku-TR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Şîfreya ku ji bo vekirina qefila {app_name} lazim e rake." - } - }, - "lg" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Ggyawo akatambi okwetengerera {app_name}." + "value" : "Verwijder het wachtwoord dat nodig is om {app_name} te ontgrendelen" } }, - "lt" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Pašalinti slaptažodį, reikalingą {app_name} atrakinti." + "value" : "Usuń hasło wymagane do odblokowania {app_name}" } }, - "lv" : { + "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Noņemt paroli, lai atbloķētu {app_name}." + "value" : "Удалить пароль, необходимый для разблокировки {app_name}" } }, - "mk" : { + "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Отстранете ја лозинката потребна за отклучување на {app_name}." + "value" : "Ta bort lösenordet som krävs för att låsa upp {app_name}" } }, - "mn" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "{app_name}-ийг нээхэд шаардлагатай нууц үгийг устгах." + "value" : "Видалити пароль, потрібний для розблокування {app_name}" } - }, - "ms" : { + } + } + }, + "passwords" : { + "extractionState" : "manual", + "localizations" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "Alih Keluar kata laluan yang diperlukan untuk membuka kunci {app_name}." + "value" : "Parollar" } }, - "my" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "{app_name} ကိုလော့ခ်ဖွင့်ရန် လိုအပ်သော စကားဝှက်ကို ဖယ်ရှားပါ။" + "value" : "Hesla" } }, - "nb" : { + "de" : { "stringUnit" : { "state" : "translated", - "value" : "Fjern passordet som kreves for å låse opp {app_name}." + "value" : "Passwörter" } }, - "nb-NO" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Fjern passordet som kreves for å låse opp {app_name}." + "value" : "Passwords" } }, - "ne-NP" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "{app_name} अनलक गर्न आवश्यक पासवर्ड हटाउनुहोस्।" + "value" : "Mots de passe" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Verwijder het wachtwoord dat nodig is om {app_name} te ontgrendelen." - } - }, - "nn-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fjern passordet nødvendig for å låsa opp {app_name}." - } - }, - "ny" : { - "stringUnit" : { - "state" : "translated", - "value" : "Chotsani achinsinsi omwe amafunika kutsegula {app_name}." - } - }, - "pa-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} ਨੂੰ ਅਨਲੌਕ ਕਰਨ ਲਈ ਲੋੜੀਂ ਦਾ ਪਾਸਵਰਡ ਹਟਾਓ।" + "value" : "Wachtwoorden" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Usuń hasło wymagane do odblokowania aplikacji {app_name}." - } - }, - "ps" : { - "stringUnit" : { - "state" : "translated", - "value" : "هغه پاسورډ لرې کړئ چې د {app_name} خلاصولو لپاره اړین دی." - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Remova a senha requerida para desbloquear {app_name}." - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Remova a palavra-passe necessária para desbloquear {app_name}." + "value" : "Hasła" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Elimină parola necesară pentru a debloca {app_name}." + "value" : "Parole" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Удалить пароль, необходимый для разблокировки {app_name}." - } - }, - "sh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ukloni lozinku potrebnu za otključavanje {app_name}." - } - }, - "si-LK" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} අගුළු විවෘත කිරීමට අවශ්‍ය මුරපදය ඉවත් කරන්න." - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Odstrániť heslo potrebné na odomknutie {app_name}." - } - }, - "sl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Odstrani geslo, potrebno za odklepanje {app_name}." - } - }, - "sq" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hiqe fjalëkalimin e nevojshëm për të zhbllokuar {app_name}-in." - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Уклони лозинку потребну за откључавање {app_name}." - } - }, - "sr-Latn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ukloni lozinku potrebnu za otključavanje {app_name}." + "value" : "Пароли" } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Ta bort lösenordet som krävs för att låsa upp {app_name}." - } - }, - "sw" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ondoa nywila inayotakiwa kufungua {app_name}." - } - }, - "ta" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} இற்கு அணுக அடியாக கடவுச்சொல்லை நீக்கவும்." - } - }, - "te" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name}ని అన్లాక్ చేయడానికి అవసరమైన పాస్వర్డ్ తొలగించు." - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "ลบรหัสผ่านที่ต้องใช้เพื่อปลดล็อก {app_name}" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} uygulamasının kilidini açmak için gereken şifreyi kaldırın." + "value" : "Lösenord" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Видалити пароль, який потрібен для розблокування {app_name}." - } - }, - "ur-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} کو ان لاک کرنے کے لئے درکار پاس ورڈ کو ہٹا دیں۔" - } - }, - "uz" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} ni ochish uchun talab qilinadigan parolni olib tashlash." - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Xóa mật khẩu cần thiết để mở khóa {app_name}." - } - }, - "xh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Susa iphasiwedi efunekayo ukuze uvule {app_name}." - } - }, - "zh-CN" : { - "stringUnit" : { - "state" : "translated", - "value" : "删除{app_name}的解锁密码。" - } - }, - "zh-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "去除解鎖 {app_name} 的密碼。" + "value" : "Паролі" } } } @@ -335973,481 +339076,605 @@ } } }, - "passwordSetDescription" : { + "passwordSetDescriptionToast" : { "extractionState" : "manual", "localizations" : { - "af" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "Jou wagwoord is gestel. Hou dit asseblief veilig." + "value" : "Parolunuz təyin edilib. Lütfən onu güvəndə saxlayın." } }, - "ar" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "تم تعيين كلمة المرور الخاصة بك. احفظها في مامن من فضلك." + "value" : "Vaše heslo bylo nastaveno. Pečlivě si jej uložte." } }, - "az" : { + "de" : { "stringUnit" : { "state" : "translated", - "value" : "Parolunuz təyin edildi. Lütfən, onu güvəndə saxlayın." + "value" : "Dein Passwort wurde festgelegt. Bitte bewahre es sicher auf." } }, - "bal" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "ما گپ درخواست قبول کردی بیک پاسکوڈ برنکی. براہپس محفوظے کہ." + "value" : "Your password has been set. Please keep it safe." } }, - "be" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Ваш пароль быў усталяваны. Захавайце яго ў бяспецы." + "value" : "Votre mot de passe a été défini. Veuillez le conserver en sécurité." } }, - "bg" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Вашата парола беше зададена. Моля, пазете я безопасно." + "value" : "Uw wachtwoord is ingesteld. Hou het veilig." } }, - "bn" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "আপনার পাসওয়ার্ড সেট করা হয়েছে। দয়া করে এটি নিরাপদ রাখুন।" + "value" : "Twoje hasło zostało utworzone. Zapisz je w bezpiecznym miejscu." } }, - "ca" : { + "ro" : { "stringUnit" : { "state" : "translated", - "value" : "La vostra contrasenya s'ha definit. Mantingueu-la segura." + "value" : "Parola ta a fost setata. Securizați-va parola." } }, - "cs" : { + "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Tvé heslo bylo nastaveno. Pečlivě si ho odlož." + "value" : "Ваш пароль установлен. Пожалуйста, храните его в безопасном месте." } }, - "cy" : { + "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Mae eich cyfrinair wedi'i osod. Cadwch ef yn ddiogel." + "value" : "Ditt lösenord har angetts. Håll det säkert." } }, - "da" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Din adgangskode er blevet indstillet. Venligst hold den sikker." + "value" : "Ваш пароль встановлено. Будь ласка, зберігайте його надійно." } - }, - "de" : { + } + } + }, + "passwordSetShortDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "Dein Passwort wurde festgelegt. Bitte bewahre es sicher auf." + "value" : "Açılışda {app_name} kilidini açmaq üçün parol tələb edilsin." } }, - "el" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Ο κωδικός πρόσβασής σας έχει οριστεί. Παρακαλώ κρατήστε τον ασφαλή." + "value" : "Vyžadovat heslo k odemknutí {app_name} při spuštění." } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Your password has been set. Please keep it safe." + "value" : "Require password to unlock {app_name} on startup." } }, - "eo" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Via pasvorto estas agordita. Bonvolu konservi ĝin sekura." + "value" : "Mot de passe requis pour déverrouiller {app_name} au démarrage." } }, - "es-419" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Tu contraseña ha sido establecida. Por favor, mantenla segura." + "value" : "Wachtwoord vereisen om {app_name} bij het opstarten te ontgrendelen." } }, - "es-ES" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Tu contraseña ha sido establecida. Por favor, manténgala segura." + "value" : "Wymagaj hasła do odblokowania {app_name} przy uruchomieniu." } }, - "et" : { + "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Teie parool on määratud. Hoidke seda turvaliselt." + "value" : "Требовать пароль для разблокировки {app_name} при запуске." } }, - "eu" : { + "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Zure pasahitza ezarri da. Gorde seguru batean." + "value" : "Kräv ett lösenord för ett låsa upp {app_name} vid start." } }, - "fa" : { + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вимагати пароль для розблокування {app_name} при вході." + } + } + } + }, + "passwordStrengthCharLength" : { + "extractionState" : "manual", + "localizations" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "رمز عبور شما فعال شد. لطفا آن را در جای امنی ذخیره کنید." + "value" : "12 xarakterdən uzun" } }, - "fi" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Salasanasi on asetettu. Pidä se turvassa." + "value" : "Delší než 12 znaků" } }, - "fil" : { + "de" : { "stringUnit" : { "state" : "translated", - "value" : "Nabago na ang iyong password. Pakisuyong itago ito." + "value" : "Länger als 12 Zeichen" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Longer than 12 characters" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Votre mot de passe a été défini. Veuillez le conserver en sécurité." + "value" : "Plus de 12 caractères" } }, - "gl" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "O teu contrasinal foi configurado. Por favor, mantéñeo seguro." + "value" : "Langer dan 12 tekens" } }, - "ha" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "An saita kalmar sirrinku. Da fatan za a kiyaye shi lafiya." + "value" : "Dłuższe niż 12 znaków" } }, - "he" : { + "ro" : { "stringUnit" : { "state" : "translated", - "value" : "הסיסמה שלך הוגדרה. שמור עליה בבטחה." + "value" : "Mai mare de 12 caractere" } }, - "hi" : { + "ru" : { "stringUnit" : { "state" : "translated", - "value" : "आपका पासवर्ड सेट कर दिया गया है। कृपया इसे सुरक्षित रखें।" + "value" : "Длина больше 12 символов" } }, - "hr" : { + "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Vaša lozinka je postavljena. Molimo, čuvajte je na sigurnom." + "value" : "Längre än 12 tecken" } }, - "hu" : { + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Довший 12 символів" + } + } + } + }, + "passwordStrengthIncludeNumber" : { + "extractionState" : "manual", + "localizations" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "A jelszó be lett állítva. Tartsd biztonságos helyen!" + "value" : "Bir rəqəm ehtiva etməlidir" } }, - "hy-AM" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Ձեր գաղտնաբառը սահմանվել է։ Խնդրում ենք անվտանգ պահել։" + "value" : "Obsahuje číslici" } }, - "id" : { + "de" : { "stringUnit" : { "state" : "translated", - "value" : "Kata sandi anda telah disetel. Harap untuk menjaganya." + "value" : "Enthält eine Zahl" } }, - "it" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "La tua password è stata impostata. Si prega di tenerla al sicuro." + "value" : "Includes a number" } }, - "ja" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "パスワードが設定されました。安全に保管してください。" + "value" : "Inclut un chiffre" } }, - "ka" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "თქვენი პაროლი დაყენებულია. გთხოვთ, შეინახეთ იგი უსაფრთხოდ." + "value" : "Bevat een cijfer" } }, - "km" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "ពាក្យសម្ងាត់របស់អ្នកត្រូវបានកំណត់។ សូមរក្សាវាឲ្យមានសុវត្ថិភាព។" + "value" : "Zawiera cyfrę" } }, - "kn" : { + "ro" : { "stringUnit" : { "state" : "translated", - "value" : "ನಿಮ್ಮ ಗುಪ್ತಪದವನ್ನು ಹೊಂದಿಸಲಾಗಿದೆ. ಅದು ಸುರಕ್ಷಿತವಾಗಿರಿಸಿ." + "value" : "Include un număr" } }, - "ko" : { + "ru" : { "stringUnit" : { "state" : "translated", - "value" : "비밀번호 설정이 완료되었습니다. 안전히 관리하시기 바랍니다." + "value" : "Содержит цифру" } }, - "ku" : { + "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "وشەی پەرەسەدت دابینکرا. تکایە ئەوە بەندەن پارێزەر بێت." + "value" : "Inkluderar en siffra" } }, - "ku-TR" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Zoom In" + "value" : "Містить цифру" + } + } + } + }, + "passwordStrengthIncludesLowercase" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bir kiçik hərf ehtiva etməlidir" } }, - "lg" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Password yo ekatebatibwawo. Kaakasa nti bagutemye mu kifo ekinyuuse eritassaneyebwa." + "value" : "Obsahuje malé písmeno" } }, - "lt" : { + "de" : { "stringUnit" : { "state" : "translated", - "value" : "Jūsų slaptažodis buvo nustatytas. Prašome saugoti jį saugiai." + "value" : "Enthält einen Kleinbuchstaben" } }, - "lv" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Jūsu parole tika iestatīta. Lūdzu, saglabājiet to drošībā." + "value" : "Includes a lowercase letter" } }, - "mk" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Вашата лозинка е поставена. Ве молиме чувајте ја безбедно." + "value" : "Comprend une lettre minuscule" } }, - "mn" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Таны нууц үг томилогдсон байна. Нууц үгээ хамгаалж байгаарай." + "value" : "Bevat een kleine letter" } }, - "ms" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Kata laluan anda telah ditetapkan. Sila simpan dengan selamat." + "value" : "Zawiera małą literę" } }, - "my" : { + "ro" : { "stringUnit" : { "state" : "translated", - "value" : "သင်၏ စကားဝှက် ဖြင့်ထားသည်။ ထိန်းသိမ်းပါ။" + "value" : "Include o literă mică" } }, - "nb" : { + "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Passordet ditt er stilt. Vennligst oppbevar det trygt." + "value" : "Содержит строчную букву" } }, - "nb-NO" : { + "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Passordet er blitt stilt. Vennligst oppbevar det trygt." + "value" : "Inkluderar en liten bokstav" } }, - "ne-NP" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "तपाईँको पासवर्ड सेट गरिएको छ। कृपया यसलाई सुरक्षित राख्नुहोस्।" + "value" : "Містить літеру нижнього регістру" + } + } + } + }, + "passwordStrengthIncludesSymbol" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bir simvol daxildir" } }, - "nl" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Uw wachtwoord is ingesteld. Hou het veilig." + "value" : "Obsahuje symbol" } }, - "nn-NO" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Passordet ditt er blitt satt. Vennligst oppbevar det trygt." + "value" : "Includes a symbol" } }, - "ny" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Password yanu yakhalapo. Chonde sungani mosamala." + "value" : "Inclut un symbole" } }, - "pa-IN" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "ਤੁਹਾਡਾ ਪਾਸਵਰਡ ਸੈਟ ਕੀਤਾ ਗਿਆ ਹੈ। ਕਿਰਪਾ ਕਰਕੇ ਇਸਨੂੰ ਸੁਰੱਖਿਅਤ ਰੱਖੋ।" + "value" : "Bevat een symbool" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Ustawiono hasło. Zachowaj je w bezpiecznym miejscu." + "value" : "Zawiera symbol" } }, - "ps" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "ستاسو پاسورډ ټاکل شوی دی. مهرباني وکړۍ، دا خوندي وساتئ." + "value" : "Містить символ" + } + } + } + }, + "passwordStrengthIncludesUppercase" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bir böyük hərf ehtiva etməlidir" } }, - "pt-BR" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Sua senha foi definida. Por favor, mantenha-a segura." + "value" : "Obsahuje velké písmeno" } }, - "pt-PT" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enthält einen Großbuchstaben" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Includes a uppercase letter" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Contient une lettre majuscule" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bevat een hoofdletter" + } + }, + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "A sua palavra-passe foi definida. Por favor, mantenha-a segura." + "value" : "Zawiera wielką literę" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Parola ta a fost setată. Te rugăm să o păstrezi în siguranță." + "value" : "Include o literă mare" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Ваш пароль установлен. Пожалуйста, храните его в безопасном месте." + "value" : "Содержит заглавную букву" } }, - "sh" : { + "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Tvoja šifra je postavljena. Molimo, čuvaj je na sigurnom." + "value" : "Inkluderar en stor bokstav" } }, - "si-LK" : { + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Містить літеру верхнього регістру" + } + } + } + }, + "passwordStrengthIndicator" : { + "extractionState" : "manual", + "localizations" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "ඔබගේ මුරපදය සකසා ඇත. කරුණාකර එය ආරක්ෂිතව තබා ගන්න." + "value" : "Parol gücü göstəricisi" } }, - "sk" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Vaše heslo bolo nastavené. Uchovajte ho prosím v bezpečí." + "value" : "Indikátor síly hesla" } }, - "sl" : { + "de" : { "stringUnit" : { "state" : "translated", - "value" : "Vaše geslo je bilo nastavljeno. Prosim, hranite ga na varnem mestu." + "value" : "Passwortstärke-Anzeige" } }, - "sq" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Fjalëkalimi juaj është vendosur. Ju lutemi ta mbani të sigurt." + "value" : "Password Strength Indicator" } }, - "sr" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Ваша лозинка је подешена. Молимо вас да је сачувате." + "value" : "Indicateur de robustesse du mot de passe" } }, - "sr-Latn" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Vaša lozinka je podešena. Čuvajte je na sigurnom mestu." + "value" : "Wachtwoordsterkte indicator" } }, - "sv-SE" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Ditt lösenord har angetts. Håll det säkert." + "value" : "Wskaźnik siły hasła" } }, - "sw" : { + "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Nenosiri lako limewekwa. Tafadhali lihifadhi salama." + "value" : "Indicator de parolă puternică" } }, - "ta" : { + "ru" : { "stringUnit" : { "state" : "translated", - "value" : "உங்களின் கடவுச்சொல் அமைக்கப்பட்டுள்ளது. தயவுசெய்து அதை பாதுகாப்பாக வைத்திருங்கள்." + "value" : "Индикатор надёжности пароля" } }, - "te" : { + "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "మీ పాస్‌వర్డ్ సెట్ చేయబడింది. దయచేసి దాన్ని సురక్షితంగా ఉంచండి." + "value" : "Indikator för lösenordsstyrka" } }, - "th" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "รหัสผ่านของคุณถูกตั้งแล้ว กรุณารักษาเอาไว้ให้ปลอดภัย" + "value" : "Індикатор надійності паролю" + } + } + } + }, + "passwordStrengthIndicatorDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Güclü bir parol təyin etmək, cihazınız itsə və ya oğurlansa belə mesajlarınızı və qoşmalarınızı qorumağa kömək edir." } }, - "tr" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Şifreniz ayarlandı. Lütfen güvende tutunuz." + "value" : "Nastavení silného hesla pomáhá chránit vaše zprávy a přílohy v případě ztráty nebo odcizení zařízení." } }, - "uk" : { + "de" : { "stringUnit" : { "state" : "translated", - "value" : "Ваш пароль встановлено. Будь ласка, збережіть його в безпеці." + "value" : "Ein schwieriges Passwort hilft deine Nachrichten und Anlagen zu schützen, wenn dein Gerät jemals verloren geht oder gestohlen wird." } }, - "ur-IN" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "آپ کا پاس ورڈ مقرر ہو گیا ہے۔ براہ کرم اسے محفوظ رکھیں۔" + "value" : "Setting a strong password helps protect your messages and attachments if your device is ever lost or stolen." } }, - "uz" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Parolingiz olib tashlandi." + "value" : "Définir un mot de passe robuste permet de protéger vos messages et pièces jointes en cas de perte ou de vol de votre appareil." } }, - "vi" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Mật khẩu của bạn đã được đặt. Hãy giữ nó cẩn thận." + "value" : "Een sterk wachtwoord helpt je berichten en bijlagen te beschermen als je apparaat ooit verloren raakt of wordt gestolen." } }, - "xh" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Iphasiwedi yakho isetiwe. Nceda uyigcine ikhuselekile." + "value" : "Ustawienie silnego hasła pomaga chronić Twoje wiadomości i załączniki w przypadku utraty lub kradzieży urządzenia." } }, - "zh-CN" : { + "ro" : { "stringUnit" : { "state" : "translated", - "value" : "您的密码已设定。请妥善保管。" + "value" : "Setarea unei parole puternice ajută la protejarea mesajelor și fișierelor în cazul pierderii sau furtului dispozitivului dumneavoastră." } }, - "zh-TW" : { + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Надёжный пароль помогает защитить ваши сообщения и вложения в случае утери или кражи устройства." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Att skapa ett starkt lösenord hjälper till att skydda dina meddelanden och bilagor om din enhet skulle gå förlorad eller bli stulen." + } + }, + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "您的密碼設定完成。請注意保管。" + "value" : "Встановлення надійного пароля допомагає захистити ваші повідомлення та вкладення у разі втрати або крадіжки пристрою." } } } @@ -336920,7 +340147,7 @@ "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "粘帖" + "value" : "粘贴" } }, "zh-TW" : { @@ -336982,6 +340209,18 @@ "value" : "Ŝanĝi permesojn" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cambiar Permisos" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cambio de permiso" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -337006,6 +340245,18 @@ "value" : "Persetujuan Diubah" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Modifica autorizzazione" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "権限の変更" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -337024,6 +340275,18 @@ "value" : "Zmiana uprawnień" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alteração de permissão" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Modificare permisiune" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -337036,6 +340299,12 @@ "value" : "Ändra tillåtelse" } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İzin Değişimi" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -337053,6 +340322,12 @@ "state" : "translated", "value" : "授权变更" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "權限變更" + } } } }, @@ -337074,7 +340349,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Fayl, musiqi və səs göndərə bilməyiniz üçün {app_name} musiqi və səslərə müraciət etməlidir, ancaq bu icazəyə birdəfəlik rədd cavabı verilib. Ayarlar → \"İcazələr\"ə toxunun və \"Musiqi və səs\"i işə salın." + "value" : "Fayl, musiqi və səs göndərə bilməyiniz üçün {app_name} musiqi və səslərə erişməlidir, ancaq bu erişimə birdəfəlik rədd cavabı verilib. Ayarlar → \"İcazələr\"ə toxunun və \"Musiqi və səs\"i işə salın." } }, "bal" : { @@ -338990,7 +342265,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Görüntülü zəng etmək üçün kameraya müraciət tələb olunur. Davam etmək üçün Ayarlarda \"Kamera\" icazəsini işə salın." + "value" : "Görüntülü zəng etmək üçün kameraya erişim tələb olunur. Davam etmək üçün Ayarlarda \"Kamera\" icazəsini işə salın." } }, "ca" : { @@ -339023,6 +342298,18 @@ "value" : "Camera access is required to make video calls. Toggle the \"Camera\" permission in Settings to continue." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se requiere acceso a la cámara para realizar videollamadas. Activa el permiso de \"Cámara\" en Configuración para continuar." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se requiere acceso a la cámara para realizar videollamadas. Activa el permiso de \"Cámara\" en Configuración para continuar." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -339041,6 +342328,18 @@ "value" : "A videohívások indításához kamerához való hozzáférés szükséges. Kapcsolja be a „Kamera” engedélyt a Beállításokban a folytatáshoz." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "L'accesso alla fotocamera è necessario per effettuare videochiamate. Per continuare, attiva l'autorizzazione \"Fotocamera\" nelle Impostazioni." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ビデオ通話を行うにはカメラへのアクセスが必要です。続行するには、設定で「カメラ」の許可をオンにしてください。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -339059,6 +342358,18 @@ "value" : "Do prowadzenia rozmów wideo wymagany jest dostęp do kamery. Aby kontynuować, przełącz uprawnienia „Aparat” w Ustawieniach." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "É necessário acesso à câmara para fazer chamadas de vídeo. Ative a permissão \"Câmara\" nas Definições para continuar." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Este necesar accesul la cameră pentru a efectua apeluri video. Comută permisiunea \"Cameră\" în Setări pentru a continua." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -339071,6 +342382,12 @@ "value" : "Kameraåtkomst krävs för att ringa videosamtal. Växla behörigheten \"Kamera\" i Inställningar för att fortsätta." } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Görüntülü arama yapmak için kamera erişimi gereklidir. Devam etmek için Ayarlar'dan \"Kamera\" iznini açın." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -339082,6 +342399,12 @@ "state" : "translated", "value" : "需要摄像头访问权限才能进行通话。在设置中允许“摄像头”权限以继续。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "進行視訊通話需要啟用相機權限。請在設定中開啟「相機」權限以繼續。" + } } } }, @@ -339097,7 +342420,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Kamera müraciəti hazırda fəaldır. Onu sıradan çıxartmaq üçün Ayarlarda \"Kamera\" icazəsini söndürün." + "value" : "Kamera erişimi hazırda fəaldır. Onu sıradan çıxartmaq üçün Ayarlarda \"Kamera\" icazəsini söndürün." } }, "ca" : { @@ -339130,6 +342453,18 @@ "value" : "Camera access is currently enabled. To disable it, toggle the \"Camera\" permission in Settings." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "El acceso a la cámara está activado actualmente. Para desactivarlo, desactiva el permiso de \"Cámara\" en Configuración." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "El acceso a la cámara está activado actualmente. Para desactivarlo, desactiva el permiso de \"Cámara\" en Configuración." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -339142,12 +342477,30 @@ "value" : "कैमरा एक्सेस वर्तमान में सक्षम है। इसे अक्षम करने के लिए, सेटिंग्स में \"कैमरा\" अनुमति टॉगल करें।" } }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "A kamerához való hozzáférés jelenleg engedélyezve van. A letiltáshoz kapcsolja ki a „Kamera” engedélyt a beállításokban." + } + }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Akses kamera saat ini diaktifkan. Untuk mematikan, pilih izin \"Kamera\" dalam Pengaturan." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "L'accesso alla fotocamera è attualmente abilitato. Per disattivarlo, disattiva l'autorizzazione \"Fotocamera\" nelle Impostazioni." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "カメラへのアクセスは現在有効になっています。無効にするには、設定で「カメラ」の許可をオフにしてください。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -339166,6 +342519,18 @@ "value" : "Dostęp do aparatu jest obecnie włączony. Aby go wyłączyć, przełącz uprawnienie „Aparat” w Ustawieniach." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "O acesso à câmara está ativado de momento. Para o desativar, altere a permissão \"Câmara\" nas Definições." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Accesul la cameră este activat în prezent. Pentru a-l dezactiva, comută permisiunea \"Cameră\" în Setări." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -339178,6 +342543,12 @@ "value" : "Kameraåtkomst är för närvarande aktiverad. För att inaktivera det, växla behörigheten \"Kamera\" i Inställningar." } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kamera erişimi şu anda etkin. Devre dışı bırakmak için Ayarlar'dan \"Kamera\" iznini kapatın." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -339189,6 +342560,12 @@ "state" : "translated", "value" : "摄像头访问权限已允许。如需禁用,请在设置中禁用“摄像头”权限。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "目前已啟用相機權限。若要停用,請在設定中關閉「相機」權限。" + } } } }, @@ -339210,7 +342587,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Foto və video göndərə bilməyiniz üçün {app_name} kameraya müraciət etməlidir, ancaq bu icazəyə birdəfəlik rədd cavabı verilib. Ayarlar → \"İcazələr\"ə toxunun və \"Kamera\"nı işə salın." + "value" : "Foto və video göndərə bilməyiniz üçün {app_name} kameraya erişməlidir, ancaq bu icazəyə birdəfəlik rədd cavabı verilib. Ayarlar → \"İcazələr\"ə toxunun və \"Kamera\"nı işə salın." } }, "bal" : { @@ -339671,7 +343048,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Görüntülü zənglər üçün kameraya müraciətə icazə ver." + "value" : "Görüntülü zənglər üçün kameraya erişimə icazə ver." } }, "ca" : { @@ -339710,6 +343087,18 @@ "value" : "Permesu aliron al kamerao por video vokoj." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Permite el acceso a la cámara para las videollamadas." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Permite el acceso a la cámara para las videollamadas." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -339734,6 +343123,18 @@ "value" : "Izinkan akses kamera untuk video call." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Consenti l'accesso alla fotocamera per le videochiamate." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ビデオ通話のためにカメラへのアクセスを許可してください。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -339752,6 +343153,18 @@ "value" : "Zezwól na dostęp do kamery na potrzeby rozmów wideo." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Permitir acesso à câmara para chamadas de vídeo." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Permite accesul la cameră pentru apeluri video." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -339764,6 +343177,12 @@ "value" : "Tillåt åtkomst till kamera för videosamtal." } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Görüntülü aramalar için kamera erişimine izin verin." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -339775,6 +343194,12 @@ "state" : "translated", "value" : "请允许访问摄像头以进行视频通话。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "允許使用相機以進行視訊通話。" + } } } }, @@ -340739,484 +344164,82 @@ "permissionsKeepInSystemTrayDescription" : { "extractionState" : "manual", "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} loop aan in die agtergrond wanneer jy die venster sluit" - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} يستمر في العمل في الخلفية عندما تقوم بإغلاق النافذة" - } - }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "Pəncərəni bağladıqda {app_name} arxaplanda işləməyə davam edir" - } - }, - "bal" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} ھنوک پد پسینی چشماں بند بو تک پس زمینه أٹھے چا" - } - }, - "be" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} працягвае працу ў фонавым рэжыме, калі вы закрываеце акно" - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "При затваряне на прозореца с програмата {app_name}, тя остава включена и продължава паралелно да работи" - } - }, - "bn" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} উইন্ডো বন্ধ করলেও后台 চালু থাকে" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} continua funcionant en segon pla quan tanqueu la finestra" + "value" : "Pəncərəni bağladığınız zaman {app_name} arxaplanda çalışmağa davam edir." } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "{app_name} pokračuje v běhu na pozadí, když zavřete okno" - } - }, - "cy" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} yn parhau i redeg yn y cefndir pan fyddwch yn cau'r ffenestr" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} kører fortsat i baggrunden, når du lukker vinduet" + "value" : "{app_name} pokračuje v běhu na pozadí, když zavřete okno." } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "{app_name} läuft im Hintergrund weiter, wenn du das Fenster schließt" + "value" : "{app_name} läuft im Hintergrund weiter, wenn du das Fenster schließt." } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Το {app_name} συνεχίζει να εκτελείται στο παρασκήνιο όταν κλείνετε το παράθυρο" + "value" : "Το {app_name} συνεχίζει να εκτελείται στο παρασκήνιο όταν κλείνετε το παράθυρο." } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "{app_name} continues running in the background when you close the window" - } - }, - "eo" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} daŭrigas funkcii en la fono kiam vi fermas la fenestron" - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} continúa ejecutándose en segundo plano al cerrar la ventana" - } - }, - "es-ES" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} continúa ejecutándose en segundo plano cuando cierras la ventana" - } - }, - "et" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} jätkab taustal töötamist, kui sulgete akna" - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name}(e)k lanean jarraitzen du atzealdean leihoa ixten duzunean" - } - }, - "fa" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} با بستن پنجره، همچنان در پس‌زمینه اجرا می‌شود." - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} pysyy käynnissä taustalla, kun suljet ikkunan" - } - }, - "fil" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} ay patuloy na gumagana sa background kapag isinara mo ang window" + "value" : "{app_name} continues running in the background when you close the window." } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "{app_name} continue à fonctionner en arrière-plan lorsque vous fermez la fenêtre" - } - }, - "gl" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} segue a executarse en segundo plano cando pechas a ventá" - } - }, - "ha" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} yana ci gaba da gudana a bango lokacin da ka rufe taga" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} ממשיך לפעול ברקע כאשר אתה סוגר את החלון" - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "जब आप विंडो बंद करते हैं तो {app_name} पृष्ठभूमि में चलता रहता है" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} nastavlja raditi u pozadini kada zatvorite prozor" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "A(z) {app_name} az ablak bezárása után is tovább fut a háttérben" - } - }, - "hy-AM" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} շարունակում է աշխատել ֆոնային ռեժիմում, երբ դուք փակում եք պատուհանը" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} bekerja di background ketika anda menutup jendela" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} continuerà ad essere eseguito in background quando chiudi la finestra" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name}はウィンドウを閉じてもバックグラウンドで実行され続けます" - } - }, - "ka" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} განაგრძობს მუშაობას ფონის რეჟიმში, როდესაც ფანჯარას დახურავთ" - } - }, - "km" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} នឹងបន្តដំណើរការនៅផ្ទៃខាងក្រោយនៅពេលអ្នកបិទផ្ទាំងបង្អួច" - } - }, - "kn" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} ಕಿಟಕಿ ಮುಚ್ಚಿದಾಗ ಹಿನ್ನೆಲೆ ಕಾರ್ಯನಿರ್ವಹಣೆ ಮುಂದುವರಿಸುತ್ತದೆ." - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} 창을 닫아도 백그라운드에서 실행됩니다." - } - }, - "ku" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} هەموو دایما کار دەکات لە پاشچاوە ڕوونکردنی دەروازەکان" - } - }, - "ku-TR" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} di dema ku em pencereyê digirin, berdewam dike maju di piştî deran de bixebitîne" - } - }, - "lg" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} ekwata mu kutambuzibwa munda nga tosendewo olukindu" - } - }, - "lo" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} ໄດ້ດໍາເນີນການຕໍ່ໄປໃນພື້ນຫລັງໃນຂະນະທີ່ທ່ານປິດປ່ອງ" - } - }, - "lt" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} toliau veikia fone, kai užveriate langą" - } - }, - "lv" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} turpina darboties fonā, kad aizverat logu" - } - }, - "mk" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} продолжува да работи во заднина кога ќе го затворите прозорецот" - } - }, - "mn" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} нь та цонхыг хаахад арын горимд ажилласаар байна" - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} terus berjalan di latar belakang apabila anda menutup tetingkap" - } - }, - "my" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} ကိုပိတ်လိုက်ချိန် ဝင်းဒိုးကိုပိတ်လိုက်ရင် မီးနောက်ဆုံးဝင်ရောက်ထဲမှာ ဆက်လက် လုပ်ဆောင်နေပါသည်" + "value" : "{app_name} continue à fonctionner en arrière-plan lorsque vous fermez la fenêtre." } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "{app_name} fortsetter å kjøre i bakgrunnen når du lukker vinduet" - } - }, - "nb-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} fortsetter å kjøre i bakgrunnen når du lukker vinduet" - } - }, - "ne-NP" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} विन्डो बन्द गर्दा पृष्ठभूमिमा चलिरहन्छ" + "value" : "{app_name} fortsetter å kjøre i bakgrunnen når du lukker vinduet." } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "{app_name} blijft op de achtergrond draaien wanneer je het venster sluit" - } - }, - "nn-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} fortsetter å kjøre i bakgrunnen når du lukker vinduet" - } - }, - "ny" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} imachitlikira ntchifukwa chakuti cikhale m'kumbuyo mukatseka zenera" - } - }, - "pa-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} ਬੈਕਗ੍ਰਾਊਂਡ ਵਿੱਚ ਚੱਲਦਾ ਰਹਿੰਦਾ ਹੈ ਜਦੋਂ ਤੁਸੀਂ ਖਿੜਕੀ ਬੰਦ ਕਰਦੇ ਹੋ" + "value" : "{app_name} blijft op de achtergrond draaien wanneer je het venster sluit." } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Aplikacja {app_name} nadal działa w tle po zamknięciu okna" - } - }, - "ps" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} په شالید کې چلیږي کله چې تاسو کړکۍ وتړئ" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} continua sendo executado em segundo plano quando você fecha a janela" - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} continua a correr em segundo plano quando fecha a janela" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} va continua să ruleze în fundal după închiderea ferestrei" + "value" : "{app_name} nadal działa w tle po zamknięciu okna." } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "{app_name} продолжает работать в фоновом режиме даже после закрытия окна" - } - }, - "sh" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} nastavlja raditi u pozadini kada zatvorite prozor" - } - }, - "si-LK" : { - "stringUnit" : { - "state" : "translated", - "value" : "ඔබ කවුළුව වැසූ විට {app_name} පසුබිමේ දිගටම ක්‍රියාත්මක වේ" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} bude pokračovať v behu na pozadí, keď zavrieš okno" - } - }, - "sl" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} se nadaljuje v ozadju, ko zaprete okno" - } - }, - "sq" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} vazhdon të funksionojë në sfond kur mbyllni dritaren" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} наставља да ради у позадини када затворите прозор" - } - }, - "sr-Latn" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} nastavlja rad u pozadini kada zatvorite prozor" + "value" : "{app_name} продолжит работать в фоновом режиме даже после закрытия окна." } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "{app_name} fortsätter att köras i bakgrunden när du stänger fönstret" - } - }, - "sw" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} inaendelea kukimbia chinichini ukiwa umefunga dirisha" - } - }, - "ta" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} சாளரத்தை மூடிக்கொண்ட பின்னரும் பின்னணி செயல்பாடுகளில் தொடர்ச்சியாக இயங்கிவரும்" - } - }, - "te" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} విండోను మూసినప్పుడు నేపథ్యంలో కొనసాగుతుంది" - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} ยังคงทำงานต่อไปในพื้นหลังเมื่อคุณปิดหน้าต่าง" + "value" : "{app_name} fortsätter köras i bakgrunden när du stänger fönstret." } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "{app_name} pencereyi kapattığınızda arka planda çalışmaya devam eder" + "value" : "{app_name} pencereyi kapattığınızda arka planda çalışmaya devam eder." } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "{app_name} продовжує працювати у фоновому режимі, коли ви закриваєте вікно" - } - }, - "ur-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} پس منظر میں چلتا رہتا ہے جب آپ ونڈو بند کرتے ہیں" - } - }, - "uz" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} oyna yopilganda ham fon rejimida ishlashda davom etadi" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} tiếp tục chạy nền khi bạn đóng cửa sổ" - } - }, - "xh" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name} iqhubeka isebenza ngasemva xa uvala ifestile" - } - }, - "zh-CN" : { - "stringUnit" : { - "state" : "translated", - "value" : "{app_name}会在您关闭窗口后继续在后台运行" - } - }, - "zh-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "當您關閉視窗之後,{app_name} 會繼續在系統後台執行" + "value" : "{app_name} продовжує працювати у фоновому режимі, коли ви його згортає." } } } @@ -341239,7 +344262,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "{app_name} davam etmək üçün foto kitabxanasına müraciət etməlidir. iOS ayarlarında müraciəti fəallaşdıra bilərsiniz." + "value" : "{app_name} davam etmək üçün foto kitabxanasına erişməlidir. Erişimi iOS ayarlarında fəallaşdıra bilərsiniz." } }, "bal" : { @@ -341712,7 +344735,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Zəngləri asanlaşdırmaq üçün Lokal şəbəkə müraciəti tələb olunur. Davam etmək üçün Ayarlarda \"Lokal şəbəkə\" icazəsini işə salın." + "value" : "Zəngləri asanlaşdırmaq üçün Lokal şəbəkə erişimi tələb olunur. Davam etmək üçün Ayarlarda \"Lokal şəbəkə\" icazəsini işə salın." } }, "ca" : { @@ -341745,6 +344768,18 @@ "value" : "Local Network access is required to facilitate calls. Toggle the \"Local Network\" permission in Settings to continue." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se requiere acceso a la Red Local para realizar llamadas. En Configuración, active el permiso de \"Red Local\" para continuar." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se requiere acceso a la red local para facilitar las llamadas. Activa el permiso de \"Red local\" en Configuración para continuar." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -341763,6 +344798,18 @@ "value" : "A hívások lehetővé tételéhez szükséges a helyi hálózathoz való hozzáférés. Kapcsolja be a „Helyi hálózat” engedélyt a Beállításokban a folytatáshoz." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "L'accesso alla rete locale è necessario per facilitare le chiamate. Per continuare, attiva l'autorizzazione \"Rete locale\" nelle Impostazioni." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "通話を行うにはローカルネットワークへのアクセスが必要です。続行するには、設定で「ローカルネットワーク」の許可をオンにしてください。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -341781,6 +344828,18 @@ "value" : "Aby móc wykonywać połączenia, wymagany jest dostęp do sieci lokalnej. Aby kontynuować, przełącz uprawnienia „Sieć lokalna” w Ustawieniach." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "É necessário acesso à rede local para permitir chamadas. Ative a permissão \"Rede local\" nas Definições para continuar." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Accesul la rețeaua locală este necesar pentru a facilita apelurile. Activează permisiunea „Rețea locală” din Setări pentru a continua." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -341793,6 +344852,12 @@ "value" : "Lokal nätverksåtkomst krävs för att underlätta samtal. Växla behörigheten \"Lokalt nätverk\" i Inställningar för att fortsätta." } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aramaları sağlamak için Yerel Ağ erişimi gereklidir. Devam etmek için Ayarlar'dan \"Yerel Ağ\" iznini açın." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -341804,6 +344869,12 @@ "state" : "translated", "value" : "需要本地网络访问权限才能进行通话。在设置中允许“本地网络”权限以继续。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "需要存取本地網路才能進行通話。請在「設定」中切換「本地網路」權限以繼續。" + } } } }, @@ -341813,7 +344884,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "{app_name}, səsli və görüntülü zənglər edə bilmək üçün daxili şəbəkəyə müraciət etməlidir." + "value" : "{app_name}, səsli və görüntülü zənglər edə bilmək üçün lokal şəbəkəyə erişməlidir." } }, "ca" : { @@ -341852,6 +344923,18 @@ "value" : "{app_name} bezonas aliron al loka reto por fari voĉajn kaj video vokojn." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} necesita acceso a la red local para realizar llamadas de voz y video." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} necesita acceso a la red local para realizar llamadas de voz y video." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -341870,6 +344953,18 @@ "value" : "A(z) {app_name} alkalmazásnak hozzáférésre van szüksége a helyi hálózathoz a hang- és videohívások indításához." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} necessita dell'accesso alla rete locale per effettuare chiamate vocali e video." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} は音声・ビデオ通話を行うためにローカルネットワークへのアクセスが必要です。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -341888,6 +344983,18 @@ "value" : "{app_name} potrzebuje dostępu do sieci lokalnej, aby wykonywać połączenia głosowe i wideo." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} precisa de acesso à rede local para efetuar chamadas de voz e vídeo." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} are nevoie de acces la rețeaua locală pentru a efectua apeluri vocale și video." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -341900,6 +345007,12 @@ "value" : "{app_name} behöver åtkomst till det lokala nätverket för att kunna ringa röst och videosamtal." } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} uygulamasının sesli ve görüntülü arama yapabilmesi için yerel ağa erişmesi gerekiyor." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -341917,6 +345030,12 @@ "state" : "translated", "value" : "{app_name}需要访问本地网络才能进行语音和视频通话。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} 需要存取本地網路以進行語音與視訊通話。" + } } } }, @@ -341926,7 +345045,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Lokal şəbəkə müraciəti hazırda fəaldır. Onu sıradan çıxartmaq üçün Ayarlarda \"Lokal şəbəkə\" icazəsini söndürün." + "value" : "Lokal şəbəkə erişimi hazırda fəaldır. Onu sıradan çıxartmaq üçün Ayarlarda \"Lokal şəbəkə\" icazəsini söndürün." } }, "ca" : { @@ -341959,6 +345078,18 @@ "value" : "Local Network access is currently enabled. To disable it, toggle the \"Local Network\" permission in Settings." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "El acceso a la red local está activado actualmente. Para desactivarlo, desactiva el permiso de \"Red local\" en Configuración." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "El acceso a la red local está activado actualmente. Para desactivarlo, desactiva el permiso de \"Red local\" en Configuración." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -341971,6 +345102,24 @@ "value" : "स्थानीय नेटवर्क एक्सेस वर्तमान में सक्षम है। इसे अक्षम करने के लिए, सेटिंग्स में \"स्थानीय नेटवर्क\" अनुमति टॉगल करें।" } }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "A helyi hálózati hozzáférés jelenleg engedélyezve van. A letiltáshoz kapcsolja ki a „Helyi hálózat” engedélyt a beállításokban." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "L'accesso alla rete locale è attualmente abilitato. Per disattivarlo, disattiva l'autorizzazione \"Rete locale\" nelle Impostazioni." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ローカルネットワークへのアクセスは現在有効になっています。無効にするには、設定画面の「ローカルネットワーク」権限をオフにしてください。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -341989,6 +345138,18 @@ "value" : "Dostęp do sieci lokalnej jest obecnie włączony. Aby go wyłączyć, przełącz uprawnienie „Sieć lokalna” w Ustawieniach." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "O acesso à rede local está atualmente ativado. Para desativar, desligue a permissão \"Rede local\" nas Definições." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Accesul la rețeaua locală este activat în prezent. Pentru a-l dezactiva, comută permisiunea „Rețea locală” din Setări." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -342001,6 +345162,12 @@ "value" : "Lokal nätverksåtkomst är aktiverad. För att inaktivera det, växla behörigheten \"Lokalt nätverk\" i inställningar." } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yerel Ağ erişimi şu anda etkin. Devre dışı bırakmak için Ayarlar'dan \"Yerel Ağ\" iznini kapatın." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -342012,6 +345179,12 @@ "state" : "translated", "value" : "本地网络访问权限已允许。如需禁用,请在设置中禁用“本地网络”权限。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "目前已啟用本地網路存取。如需停用,請在「設定」中切換「本地網路」權限。" + } } } }, @@ -342027,7 +345200,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Səsli və görüntülü zəngləri asanlaşdırmaq üçün lokal şəbəkəyə müraciətə icazə verin." + "value" : "Səsli və görüntülü zəngləri asanlaşdırmaq üçün lokal şəbəkəyə erişimə icazə verin." } }, "ca" : { @@ -342066,6 +345239,18 @@ "value" : "Permesu aliron al loka reto por faciligi voĉajn kaj video vokojn." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Permitir acceso a la red local para realizar llamadas de voz y video." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Permitir acceso a la red local para facilitar llamadas de voz y video." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -342084,6 +345269,18 @@ "value" : "Engedélyezze a helyi hálózathoz való hozzáférést a hang- és videohívások lehetővé tételéhez." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Consenti l'accesso alla rete locale per facilitare le chiamate vocali e video." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "音声通話およびビデオ通話を行うためにローカルネットワークへのアクセスを許可してください。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -342102,6 +345299,18 @@ "value" : "Zezwól na dostęp do sieci lokalnej, aby ułatwić połączenia głosowe i wideo." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Permitir acesso à rede local para facilitar chamadas de voz e vídeo." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Permite accesul la rețeaua locală pentru a facilita apelurile vocale și video." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -342114,6 +345323,12 @@ "value" : "Tillåt åtkomst till lokala nätverk för att underlätta röst- och videosamtal." } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sesli ve görüntülü aramaları sağlamak için yerel ağa erişime izin verin." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -342125,6 +345340,12 @@ "state" : "translated", "value" : "请允许访问本地网络访问以进行语音和视频通话。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "允許存取本地網路以便進行語音與視訊通話。" + } } } }, @@ -342179,6 +345400,18 @@ "value" : "Loka reto" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Red Local" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Red local" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -342203,6 +345436,18 @@ "value" : "Jaringan Lokal" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rete locale" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ローカルネットワーク" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -342221,6 +345466,18 @@ "value" : "Sieć lokalna" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rede local" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rețea locală" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -342233,6 +345490,12 @@ "value" : "Lokalt Nätverk" } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yerel ağ" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -342250,6 +345513,12 @@ "state" : "translated", "value" : "本地网络" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "本地網路" + } } } }, @@ -342750,7 +346019,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "{app_name}, zəng etmək və səsli mesaj göndərmək üçün mikrofona müraciət etməlidir, ancaq bu müraciətə həmişəlik rədd cavabı verilib. Ayarlara → İcazələr bölməsinə gedin və \"Mikrofon\"u işə salın." + "value" : "{app_name}, zəng etmək və səsli mesaj göndərmək üçün mikrofona erişməlidir, ancaq bu erişimə həmişəlik rədd cavabı verilib. Ayarlara → İcazələr bölməsinə gedin və \"Mikrofon\"u işə salın." } }, "bal" : { @@ -343217,7 +346486,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Zəng etmək və səsli mesajları yazmaq üçün mikrofona müraciət tələb olunur. Davam etmək üçün Ayarlarda \"Mikrofon\" icazəsini işə salın." + "value" : "Zəng etmək və səsli mesajları yazmaq üçün mikrofona erişim tələb olunur. Davam etmək üçün Ayarlarda \"Mikrofon\" icazəsini işə salın." } }, "ca" : { @@ -343250,6 +346519,18 @@ "value" : "Microphone access is required to make calls and record audio messages. Toggle the \"Microphone\" permission in Settings to continue." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se requiere acceso al micrófono para realizar llamadas y grabar mensajes de audio. Activa el permiso de \"Micrófono\" en Configuración para continuar." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se requiere acceso al micrófono para realizar llamadas y grabar mensajes de audio. Activa el permiso de \"Micrófono\" en Configuración para continuar." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -343268,6 +346549,18 @@ "value" : "A hívások indításához és hangüzenetek rögzítéséhez mikrofonhoz való hozzáférés szükséges. Kapcsolja be a „Mikrofon” engedélyt a Beállításokban a folytatáshoz." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "L'accesso al microfono è necessario per effettuare chiamate e registrare messaggi audio. Per continuare, attiva l'autorizzazione \"Microfono\" nelle Impostazioni." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "通話および音声メッセージの録音にはマイクへのアクセスが必要です。続行するには、設定で「マイク」の許可をオンにしてください。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -343286,6 +346579,18 @@ "value" : "Do wykonywania połączeń i nagrywania wiadomości audio wymagany jest dostęp do mikrofonu. Aby kontynuować, przełącz uprawnienia „Mikrofon” w Ustawieniach." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "É necessário acesso ao microfone para fazer chamadas e gravar mensagens de áudio. Ative a permissão \"Microfone\" nas Definições para continuar." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Este necesar accesul la microfon pentru a efectua apeluri și a înregistra mesaje audio. Activează permisiunea „Microfon” din Setări pentru a continua." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -343298,6 +346603,12 @@ "value" : "Tillgång till mikrofon krävs för att ringa samtal och spela in ljudmeddelanden. Växla behörigheten \"Mikrofon\" i Inställningar för att fortsätta." } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Arama yapmak ve sesli mesaj kaydetmek için mikrofon erişimi gereklidir. Devam etmek için Ayarlar'dan \"Mikrofon\" iznini açın." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -343309,6 +346620,12 @@ "state" : "translated", "value" : "需要麦克风访问权限以进行通话和录制语音消息。在设置中打开“麦克风”权限以继续。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "進行通話和錄製語音訊息需要啟用麥克風權限。請在設定中開啟「麥克風」權限以繼續。" + } } } }, @@ -343330,7 +346647,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "{app_name} gizlilik ayarlarında mikrofona müraciəti fəallaşdıra bilərsiniz" + "value" : "{app_name} gizlilik ayarlarında mikrofona erişimi fəallaşdıra bilərsiniz" } }, "bal" : { @@ -344288,7 +347605,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Mikrofon müraciəti hazırda fəaldır. Onu sıradan çıxartmaq üçün Ayarlarda \"Mikrofon\" icazəsini söndürün." + "value" : "Mikrofon erişimi hazırda fəaldır. Onu sıradan çıxartmaq üçün Ayarlarda \"Mikrofon\" icazəsini söndürün." } }, "ca" : { @@ -344321,18 +347638,54 @@ "value" : "Microphone access is currently enabled. To disable it, toggle the \"Microphone\" permission in Settings." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "El acceso al micrófono está activado actualmente. Para desactivarlo, desactiva el permiso de \"Micrófono\" en Configuración." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "El acceso al micrófono está activado actualmente. Para desactivarlo, desactiva el permiso de \"Micrófono\" en Configuración." + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "L'accès au microphone est actuellement activé. Pour le désactiver, décochez l'autorisation \"Microphone\" dans les Paramètres." } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "माइक्रोफोन एक्सेस वर्तमान में सक्षम है। इसे अक्षम करने के लिए, सेटिंग्स में \"माइक्रोफोन\" अनुमति टॉगल करें।" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "A mikrofonhoz való hozzáférés jelenleg engedélyezve van. A letiltáshoz kapcsolja ki a „Mikrofon” engedélyt a beállításokban." + } + }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Akses mikrofon saat ini diaktifkan. Untuk mematikan, pilih izin \"Mikrofon\" dalam Pengaturan." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "L'accesso al microfono è attualmente abilitato. Per disabilitarlo, disattiva l'autorizzazione \"Microfono\" nelle Impostazioni." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "マイクへのアクセスは現在有効です。無効にするには、設定で「マイク」の許可をオフにしてください。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -344351,6 +347704,18 @@ "value" : "Dostęp do mikrofonu jest obecnie włączony. Aby go wyłączyć, przełącz uprawnienie „Mikrofon” w Ustawieniach." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "O acesso ao microfone está ativado de momento. Para o desativar, altere a permissão \"Microfone\" nas Definições." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Accesul la microfon este activat în prezent. Pentru a-l dezactiva, comută permisiunea \"Microfon\" în Setări." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -344363,6 +347728,12 @@ "value" : "Tillgång till mikrofon är för närvarande aktiverad. För att inaktivera det, växla behörigheten \"Mikrofon\" i inställningar." } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mikrofon erişimi şu anda etkin. Devre dışı bırakmak için Ayarlar'dan \"Mikrofon\" iznini kapatın." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -344374,6 +347745,12 @@ "state" : "translated", "value" : "麦克风访问权限已允许。如需禁用,请在设置中禁用“麦克风”权限。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "目前已啟用麥克風權限。若要停用,請在設定中關閉「麥克風」權限。" + } } } }, @@ -344395,7 +347772,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Mikrofona müraciətə icazə verin." + "value" : "Mikrofona erişim icazəsi verin." } }, "bal" : { @@ -344862,7 +348239,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Səsli zənglər və səsli mesajlar üçün mikrofona müraciətə icazə verin." + "value" : "Səsli zənglər və səsli mesajlar üçün mikrofona erişimə icazə verin." } }, "ca" : { @@ -344901,6 +348278,18 @@ "value" : "Permesu aliron al mikrofono por voĉaj vokoj kaj aŭdiomesaĝoj." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Permite el acceso al micrófono para llamadas de voz y mensajes de audio." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Permite el acceso al micrófono para llamadas de voz y mensajes de audio." + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -344919,6 +348308,18 @@ "value" : "Engedélyezze a mikrofonhoz való hozzáférést hanghívások és hangüzenetek küldéséhez." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Consenti l'accesso al microfono per le chiamate vocali e i messaggi audio." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "音声通話および音声メッセージのためにマイクへのアクセスを許可してください。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -344937,6 +348338,18 @@ "value" : "Zezwól na dostęp do mikrofonu dla połączeń głosowych i wiadomości audio." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Permitir acesso ao microfone para chamadas de voz e mensagens de áudio." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Permite accesul la microfon pentru apeluri vocale și mesaje audio." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -344949,6 +348362,12 @@ "value" : "Tillåt åtkomst till mikrofon för röstsamtal och ljudmeddelanden." } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sesli aramalar ve sesli mesajlar için mikrofon erişimine izin verin." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -344960,6 +348379,12 @@ "state" : "translated", "value" : "请允许访问麦克风以进行语音通话和录制语音消息。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "允許使用麥克風以進行語音通話與語音訊息。" + } } } }, @@ -344981,7 +348406,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Fayl, musiqi və səs göndərə bilməyiniz üçün {app_name} musiqi və səslərə müraciət etməlidir." + "value" : "Fayl, musiqi və səs göndərə bilməyiniz üçün {app_name} musiqi və səslərə erişməlidir." } }, "bal" : { @@ -345927,7 +349352,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Foto və video göndərə bilməyiniz üçün {app_name} foto kitabxanasına müraciət etməlidir, ancaq bu icazəyə birdəfəlik rədd cavabı verilib. Ayarlar → \"İcazələr\"ə toxunun və \"Foto və videolar\"ı işə salın." + "value" : "Foto və video göndərə bilməyiniz üçün {app_name} foto kitabxanasına erişməlidir, ancaq bu erişimə birdəfəlik rədd cavabı verilib. Ayarlar → \"İcazələr\"ə toxunun və \"Foto və videolar\"ı işə salın." } }, "bal" : { @@ -346861,7 +350286,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "{app_name} qoşmaları və medianı saxlamaq üçün anbara müraciət etməlidir." + "value" : "{app_name} qoşmaları və medianı saxlamaq üçün anbara erişməlidir." } }, "bal" : { @@ -347346,7 +350771,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "{app_name} foto və videoları saxlamaq üçün anbara müraciət etməlidir, ancaq bu icazəyə həmişəlik rədd cavabı verilib. Lütfən tətbiq ayarlarına gedin, \"İcazələr\"i seçin və \"Anbar\" icazəsini fəallaşdırın." + "value" : "{app_name} foto və videoları saxlamaq üçün anbara erişməlidir, ancaq bu erişimə həmişəlik rədd cavabı verilib. Lütfən tətbiq ayarlarına gedin, \"İcazələr\"i seçin və \"Anbar\" icazəsini fəallaşdırın." } }, "bal" : { @@ -348301,6 +351726,12 @@ "permissionsWriteCommunity" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu icmada yazma icazəniz yoxdur" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -348313,6 +351744,12 @@ "value" : "V této komunitě nemáte oprávnění k zápisu" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Du hast keine Schreibrechte in dieser Community" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -348325,29 +351762,113 @@ "value" : "Vi ne havas permesojn por skribi en tiu ĉi komunumo" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "No tienes permisos de escritura en esta comunidad" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "No tienes permisos de escritura en esta comunidad" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vous n'avez pas la permission d'écrire dans cette communauté" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "इस Community में आपके पास लिखने की अनुमति नहीं है" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Nincs írási jogosultsága ebben a közösségben" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Non hai i permessi di scrittura in questa comunità" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このコミュニティでは書き込み権限がありません" + } + }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이 커뮤니티에서 작정 권한이 없습니다" } }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je hebt geen schrijfrechten in deze Community" + } + }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nie masz uprawnień do zapisu w tej społeczności" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Você não tem permissões de escrita nesta Comunidade" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nu aveți permisiune de scriere în această comunitate" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "У вас нет прав на отправку в этом сообществе" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Du har inte skrivrättigheter i denna Community" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu toplulukta yazma izniniz yok" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Вам не надано дозвіл на дописування у цій спільноті" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "你没有在该社群中写入的权限" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "您在此社群中沒有發文權限" + } } } }, @@ -350267,6 +353788,177 @@ } } }, + "plusLoadsMore" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Üstəgəl daha çoxu gəlir..." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Plus načte další..." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Plus Loads More..." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Plus de téléchargement..." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Plus laad meer..." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Та багато іншого..." + } + } + } + }, + "plusLoadsMoreDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} üçün yeni özəlliklər tezliklə gəlir. {icon} {pro} Yol Xəritəsində yenilikləri kəşf edin" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nové funkce {pro} již brzy. Podívejte se, co chystáme, na plánu vývoje {pro} {icon}" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "New features coming soon to {pro}. Discover what's next on the {pro} Roadmap {icon}" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nouvelles fonctionnalités {pro} à venir. Découvrez ce qui vous attend dans la feuille de route {pro} {icon}" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nieuwe functies komen binnenkort naar {pro}. Ontdek wat er komt op de {pro} Roadmap {icon}" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nowe funkcje niedługo pojawią się w {pro}. Odkryj, co nowego na {pro} Roadmap {icon}" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Нові можливості незабаром виникнуть у {pro}. Пізнай, що буде далі, у дороговказі {pro} {icon}" + } + } + } + }, + "preferences" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tərcihlər" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Předvolby" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Einstellungen" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Preferences" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Preferencias" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Preferencias" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Préférences" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Voorkeuren" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Preferencje" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Preferințe" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Предпочтения" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inställningar" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Налаштування" + } + } + } + }, "preview" : { "extractionState" : "manual", "localizations" : { @@ -350746,4104 +354438,9479 @@ } } }, - "proActivated" : { + "previewNotification" : { "extractionState" : "manual", "localizations" : { - "cs" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "Aktivováno" + "value" : "Bildirişi önizlə" } }, - "en" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Activated" + "value" : "Náhled upozornění" } }, - "uk" : { + "de" : { "stringUnit" : { "state" : "translated", - "value" : "активовано" + "value" : "Benachrichtigungsvorschau" } - } - } - }, - "proAlreadyPurchased" : { - "extractionState" : "manual", - "localizations" : { - "cs" : { + }, + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Už máte" + "value" : "Preview Notification" } }, - "en" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "You’ve already got" + "value" : "Aperçu de la notification" } }, - "uk" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "У вас вже є" + "value" : "Voorbeeldmelding" } - } - } - }, - "proAnimatedDisplayPicture" : { - "extractionState" : "manual", - "localizations" : { - "cs" : { + }, + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Jako váš zobrazovaný profilový obrázek nastavte animovaný GIF nebo WebP!" + "value" : "Podgląd powiadomień" } }, - "en" : { + "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Go ahead and upload GIFs and animated WebP images for your display picture!" + "value" : "Предпросмотр уведомления" } }, - "uk" : { + "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Не зволікайте і завантажуйте GIF та анімовані WebP картинки для свого аватара!" + "value" : "Förhandsgranska avisering" } - } - } - }, - "proAnimatedDisplayPictureCallToActionDescription" : { - "extractionState" : "manual", - "localizations" : { - "en" : { + }, + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Get animated display pictures and unlock premium features with {app_pro}" + "value" : "Попередній перегляд сповіщень" } } } }, - "proAnimatedDisplayPictureModalDescription" : { + "proActivated" : { "extractionState" : "manual", "localizations" : { - "cs" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "uživatelé mohou nahrávat GIFy" + "value" : "Aktivləşdirildi" } }, - "en" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "users can upload GIFs" + "value" : "Aktivováno" } }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "користувачі можуть завантажувати GIF" - } - } - } - }, - "proAnimatedDisplayPicturesNonProModalDescription" : { - "extractionState" : "manual", - "localizations" : { - "cs" : { + "de" : { "stringUnit" : { "state" : "translated", - "value" : "Nahrajte GIFy se" + "value" : "Aktiviert" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Upload GIFs with" + "value" : "Activated" } }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Завантажувати GIF з" - } - } - } - }, - "proCallToActionLongerMessages" : { - "extractionState" : "manual", - "localizations" : { - "en" : { + "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Want to send longer messages? Send more text and unlock premium features with {app_pro}" + "value" : "Activado" } - } - } - }, - "proCallToActionPinnedConversations" : { - "extractionState" : "manual", - "localizations" : { - "en" : { + }, + "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Want more pins? Organize your chats and unlock premium features with {app_pro}" + "value" : "Activado" } - } - } - }, - "proCallToActionPinnedConversationsMoreThan" : { - "extractionState" : "manual", - "localizations" : { - "en" : { + }, + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Want more than 5 pins? Organize your chats and unlock premium features with {app_pro}" + "value" : "Activé" } - } - } - }, - "proFeatureListAnimatedDisplayPicture" : { - "extractionState" : "manual", - "localizations" : { - "cs" : { + }, + "hi" : { "stringUnit" : { "state" : "translated", - "value" : "Nahrajte GIF a WebP jako zobrazovaný obrázek" + "value" : "सक्रिय किया गया" } }, - "en" : { + "it" : { "stringUnit" : { "state" : "translated", - "value" : "Upload GIF and WebP display pictures" + "value" : "Attivato" } }, - "uk" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Завантажуйте GIF та WebP аватари" + "value" : "アクティベート済み" } - } - } - }, - "proFeatureListLargerGroups" : { - "extractionState" : "manual", - "localizations" : { - "ca" : { + }, + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Xateja de grups més grans fins a 300 membres" + "value" : "Geactiveerd" } }, - "cs" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Větší soukromé skupiny až 300 členů" + "value" : "Aktywowano" } }, - "en" : { + "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Larger group chats up to 300 members" + "value" : "Ativado" } }, - "hu" : { + "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Nagyobb csoportos beszélgetések akár 300 taggal" + "value" : "Activat" } }, - "uk" : { + "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Більша кількість — до 300 учасників — групових чатів" + "value" : "Активирован" } - } - } - }, - "proFeatureListLoadsMore" : { - "extractionState" : "manual", - "localizations" : { - "ca" : { + }, + "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Plus carrega funcions més exclusives" + "value" : "Aktiverat" } }, - "cs" : { + "tr" : { "stringUnit" : { "state" : "translated", - "value" : "A další exkluzivních funkce" + "value" : "Etkinleştirildi" } }, - "en" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Plus loads more exclusive features" + "value" : "активовано" } }, - "hu" : { + "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "Plusz még több exkluzív funkció" + "value" : "已激活" } }, - "uk" : { + "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "Та велика кількість ексклюзивних можливостей" + "value" : "已啟用" } } } }, - "proFeatureListLongerMessages" : { + "proAllSet" : { "extractionState" : "manual", "localizations" : { - "ca" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "Missatges de fins a 10,000 caràcters" + "value" : "Hər şey hazırdır!" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Zprávy až do 10000 znaků" + "value" : "Vše je nastaveno!" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Messages up to 10,000 characters" + "value" : "You're all set!" } }, - "hu" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Legfeljebb 10 000 karakteres üzenetek" + "value" : "Tout est prêt !" } }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Повідомлення до 10 000 символів" - } - } - } - }, - "proFeatureListPinnedConversations" : { - "extractionState" : "manual", - "localizations" : { - "cs" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Připněte neomezený počet konverzací" + "value" : "Alles is geregeld!" } }, - "en" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Pin unlimited conversations" + "value" : "Wszystko gotowe!" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Закріплюйте необмежену кількість бесід" + "value" : "Готово!" } } } }, - "profile" : { + "proAllSetDescription" : { "extractionState" : "manual", "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Profiel" - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "الملف الشخصي" - } - }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "Profil" + "value" : "{app_pro} planınız güncəlləndi! Hazırkı {pro} planınız avtomatik olaraq {date} tarixində yeniləndiyi zaman ödəniş haqqı alınacaq." } }, - "bal" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "پروفائل" + "value" : "Váš tarif {app_pro} byl aktualizován! Účtování proběhne při automatickém obnovení vašeho aktuálního tarifu {pro} dne {date}." } }, - "be" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Профіль" + "value" : "Your {app_pro} plan was updated! You will be billed when your current {pro} plan is automatically renewed on {date}." } }, - "bg" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Профил" + "value" : "Votre forfait {app_pro} a été mis à jour ! Vous serez facturé lorsque votre forfait {pro} actuel sera automatiquement renouvelé le {date}." } }, - "bn" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "প্রোফাইল" + "value" : "Je {app_pro} abonnement is bijgewerkt! Je wordt gefactureerd wanneer je huidige {pro} abonnement automatisch wordt verlengd op {date}." } }, - "ca" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Perfil" + "value" : "Twój plan {app_pro} został zaktualizowany! Opłata zostanie pobrana, kiedy Twój obecny plan {pro} odnowi się automatycznie {date}." } }, - "cs" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Profil" + "value" : "Твою підписку {app_pro} оновлено. {date} коли підписку {pro} буде подовжено, тоді й стягнуть гроші." } - }, - "cy" : { + } + } + }, + "proAlreadyPurchased" : { + "extractionState" : "manual", + "localizations" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "Proffil" + "value" : "Artıq yüksəltdiniz" } }, - "da" : { + "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Profil" + "value" : "Ja ho tens" } }, - "de" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Profil" + "value" : "Už máte" } }, - "el" : { + "de" : { "stringUnit" : { "state" : "translated", - "value" : "Προφίλ" + "value" : "Du hast bereits" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Profile" - } - }, - "eo" : { - "stringUnit" : { - "state" : "translated", - "value" : "Profilo" + "value" : "You’ve already got" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Perfil" + "value" : "Ya tienes" } }, "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Perfil" - } - }, - "et" : { - "stringUnit" : { - "state" : "translated", - "value" : "Profiil" - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Profil" + "value" : "Ya tienes" } }, - "fa" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "نمایه" + "value" : "Vous avez déjà" } }, - "fi" : { + "hi" : { "stringUnit" : { "state" : "translated", - "value" : "Profiili" + "value" : "आपके पास पहले से ही है" } }, - "fil" : { + "it" : { "stringUnit" : { "state" : "translated", - "value" : "Profile" + "value" : "Hai già attivato" } }, - "fr" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Profil" + "value" : "すでにご利用中です" } }, - "gl" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Perfil" + "value" : "Je hebt al" } }, - "ha" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Bayanin kai" + "value" : "Masz już" } }, - "he" : { + "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "פרופיל" + "value" : "Já tem" } }, - "hi" : { + "ro" : { "stringUnit" : { "state" : "translated", - "value" : "प्रोफ़ाइल" + "value" : "Deja ai" } }, - "hr" : { + "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Profil" + "value" : "У вас уже есть" } }, - "hu" : { + "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Profil" + "value" : "Du har redan" } }, - "hy-AM" : { + "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Հաշիվ" + "value" : "Zaten sahipsiniz" } }, - "id" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Profil" + "value" : "У вас вже є" } }, - "it" : { + "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "Profilo" + "value" : "您已拥有" } }, - "ja" : { + "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "プロフィール" + "value" : "您已擁有" } - }, - "ka" : { + } + } + }, + "proAnimatedDisplayPicture" : { + "extractionState" : "manual", + "localizations" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "პროფილი" + "value" : "Getdik və ekran şəkliniz üçün GIF-lər və animasiyalı WebP təsvirləri yükləyin!" } }, - "km" : { + "ca" : { "stringUnit" : { "state" : "translated", - "value" : "ប្រវត្តិរូប" + "value" : "Endavant i penja GIFs i imatges del webp animat per a la teva imatge de visualització!" } }, - "kn" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "ಪ್ರೊಫೈಲ್" + "value" : "Jako váš zobrazovaný profilový obrázek nastavte animovaný GIF nebo WebP!" } }, - "ko" : { + "de" : { "stringUnit" : { "state" : "translated", - "value" : "프로필" + "value" : "Lade GIF- und animierte WebP-Bilder als Profilbild hoch!" } }, - "ku" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "پرۆفایل" + "value" : "Go ahead and upload GIFs and animated WebP images for your display picture!" } }, - "ku-TR" : { + "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Profîl" + "value" : "¡Adelante, sube GIFs e imágenes WebP animadas para tu imagen de perfil!" } }, - "lg" : { + "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Shift" + "value" : "¡Adelante, sube GIFs e imágenes WebP animadas para tu imagen de perfil!" } }, - "lt" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Profilis" + "value" : "Téléchargez des GIF et des images WebP animées pour votre photo de profil !" } }, - "lv" : { + "hi" : { "stringUnit" : { "state" : "translated", - "value" : "Profils" + "value" : "आगे बढ़ें और अपनी डिस्प्ले तस्वीर के लिए GIF और एनिमेटेड WebP इमेज अपलोड करें!" } }, - "mk" : { + "it" : { "stringUnit" : { "state" : "translated", - "value" : "Профил" + "value" : "Carica GIF e immagini WebP animate per la tua immagine del profilo!" } }, - "mn" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Профайл" + "value" : "ディスプレイ画像としてGIFやアニメーションWebP画像をアップロードできます!" } }, - "ms" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Profil" + "value" : "Upload nu GIF's en geanimeerde WebP-afbeeldingen voor je profielfoto!" } }, - "my" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "ကိုယ်ရေး" + "value" : "Możesz przesyłać GIF-y i animowane obrazy WebP jako swoje zdjęcie profilowe!" } }, - "nb" : { + "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Profil" + "value" : "Agora pode enviar GIFs e imagens WebP animadas para a sua imagem de exibição!" } }, - "nb-NO" : { + "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Profil" + "value" : "Mergi mai departe și încarcă GIF-uri și imagini WebP animate pentru imaginea ta de profil!" } }, - "ne-NP" : { + "ru" : { "stringUnit" : { "state" : "translated", - "value" : "प्रोफाइल" + "value" : "Вперёд! И загружай анимированные GIF и WebP для вашего изображения!" } }, - "nl" : { + "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Profiel" + "value" : "Fortsätt och ladda upp GIF:ar och animerade WebP-bilder som visningsbild!" } }, - "nn-NO" : { + "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Profil" + "value" : "Hadi, profil resminiz için GIF'ler ve animasyonlu WebP görselleri yükleyin!" } }, - "ny" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Mbiri" + "value" : "Не зволікайте і завантажуйте GIF та анімовані WebP картинки для свого аватара!" } }, - "pa-IN" : { + "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "ਪ੍ਰੋਫਾਈਲ" + "value" : "快去为头像上传 GIF 或动画 WebP 图片吧!" } }, - "pl" : { + "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "Profil" + "value" : "您可以為您的顯示圖片上傳 GIF 或動畫 WebP 圖片了!" } - }, - "ps" : { + } + } + }, + "proAnimatedDisplayPictureCallToActionDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "پروفایل" + "value" : "{app_pro} ilə animasiyalı ekran şəkillərini endirin və premium özəlliklərin kilidini açın" } }, - "pt-BR" : { + "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Perfil" + "value" : "Obteniu imatges de visualització animada i desbloquegeu funcions premium amb {app_pro}" } }, - "pt-PT" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Perfil" + "value" : "Získejte možnost nahrát animovaný zobrazovaný obrázek profilu a další prémiové funkce se Session Pro" } }, - "ro" : { + "de" : { "stringUnit" : { "state" : "translated", - "value" : "Profil" + "value" : "Hole dir animierte Profilbilder und schalte Premium-Funktionen mit {app_pro} frei" } }, - "ru" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Профиль" + "value" : "Get animated display pictures and unlock premium features with {app_pro}" } }, - "sh" : { + "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Profil" + "value" : "Consigue imágenes de perfil animadas y desbloquea funciones premium con {app_pro}" } }, - "si-LK" : { + "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "පැතිකඩ" + "value" : "Consigue imágenes de perfil animadas y desbloquea funciones premium con {app_pro}" } }, - "sk" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Profil" + "value" : "Obtenez des photos de profil animées et débloquez des fonctionnalités premium avec {app_pro}" } }, - "sl" : { + "hi" : { "stringUnit" : { "state" : "translated", - "value" : "Profil" + "value" : "एनीमेटेड डिस्प्ले तस्वीरें प्राप्त करें और {app_pro} के साथ प्रीमियम सुविधाओं का अनलॉक करें" } }, - "sq" : { + "it" : { "stringUnit" : { "state" : "translated", - "value" : "Profil" + "value" : "Ottieni immagini del profilo animate e sblocca funzionalità premium con {app_pro}" } }, - "sr" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Профил" + "value" : "アニメーションディスプレイ画像を取得し、{app_pro}でプレミアム機能を解除しましょう" } }, - "sr-Latn" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Profil" + "value" : "Krijg geanimeerde profielfoto's en ontgrendel premiumfuncties met {app_pro}" } }, - "sv-SE" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Profil" + "value" : "Zyskaj animowane zdjęcia profilowe i odblokuj funkcje premium dzięki {app_pro}" } }, - "sw" : { + "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Profaili" + "value" : "Obtenha imagens de exibição animadas e desbloqueie funcionalidades premium com o {app_pro}" } }, - "ta" : { + "ro" : { "stringUnit" : { "state" : "translated", - "value" : "சுயவிவரம்" + "value" : "Obține imagini de profil animate și deblochează funcționalități premium cu {app_pro}" } }, - "te" : { + "ru" : { "stringUnit" : { "state" : "translated", - "value" : "ప్రొఫైల్" + "value" : "Получите анимированное изображение профиля и другие разблокированные премиум функции с {app_pro}" } }, - "th" : { + "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "โปรไฟล์" + "value" : "Skaffa animerade visningsbilder och lås upp premiumfunktioner med {app_pro}" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Profil" + "value" : "Animasyonlu profil resimleri edinin ve {app_pro} ile premium özelliklerin kilidini açın" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Профіль" - } - }, - "ur-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "پروفائل" - } - }, - "uz" : { - "stringUnit" : { - "state" : "translated", - "value" : "Profil" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hồ sơ cá nhân" - } - }, - "xh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Iprofayile" + "value" : "Отримайте анімовані аватари та розблокуйте преміальні функції з {app_pro}" } }, "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "个人资料" + "value" : "获取动画头像并使用 {app_pro} 解锁高级功能" } }, "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "個人檔案" + "value" : "取得動畫顯示圖片並透過 {app_pro} 解鎖進階功能" } } } }, - "profileDisplayPicture" : { + "proAnimatedDisplayPictureFeature" : { "extractionState" : "manual", "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vertoon Prent" - } - }, - "ar" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "صورة العرض" + "value" : "Animasiyalı profil şəkli" } }, - "az" : { + "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Ekran şəkli" + "value" : "Imatge de pantalla animada" } }, - "bal" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "تصویر دکھائیں" + "value" : "Animovaný zobrazovaný obrázek" } }, - "be" : { + "de" : { "stringUnit" : { "state" : "translated", - "value" : "Выява для адлюстравання" + "value" : "Animiertes Profilbild" } }, - "bg" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Профилна снимка" + "value" : "Animated Display Picture" } }, - "bn" : { + "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "প্রদর্শনী ছবি" + "value" : "Imagen de perfil animada" } }, - "ca" : { + "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Imatge de perfil" + "value" : "Imagen de perfil animada" } }, - "cs" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Zobrazovaný obrázek" + "value" : "Photo de profil animée" } }, - "cy" : { + "hi" : { "stringUnit" : { "state" : "translated", - "value" : "Dangos llun" + "value" : "एनिमेटेड डिस्प्ले तस्वीर" } }, - "da" : { + "it" : { "stringUnit" : { "state" : "translated", - "value" : "Display Picture" + "value" : "Immagine del profilo animata" } }, - "de" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Anzeigebild" + "value" : "アニメーション表示画像" } }, - "el" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Display Picture" + "value" : "Geanimeerde profielfoto" } }, - "en" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Display Picture" + "value" : "Animowany obraz profilowy" } }, - "eo" : { + "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Montrata Bildo" + "value" : "Imagem de exibição animada" } }, - "es-419" : { + "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Imagen de perfil" + "value" : "Poză de profil animată" } }, - "es-ES" : { + "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Imagen de perfil" + "value" : "Анимированное изображение профиля" } }, - "et" : { + "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Kuvapilt" + "value" : "Animerad visningsbild" } }, - "eu" : { + "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Erakutsi Irudia" + "value" : "Profil Onur Seçin" } }, - "fa" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "نمایش تصویر نمایه" + "value" : "Анімоване зображення профілю" } }, - "fi" : { + "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "Näyttökuva" + "value" : "动画头像" } }, - "fil" : { + "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "Display Picture" + "value" : "動畫顯示圖片" } - }, - "fr" : { + } + } + }, + "proAnimatedDisplayPictureModalDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "Définir une photo de profil" + "value" : "istifadəçiləri GIF-ləri yükləyə bilər" } }, - "gl" : { + "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Imaxe de perfil" + "value" : "els usuaris poden penjar GIFs" } }, - "ha" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Hoto na Nuna Fuska" + "value" : "uživatelé mohou nahrávat GIFy" } }, - "he" : { + "de" : { "stringUnit" : { "state" : "translated", - "value" : "תמונת תצוגה" + "value" : "Nutzer können GIFs hochladen" } }, - "hi" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "प्रदर्शन चित्र" + "value" : "users can upload GIFs" } }, - "hr" : { + "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Slika za prikaz" + "value" : "los usuarios pueden subir GIFs" } }, - "hu" : { + "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Profilkép" + "value" : "los usuarios pueden subir GIFs" } }, - "hy-AM" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Ցուցադրվող գլուխ" + "value" : "les utilisateurs peuvent télécharger des GIFs" } }, - "id" : { + "hi" : { "stringUnit" : { "state" : "translated", - "value" : "Tampilan Gambar" + "value" : "उपयोगकर्ता GIF अपलोड कर सकते हैं" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Mostra immagine" + "value" : "gli utenti possono caricare GIF" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "ディスプレイの画像" - } - }, - "ka" : { - "stringUnit" : { - "state" : "translated", - "value" : "პროფილის სურათი" - } - }, - "km" : { - "stringUnit" : { - "state" : "translated", - "value" : "បង្ហាញរូបភាព" + "value" : "ユーザーはGIFをアップロードできます" } }, - "kn" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "ಪ್ರದರ್ಶನ ಚಿತ್ರ" + "value" : "gebruikers kunnen GIF's uploaden" } }, - "ko" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "프로필 사진 설정" + "value" : "użytkownicy mogą przesyłać GIF-y" } }, - "ku" : { + "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "پیشاندانی وێنە" + "value" : "os utilizadores podem carregar GIFs" } }, - "ku-TR" : { + "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Resmê Xuya bike" + "value" : "utilizatorii pot încărca GIF-uri" } }, - "lg" : { + "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Ekifananyi Kyongezebwa" + "value" : "пользователи могут загружать GIF-файлы" } }, - "lo" : { + "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "ຮູບພາບສະແດງ" + "value" : "användare kan ladda upp GIF:ar" } }, - "lt" : { + "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Rodomas paveikslas" + "value" : "kullanıcılar GIF yükleyebilir" } }, - "lv" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Atainojamais attēls" + "value" : "користувачі можуть завантажувати GIF" } }, - "mk" : { + "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "Слика за прикажување" + "value" : "用户可上传 GIF" } }, - "mn" : { + "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "Дүр зураг" + "value" : "用戶可以上傳 GIF" } - }, - "ms" : { + } + } + }, + "proAnimatedDisplayPictures" : { + "extractionState" : "manual", + "localizations" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "Paparan Gambar" + "value" : "Animasiyalı ekran şəkilləri" } }, - "my" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "ပုံပြပါမည့်ဓာတ်ပုံ" + "value" : "Animované zobrazované obrázky" } }, - "nb" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Visningsbilde" + "value" : "Animated Display Pictures" } }, - "nb-NO" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Display Picture" + "value" : "Photos de profil animées" } }, - "ne-NP" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "प्रदर्शन तस्वीर" + "value" : "Geanimeerde profielfoto's" } }, - "nl" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Toon afbeelding" + "value" : "Animowane obrazy profilu" } }, - "nn-NO" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Visingsbilde" + "value" : "Анімовані зображення облікового запису" } - }, - "ny" : { + } + } + }, + "proAnimatedDisplayPicturesDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "Chithunzi Chowonetsera" + "value" : "Animasiyalı GIF və WebP təsvirlərini ekran şəklini olaraq təyin edin." } }, - "pa-IN" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "ਪ੍ਰਦਰਸ਼ਨ ਚਿੱਤਰ" + "value" : "Nastavte si animované obrázky GIF a WebP jako svůj zobrazovaný profilový obrázek." } }, - "pl" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Zdjęcie profilowe" + "value" : "Set animated GIFs and WebP images as your display picture." } }, - "ps" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "د نندارې انځور" + "value" : "Définissez des GIF et des images WebP animées comme photo de profil." } }, - "pt-BR" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Imagem de Exibição" + "value" : "Stel geanimeerde GIF's en WebP-afbeeldingen in als je profielfoto." } }, - "pt-PT" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Exibir Imagem" + "value" : "Ustawiaj animowane obrazy GIF i WebP jako swój obraz profilu." } }, - "ro" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Afișează imaginea" + "value" : "Встановлювати анімовані зображення GIF та WebP як ваше зображення облікового запису." } - }, - "ru" : { + } + } + }, + "proAnimatedDisplayPicturesNonProModalDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "Изображение профиля" + "value" : "GIF-ləri yükləyin" } }, - "sh" : { + "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Prikaz slike" + "value" : "Penja els gifs amb" } }, - "si-LK" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "ප්‍රදර්ශන ඡායාරූපය" + "value" : "Nahrajte GIFy se" } }, - "sk" : { + "de" : { "stringUnit" : { "state" : "translated", - "value" : "Profilový obrázok" + "value" : "GIFs hochladen mit" } }, - "sl" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Prikazna slika" + "value" : "Upload GIFs with" } }, - "sq" : { + "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Fotografi për ekran" + "value" : "Sube GIFs con" } }, - "sr" : { + "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Слика за приказ" + "value" : "Sube GIFs con" } }, - "sr-Latn" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Prikaz slike" + "value" : "Téléversez des GIF avec" } }, - "sv-SE" : { + "hi" : { "stringUnit" : { "state" : "translated", - "value" : "Visa bild" + "value" : "GIF अपलोड करें" } }, - "sw" : { + "it" : { "stringUnit" : { "state" : "translated", - "value" : "Picha ya Onyesho" + "value" : "Carica GIF con" } }, - "ta" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "அடுத்தப்படியாக" + "value" : "GIFをアップロード(PRO)" } }, - "te" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "ప్రదర్శన చిత్రం" + "value" : "Upload GIF's met" } }, - "th" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "รูปภาพที่แสดง" + "value" : "Przesyłaj GIF-y z" } }, - "tr" : { + "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Profil Resmini Seçin" + "value" : "Carregue GIFs com" } }, - "uk" : { + "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Встановити зображення для показу" + "value" : "Încarcă GIF-uri cu" } }, - "ur-IN" : { + "ru" : { "stringUnit" : { "state" : "translated", - "value" : "ڈسپلے تصویر" + "value" : "Загружайте GIF с" } }, - "uz" : { + "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Displey rasm" + "value" : "Ladda upp GIF:ar med" } }, - "vi" : { + "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Display Picture" + "value" : "ile GIF Yükleyin" } }, - "xh" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Umfanekiso Okhombisa Ubuso" + "value" : "Завантажувати GIF з" } }, "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "头像" + "value" : "使用 PRO 上传 GIF" } }, "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "顯示圖片" + "value" : "使用 {app_pro} 上傳 GIF 圖片" } } } }, - "profileDisplayPictureRemoveError" : { + "proAutoRenewTime" : { "extractionState" : "manual", "localizations" : { - "af" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "Kon nie vertoonbeeld verwyder nie." + "value" : "{pro}, {time} tarixində avto-yenilənir" } }, - "ar" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "فشل في إزالة صورة العرض." + "value" : "{pro} se automaticky obnoví za {time}" } }, - "az" : { + "de" : { "stringUnit" : { "state" : "translated", - "value" : "Ekran şəklini silmə uğursuz oldu." + "value" : "Automatische {pro} Erneuerung in {time}" } }, - "bal" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "ڈسپلے تصویر کو ہٹانے میں ناکامی" + "value" : "{pro} auto-renewing in {time}" } }, - "be" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Не атрымалася выдаліць выяву." + "value" : "{pro} se renouvelle automatiquement dans {time}" } }, - "bg" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Неуспешно премахване на картината за показване." + "value" : "{pro} wordt automatisch verlengd over {time}" } }, - "bn" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "প্রদর্শন ছবি সরাতে ব্যর্থ হয়েছে।" + "value" : "Automatyczne odnowienie subskrypcji {pro}: {time}" } }, - "ca" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Error en eliminar la imatge de perfil." + "value" : "{pro} автоматично оновиться за {time}" } - }, - "cs" : { + } + } + }, + "proBadge" : { + "extractionState" : "manual", + "localizations" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "Chyba při odstraňování zobrazovaného obrázku." + "value" : "{pro} nişanı" } }, - "cy" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Methwyd tynnu'r llun arddangos." + "value" : "Odznak {pro}" } }, - "da" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Kunne ikke fjerne displaybillede." + "value" : "{pro} Badge" } }, - "de" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Fehler beim Entfernen des Profilbildes." + "value" : "Badge {pro}" } }, - "el" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Αποτυχία κατάργησης εικόνας εμφάνισης." + "value" : "{pro} badge" } }, - "en" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Failed to remove display picture." + "value" : "Odznaka {pro}" } }, - "eo" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Malsukcesis forigi montrotan bildon." + "value" : "Значок {pro}" } - }, - "es-419" : { + } + } + }, + "proBadges" : { + "extractionState" : "manual", + "localizations" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "Falló al remover foto de perfil." + "value" : "Nişanlar" } }, - "es-ES" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Fallo al eliminar la foto de perfil." + "value" : "Odznaky" } }, - "et" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Kuvapildi eemaldamine ebaõnnestus." + "value" : "Badges" } }, - "eu" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Ez da posible izan erakusizko irudia kentzea." + "value" : "Badges" } }, - "fa" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "حذف تصویر نمایشی ناموفق بود." + "value" : "Badges" } }, - "fi" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Näyttökuvan poisto ei onnistunut." + "value" : "Odznaki" } }, - "fil" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Nabigo sa pag-alis ng display picture." + "value" : "Позначки" } - }, - "fr" : { + } + } + }, + "proBadgesDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "Échec de suppression de la photo de profil." + "value" : "Ekran adınızın yanında eksklüziv bir nişanla {app_name} tətbiqini dəstəklədiyinizi göstərin." } }, - "gl" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Non se puido eliminar a imaxe de perfil." + "value" : "Vyjádřete svou podporu {app_name} pomocí exkluzivního odznaku vedle svého zobrazovaného jména." } }, - "ha" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "An kasa cire hoton nunawa." + "value" : "Show your support for {app_name} with an exclusive badge next to your display name." } }, - "he" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "נכשל הסרת תמונת הצגה." + "value" : "Affichez votre soutien à {app_name} avec un badge exclusif, à côté de votre nom d'affichage." } }, - "hi" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "डिस्प्ले तस्वीर हटाने में विफल।" + "value" : "Toon je steun voor {app_name} met een exclusieve badge naast je schermnaam." } }, - "hr" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Uklanjanje slike za prikaz nije uspjelo." + "value" : "Pokaż swoje wsparcie dla {app_name} ekskluzywną odznaką obok swojej nazwy wyświetlanej." } }, - "hu" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Nem sikerült törölni a profilképet." + "value" : "Продемонструйте свою підтримку {app_name} з ексклюзивним значком поруч з власним іменем." } - }, - "hy-AM" : { + } + } + }, + "proBadgesSent" : { + "extractionState" : "manual", + "localizations" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "Չհաջողվեց հեռացնել ցուցադրվող լուսանկարը։" + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} {pro} nişan göndərildi" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} {pro} nişan göndərildi" + } + } + } + } + } } }, - "id" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Gagal menghapus gambar profil." + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} {pro} odznaky odeslány" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} {pro} odznaků odesláno" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} {pro} odznak odeslán" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} {pro} odznaků odesláno" + } + } + } + } + } } }, - "it" : { + "el" : { "stringUnit" : { "state" : "translated", - "value" : "Impossibile rimuovere l'immagine del profilo." + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} {pro} Σήμα στάλθηκε" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} {pro} Εμβλήματα στάλθηκαν" + } + } + } + } + } } }, - "ja" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "表示画像の削除に失敗しました。" + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} {pro} Badge Sent" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} {pro} Badges Sent" + } + } + } + } + } } }, - "ka" : { + "et" : { "stringUnit" : { "state" : "translated", - "value" : "ვერ შევძელიში სურათის გამოღების მოცილება" + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} {pro} Tunnusmärk Saadetud" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} {pro} Tunnusmärki Saadetud" + } + } + } + } + } } }, - "km" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "បរាជ័យក្នុងការដករូបតំណាងបង្ហាញចេញ។" + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Badge {total} {pro} envoyé" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Badge {total} {pro} envoyé" + } + } + } + } + } } }, - "kn" : { + "nb" : { "stringUnit" : { "state" : "translated", - "value" : "ಪ್ರದರ್ಶನ ಚಿತ್ರವನ್ನು ತೆಗೆದುಹಾಕಲು ವಿಫಲವಾಗಿದೆ." - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "표시 사진을 제거하지 못했습니다." + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} {pro} Merke Sendt" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} {pro} Merker Sendt" + } + } + } + } + } } }, - "ku" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "شکستی پاشەکەوتکردنی وێنەی پەیپەر" + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wysłano {total} odznaki {pro}" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wysłano {total} odznak {pro}" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wysłano {total} odznakę {pro}" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wysłano {total} odznak {pro}" + } + } + } + } + } } - }, - "ku-TR" : { + } + } + }, + "proBadgeVisible" : { + "extractionState" : "manual", + "localizations" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "Rûberê profîlê remove têbîne" + "value" : "{app_pro} nişanını digər istifadəçilərə göstər" } }, - "lg" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Ensobi okwogolola ekifo ky'ebifaananyi." + "value" : "Zobrazit odznak {app_pro} ostatním uživatelům" } }, - "lt" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Nepavyko pašalinti profilio paveiksliuko." + "value" : "Show {app_pro} badge to other users" } }, - "lv" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Neizdevās noņemt profila attēlu." + "value" : "Afficher l’insigne {app_pro} aux autres utilisateurs" } }, - "mk" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Неуспешно отстранување на слика за профил." + "value" : "Toon het {app_pro} badge aan andere gebruikers" } }, - "mn" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Харагдах зургыг устгахад алдаа гарлаа." + "value" : "Pokaż odznakę {app_pro} innym użytkownikom" } }, - "ms" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Gagal mengeluarkan gambar paparan." + "value" : "Показувати значок {app_pro} іншим користувачам" } - }, - "my" : { + } + } + }, + "proBetaFeatures" : { + "extractionState" : "manual", + "localizations" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "ပြထားသောပုံကို ဖယ်ရန် မဖြစ်နိုင်ပါ" + "value" : "{pro} Beta Xüsusiyyətləri" } }, - "nb" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Kunne ikke fjerne profilbilde." + "value" : "Funkce beta verze {pro}" } }, - "nb-NO" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Kunne ikke fjerne visningsbildet." + "value" : "{pro} Beta Features" } }, - "ne-NP" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "प्रदर्शन तस्वीर हटाउन असफल" + "value" : "Fonctionnalités {pro}" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Verwijderen profielfoto mislukt." + "value" : "{pro} functies" } }, - "nn-NO" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Klarte ikkje fjerna visningsbilete." + "value" : "Funkcje {pro}" } }, - "ny" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Zalephera kuchotsa chithunzi chowonetsera." + "value" : "Можливості {pro}" } - }, - "pa-IN" : { + } + } + }, + "proBilledAnnually" : { + "extractionState" : "manual", + "localizations" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "ਡਿਸਪਲੇ ਚਿੱਤਰ ਨੂੰ ਹਟਾਉਣ ਵਿੱਚ ਅਸਫਲ" + "value" : "{price} - illik haqq" } }, - "pl" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Nie udało się usunąć zdjęcia profilowego." + "value" : "{price} účtováno ročně" } }, - "ps" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "د نمایش انځور لرې کولو کې ناکام" + "value" : "{price} Billed Annually" } }, - "pt-BR" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Falha ao remover a imagem de exibição." + "value" : "{price} facturé annuellement" } }, - "pt-PT" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Erro ao remover a foto do perfil." + "value" : "{price} Jaarlijks gefactureerd" } }, - "ro" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Nu s-a putut elimina imaginea de profil." + "value" : "Opłata roczna: {price}" } }, - "ru" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Не удалось удалить изображение профиля." + "value" : "{price} сплата щорічно" } - }, - "sh" : { + } + } + }, + "proBilledMonthly" : { + "extractionState" : "manual", + "localizations" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "Nije uspjelo uklanjanje prikazne slike." + "value" : "{price} - aylıq haqq" } }, - "si-LK" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "පේෂණ ඡායාරූපය ඉවත් කිරීමට අසමත් විය." + "value" : "{price} účtováno měsíčně" } }, - "sk" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Nepodarilo sa odstrániť profilový obrázok." + "value" : "{price} Billed Monthly" } }, - "sl" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Ni uspelo odstraniti prikazne slike." + "value" : "{price} facturé mensuellement" } }, - "sq" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Dështoi heqja e figurës së paraqitjes." + "value" : "{price} Maandelijks gefactureerd" } }, - "sr" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Неуспех у уклањању слике профила" + "value" : "Opłata miesięczna: {price}" } }, - "sr-Latn" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Neuspelo uklanjanje slike profila." + "value" : "{price} сплата щомісячно" } - }, - "sv-SE" : { + } + } + }, + "proBilledQuarterly" : { + "extractionState" : "manual", + "localizations" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "Misslyckades med att ta bort visningsbild." + "value" : "{price} - rüblük haqq" } }, - "sw" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Imeshindikana kuondoa picha ya kuonyesha." + "value" : "{price} účtováno čtvrtletně" } }, - "ta" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "காட்சி படம் நீக்க முடியவில்லை." + "value" : "{price} Billed Quarterly" } }, - "te" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "ప్రదర్శన చిత్రాన్ని తొలగించడంలో విఫలమైంది." + "value" : "{price} facturé trimestriellement" } }, - "th" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "การลบรูปโปรไฟล์ล้มเหลว" + "value" : "{price} per kwartaal gefactureerd" } }, - "tr" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Profil resmi kaldırılamadı." + "value" : "Opłata kwartalna: {price}" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Не вдалося видалити зображення профілю" + "value" : "{price} сплата щоквартально" } - }, - "ur-IN" : { + } + } + }, + "proCallToActionLongerMessages" : { + "extractionState" : "manual", + "localizations" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "ڈسپلے تصویر ہٹانے میں ناکام" + "value" : "Daha uzun mesajlar göndərmək istəyirsiniz? {app_pro} ilə daha çox mətn göndərin və premium özəlliklərin kilidini açın" } }, - "uz" : { + "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Displey rasmini olib tashlashda muammo chiqdi." + "value" : "Voleu enviar missatges més llargs? Envia més text i desbloqueja funcions premium amb {app_pro}" } }, - "vi" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Không thể xóa hình đại diện." + "value" : "Chcete posílat delší zprávy? Posílejte více textu odemknutím prémiových funkcí Session Pro" } }, - "xh" : { + "de" : { "stringUnit" : { "state" : "translated", - "value" : "Koyekile ukususa umfanekiso wokubonisa." + "value" : "Du möchtest längere Nachrichten senden? Sende mehr Text und schalte Premium-Funktionen mit {app_pro} frei" } }, - "zh-CN" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "移除头像失败。" + "value" : "Want to send longer messages? Send more text and unlock premium features with {app_pro}" } }, - "zh-TW" : { + "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "無法刪除顯示圖片。" + "value" : "¿Quieres enviar mensajes más largos? Envía más texto y desbloquea funciones premium con {app_pro}" } - } - } - }, - "profileDisplayPictureSet" : { - "extractionState" : "manual", - "localizations" : { - "af" : { + }, + "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Stel vertoon prent" + "value" : "¿Quieres enviar mensajes más largos? Envía más texto y desbloquea funciones premium con {app_pro}" } }, - "ar" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "تعيين صورة العرض" + "value" : "Vous voulez envoyer des messages plus longs ? Envoyez plus de messages et débloqué les fonctionnalités premium avec {app_pro}" } }, - "az" : { + "hi" : { "stringUnit" : { "state" : "translated", - "value" : "Ekran şəklini ayarla" + "value" : "लंबे संदेश भेजना चाहते हैं? अधिक टेक्स्ट भेजें और {app_pro} के साथ प्रीमियम सुविधाओं का अनलॉक करें" } }, - "bal" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "پاره نمای گونیکی مقرر کـــــــن" + "value" : "Szeretne hosszabb üzeneteket küldeni? Küldjön több szöveget és oldja fel a prémium funkciókat a {app_pro} szolgáltatással" } }, - "be" : { + "it" : { "stringUnit" : { "state" : "translated", - "value" : "Усталюйце выяву для адлюстравання" + "value" : "Vuoi inviare messaggi più lunghi? Invia più testo e sblocca funzionalità premium con {app_pro}" } }, - "bg" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Задаване на профилна снимка" + "value" : "長文を送りたいですか?{app_pro}でより多くのテキストを送り、プレミアム機能を解除しましょう。" } }, - "bn" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "প্রদর্শন চিত্র সেট করুন" + "value" : "Wil je langere berichten versturen? Verstuur meer tekst en ontgrendel premiumfuncties met {app_pro}" } }, - "ca" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Definiu la imatge del perfil" + "value" : "Chcesz wysyłać dłuższe wiadomości? Wyślij więcej tekstu i odblokuj funkcje premium dzięki {app_pro}" } }, - "cs" : { + "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Nastavit zobrazovaný obrázek" + "value" : "Quer enviar mensagens mais longas? Envie mais texto e desbloqueie funcionalidades premium com {app_pro}" } }, - "cy" : { + "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Gosod Llun Arddangos" + "value" : "Vrei să trimiți mesaje mai lungi? Trimite mai mult text și deblochează funcții premium cu {app_pro}" } }, - "da" : { + "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Indstil profibillede" + "value" : "Хотите отправлять более длинные сообщения? Отправляйте больше текста и используйте премиум функции с {app_pro}" } }, - "de" : { + "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Anzeigebild festlegen" + "value" : "Vill du skicka längre meddelanden? Skicka mer text och lås upp premiumfunktioner med {app_pro}" } }, - "el" : { + "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Ορισμός Εικόνας Εμφάνισης" + "value" : "Daha uzun mesajlar mı göndermek istiyorsunuz? {app_pro} ile daha fazla metin gönderin ve premium özelliklerin kilidini açın" } }, - "en" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Set Display Picture" + "value" : "Хочете відправляти довші повідомлення? Надсилайте більше тексту та розблокуйте преміальні функції застосунку з Session Pro" } }, - "eo" : { + "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "Agordi Profilbildon" + "value" : "想发送更长的消息?使用 {app_pro} 发送更多文本并解锁高级功能" } }, - "es-419" : { + "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "Establecer Imagen de Perfil" + "value" : "想傳送更長的訊息嗎?與 {app_pro} 一起傳送更多文字並解鎖進階功能" + } + } + } + }, + "proCallToActionPinnedConversations" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Daha çoxunu sancmaq istəyirsiniz? {app_pro} ilə söhbətlərinizi təşkil edin və premium özəlliklərin kilidini açın" } }, - "es-ES" : { + "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Establecer imagen de perfil" + "value" : "Vols més pins? Organitzes els teus xats i desbloqueges les funcions premium amb {app_pro}" } }, - "et" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Määra kuvapilt" + "value" : "Chcete více připnutí? Organizujte své chaty a odemkněte prémiové funkce pomocí Session Pro" } }, - "eu" : { + "de" : { "stringUnit" : { "state" : "translated", - "value" : "Erakutsi argazkia ezarri" + "value" : "Mehr Anheftungen gewünscht? Organisiere deine Chats und schalte Premium-Funktionen mit {app_pro} frei" } }, - "fa" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "تنظیم تصویر نمایشی" + "value" : "Want more pins? Organize your chats and unlock premium features with {app_pro}" } }, - "fi" : { + "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Aseta näyttökuva" + "value" : "¿Quieres más conversaciones fijadas? Organiza tus chats y desbloquea funciones premium con {app_pro}" } }, - "fil" : { + "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Itakda ang Display Picture" + "value" : "¿Quieres más conversaciones fijadas? Organiza tus chats y desbloquea funciones premium con {app_pro}" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Définir une photo de profil" + "value" : "Vous voulez plus de messages épinglés ? Organisez vos chats et débloquez les fonctionnalités premium avec {app_pro}" } }, - "gl" : { + "hi" : { "stringUnit" : { "state" : "translated", - "value" : "Establecer Imaxe para Mostrar" + "value" : "अधिक पिन करना चाहते हैं? अपनी चैट व्यवस्थित करें और {app_pro} के साथ प्रीमियम सुविधाओं का अनलॉक करें" } }, - "ha" : { + "it" : { "stringUnit" : { "state" : "translated", - "value" : "Saita Hoton Mai Nunawa" + "value" : "Vuoi più chat bloccate? Organizza le tue chat e sblocca le funzionalità premium con {app_pro}" } }, - "he" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "הגדר תמונת פרופיל" + "value" : "さらにピン留めしますか?{app_pro}でチャットを整理して、プレミアム機能を解除しましょう" } }, - "hi" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "डिस्प्ले तस्वीर सेट करें" + "value" : "Wil je meer vastzetten? Organiseer je chats en ontgrendel premiumfuncties met {app_pro}" } }, - "hr" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Postavi sliku za prikaz" + "value" : "Chcesz przypinać więcej czatów? Zorganizuj konwersacje i odblokuj funkcje premium dzięki {app_pro}" } }, - "hu" : { + "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Profilkép beállítása" + "value" : "Quer fixar mais conversas? Organize os seus chats e desbloqueie funcionalidades premium com o {app_pro}" } }, - "hy-AM" : { + "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Սահմանել պրոֆիլի նկարը" + "value" : "Vrei mai multe fixări? Organizează-ți conversațiile și deblochează funcționalități premium cu {app_pro}" } }, - "id" : { + "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Atur Tampilan Gambar" + "value" : "Хотите больше закреплений? Организуйте свои чаты и получайте доступ к премиум функциям с {app_pro}" } }, - "it" : { + "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Imposta foto profilo" + "value" : "Vill du ha fler fästen? Organisera dina chattar och lås upp premiumfunktioner med {app_pro}" } }, - "ja" : { + "tr" : { "stringUnit" : { "state" : "translated", - "value" : "ディスプレイの画像をセット" + "value" : "Daha fazla sabitleme mi istiyorsunuz? Sohbetlerinizi düzenleyin ve {app_pro} ile premium özelliklerin kilidini açın" } }, - "ka" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "ავატარის არჩევა" + "value" : "Потрібно більше закріплених бесід? Впорядкуйте свої чати та розблокуйте преміальні функції з {app_pro}" } }, - "km" : { + "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "កំណត់រូបបង្ហាញ" + "value" : "想要固定更多对话?使用 {app_pro} 整理你的聊天并解锁高级功能" } }, - "kn" : { + "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "ಪ್ರೊಫೈಲ್ ಡಿಸ್ಪ್ಲೇ ಚಿತ್ರವನ್ನು ಸೆಟ್ ಮಾಡಿ" + "value" : "想要釘選更多對話嗎?使用 {app_pro} 整理您的聊天並解鎖進階功能" } - }, - "ko" : { + } + } + }, + "proCallToActionPinnedConversationsMoreThan" : { + "extractionState" : "manual", + "localizations" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "프로필 사진 설정" + "value" : "5-dən çoxunu sancmaq istəyirsiniz? {app_pro} ilə söhbətlərinizi təşkil edin və premium özəlliklərin kilidini açın" } }, - "ku" : { + "ca" : { "stringUnit" : { "state" : "translated", - "value" : "دانانی وێنەی پیشانده‌رەو" + "value" : "Vols més de 5 pins? Organitzes els teus xats i desbloqueges les funcions premium amb {app_pro}" } }, - "ku-TR" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Rismê Profîlê Çêke" + "value" : "Chcete více než 5 připnutí? Organizujte své chaty a odemkněte prémiové funkce pomocí Session Pro" } }, - "lg" : { + "de" : { "stringUnit" : { "state" : "translated", - "value" : "Tereka Ekifaananyi Ekirabika" + "value" : "Mehr als 5 Anheftungen gewünscht? Organisiere deine Chats und schalte Premium-Funktionen mit {app_pro} frei" } }, - "lt" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Nustatyti rodomą paveikslėlį" + "value" : "Want more than 5 pins? Organize your chats and unlock premium features with {app_pro}" } }, - "lv" : { + "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Iestatīt Atainojamo Attēlu" + "value" : "¿Quieres más de 5 conversaciones fijadas? Organiza tus chats y desbloquea funciones premium con {app_pro}" } }, - "mk" : { + "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Постави Слика за Профил" + "value" : "¿Quieres más de 5 conversaciones fijadas? Organiza tus chats y desbloquea funciones premium con {app_pro}" } }, - "mn" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Дэлгэцийн зургаа тохируулах" + "value" : "Vous voulez plus que 5 messages épinglés ? Organisez vos chats et débloquez les fonctionnalités premium avec {app_pro}" } }, - "ms" : { + "hi" : { "stringUnit" : { "state" : "translated", - "value" : "Tetapkan Gambar Paparan" + "value" : "5 से अधिक पिन करना चाहते हैं? अपनी चैट व्यवस्थित करें और {app_pro} के साथ प्रीमियम सुविधाओं का अनलॉक करें" } }, - "my" : { + "it" : { "stringUnit" : { "state" : "translated", - "value" : "ပုံပြင်ဆင်ထားသည့်ပုံကိုသတ်မှတ်မည်" + "value" : "Vuoi più di 5 chat bloccate? Organizza le tue chat e sblocca le funzionalità premium con {app_pro}" } }, - "nb" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Sett profilbilde" + "value" : "5件以上ピン留めしたいですか?{app_pro}でチャットを整理して、プレミアム機能を解除しましょう" } }, - "nb-NO" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Angi visningsbilde" + "value" : "Wil je meer dan 5 vastgezette gesprekken? Organiseer je chats en ontgrendel premiumfuncties met {app_pro}" } }, - "ne-NP" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "प्रदर्शन तस्वीर सेट गर्नुहोस्" + "value" : "Chcesz przypiąć więcej niż 5 czatów? Zorganizuj konwersacje i odblokuj funkcje premium dzięki {app_pro}" } }, - "nl" : { + "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Profielfoto instellen" + "value" : "Quer fixar mais de 5 conversas? Organize os seus chats e desbloqueie funcionalidades premium com {app_pro}" } }, - "nn-NO" : { + "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Set Display Picture" + "value" : "Vrei mai mult de 5 fixări? Organizează-ți conversațiile și deblochează funcționalități premium cu {app_pro}" } }, - "ny" : { + "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Set Display Picture" + "value" : "Нужно более 5 закреплений? С {app_pro} организуйте свои чаты и получите доступ к премиум функциям" } }, - "pa-IN" : { + "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "ਡਿਸਪਲੇ ਤਸਵੀਰ ਸੈੱਟ ਕਰੋ" + "value" : "Vill du ha mer än 5 fästisar? Organisera dina chattar och lås upp premiumfunktioner med {app_pro}" } }, - "pl" : { + "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Ustaw zdjęcie profilowe" + "value" : "5'ten fazla sabitleme mi istiyorsunuz? Sohbetlerinizi düzenleyin ve {app_pro} ile premium özelliklerin kilidini açın" } }, - "ps" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "ډیسپلې انځور تنظیمول" + "value" : "Потрібно понад 5 закріплених бесід? Впорядкуйте свої бесіди та розблокуйте преміальні функції з {app_pro}" } }, - "pt-BR" : { + "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "Definir Imagem de Exibição" + "value" : "想要固定超过 5 个对话?使用 {app_pro} 整理你的聊天并解锁高级功能" } }, - "pt-PT" : { + "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "Definir imagem a exibir" + "value" : "想要釘選超過 5 則對話嗎?使用 {app_pro} 整理您的聊天並解鎖進階功能" } - }, - "ro" : { + } + } + }, + "proCancellation" : { + "extractionState" : "manual", + "localizations" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Setează imaginea de profil" + "value" : "Zrušit" } }, - "ru" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Установить изображение профиля" + "value" : "Cancellation" } - }, - "sh" : { + } + } + }, + "proCancellationDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Postavi sliku profila" + "value" : "Canceling your {app_pro} plan will prevent your plan from automatically renewing before your {pro} plan expires. Canceling {pro} does not result in a refund. You will continue to be able to use {app_pro} features until your plan expires.

Because you originally signed up for {app_pro} using your {platform_account}, you'll need to use the same {platform_account} to cancel your plan." } - }, - "si-LK" : { + } + } + }, + "proCancellationOptions" : { + "extractionState" : "manual", + "localizations" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "ප්‍රදර්ශන ඡායාරූපය සකසන්න" + "value" : "Dva způsoby, jak zrušit váš tarif:" } }, - "sk" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Nastaviť profilový obrázok" + "value" : "Two ways to cancel your plan:" } - }, - "sl" : { + } + } + }, + "processingRefundRequest" : { + "extractionState" : "manual", + "localizations" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Nastavi prikazno sliko" + "value" : "{platform} is processing your refund request" + } + } + } + }, + "proClearAllDataDevice" : { + "extractionState" : "manual", + "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opravdu chcete smazat svá data z tohoto zařízení?

{app_pro} nelze převést na jiný účet. Uložte si své heslo pro obnovení, abyste mohli svůj tarif {pro} později obnovit." } }, - "sq" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Vendos Paraqitjen e Profilit" + "value" : "Are you sure you want to delete your data from this device?

{app_pro} cannot be transferred to another account. Please save your Recovery Password to ensure you can restore your {pro} plan later." + } + } + } + }, + "proClearAllDataNetwork" : { + "extractionState" : "manual", + "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opravdu chcete smazat svá data ze sítě? Pokud budete pokračovat, nebudete moci obnovit vaše zprávy ani kontakty.

{app_pro} nelze převést na jiný účet. Uložte si své heslo pro obnovení, abyste mohli svůj tarif {pro} později obnovit." } }, - "sr" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Постави слику профила" + "value" : "Are you sure you want to delete your data from the network? If you continue, you will not be able to restore your messages or contacts.

{app_pro} cannot be transferred to another account. Please save your Recovery Password to ensure you can restore your {pro} plan later." + } + } + } + }, + "proDiscountTooltip" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hazırkı planınızda artıq tam {app_pro} qiymətinin {percent}% endirimi mövcuddur." } }, - "sr-Latn" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Postavi sliku profila" + "value" : "Váš aktuální tarif je již zlevněn o {percent} % z plné ceny {app_pro}." } }, - "sv-SE" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Ange visningsbild" + "value" : "Your current plan is already discounted by {percent}% of the full {app_pro} price." } }, - "sw" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Weka Picha ya Kuonyesha" + "value" : "Votre abonnement actuel bénéficie déjà d'une remise de {percent}% sur le prix de {app_pro}." } }, - "ta" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "காட்டி புகைப்படத்தை அமை" + "value" : "Je huidige abonnement is al met {percent}% korting ten opzichte van de volledige {app_pro} prijs." } }, - "te" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "ప్రొఫైల్ చిత్రాన్ని సెట్ చేయి" + "value" : "Cena Twojego obecnego planu jest obniżona o {percent}% pełnej ceny {app_pro}." } }, - "th" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "ตั้งรูปภาพโปรไฟล์" + "value" : "На поточну підписку ти вже маєш знижку {percent}% від загальної ціни {app_pro}." + } + } + } + }, + "proErrorRefreshingStatus" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} statusunu təzələmə xətası" } }, - "tr" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Profil Resmini Seçin" + "value" : "Chyba obnovování stavu {pro}" } }, - "uk" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Error refreshing {pro} status" + } + } + } + }, + "proExpired" : { + "extractionState" : "manual", + "localizations" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "Обрати картинку для показу" + "value" : "Müddəti bitib" } }, - "ur-IN" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "ڈسپلے تصویر سیٹ کریں" + "value" : "Platnost vypršela" } }, - "uz" : { + "de" : { "stringUnit" : { "state" : "translated", - "value" : "Displey rasmini belgilang" + "value" : "Abgelaufen" } }, - "vi" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Thiết Lập Hình ảnh Đại diện" + "value" : "Expired" } }, - "xh" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Set Display Picture" + "value" : "Expiré" } }, - "zh-CN" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "设置头像" + "value" : "Verlopen" } }, - "zh-TW" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "設定顯示圖片" + "value" : "Підписка сплила" } } } }, - "profileDisplayPictureSizeError" : { + "proExpiredDescription" : { "extractionState" : "manual", "localizations" : { - "af" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "Kies 'n kleiner lêer." + "value" : "Təəssüf ki, {pro} planınızın müddəti bitib. {app_pro} tətbiqinin eksklüziv imtiyazlarına və özəlliklərinə erişimi davam etdirmək üçün yeniləyin." } }, - "ar" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "الرجاء اختيار ملف أصغر." + "value" : "Bohužel, váš tarif {pro} vypršel. Obnovte jej, abyste nadále měli přístup k exkluzivním výhodám a funkcím {app_pro}." } }, - "az" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Lütfən daha kiçik bir fayl götürün." + "value" : "Unfortunately, your {pro} plan has expired. Renew to keep accessing the exclusive perks and features of {app_pro}." } }, - "bal" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "براہء مہربانی ایک چھوٹا فائل منتخب کنیں." + "value" : "Malheureusement, votre formule {pro} a expiré. Renouvelez-la pour continuer à profiter des avantages et fonctionnalités exclusifs de {app_pro}." } }, - "be" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Калі ласка, абярыце меншы файл." + "value" : "Helaas is je {pro} abonnement verlopen. Verleng om toegang te blijven houden tot de exclusieve voordelen en functies van {app_pro}." } }, - "bg" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Моля, изберете по-малък файл." + "value" : "Niestety, Twój plan {pro} wygasł. Odnów go, by odzyskać dostęp do ekskluzywnych korzyści i funkcji {app_pro}." } }, - "bn" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "দয়া করে একটি ছোট ফাইল নির্বাচন করুন।" + "value" : "На жаль, підписка {pro} сплила. Онови її задля збереження переваг і можливостей {app_pro}." } - }, - "ca" : { + } + } + }, + "proExpiringSoon" : { + "extractionState" : "manual", + "localizations" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "Si us plau, selecciona un fitxer més petit." + "value" : "Tezliklə bitir" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Prosím vyberte menší soubor." + "value" : "Brzy vyprší" } }, - "cy" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Dewiswch ffeil llai." + "value" : "Expiring Soon" } }, - "da" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Venligst vælg en mindre fil." + "value" : "Expiration imminente" } }, - "de" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Bitte wähle eine kleinere Datei." + "value" : "Verloopt binnenkort" } }, - "el" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Παρακαλώ επιλέξτε ένα μικρότερο αρχείο." + "value" : "Niedługo wygaśnie" } }, - "en" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Please pick a smaller file." + "value" : "Невдовзі спливе підписка" + } + } + } + }, + "proExpiringSoonDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} planınızın müddəti {time} vaxtında bitir. {app_pro} tətbiqinin eksklüziv imtiyazlarına və özəlliklərinə erişimi davam etdirmək üçün planınızı güncəlləyin." } }, - "eo" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Bonvolu elekti plej malgrandan dosieron." + "value" : "Váš tarif {pro} vyprší za {time}. Aktualizujte si tarif, abyste i nadále měli přístup k exkluzivním výhodám a funkcím {app_pro}." } }, - "es-419" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Por favor, elija un archivo más pequeño." + "value" : "Your {pro} plan is expiring in {time}. Update your plan to keep accessing the exclusive perks and features of {app_pro}." } }, - "es-ES" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Por favor, elija un archivo más pequeño." + "value" : "Votre formule {pro} expire dans {time}. Mettez à jour votre formule pour continuer à profiter des avantages et fonctionnalités exclusifs de {app_pro}." } }, - "et" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Palun valige väiksem fail." + "value" : "Je {pro} abonnement verloopt over {time}. Werk je abonnement bij om toegang te blijven houden tot de exclusieve voordelen en functies van {app_pro}." } }, - "eu" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Mesedez, hautatu fitxategi txikiago bat." + "value" : "Twój plan {pro} wygasa za {time}. Zaktualizuj swój plan, aby zachować dostęp do ekskluzywnych korzyści i funkcji {app_pro}." } }, - "fa" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "لطفا فایل کوچکتری انتخاب کنید." + "value" : "Підписка {pro} спливе {time}. Онови підписку задля збереження переваг і можливостей {app_pro}." + } + } + } + }, + "proExpiringTime" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro}, {time} vaxtında başa çatır" } }, - "fi" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Valitse pienempi tiedosto." + "value" : "{pro} vyprší za {time}" } }, - "fil" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Pakipili ang mas maliit na file." + "value" : "{pro} expiring in {time}" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Veuillez choisir un fichier plus petit." + "value" : "{pro} expire dans {time}" } }, - "gl" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Por favor, escolle un ficheiro máis pequeno." + "value" : "{pro} спливає за {time}" } - }, - "ha" : { + } + } + }, + "proFaq" : { + "extractionState" : "manual", + "localizations" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "Zaɓi ƙaramin fayil." + "value" : "{pro} TVS" } }, - "he" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "אנא בחר קובץ קטן יותר." + "value" : "{pro} FAQ" } }, - "hi" : { + "de" : { "stringUnit" : { "state" : "translated", - "value" : "Please pick a smaller file." + "value" : "{pro} FAQ" } }, - "hr" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Molimo odaberite manju datoteku." + "value" : "{pro} FAQ" } }, - "hu" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Válassz egy kisebb fájlt." + "value" : "FAQ {pro}" } }, - "hy-AM" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Խնդրում ենք ընտրել ավելի փոքր ֆայլ:" + "value" : "{pro} FAQ" } }, - "id" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Silakan pilih berkas yang lebih kecil." + "value" : "FAQ {pro}" } }, - "it" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Scegli un file più piccolo." + "value" : "{pro} ЧАП" } - }, - "ja" : { + } + } + }, + "proFaqDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "小さいファイルを選んでください" + "value" : "{app_pro} TVS-da tez-tez verilən suallara cavab tapın." } }, - "ka" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "გთხოვთ აირჩიოთ პატარა ფაილი." + "value" : "Najděte odpovědi na časté dotazy v nápovědě {app_pro}." } }, - "km" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "សូមជ្រើសរើសឯកសារតិចជាង." + "value" : "Find answers to common questions in the {app_pro} FAQ." } }, - "kn" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "ದಯವಿಟ್ಟು ಒಂದು ಕುಿರುವಾದ ಕಡತವನ್ನು ಆಯ್ಕೆ ಮಾಡಿ." + "value" : "Trouvez des réponses aux questions fréquentes dans la FAQ de {app_pro}." } }, - "ko" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "더 작은 파일을 선택해 주세요." + "value" : "Vind antwoorden op veelgestelde vragen in de {app_pro} FAQ." } }, - "ku" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "تکایە فایلێکی بچووک بە کار بێنە." + "value" : "Znajdź odpowiedzi na często zadawane pytania w sekcji FAQ {app_pro}." } }, - "ku-TR" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Ji kerema xwe pêveke zêdetir bicikne." + "value" : "Відповіді на загальні запитання знайдеш у ЧаПи {app_pro}." } - }, - "lg" : { + } + } + }, + "proFeatureListAnimatedDisplayPicture" : { + "extractionState" : "manual", + "localizations" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "Londa fayilo etono." + "value" : "GIF və WebP ekran şəkilləri yüklə" } }, - "lt" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Pasirinkite mažesnį failą." + "value" : "Nahrajte GIF a WebP jako zobrazovaný obrázek" } }, - "lv" : { + "de" : { "stringUnit" : { "state" : "translated", - "value" : "Lūdzu, izvēlieties mazāku failu." + "value" : "GIF- und WebP-Profilbilder hochladen" } }, - "mk" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Ве молиме изберете помала датотека." + "value" : "Upload GIF and WebP display pictures" } }, - "mn" : { + "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Бага хэмжээтэй файлыг сонгоно уу." + "value" : "Sube imágenes de perfil en formato GIF y WebP" } }, - "ms" : { + "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Sila pilih fail yang lebih kecil." + "value" : "Sube imágenes de perfil en formato GIF y WebP" } }, - "my" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "ပိုတည်းသော ဖိုင်ကို ရွေးချယ်ပါ" + "value" : "Téléversez des photos de profil au format GIF ou WebP" } }, - "nb" : { + "hi" : { "stringUnit" : { "state" : "translated", - "value" : "Vennligst velg en mindre fil." + "value" : "GIF और WebP डिस्प्ले तस्वीरें अपलोड करें" } }, - "nb-NO" : { + "it" : { "stringUnit" : { "state" : "translated", - "value" : "Vennligst velg en mindre fil." + "value" : "Carica immagini profilo in formato GIF e WebP" } }, - "ne-NP" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "कृपया सानो फाइल चयन गर्नुहोस्।" + "value" : "GIFとWebPのディスプレイ画像をアップロード" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Kies een kleiner bestand." + "value" : "Upload GIF- en WebP-profielfoto's" } }, - "nn-NO" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Vennligst velg ei mindre fil." + "value" : "Prześlij obrazy profilowe w formacie GIF i WebP" } }, - "ny" : { + "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Chonde sonkhanitsani fayilo yaying’ono." + "value" : "Carregue imagens de exibição em GIF e WebP" } }, - "pa-IN" : { + "ro" : { "stringUnit" : { "state" : "translated", - "value" : "ਕਿਰਪਾ ਕਰਕੇ ਇੱਕ ਛੋਟੀ ਫਾਇਲ ਚੁਣੋ।" + "value" : "Încarcă imagini de profil GIF și WebP" } }, - "pl" : { + "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Wybierz mniejszy plik." + "value" : "Загрузка изображений в формате GIF и WebP" } }, - "ps" : { + "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "مهرباني وکړئ یو کوچنۍ فایل غوره کړئ." + "value" : "Ladda upp GIF- och WebP-visningsbilder" } }, - "pt-BR" : { + "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Escolha um arquivo menor, por favor." + "value" : "GIF ve WebP profil resmi yükleme" } }, - "pt-PT" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Por favor, escolha um ficheiro menor." + "value" : "Завантажуйте GIF та WebP аватари" } }, - "ro" : { + "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "Vă rugăm alegeți un fișier mai mic." + "value" : "上传 GIF 和 WebP 头像" } }, - "ru" : { + "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "Пожалуйста, выберите файл меньшего размера." + "value" : "上傳 GIF 和 WebP 顯示圖片" + } + } + } + }, + "proFeatureListLargerGroups" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "300 üzvə qədər daha da böyük qrup söhbətləri" } }, - "sh" : { + "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Molimo izaberite manju datoteku." + "value" : "Xateja de grups més grans fins a 300 membres" } }, - "si-LK" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "කරුණාකර කුඩා ගොනුවක් තෝරන්න." + "value" : "Větší soukromé skupiny až 300 členů" } }, - "sk" : { + "de" : { "stringUnit" : { "state" : "translated", - "value" : "Prosím zvoľte menší súbor." + "value" : "Größere Gruppenchats mit bis zu 300 Mitgliedern" } }, - "sl" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Prosimo, izberite manjšo datoteko." + "value" : "Larger group chats up to 300 members" } }, - "sq" : { + "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Ju lutemi zgjedhni një skedar më të vogël." + "value" : "Chats grupales más grandes de hasta 300 miembros" } }, - "sr" : { + "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Изаберите мању датотеку." + "value" : "Chats grupales más grandes de hasta 300 miembros" } }, - "sr-Latn" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Molimo izaberite manju datoteku." + "value" : "Groupes de discussions plus larges, jusqu'à 300 participants" } }, - "sv-SE" : { + "hi" : { "stringUnit" : { "state" : "translated", - "value" : "Vänligen välj en mindre fil." + "value" : "300 सदस्यों तक बड़े समूह चैट" } }, - "sw" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Tafadhali chagua faili ndogo." + "value" : "Nagyobb csoportos beszélgetések akár 300 taggal" } }, - "ta" : { + "it" : { "stringUnit" : { "state" : "translated", - "value" : "குறைந்த அளவிலான கோப்பைத் தேர்ந்தெடுக்கவும்." + "value" : "Chat di gruppo maggiori fino a 300 membri" } }, - "te" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "దయచేసి చిన్న ఫైల్ ఎంపిక చేయండి." + "value" : "最大300人の大型グループチャット" } }, - "th" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "โปรดเลือกไฟล์ที่เล็กลง" + "value" : "Groepsgesprekken tot wel 300 leden" } }, - "tr" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Lütfen daha küçük bir dosya seçin." + "value" : "Większe czaty grupowe do 300 członków" } }, - "uk" : { + "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Будь ласка, виберіть файл меншого розміру." + "value" : "Conversas de grupo maiores com até 300 membros" } }, - "ur-IN" : { + "ro" : { "stringUnit" : { "state" : "translated", - "value" : "براہ کرم ایک چھوٹی فائل کا انتخاب کریں۔" + "value" : "Conversații de grup mai mari, cu până la 300 de membri" } }, - "uz" : { + "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Iltimos, kichikroq faylni tanlang." + "value" : "Групповые чаты до 300 участников" } }, - "vi" : { + "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Vui lòng chọn tệp nhỏ hơn." + "value" : "Större gruppchattar upp till 300 medlemmar" } }, - "xh" : { + "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Nceda ukhethe ifayile encinci." + "value" : "300 üyeye kadar daha büyük grup sohbetleri" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Більша кількість — до 300 учасників — групових чатів" } }, "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "请选择一个更小的文件。" + "value" : "更大的群组聊天,最多可容纳 300 名成员" } }, "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "請選擇一個較小的檔案。" + "value" : "最大支援 300 位成員的大型群組聊天室" } } } }, - "profileErrorUpdate" : { + "proFeatureListLoadsMore" : { "extractionState" : "manual", "localizations" : { - "af" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "Kon nie profiel opdateer nie." + "value" : "Üstəgəl, daha çox eksklüziv özəlliklər" } }, - "ar" : { + "ca" : { "stringUnit" : { "state" : "translated", - "value" : "فشل تحديث الملف الشخصي." + "value" : "Plus carrega funcions més exclusives" } }, - "az" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Profili güncəlləmək uğursuz oldu." + "value" : "A další exkluzivních funkce" } }, - "bal" : { + "de" : { "stringUnit" : { "state" : "translated", - "value" : "پروفائل کو اپڈیٹ کرنے میں ناکامی" + "value" : "Und viele weitere exklusive Funktionen" } }, - "be" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Не ўдалося абнавіць профіль." + "value" : "Plus loads more exclusive features" } }, - "bg" : { + "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Неуспешно обновяване на профила." + "value" : "Y muchas funciones exclusivas más" } }, - "bn" : { + "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "প্রোফাইল আপডেট করতে ব্যর্থ হয়েছে।" + "value" : "Y muchas funciones exclusivas más" } }, - "ca" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "No s'ha pogut actualitzar el perfil." + "value" : "Plus offre des fonctions exclusives additionnelles" } }, - "cs" : { + "hi" : { "stringUnit" : { "state" : "translated", - "value" : "Nepodařilo se aktualizovat profil." + "value" : "साथ में कई और विशेष सुविधाएं" } }, - "cy" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Methwyd diweddaru proffil." + "value" : "Plusz még több exkluzív funkció" } }, - "da" : { + "it" : { "stringUnit" : { "state" : "translated", - "value" : "Kunne ikke opdatere profil." + "value" : "E tante altre funzionalità esclusive" } }, - "de" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Aktualisierung des Profils fehlgeschlagen." + "value" : "さらに多数の限定機能" } }, - "el" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Αποτυχία ενημέρωσης προφίλ." + "value" : "En nog veel meer exclusieve functies" } }, - "en" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Failed to update profile." + "value" : "I wiele więcej ekskluzywnych funkcji" } }, - "eo" : { + "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Malsukcesis ĝisdatigi profilon." + "value" : "E muitas outras funcionalidades exclusivas" } }, - "es-419" : { + "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Fallo al actualizar el perfil." + "value" : "Și multe alte funcționalități exclusive" } }, - "es-ES" : { + "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Fallo al actualizar el perfil." + "value" : "+ множество эксклюзивных функций" } }, - "et" : { + "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Profiili uuendamine nurjus." + "value" : "Plus många fler exklusiva funktioner" } }, - "eu" : { + "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Ez zara posible izan profil eguneratzea." + "value" : "Ayrıca daha birçok özel özellik" } }, - "fa" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "خطا در به‌روزرسانی نمایه." + "value" : "Та велика кількість ексклюзивних можливостей" } }, - "fi" : { + "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "Profiilia ei voitu päivittää." + "value" : "还有更多专属功能等你解锁" } }, - "fil" : { + "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "Bigong ma-update ang profile." + "value" : "以及更多獨家功能" + } + } + } + }, + "proFeatureListLongerMessages" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "10,000 xarakterə qədər mesajlar" } }, - "fr" : { + "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Échec de mise à jour du profil." + "value" : "Missatges de fins a 10,000 caràcters" } }, - "gl" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Erro ao actualizar o perfil." + "value" : "Zprávy až do 10000 znaků" } }, - "ha" : { + "de" : { "stringUnit" : { "state" : "translated", - "value" : "An kasa sabunta bayanin martaba." + "value" : "Nachrichten mit bis zu 10.000 Zeichen" } }, - "he" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "נכשל בעדכון הפרופיל." + "value" : "Messages up to 10,000 characters" } }, - "hi" : { + "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "प्रोफ़ाइल अपडेट करने में विफल।" + "value" : "Mensajes de hasta 10.000 caracteres" } }, - "hr" : { + "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Neuspješno ažuriranje profila." + "value" : "Mensajes de hasta 10.000 caracteres" } }, - "hu" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Nem sikerült frissíteni a profilt." + "value" : "Messages jusqu'à 10,000 caractères" } }, - "hy-AM" : { + "hi" : { "stringUnit" : { "state" : "translated", - "value" : "Չհաջողվեց թարմացնել պրոֆիլը։" + "value" : "10,000 वर्णों तक संदेश" } }, - "id" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Gagal memperbarui profil." + "value" : "Legfeljebb 10 000 karakteres üzenetek" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Impossibile aggiornare il profilo." + "value" : "Messaggi fino a 10.000 caratteri" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "プロフィールを更新できませんでした" + "value" : "最大10,000文字までのメッセージ" } }, - "ka" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "პროფილის განახლება ვერ მოხერხდა." + "value" : "Berichten tot 10.000 tekens" } }, - "km" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "បរាជ័យក្នុងការធ្វើបច្ចុប្បន្នភាពប្រវត្តិរូប។" + "value" : "Wiadomości do 10,000 znaków" } }, - "kn" : { + "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "ಪ್ರೊಫೈಲ್ ಅನ್ನು ನವೀಕರಿಸಲು ವಿಫಲವಾಗಿದೆ." + "value" : "Mensagens até 10 000 caracteres" } }, - "ko" : { + "ro" : { "stringUnit" : { "state" : "translated", - "value" : "프로필 업데이트 실패." + "value" : "Mesaje de până la 10.000 de caractere" } }, - "ku" : { + "ru" : { "stringUnit" : { "state" : "translated", - "value" : "هەڵەیەک ڕوویدا لە نوێکردنەوەی پرۆفایل." + "value" : "Сообщения до 10 тыс. символов" } }, - "ku-TR" : { + "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Nûvekirina profîlê têk çû." + "value" : "Meddelanden upp till 10 000 tecken" } }, - "lg" : { + "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Kino kyalemye okukyusa ekifaananyi ky'omuserikale." + "value" : "10.000 karaktere kadar mesajlar" } }, - "lt" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Nepavyko atnaujinti profilio." + "value" : "Повідомлення до 10 000 символів" } }, - "lv" : { + "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "Neizdevās atjaunināt profilu." + "value" : "消息长度上限为 10,000 个字符" } }, - "mk" : { + "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "Не успеа да се ажурира профилот." + "value" : "支援最多 10,000 字元的訊息" } - }, - "mn" : { + } + } + }, + "proFeatureListPinnedConversations" : { + "extractionState" : "manual", + "localizations" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "Профайлыг шинэчлэхэд алдаа гарлаа." + "value" : "Limitsiz danışığı sancın" } }, - "ms" : { + "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Gagal untuk kemas kini profil." + "value" : "Pin converses il·limitades" } }, - "my" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "ပရိုဖိုင်းကို အပ်ဒိတ်လုပ်၍မရပါ" + "value" : "Připněte neomezený počet konverzací" } }, - "nb" : { + "de" : { "stringUnit" : { "state" : "translated", - "value" : "Kunne ikke oppdatere profil." + "value" : "Unbegrenzt viele Unterhaltungen anheften" } }, - "nb-NO" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Klarte ikke å oppdatere profilen." + "value" : "Pin unlimited conversations" } }, - "ne-NP" : { + "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "झण्डाहरू" + "value" : "Fija conversaciones sin límite" } }, - "nl" : { + "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Profiel bijwerken mislukt." + "value" : "Fija conversaciones sin límite" } }, - "nn-NO" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Klarte ikkje å oppdatera profil" + "value" : "Épinglez un nombre illimité de conversations" } }, - "ny" : { + "hi" : { "stringUnit" : { "state" : "translated", - "value" : "Mana zatha sikirali sora." + "value" : "अनंत वार्तालाप पिन करें" } }, - "pa-IN" : { + "it" : { "stringUnit" : { "state" : "translated", - "value" : "ਪਰੋਫ਼ਾਈਲ ਅੱਪਡੇਟ ਕਰਨ ਵਿੱਚ ਨਾਕਾਮ." + "value" : "Blocca un numero illimitato di conversazioni" } }, - "pl" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Nie udało się zaktualizować profilu." + "value" : "ピン留め可能な会話が無制限" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Onbeperkt gesprekken vastzetten" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Przypinaj nieograniczoną liczbę konwersacji" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fixe conversas ilimitadas" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fixează un număr nelimitat de conversații" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Закрепление неограниченного количества бесед" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fäst obegränsat antal konversationer" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sınırsız sohbet sabitleme" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Закріплюйте необмежену кількість бесід" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "无限固定对话" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "釘選不限數量的對話" + } + } + } + }, + "profile" : { + "extractionState" : "manual", + "localizations" : { + "af" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profiel" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "الملف الشخصي" + } + }, + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profil" + } + }, + "bal" : { + "stringUnit" : { + "state" : "translated", + "value" : "پروفائل" + } + }, + "be" : { + "stringUnit" : { + "state" : "translated", + "value" : "Профіль" + } + }, + "bg" : { + "stringUnit" : { + "state" : "translated", + "value" : "Профил" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "প্রোফাইল" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Perfil" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profil" + } + }, + "cy" : { + "stringUnit" : { + "state" : "translated", + "value" : "Proffil" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profil" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profil" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Προφίλ" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profile" + } + }, + "eo" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profilo" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Perfil" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Perfil" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profiil" + } + }, + "eu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profil" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "نمایه" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profiili" + } + }, + "fil" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profile" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profil" + } + }, + "gl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Perfil" + } + }, + "ha" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bayanin kai" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "פרופיל" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "प्रोफ़ाइल" + } + }, + "hr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profil" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profil" + } + }, + "hy-AM" : { + "stringUnit" : { + "state" : "translated", + "value" : "Հաշիվ" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profil" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profilo" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "プロフィール" + } + }, + "ka" : { + "stringUnit" : { + "state" : "translated", + "value" : "პროფილი" + } + }, + "km" : { + "stringUnit" : { + "state" : "translated", + "value" : "ប្រវត្តិរូប" + } + }, + "kn" : { + "stringUnit" : { + "state" : "translated", + "value" : "ಪ್ರೊಫೈಲ್" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "프로필" + } + }, + "ku" : { + "stringUnit" : { + "state" : "translated", + "value" : "پرۆفایل" + } + }, + "ku-TR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profîl" + } + }, + "lg" : { + "stringUnit" : { + "state" : "translated", + "value" : "Shift" + } + }, + "lt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profilis" + } + }, + "lv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profils" + } + }, + "mk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Профил" + } + }, + "mn" : { + "stringUnit" : { + "state" : "translated", + "value" : "Профайл" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profil" + } + }, + "my" : { + "stringUnit" : { + "state" : "translated", + "value" : "ကိုယ်ရေး" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profil" + } + }, + "nb-NO" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profil" + } + }, + "ne-NP" : { + "stringUnit" : { + "state" : "translated", + "value" : "प्रोफाइल" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profiel" + } + }, + "nn-NO" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profil" + } + }, + "ny" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mbiri" + } + }, + "pa-IN" : { + "stringUnit" : { + "state" : "translated", + "value" : "ਪ੍ਰੋਫਾਈਲ" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profil" } }, "ps" : { "stringUnit" : { "state" : "translated", - "value" : "پروفایل تازه کولو کې پاتې راغی." + "value" : "پروفایل" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Falha na atualização do perfil." + "value" : "Perfil" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Não foi possível atualizar o perfil." + "value" : "Perfil" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Eroare la actualizarea profilului." + "value" : "Profil" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Ошибка обновления профиля." + "value" : "Профиль" } }, "sh" : { "stringUnit" : { "state" : "translated", - "value" : "Ažuriranje profila nije uspjelo." + "value" : "Profil" } }, "si-LK" : { "stringUnit" : { "state" : "translated", - "value" : "පැතිකඩ යාවත්කාල කිරීම අසාර්ථක විය." + "value" : "පැතිකඩ" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Nepodarilo sa aktualizovať profil." + "value" : "Profil" } }, "sl" : { "stringUnit" : { "state" : "translated", - "value" : "Posodobitev profila ni uspela." + "value" : "Profil" } }, "sq" : { "stringUnit" : { "state" : "translated", - "value" : "Dështoi përditësimi i profilit." + "value" : "Profil" } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : "Неуспешно ажурирање профила." + "value" : "Профил" } }, "sr-Latn" : { "stringUnit" : { "state" : "translated", - "value" : "Ažuriranje profila nije uspelo." + "value" : "Profil" } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Misslyckades att uppdatera profilen." + "value" : "Profil" } }, "sw" : { "stringUnit" : { "state" : "translated", - "value" : "Imeshindikana kusasisha wasifu." + "value" : "Profaili" } }, "ta" : { "stringUnit" : { "state" : "translated", - "value" : "சுயவிவரத்தைப் புதுப்பிக்க முடியவில்லை." + "value" : "சுயவிவரம்" } }, "te" : { "stringUnit" : { "state" : "translated", - "value" : "ప్రొఫైల్ అప్‌డేట్ చేయడం విఫలమైంది." + "value" : "ప్రొఫైల్" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "อัปเดตโปรไฟล์ล้มเหลว" + "value" : "โปรไฟล์" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Profil güncellenemedi." + "value" : "Profil" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Не вдалося оновити профіль." + "value" : "Профіль" } }, "ur-IN" : { "stringUnit" : { "state" : "translated", - "value" : "پروفائل اپ ڈیٹ کرنے میں ناکام" + "value" : "پروفائل" } }, "uz" : { "stringUnit" : { "state" : "translated", - "value" : "Profilni yangilashda muammo chiqdi." + "value" : "Profil" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Không thể cập nhật hồ sơ." + "value" : "Hồ sơ cá nhân" } }, "xh" : { "stringUnit" : { "state" : "translated", - "value" : "Koyekile ukuhlaziya iphrofayile." + "value" : "Iprofayile" } }, "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "更新资料失败。" + "value" : "个人资料" } }, "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "更新個人資料失敗。" + "value" : "個人檔案" } } } }, - "promote" : { + "profileDisplayPicture" : { "extractionState" : "manual", "localizations" : { "af" : { "stringUnit" : { "state" : "translated", - "value" : "Bevorder" + "value" : "Vertoon Prent" } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "ترقية" + "value" : "صورة العرض" } }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "Yüksəlt" + "value" : "Ekran şəkli" } }, "bal" : { "stringUnit" : { "state" : "translated", - "value" : "ترقی" + "value" : "تصویر دکھائیں" } }, "be" : { "stringUnit" : { "state" : "translated", - "value" : "Павышэнне" + "value" : "Выява для адлюстравання" } }, "bg" : { "stringUnit" : { "state" : "translated", - "value" : "Промоция" + "value" : "Профилна снимка" } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "প্রচার" + "value" : "প্রদর্শনী ছবি" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Promou" + "value" : "Imatge de perfil" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Povýšit" + "value" : "Zobrazovaný obrázek" } }, "cy" : { "stringUnit" : { "state" : "translated", - "value" : "Hyrwyddo" + "value" : "Dangos llun" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Fremme" + "value" : "Display Picture" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Befördern" + "value" : "Anzeigebild" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Προώθηση" + "value" : "Display Picture" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Promote" + "value" : "Display Picture" } }, "eo" : { "stringUnit" : { "state" : "translated", - "value" : "Promocii" + "value" : "Montrata Bildo" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Promover" + "value" : "Imagen de perfil" } }, "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "Promover" + "value" : "Imagen de perfil" } }, "et" : { "stringUnit" : { "state" : "translated", - "value" : "Edenda" + "value" : "Kuvapilt" } }, "eu" : { "stringUnit" : { "state" : "translated", - "value" : "Sustatu" + "value" : "Erakutsi Irudia" } }, "fa" : { "stringUnit" : { "state" : "translated", - "value" : "ارتقاء" + "value" : "نمایش تصویر نمایه" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Ylennä" + "value" : "Näyttökuva" } }, "fil" : { "stringUnit" : { "state" : "translated", - "value" : "I-promote" + "value" : "Display Picture" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Promouvoir" + "value" : "Définir une photo de profil" } }, "gl" : { "stringUnit" : { "state" : "translated", - "value" : "Promover" + "value" : "Imaxe de perfil" } }, "ha" : { "stringUnit" : { "state" : "translated", - "value" : "Inganta" + "value" : "Hoto na Nuna Fuska" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "קדם" + "value" : "תמונת תצוגה" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "पदोन्नत करें" + "value" : "प्रदर्शन चित्र" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Promoviraj" + "value" : "Slika za prikaz" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Előléptetés" + "value" : "Profilkép" } }, "hy-AM" : { "stringUnit" : { "state" : "translated", - "value" : "Խթանել" + "value" : "Ցուցադրվող գլուխ" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Promosikan" + "value" : "Tampilan Gambar" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Promuovi" + "value" : "Mostra immagine" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "昇格" + "value" : "ディスプレイの画像" } }, "ka" : { "stringUnit" : { "state" : "translated", - "value" : "დაწინაურება" + "value" : "პროფილის სურათი" } }, "km" : { "stringUnit" : { "state" : "translated", - "value" : "Promote" + "value" : "បង្ហាញរូបភាព" } }, "kn" : { "stringUnit" : { "state" : "translated", - "value" : "ಬೆಳೆಸಿರಿ" + "value" : "ಪ್ರದರ್ಶನ ಚಿತ್ರ" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "승격" + "value" : "프로필 사진 설정" } }, "ku" : { "stringUnit" : { "state" : "translated", - "value" : "پەرەبوون" + "value" : "پیشاندانی وێنە" } }, "ku-TR" : { "stringUnit" : { "state" : "translated", - "value" : "Daxwazên peyamê ya gurûp bişîne" + "value" : "Resmê Xuya bike" } }, "lg" : { "stringUnit" : { "state" : "translated", - "value" : "Sinziira" + "value" : "Ekifananyi Kyongezebwa" + } + }, + "lo" : { + "stringUnit" : { + "state" : "translated", + "value" : "ຮູບພາບສະແດງ" } }, "lt" : { "stringUnit" : { "state" : "translated", - "value" : "Paaukštinti" + "value" : "Rodomas paveikslas" } }, "lv" : { "stringUnit" : { "state" : "translated", - "value" : "Paaugstināt amatā" + "value" : "Atainojamais attēls" } }, "mk" : { "stringUnit" : { "state" : "translated", - "value" : "Промовирај" + "value" : "Слика за прикажување" } }, "mn" : { "stringUnit" : { "state" : "translated", - "value" : "Албан тушаал ахих" + "value" : "Дүр зураг" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Promosi" + "value" : "Paparan Gambar" } }, "my" : { "stringUnit" : { "state" : "translated", - "value" : "မြှင့်တင်" + "value" : "ပုံပြပါမည့်ဓာတ်ပုံ" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Promotere" + "value" : "Visningsbilde" } }, "nb-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Promoter" + "value" : "Display Picture" } }, "ne-NP" : { "stringUnit" : { "state" : "translated", - "value" : "उन्नति गर्नुहोस्" + "value" : "प्रदर्शन तस्वीर" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Promoveren" + "value" : "Toon afbeelding" } }, "nn-NO" : { "stringUnit" : { "state" : "translated", - "value" : "Promotere" + "value" : "Visingsbilde" } }, "ny" : { "stringUnit" : { "state" : "translated", - "value" : "Limbikitsani" + "value" : "Chithunzi Chowonetsera" } }, "pa-IN" : { "stringUnit" : { "state" : "translated", - "value" : "ਪ੍ਰੋਮੋਟ" + "value" : "ਪ੍ਰਦਰਸ਼ਨ ਚਿੱਤਰ" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Awansuj" + "value" : "Zdjęcie profilowe" } }, "ps" : { "stringUnit" : { "state" : "translated", - "value" : "ترقي" + "value" : "د نندارې انځور" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Promover" + "value" : "Imagem de Exibição" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Promover" + "value" : "Exibir Imagem" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Promovează" + "value" : "Afișează imaginea" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Повысить" + "value" : "Изображение профиля" } }, "sh" : { "stringUnit" : { "state" : "translated", - "value" : "Promoviraj" + "value" : "Prikaz slike" } }, "si-LK" : { "stringUnit" : { "state" : "translated", - "value" : "Promote" + "value" : "ප්‍රදර්ශන ඡායාරූපය" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Zvýšiť úroveň" + "value" : "Profilový obrázok" } }, "sl" : { "stringUnit" : { "state" : "translated", - "value" : "Promoviraj" + "value" : "Prikazna slika" } }, "sq" : { "stringUnit" : { "state" : "translated", - "value" : "Promovoj" + "value" : "Fotografi për ekran" } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : "Унапреди" + "value" : "Слика за приказ" } }, "sr-Latn" : { "stringUnit" : { "state" : "translated", - "value" : "Promoviši" + "value" : "Prikaz slike" } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Främja" + "value" : "Visa bild" } }, "sw" : { "stringUnit" : { "state" : "translated", - "value" : "Panda" + "value" : "Picha ya Onyesho" } }, "ta" : { "stringUnit" : { "state" : "translated", - "value" : "மேம்படுத்தவும்" + "value" : "அடுத்தப்படியாக" } }, "te" : { "stringUnit" : { "state" : "translated", - "value" : "ప్రచారం చేయండి" + "value" : "ప్రదర్శన చిత్రం" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "โปรโมต" + "value" : "รูปภาพที่แสดง" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Yükselt" + "value" : "Profil Resmini Seçin" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Підвищити" + "value" : "Аватар" } }, "ur-IN" : { "stringUnit" : { "state" : "translated", - "value" : "پروموٹ" + "value" : "ڈسپلے تصویر" } }, "uz" : { "stringUnit" : { "state" : "translated", - "value" : "Imtiyoz" + "value" : "Displey rasm" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Quảng bá" + "value" : "Display Picture" } }, "xh" : { "stringUnit" : { "state" : "translated", - "value" : "Phakamisa" + "value" : "Umfanekiso Okhombisa Ubuso" } }, "zh-CN" : { "stringUnit" : { "state" : "translated", - "value" : "授权" + "value" : "头像" } }, "zh-TW" : { "stringUnit" : { "state" : "translated", - "value" : "提升" + "value" : "顯示圖片" } } } }, - "promotionFailed" : { + "profileDisplayPictureRemoveError" : { "extractionState" : "manual", "localizations" : { + "af" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kon nie vertoonbeeld verwyder nie." + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "فشل في إزالة صورة العرض." + } + }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Yüksəltmə uğursuz oldu" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Yüksəltmələr uğursuz oldu" - } - } - } - } - } + "value" : "Ekran şəklini silmə uğursuz oldu." + } + }, + "bal" : { + "stringUnit" : { + "state" : "translated", + "value" : "ڈسپلے تصویر کو ہٹانے میں ناکامی" + } + }, + "be" : { + "stringUnit" : { + "state" : "translated", + "value" : "Не атрымалася выдаліць выяву." + } + }, + "bg" : { + "stringUnit" : { + "state" : "translated", + "value" : "Неуспешно премахване на картината за показване." + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "প্রদর্শন ছবি সরাতে ব্যর্থ হয়েছে।" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "La promoció ha fallat" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Les promocions han fallat" - } - } - } - } - } + "value" : "Error en eliminar la imatge de perfil." } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "few" : { - "stringUnit" : { - "state" : "translated", - "value" : "Povýšení selhala" - } - }, - "many" : { - "stringUnit" : { - "state" : "translated", - "value" : "Povýšení selhala" - } - }, - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Povýšení selhalo" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Povýšení selhala" - } - } - } - } - } + "value" : "Chyba při odstraňování zobrazovaného obrázku." + } + }, + "cy" : { + "stringUnit" : { + "state" : "translated", + "value" : "Methwyd tynnu'r llun arddangos." } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Forfremmelsen mislykkedes" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Forfremmelserne mislykkedes" - } - } - } - } - } + "value" : "Kunne ikke fjerne displaybillede." } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Beförderung fehlgeschlagen" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Beförderungen fehlgeschlagen" - } - } - } - } - } + "value" : "Fehler beim Entfernen des Profilbildes." + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Αποτυχία κατάργησης εικόνας εμφάνισης." } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Promotion Failed" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Promotions Failed" - } - } - } - } - } + "value" : "Failed to remove display picture." } }, "eo" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Promocio fiaskis" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Promocioj fiaskis" - } - } - } - } - } + "value" : "Malsukcesis forigi montrotan bildon." } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Promoción fallida" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Promociones fallidas" - } - } - } - } - } + "value" : "Falló al remover foto de perfil." } }, "es-ES" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Promoción fallida" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Promociones fallidas" - } - } - } - } - } + "value" : "Fallo al eliminar la foto de perfil." } }, - "fr" : { + "et" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Échec de la promotion" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Échec des promotions" - } - } - } - } - } + "value" : "Kuvapildi eemaldamine ebaõnnestus." } }, - "hi" : { + "eu" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "प्रमोशन विफल" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "प्रमोशन विफल" - } - } - } - } - } + "value" : "Ez da posible izan erakusizko irudia kentzea." } }, - "hu" : { + "fa" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sikertelen előléptetés" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sikertelen előléptetések" - } - } - } - } - } + "value" : "حذف تصویر نمایشی ناموفق بود." } }, - "id" : { + "fi" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Promosi Gagal" - } - } - } - } - } + "value" : "Näyttökuvan poisto ei onnistunut." } }, - "it" : { + "fil" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Promozione Fallita" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Promozioni Fallite" - } - } - } - } - } + "value" : "Nabigo sa pag-alis ng display picture." } }, - "ko" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "승격 실패" - } - } - } - } - } + "value" : "Échec de suppression de la photo de profil." } }, - "ku-TR" : { + "gl" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { + "value" : "Non se puido eliminar a imaxe de perfil." + } + }, + "ha" : { + "stringUnit" : { + "state" : "translated", + "value" : "An kasa cire hoton nunawa." + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "נכשל הסרת תמונת הצגה." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "डिस्प्ले तस्वीर हटाने में विफल।" + } + }, + "hr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uklanjanje slike za prikaz nije uspjelo." + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nem sikerült törölni a profilképet." + } + }, + "hy-AM" : { + "stringUnit" : { + "state" : "translated", + "value" : "Չհաջողվեց հեռացնել ցուցադրվող լուսանկարը։" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gagal menghapus gambar profil." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Impossibile rimuovere l'immagine del profilo." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "表示画像の削除に失敗しました。" + } + }, + "ka" : { + "stringUnit" : { + "state" : "translated", + "value" : "ვერ შევძელიში სურათის გამოღების მოცილება" + } + }, + "km" : { + "stringUnit" : { + "state" : "translated", + "value" : "បរាជ័យក្នុងការដករូបតំណាងបង្ហាញចេញ។" + } + }, + "kn" : { + "stringUnit" : { + "state" : "translated", + "value" : "ಪ್ರದರ್ಶನ ಚಿತ್ರವನ್ನು ತೆಗೆದುಹಾಕಲು ವಿಫಲವಾಗಿದೆ." + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "표시 사진을 제거하지 못했습니다." + } + }, + "ku" : { + "stringUnit" : { + "state" : "translated", + "value" : "شکستی پاشەکەوتکردنی وێنەی پەیپەر" + } + }, + "ku-TR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rûberê profîlê remove têbîne" + } + }, + "lg" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ensobi okwogolola ekifo ky'ebifaananyi." + } + }, + "lt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nepavyko pašalinti profilio paveiksliuko." + } + }, + "lv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Neizdevās noņemt profila attēlu." + } + }, + "mk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Неуспешно отстранување на слика за профил." + } + }, + "mn" : { + "stringUnit" : { + "state" : "translated", + "value" : "Харагдах зургыг устгахад алдаа гарлаа." + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gagal mengeluarkan gambar paparan." + } + }, + "my" : { + "stringUnit" : { + "state" : "translated", + "value" : "ပြထားသောပုံကို ဖယ်ရန် မဖြစ်နိုင်ပါ" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kunne ikke fjerne profilbilde." + } + }, + "nb-NO" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kunne ikke fjerne visningsbildet." + } + }, + "ne-NP" : { + "stringUnit" : { + "state" : "translated", + "value" : "प्रदर्शन तस्वीर हटाउन असफल" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verwijderen profielfoto mislukt." + } + }, + "nn-NO" : { + "stringUnit" : { + "state" : "translated", + "value" : "Klarte ikkje fjerna visningsbilete." + } + }, + "ny" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zalephera kuchotsa chithunzi chowonetsera." + } + }, + "pa-IN" : { + "stringUnit" : { + "state" : "translated", + "value" : "ਡਿਸਪਲੇ ਚਿੱਤਰ ਨੂੰ ਹਟਾਉਣ ਵਿੱਚ ਅਸਫਲ" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nie udało się usunąć zdjęcia profilowego." + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "د نمایش انځور لرې کولو کې ناکام" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Falha ao remover a imagem de exibição." + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erro ao remover a foto do perfil." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nu s-a putut elimina imaginea de profil." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Не удалось удалить изображение профиля." + } + }, + "sh" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nije uspjelo uklanjanje prikazne slike." + } + }, + "si-LK" : { + "stringUnit" : { + "state" : "translated", + "value" : "පේෂණ ඡායාරූපය ඉවත් කිරීමට අසමත් විය." + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nepodarilo sa odstrániť profilový obrázok." + } + }, + "sl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ni uspelo odstraniti prikazne slike." + } + }, + "sq" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dështoi heqja e figurës së paraqitjes." + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Неуспех у уклањању слике профила" + } + }, + "sr-Latn" : { + "stringUnit" : { + "state" : "translated", + "value" : "Neuspelo uklanjanje slike profila." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Misslyckades med att ta bort visningsbild." + } + }, + "sw" : { + "stringUnit" : { + "state" : "translated", + "value" : "Imeshindikana kuondoa picha ya kuonyesha." + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "காட்சி படம் நீக்க முடியவில்லை." + } + }, + "te" : { + "stringUnit" : { + "state" : "translated", + "value" : "ప్రదర్శన చిత్రాన్ని తొలగించడంలో విఫలమైంది." + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "การลบรูปโปรไฟล์ล้มเหลว" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profil resmi kaldırılamadı." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Не вдалося видалити зображення профілю" + } + }, + "ur-IN" : { + "stringUnit" : { + "state" : "translated", + "value" : "ڈسپلے تصویر ہٹانے میں ناکام" + } + }, + "uz" : { + "stringUnit" : { + "state" : "translated", + "value" : "Displey rasmini olib tashlashda muammo chiqdi." + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không thể xóa hình đại diện." + } + }, + "xh" : { + "stringUnit" : { + "state" : "translated", + "value" : "Koyekile ukususa umfanekiso wokubonisa." + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "移除头像失败。" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法刪除顯示圖片。" + } + } + } + }, + "profileDisplayPictureSet" : { + "extractionState" : "manual", + "localizations" : { + "af" : { + "stringUnit" : { + "state" : "translated", + "value" : "Stel vertoon prent" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تعيين صورة العرض" + } + }, + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ekran şəklini ayarla" + } + }, + "bal" : { + "stringUnit" : { + "state" : "translated", + "value" : "پاره نمای گونیکی مقرر کـــــــن" + } + }, + "be" : { + "stringUnit" : { + "state" : "translated", + "value" : "Усталюйце выяву для адлюстравання" + } + }, + "bg" : { + "stringUnit" : { + "state" : "translated", + "value" : "Задаване на профилна снимка" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "প্রদর্শন চিত্র সেট করুন" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Definiu la imatge del perfil" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nastavit zobrazovaný obrázek" + } + }, + "cy" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gosod Llun Arddangos" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Indstil profibillede" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Anzeigebild festlegen" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ορισμός Εικόνας Εμφάνισης" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Set Display Picture" + } + }, + "eo" : { + "stringUnit" : { + "state" : "translated", + "value" : "Agordi Profilbildon" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Establecer Imagen de Perfil" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Establecer imagen de perfil" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Määra kuvapilt" + } + }, + "eu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erakutsi argazkia ezarri" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "تنظیم تصویر نمایشی" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aseta näyttökuva" + } + }, + "fil" : { + "stringUnit" : { + "state" : "translated", + "value" : "Itakda ang Display Picture" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Définir une photo de profil" + } + }, + "gl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Establecer Imaxe para Mostrar" + } + }, + "ha" : { + "stringUnit" : { + "state" : "translated", + "value" : "Saita Hoton Mai Nunawa" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "הגדר תמונת פרופיל" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "डिस्प्ले तस्वीर सेट करें" + } + }, + "hr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Postavi sliku za prikaz" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profilkép beállítása" + } + }, + "hy-AM" : { + "stringUnit" : { + "state" : "translated", + "value" : "Սահմանել պրոֆիլի նկարը" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Atur Tampilan Gambar" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Imposta foto profilo" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ディスプレイの画像をセット" + } + }, + "ka" : { + "stringUnit" : { + "state" : "translated", + "value" : "ავატარის არჩევა" + } + }, + "km" : { + "stringUnit" : { + "state" : "translated", + "value" : "កំណត់រូបបង្ហាញ" + } + }, + "kn" : { + "stringUnit" : { + "state" : "translated", + "value" : "ಪ್ರೊಫೈಲ್ ಡಿಸ್ಪ್ಲೇ ಚಿತ್ರವನ್ನು ಸೆಟ್ ಮಾಡಿ" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "프로필 사진 설정" + } + }, + "ku" : { + "stringUnit" : { + "state" : "translated", + "value" : "دانانی وێنەی پیشانده‌رەو" + } + }, + "ku-TR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rismê Profîlê Çêke" + } + }, + "lg" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tereka Ekifaananyi Ekirabika" + } + }, + "lt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nustatyti rodomą paveikslėlį" + } + }, + "lv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Iestatīt Atainojamo Attēlu" + } + }, + "mk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Постави Слика за Профил" + } + }, + "mn" : { + "stringUnit" : { + "state" : "translated", + "value" : "Дэлгэцийн зургаа тохируулах" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tetapkan Gambar Paparan" + } + }, + "my" : { + "stringUnit" : { + "state" : "translated", + "value" : "ပုံပြင်ဆင်ထားသည့်ပုံကိုသတ်မှတ်မည်" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sett profilbilde" + } + }, + "nb-NO" : { + "stringUnit" : { + "state" : "translated", + "value" : "Angi visningsbilde" + } + }, + "ne-NP" : { + "stringUnit" : { + "state" : "translated", + "value" : "प्रदर्शन तस्वीर सेट गर्नुहोस्" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profielfoto instellen" + } + }, + "nn-NO" : { + "stringUnit" : { + "state" : "translated", + "value" : "Set Display Picture" + } + }, + "ny" : { + "stringUnit" : { + "state" : "translated", + "value" : "Set Display Picture" + } + }, + "pa-IN" : { + "stringUnit" : { + "state" : "translated", + "value" : "ਡਿਸਪਲੇ ਤਸਵੀਰ ਸੈੱਟ ਕਰੋ" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ustaw zdjęcie profilowe" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "ډیسپلې انځور تنظیمول" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Definir Imagem de Exibição" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Definir imagem a exibir" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Setează imaginea de profil" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Установить изображение профиля" + } + }, + "sh" : { + "stringUnit" : { + "state" : "translated", + "value" : "Postavi sliku profila" + } + }, + "si-LK" : { + "stringUnit" : { + "state" : "translated", + "value" : "ප්‍රදර්ශන ඡායාරූපය සකසන්න" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nastaviť profilový obrázok" + } + }, + "sl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nastavi prikazno sliko" + } + }, + "sq" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vendos Paraqitjen e Profilit" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Постави слику профила" + } + }, + "sr-Latn" : { + "stringUnit" : { + "state" : "translated", + "value" : "Postavi sliku profila" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ange visningsbild" + } + }, + "sw" : { + "stringUnit" : { + "state" : "translated", + "value" : "Weka Picha ya Kuonyesha" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "காட்டி புகைப்படத்தை அமை" + } + }, + "te" : { + "stringUnit" : { + "state" : "translated", + "value" : "ప్రొఫైల్ చిత్రాన్ని సెట్ చేయి" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "ตั้งรูปภาพโปรไฟล์" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profil Resmini Seçin" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Встановити аватар" + } + }, + "ur-IN" : { + "stringUnit" : { + "state" : "translated", + "value" : "ڈسپلے تصویر سیٹ کریں" + } + }, + "uz" : { + "stringUnit" : { + "state" : "translated", + "value" : "Displey rasmini belgilang" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thiết Lập Hình ảnh Đại diện" + } + }, + "xh" : { + "stringUnit" : { + "state" : "translated", + "value" : "Set Display Picture" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "设置头像" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "設定顯示圖片" + } + } + } + }, + "profileDisplayPictureSizeError" : { + "extractionState" : "manual", + "localizations" : { + "af" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kies 'n kleiner lêer." + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "الرجاء اختيار ملف أصغر." + } + }, + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lütfən daha kiçik bir fayl götürün." + } + }, + "bal" : { + "stringUnit" : { + "state" : "translated", + "value" : "براہء مہربانی ایک چھوٹا فائل منتخب کنیں." + } + }, + "be" : { + "stringUnit" : { + "state" : "translated", + "value" : "Калі ласка, абярыце меншы файл." + } + }, + "bg" : { + "stringUnit" : { + "state" : "translated", + "value" : "Моля, изберете по-малък файл." + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "দয়া করে একটি ছোট ফাইল নির্বাচন করুন।" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Si us plau, selecciona un fitxer més petit." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Prosím vyberte menší soubor." + } + }, + "cy" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dewiswch ffeil llai." + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Venligst vælg en mindre fil." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bitte wähle eine kleinere Datei." + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Παρακαλώ επιλέξτε ένα μικρότερο αρχείο." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Please pick a smaller file." + } + }, + "eo" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bonvolu elekti plej malgrandan dosieron." + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Por favor, elija un archivo más pequeño." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Por favor, elija un archivo más pequeño." + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Palun valige väiksem fail." + } + }, + "eu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesedez, hautatu fitxategi txikiago bat." + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "لطفا فایل کوچکتری انتخاب کنید." + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Valitse pienempi tiedosto." + } + }, + "fil" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pakipili ang mas maliit na file." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Veuillez choisir un fichier plus petit." + } + }, + "gl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Por favor, escolle un ficheiro máis pequeno." + } + }, + "ha" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zaɓi ƙaramin fayil." + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "אנא בחר קובץ קטן יותר." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Please pick a smaller file." + } + }, + "hr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Molimo odaberite manju datoteku." + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Válassz egy kisebb fájlt." + } + }, + "hy-AM" : { + "stringUnit" : { + "state" : "translated", + "value" : "Խնդրում ենք ընտրել ավելի փոքր ֆայլ:" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Silakan pilih berkas yang lebih kecil." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Scegli un file più piccolo." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "小さいファイルを選んでください" + } + }, + "ka" : { + "stringUnit" : { + "state" : "translated", + "value" : "გთხოვთ აირჩიოთ პატარა ფაილი." + } + }, + "km" : { + "stringUnit" : { + "state" : "translated", + "value" : "សូមជ្រើសរើសឯកសារតិចជាង." + } + }, + "kn" : { + "stringUnit" : { + "state" : "translated", + "value" : "ದಯವಿಟ್ಟು ಒಂದು ಕುಿರುವಾದ ಕಡತವನ್ನು ಆಯ್ಕೆ ಮಾಡಿ." + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "더 작은 파일을 선택해 주세요." + } + }, + "ku" : { + "stringUnit" : { + "state" : "translated", + "value" : "تکایە فایلێکی بچووک بە کار بێنە." + } + }, + "ku-TR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ji kerema xwe pêveke zêdetir bicikne." + } + }, + "lg" : { + "stringUnit" : { + "state" : "translated", + "value" : "Londa fayilo etono." + } + }, + "lt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pasirinkite mažesnį failą." + } + }, + "lv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lūdzu, izvēlieties mazāku failu." + } + }, + "mk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ве молиме изберете помала датотека." + } + }, + "mn" : { + "stringUnit" : { + "state" : "translated", + "value" : "Бага хэмжээтэй файлыг сонгоно уу." + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sila pilih fail yang lebih kecil." + } + }, + "my" : { + "stringUnit" : { + "state" : "translated", + "value" : "ပိုတည်းသော ဖိုင်ကို ရွေးချယ်ပါ" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vennligst velg en mindre fil." + } + }, + "nb-NO" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vennligst velg en mindre fil." + } + }, + "ne-NP" : { + "stringUnit" : { + "state" : "translated", + "value" : "कृपया सानो फाइल चयन गर्नुहोस्।" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kies een kleiner bestand." + } + }, + "nn-NO" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vennligst velg ei mindre fil." + } + }, + "ny" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chonde sonkhanitsani fayilo yaying’ono." + } + }, + "pa-IN" : { + "stringUnit" : { + "state" : "translated", + "value" : "ਕਿਰਪਾ ਕਰਕੇ ਇੱਕ ਛੋਟੀ ਫਾਇਲ ਚੁਣੋ।" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wybierz mniejszy plik." + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "مهرباني وکړئ یو کوچنۍ فایل غوره کړئ." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Escolha um arquivo menor, por favor." + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Por favor, escolha um ficheiro menor." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vă rugăm alegeți un fișier mai mic." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пожалуйста, выберите файл меньшего размера." + } + }, + "sh" : { + "stringUnit" : { + "state" : "translated", + "value" : "Molimo izaberite manju datoteku." + } + }, + "si-LK" : { + "stringUnit" : { + "state" : "translated", + "value" : "කරුණාකර කුඩා ගොනුවක් තෝරන්න." + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Prosím zvoľte menší súbor." + } + }, + "sl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Prosimo, izberite manjšo datoteko." + } + }, + "sq" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ju lutemi zgjedhni një skedar më të vogël." + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Изаберите мању датотеку." + } + }, + "sr-Latn" : { + "stringUnit" : { + "state" : "translated", + "value" : "Molimo izaberite manju datoteku." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vänligen välj en mindre fil." + } + }, + "sw" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tafadhali chagua faili ndogo." + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "குறைந்த அளவிலான கோப்பைத் தேர்ந்தெடுக்கவும்." + } + }, + "te" : { + "stringUnit" : { + "state" : "translated", + "value" : "దయచేసి చిన్న ఫైల్ ఎంపిక చేయండి." + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "โปรดเลือกไฟล์ที่เล็กลง" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lütfen daha küçük bir dosya seçin." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Будь ласка, виберіть файл меншого розміру." + } + }, + "ur-IN" : { + "stringUnit" : { + "state" : "translated", + "value" : "براہ کرم ایک چھوٹی فائل کا انتخاب کریں۔" + } + }, + "uz" : { + "stringUnit" : { + "state" : "translated", + "value" : "Iltimos, kichikroq faylni tanlang." + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vui lòng chọn tệp nhỏ hơn." + } + }, + "xh" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nceda ukhethe ifayile encinci." + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "请选择一个更小的文件。" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "請選擇一個較小的檔案。" + } + } + } + }, + "profileErrorUpdate" : { + "extractionState" : "manual", + "localizations" : { + "af" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kon nie profiel opdateer nie." + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "فشل تحديث الملف الشخصي." + } + }, + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profili güncəlləmək uğursuz oldu." + } + }, + "bal" : { + "stringUnit" : { + "state" : "translated", + "value" : "پروفائل کو اپڈیٹ کرنے میں ناکامی" + } + }, + "be" : { + "stringUnit" : { + "state" : "translated", + "value" : "Не ўдалося абнавіць профіль." + } + }, + "bg" : { + "stringUnit" : { + "state" : "translated", + "value" : "Неуспешно обновяване на профила." + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "প্রোফাইল আপডেট করতে ব্যর্থ হয়েছে।" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "No s'ha pogut actualitzar el perfil." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nepodařilo se aktualizovat profil." + } + }, + "cy" : { + "stringUnit" : { + "state" : "translated", + "value" : "Methwyd diweddaru proffil." + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kunne ikke opdatere profil." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Das Profil konnte nicht aktualisiert werden." + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Αποτυχία ενημέρωσης προφίλ." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Failed to update profile." + } + }, + "eo" : { + "stringUnit" : { + "state" : "translated", + "value" : "Malsukcesis ĝisdatigi profilon." + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fallo al actualizar el perfil." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fallo al actualizar el perfil." + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profiili uuendamine nurjus." + } + }, + "eu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ez zara posible izan profil eguneratzea." + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "خطا در به‌روزرسانی نمایه." + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profiilia ei voitu päivittää." + } + }, + "fil" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bigong ma-update ang profile." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Échec de mise à jour du profil." + } + }, + "gl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erro ao actualizar o perfil." + } + }, + "ha" : { + "stringUnit" : { + "state" : "translated", + "value" : "An kasa sabunta bayanin martaba." + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "נכשל בעדכון הפרופיל." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "प्रोफ़ाइल अपडेट करने में विफल।" + } + }, + "hr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Neuspješno ažuriranje profila." + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nem sikerült frissíteni a profilt." + } + }, + "hy-AM" : { + "stringUnit" : { + "state" : "translated", + "value" : "Չհաջողվեց թարմացնել պրոֆիլը։" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gagal memperbarui profil." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Impossibile aggiornare il profilo." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "プロフィールを更新できませんでした" + } + }, + "ka" : { + "stringUnit" : { + "state" : "translated", + "value" : "პროფილის განახლება ვერ მოხერხდა." + } + }, + "km" : { + "stringUnit" : { + "state" : "translated", + "value" : "បរាជ័យក្នុងការធ្វើបច្ចុប្បន្នភាពប្រវត្តិរូប។" + } + }, + "kn" : { + "stringUnit" : { + "state" : "translated", + "value" : "ಪ್ರೊಫೈಲ್ ಅನ್ನು ನವೀಕರಿಸಲು ವಿಫಲವಾಗಿದೆ." + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "프로필 업데이트 실패." + } + }, + "ku" : { + "stringUnit" : { + "state" : "translated", + "value" : "هەڵەیەک ڕوویدا لە نوێکردنەوەی پرۆفایل." + } + }, + "ku-TR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nûvekirina profîlê têk çû." + } + }, + "lg" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kino kyalemye okukyusa ekifaananyi ky'omuserikale." + } + }, + "lt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nepavyko atnaujinti profilio." + } + }, + "lv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Neizdevās atjaunināt profilu." + } + }, + "mk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Не успеа да се ажурира профилот." + } + }, + "mn" : { + "stringUnit" : { + "state" : "translated", + "value" : "Профайлыг шинэчлэхэд алдаа гарлаа." + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gagal untuk kemas kini profil." + } + }, + "my" : { + "stringUnit" : { + "state" : "translated", + "value" : "ပရိုဖိုင်းကို အပ်ဒိတ်လုပ်၍မရပါ" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kunne ikke oppdatere profil." + } + }, + "nb-NO" : { + "stringUnit" : { + "state" : "translated", + "value" : "Klarte ikke å oppdatere profilen." + } + }, + "ne-NP" : { + "stringUnit" : { + "state" : "translated", + "value" : "झण्डाहरू" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profiel bijwerken mislukt." + } + }, + "nn-NO" : { + "stringUnit" : { + "state" : "translated", + "value" : "Klarte ikkje å oppdatera profil" + } + }, + "ny" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mana zatha sikirali sora." + } + }, + "pa-IN" : { + "stringUnit" : { + "state" : "translated", + "value" : "ਪਰੋਫ਼ਾਈਲ ਅੱਪਡੇਟ ਕਰਨ ਵਿੱਚ ਨਾਕਾਮ." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nie udało się zaktualizować profilu." + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "پروفایل تازه کولو کې پاتې راغی." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Falha na atualização do perfil." + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Não foi possível atualizar o perfil." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eroare la actualizarea profilului." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ошибка обновления профиля." + } + }, + "sh" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ažuriranje profila nije uspjelo." + } + }, + "si-LK" : { + "stringUnit" : { + "state" : "translated", + "value" : "පැතිකඩ යාවත්කාල කිරීම අසාර්ථක විය." + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nepodarilo sa aktualizovať profil." + } + }, + "sl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Posodobitev profila ni uspela." + } + }, + "sq" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dështoi përditësimi i profilit." + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Неуспешно ажурирање профила." + } + }, + "sr-Latn" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ažuriranje profila nije uspelo." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Misslyckades att uppdatera profilen." + } + }, + "sw" : { + "stringUnit" : { + "state" : "translated", + "value" : "Imeshindikana kusasisha wasifu." + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "சுயவிவரத்தைப் புதுப்பிக்க முடியவில்லை." + } + }, + "te" : { + "stringUnit" : { + "state" : "translated", + "value" : "ప్రొఫైల్ అప్‌డేట్ చేయడం విఫలమైంది." + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "อัปเดตโปรไฟล์ล้มเหลว" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profil güncellenemedi." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Не вдалося оновити профіль." + } + }, + "ur-IN" : { + "stringUnit" : { + "state" : "translated", + "value" : "پروفائل اپ ڈیٹ کرنے میں ناکام" + } + }, + "uz" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profilni yangilashda muammo chiqdi." + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không thể cập nhật hồ sơ." + } + }, + "xh" : { + "stringUnit" : { + "state" : "translated", + "value" : "Koyekile ukuhlaziya iphrofayile." + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "更新资料失败。" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "更新個人資料失敗。" + } + } + } + }, + "proGroupActivated" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Qrup aktivləşdirildi" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Grup activat" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skupina aktivována" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gruppe aktiviert" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Group Activated" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Grupo activado" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Grupo activado" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Groupe activé" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "समूह सक्रिय किया गया" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gruppo attivato" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "グループが有効化されました" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Groep geactiveerd" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Grupa została aktywowana" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Grupo ativado" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Grup activat" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Группа активирована" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Grupp aktiverad" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Групу активовано" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "群组已激活" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "群組已啟用" + } + } + } + }, + "proGroupActivatedDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu qurun tutumu artıb! Qrup admininin sayəsində artıq 300 nəfərə qədər üzvü dəstəkləyə bilər" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aquest grup ha ampliat la capacitat! Pot suportar fins a 300 membres perquè un administrador del grup té" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tato skupina má navýšenou kapacitu! Může podporovat až 300 členů, protože správce skupiny má" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Diese Gruppe hat mehr Kapazität! Sie kann bis zu 300 Mitglieder unterstützen, da ein Gruppen-Administrator" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "This group has expanded capacity! It can support up to 300 members because a group admin has" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "¡Este grupo tiene capacidad ampliada! Puede admitir hasta 300 miembros porque un administrador del grupo tiene" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "¡Este grupo tiene capacidad ampliada! Puede admitir hasta 300 miembros porque un administrador del grupo tiene" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ce groupe a une capacité étendue ! Il peut contenir jusqu’à 300 membres grâce à un administrateur qui dispose de" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "इस समूह की क्षमता बढ़ाई गई है! यह अब 300 सदस्यों तक का समर्थन कर सकता है क्योंकि एक समूह व्यवस्थापक के पास" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Questo gruppo ha una capacità estesa! Può supportare fino a 300 membri perché un amministratore del gruppo ha" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このグループは拡張されています!グループ管理者の設定により、最大300人のメンバーに対応できます" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deze groep heeft een grotere capaciteit! Er kunnen nu tot 300 leden deelnemen omdat een groepsbeheerder een" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ta grupa ma zwiększoną pojemność! Może obsługiwać do 300 członków, ponieważ administrator grupy posiada" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Este grupo tem capacidade expandida! Pode suportar até 300 membros porque um administrador do grupo tem" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Acest grup are capacitate extinsă! Poate susține până la 300 de membri deoarece un administrator de grup are" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "У этой группы увеличена вместимость! Теперь она поддерживает до 300 участников, потому что администратор группы активировал" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Den här gruppen har utökad kapacitet! Den kan ha upp till 300 medlemmar eftersom en gruppadministratör har" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "У цієї групи розширено можливості! Тепер вона може вміщати до 300 учасників, тому що адміністратор групи має" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "该群组已扩容!因管理员升级为 PRO,现支持最多 300 名成员" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "此群組已擴充容量!由於群組管理員的設定,現在可支援多達 300 位成員" + } + } + } + }, + "proGroupsUpgraded" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} qrup yüksəldildi" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} qrup yüksəldildi" + } + } + } + } + } + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} skupiny navýšeny" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} skupin navýšeno" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} skupina navýšena" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} skupin navýšeno" + } + } + } + } + } + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Αναβαθμίστηκε η {total} ομάδα" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Αναβαθμίστηκαν {total} ομάδες" + } + } + } + } + } + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} Group Upgraded" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} Groups Upgraded" + } + } + } + } + } + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} groupe mis à niveau" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} groupes mis à niveau" + } + } + } + } + } + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} Gruppe Oppgradert" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} Grupper Oppgradert" + } + } + } + } + } + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ulepszono {total} grupy" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ulepszono {total} grup" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ulepszono {total} grupę" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ulepszono {total} grup" + } + } + } + } + } + } + } + } + }, + "proImportantDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geri ödəniş tələbi qətidir. Əgər təsdiqlənsə, {pro} planınız dərhal ləğv ediləcək və bütün {pro} özəlliklərinə erişimi itirəcəksiniz." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Žádost o vrácení peněz je konečné. Pokud bude schváleno, váš tarif {pro} bude ihned zrušen a ztratíte přístup ke všem funkcím {pro}." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Requesting a refund is final. If approved, your {pro} plan will be canceled immediately and you will lose access to all {pro} features." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "La demande de remboursement est définitive. Si elle est approuvée, votre formule {pro} sera immédiatement annulée et vous perdrez l'accès à toutes les fonctionnalités {pro}." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Het aanvragen van een terugbetaling is definitief. Indien goedgekeurd, wordt je {pro} abonnement onmiddellijk geannuleerd en verlies je de toegang tot alle {pro} functies." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wniosek o zwrot jest ostateczny. Jeżeli zostanie zatwierdzony, Twój plan {pro} zostanie natychmiast anulowany i utracisz dostęp do wszystkich funkcji {pro}." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вимагання повернення грошей закінчено. В разі схвалення твою підписку {pro} негайно скасують і ти втратиш всі можливості {pro}." + } + } + } + }, + "proIncreasedAttachmentSizeFeature" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Artırılmış qoşma ölçüsü" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Augment de la mida de l'adhesió" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Navýšená velikost přílohy" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erhöhte Anhangsgröße" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Increased Attachment Size" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tamaño del archivo adjunto aumentado" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tamaño del archivo adjunto aumentado" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Taille de pièce jointe augmentée" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "बढ़ाया गया अटैचमेंट आकार" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dimensione allegato aumentata" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "添付ファイルサイズの増加" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verhoogde bijlagegrootte" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zwiększony rozmiar załączników" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Maior tamanho de anexo" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dimensiune mărită a atașamentului" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Увеличенный размер вложений" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Större bilagegräns" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Збільшений розмір вкладення" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "附件大小已增加" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "附件大小提升" + } + } + } + }, + "proIncreasedMessageLengthFeature" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Artırılmış mesaj uzunluğu" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Augment de la longitud del missatge" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Navýšená délka zprávy" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erhöhte Nachrichtenlänge" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Increased Message Length" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mayor longitud de mensaje" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mayor longitud de mensaje" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Longueur de message augmentée" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "बढ़ाई गई संदेश लंबाई" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lunghezza messaggio aumentata" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージの文字数増加" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verlengde berichtlengte" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zwiększona długość wiadomości" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Maior comprimento de mensagem" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lungime extinsă a mesajului" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Увеличенная длина сообщения" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Förlängd meddelandelängd" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Збільшена довжина повідомлень" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "消息长度已增加" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "訊息長度提升" + } + } + } + }, + "proLargerGroups" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Daha böyük qruplar" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Větší skupiny" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Larger Groups" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Groupes plus grands" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Grotere groepen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Większe grupy" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Більші групи" + } + } + } + }, + "proLargerGroupsDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Admin olduğunuz qruplar, avtomatik olaraq 300 üzvü dəstəkləmək üçün təkmilləşdirilir." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skupiny, ve kterých jste správcem, jsou automaticky navýšeny na kapacitu až 300 členů." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Groups you are an admin in are automatically upgraded to support 300 members." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Les groupes dont vous êtes administrateur sont automatiquement mis à niveau pour prendre en charge 300 membres." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Groepen waarvan jij beheerder bent, worden automatisch geüpgraded om 300 leden te ondersteunen." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Групи, у яких ви є адміністратором, автоматично оновлюються для підтримки до 300 учасників." + } + } + } + }, + "proLargerGroupsTooltip" : { + "extractionState" : "manual", + "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Větší skupinové chaty (až pro 300 členů) brzy budou k dispozici pro všechny uživatele Pro Beta!" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Larger group chats (up to 300 members) are coming soon for all Pro Beta users!" + } + } + } + }, + "proLongerMessages" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Daha uzun mesajlar" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delší zprávy" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Longer Messages" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Messages plus longs" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Langere berichten" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dłuższe wiadomości" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Довші повідомлення" + } + } + } + }, + "proLongerMessagesDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bütün danışıqlarda 10,000 xarakterə qədər mesaj göndərə bilərsiniz." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ve všech konverzacích můžete posílat zprávy až o délce 10 000 znaků." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "You can send messages up to 10,000 characters in all conversations." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vous pouvez envoyer des messages jusqu'à 10000 caractères dans toutes les conversations." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je kunt berichten tot 10.000 tekens verzenden in alle gesprekken." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Możesz wysyłać wiadomości aż do 10 000 znaków we wszystkich konwersacjach." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ви можете надсилати повідомлення до 10 000 символів у всіх розмовах." + } + } + } + }, + "proLongerMessagesSent" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} daha uzun mesaj göndərildi" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} daha uzun mesaj göndərildi" + } + } + } + } + } + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} delší zprávy odeslány" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} delších zpráv odesláno" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} delší zpráva odeslána" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} delších zpráv odesláno" + } + } + } + } + } + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} Μεγαλύτερο Μήνυμα εστάλη" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} Μεγαλύτερο Μήνυμα εστάλησαν" + } + } + } + } + } + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} Longer Message Sent" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} Longer Messages Sent" + } + } + } + } + } + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Message plus long {total} envoyé" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} Messages plus longs envoyés" + } + } + } + } + } + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} Lengre Melding Sendt" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} Lengre Meldinger Sendt" + } + } + } + } + } + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wysłano {total} dłuższe wiadomości" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wysłano {total} dłuższych wiadomości" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wysłano {total} dłuższą wiadomość" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wysłano {total} dłuższych wiadomości" + } + } + } + } + } + } + } + } + }, + "proMessageInfoFeatures" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu mesajda aşağıdakı {app_pro} özəllikləri istifadə olunub:" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aquest missatge va utilitzar les funcions següents {app_pro}:" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "V této zprávě byly použity následující funkce {app_pro}:" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Diese Nachricht verwendet die folgenden {app_pro}-Funktionen:" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "This message used the following {app_pro} features:" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Este mensaje utilizó las siguientes funciones de {app_pro}:" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Este mensaje utilizó las siguientes funciones de {app_pro}:" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ce message utilise les fonctionnalités suivantes de {app_pro} :" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "इस संदेश में निम्नलिखित {app_pro} विशेषताएँ उपयोग की गईं हैं:" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Questo messaggio ha utilizzato le seguenti funzionalità di {app_pro}:" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このメッセージには以下の {app_pro} 機能が使用されています:" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dit bericht maakte gebruik van de volgende {app_pro}-functies:" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ta wiadomość zawierała następujące funkcje {app_pro}:" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Esta mensagem utilizou as seguintes funcionalidades do {app_pro}:" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Acest mesaj a folosit următoarele funcționalități {app_pro}:" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Это сообщение использовало следующие функции {app_pro}:" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Detta meddelande använde följande funktioner från {app_pro}:" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "У цьому повідомленні наявні наступні функції Session Pro:" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "此消息使用了以下 {app_pro} 功能:" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "本訊息使用了以下 {app_pro} 功能:" + } + } + } + }, + "promote" : { + "extractionState" : "manual", + "localizations" : { + "af" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bevorder" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "ترقية" + } + }, + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yüksəlt" + } + }, + "bal" : { + "stringUnit" : { + "state" : "translated", + "value" : "ترقی" + } + }, + "be" : { + "stringUnit" : { + "state" : "translated", + "value" : "Павышэнне" + } + }, + "bg" : { + "stringUnit" : { + "state" : "translated", + "value" : "Промоция" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "প্রচার" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promou" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Povýšit" + } + }, + "cy" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hyrwyddo" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fremme" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Befördern" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Προώθηση" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promote" + } + }, + "eo" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promocii" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promover" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promover" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Edenda" + } + }, + "eu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sustatu" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "ارتقاء" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ylennä" + } + }, + "fil" : { + "stringUnit" : { + "state" : "translated", + "value" : "I-promote" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promouvoir" + } + }, + "gl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promover" + } + }, + "ha" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inganta" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "קדם" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "पदोन्नत करें" + } + }, + "hr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promoviraj" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Előléptetés" + } + }, + "hy-AM" : { + "stringUnit" : { + "state" : "translated", + "value" : "Խթանել" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promosikan" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promuovi" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "昇格" + } + }, + "ka" : { + "stringUnit" : { + "state" : "translated", + "value" : "დაწინაურება" + } + }, + "km" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promote" + } + }, + "kn" : { + "stringUnit" : { + "state" : "translated", + "value" : "ಬೆಳೆಸಿರಿ" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "승격" + } + }, + "ku" : { + "stringUnit" : { + "state" : "translated", + "value" : "پەرەبوون" + } + }, + "ku-TR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Daxwazên peyamê ya gurûp bişîne" + } + }, + "lg" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sinziira" + } + }, + "lt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Paaukštinti" + } + }, + "lv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Paaugstināt amatā" + } + }, + "mk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Промовирај" + } + }, + "mn" : { + "stringUnit" : { + "state" : "translated", + "value" : "Албан тушаал ахих" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promosi" + } + }, + "my" : { + "stringUnit" : { + "state" : "translated", + "value" : "မြှင့်တင်" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promotere" + } + }, + "nb-NO" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promoter" + } + }, + "ne-NP" : { + "stringUnit" : { + "state" : "translated", + "value" : "उन्नति गर्नुहोस्" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promoveren" + } + }, + "nn-NO" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promotere" + } + }, + "ny" : { + "stringUnit" : { + "state" : "translated", + "value" : "Limbikitsani" + } + }, + "pa-IN" : { + "stringUnit" : { + "state" : "translated", + "value" : "ਪ੍ਰੋਮੋਟ" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Awansuj" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "ترقي" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promover" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promover" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promovează" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Повысить" + } + }, + "sh" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promoviraj" + } + }, + "si-LK" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promote" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zvýšiť úroveň" + } + }, + "sl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promoviraj" + } + }, + "sq" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promovoj" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Унапреди" + } + }, + "sr-Latn" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promoviši" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Främja" + } + }, + "sw" : { + "stringUnit" : { + "state" : "translated", + "value" : "Panda" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "மேம்படுத்தவும்" + } + }, + "te" : { + "stringUnit" : { + "state" : "translated", + "value" : "ప్రచారం చేయండి" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "โปรโมต" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yükselt" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Підвищити" + } + }, + "ur-IN" : { + "stringUnit" : { + "state" : "translated", + "value" : "پروموٹ" + } + }, + "uz" : { + "stringUnit" : { + "state" : "translated", + "value" : "Imtiyoz" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quảng bá" + } + }, + "xh" : { + "stringUnit" : { + "state" : "translated", + "value" : "Phakamisa" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "授权" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "提升" + } + } + } + }, + "promotionFailed" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yüksəltmə uğursuz oldu" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yüksəltmələr uğursuz oldu" + } + } + } + } + } + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "La promoció ha fallat" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Les promocions han fallat" + } + } + } + } + } + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Povýšení selhala" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Povýšení selhala" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Povýšení selhalo" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Povýšení selhala" + } + } + } + } + } + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Forfremmelsen mislykkedes" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Forfremmelserne mislykkedes" + } + } + } + } + } + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Beförderung fehlgeschlagen" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Beförderungen fehlgeschlagen" + } + } + } + } + } + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Η προώθηση απέτυχε" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Οι προωθήσεις απέτυχαν" + } + } + } + } + } + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promotion Failed" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promotions Failed" + } + } + } + } + } + } + }, + "eo" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promocio fiaskis" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promocioj fiaskis" + } + } + } + } + } + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promoción fallida" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promociones fallidas" + } + } + } + } + } + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promoción fallida" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promociones fallidas" + } + } + } + } + } + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Edutamine ebaõnnestus" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Edutamised ebaõnnestusid" + } + } + } + } + } + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Échec de la promotion" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Échec des promotions" + } + } + } + } + } + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "प्रमोशन विफल" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "प्रमोशन विफल" + } + } + } + } + } + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sikertelen előléptetés" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sikertelen előléptetések" + } + } + } + } + } + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promosi Gagal" + } + } + } + } + } + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promozione Fallita" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promozioni Fallite" + } + } + } + } + } + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "昇進に失敗しました" + } + } + } + } + } + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "승격 실패" + } + } + } + } + } + } + }, + "ku-TR" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Terfîkirin têk çû" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Terfîkirin têk çû" + } + } + } + } + } + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Forfremmelse mislykket" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Forfremmelser mislykket" + } + } + } + } + } + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promotie mislukt" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promoties mislukt" + } + } + } + } + } + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promocje nie powiodły się" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promocje nie powiodły się" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promocja nie powiodła się" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promocje nie powiodły się" + } + } + } + } + } + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "A promoção falhou" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "As promoções falharam" + } + } + } + } + } + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promovări eșuate" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promovare eșuată" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promovări eșuate" + } + } + } + } + } + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Переводы не удались" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Переводы не удались" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Перевод не удался" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Переводы не удались" + } + } + } + } + } + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Befordran Misslyckades" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Befordringar Misslyckades" + } + } + } + } + } + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ayrıcalık Başarısız Oldu" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ayrıcalıklar Başarısız Oldu" + } + } + } + } + } + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Підвищення прав не відбулось" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Підвищення прав не відбулось" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Підвищення прав не відбулось" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Підвищення прав не відбулось" + } + } + } + } + } + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yêu cầu thăng cấp cho thành viên đã thất bại" + } + } + } + } + } + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "授权失败" + } + } + } + } + } + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "晉升失敗" + } + } + } + } + } + } + } + } + }, + "promotionFailedDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yüksəltmə tətbiq edilə bilmədi. Təkrar cəhd etmək istəyirsiniz?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yüksəltmələr tətbiq edilə bilmədi. Təkrar cəhd etmək istəyirsiniz?" + } + } + } + } + } + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "La promoció no es va poder aplicar. Vols tornar-ho a provar?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Les promocions no es va poder aplicar. Vols tornar-ho a provar?" + } + } + } + } + } + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Povýšení nebylo možné uplatnit. Chcete to zkusit znovu?" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Povýšení nebylo možné uplatnit. Chcete to zkusit znovu?" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Povýšení nebylo možné uplatnit. Chcete to zkusit znovu?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Povýšení nebylo možné uplatnit. Chcete to zkusit znovu?" + } + } + } + } + } + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Forfremmelsen kunne ikke gennemføres. Vil du prøve i gen?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Forfremmelserne kunne ikke gennemføres. Vil du prøve i gen?" + } + } + } + } + } + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Die Beförderung konnte nicht durchgeführt werden. Möchtest du es erneut versuchen?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Die Beförderungen konnten nicht durchgeführt werden. Möchtest du es erneut versuchen?" + } + } + } + } + } + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Η προώθηση δεν ήταν δυνατό να εφαρμοστεί. Θέλετε να προσπαθήσετε ξανά;" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Οι προωθήσεις δεν ήταν δυνατό να εφαρμοστούν. Θέλετε να προσπαθήσετε ξανά;" + } + } + } + } + } + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "The promotion could not be applied. Would you like to try again?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "The promotions could not be applied. Would you like to try again?" + } + } + } + } + } + } + }, + "eo" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "La promocio ne povas esti aplikita. Ĉu vi volas reprovi?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "La promocioj ne povas esti aplikitaj. Ĉu vi volas reprovi?" + } + } + } + } + } + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "La promoción no se ha podido aplicar. ¿Quieres volver a intentarlo?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Las promociones no se han podido aplicar. ¿Quieres volver a intentarlo?" + } + } + } + } + } + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "La promoción no se ha podido aplicar. ¿Quieres volver a intentarlo?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Las promociones no se han podido aplicar. ¿Quieres volver a intentarlo?" + } + } + } + } + } + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Edutamist ei õnnestunud rakendada. Kas soovite uuesti proovida?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Edutamisi ei õnnestunud rakendada. Kas soovite uuesti proovida?" + } + } + } + } + } + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "La promotion n'a pas pu être appliquée. Voulez-vous réessayer ?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Les promotions n'ont pas pu être appliquées. Voulez-vous réessayer ?" + } + } + } + } + } + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "प्रमोशन लागू नहीं किया जा सका। क्या आप फिर से प्रयास करना चाहेंगे?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "प्रमोशन लागू नहीं किए जा सके। क्या आप फिर से प्रयास करना चाहेंगे?" + } + } + } + } + } + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Az előléptetést nem lehetett alkalmazni. Szeretné újra megpróbálni?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Az előléptetést nem lehetett alkalmazni. Szeretné újra megpróbálni?" + } + } + } + } + } + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promosi tidak dapat diterapkan. Apakah Anda ingin mencoba lagi?" + } + } + } + } + } + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "La promozione non può essere applicata. Vuoi riprovare?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Le promozioni non possono essere applicate. Vuoi riprovare?" + } + } + } + } + } + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "昇進を適用できませんでした。再試行しますか?" + } + } + } + } + } + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "승격 시키지 못했습니다. 다시 시도하시겠습니까?" + } + } + } + } + } + } + }, + "ku-TR" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { "arg1" : { "argNum" : 1, "formatSpecifier" : "lld", @@ -354852,1070 +363919,2582 @@ "one" : { "stringUnit" : { "state" : "translated", - "value" : "Terfîkirin têk çû" + "value" : "Terfîkirin nikare were tetbîqkirin. Tu dixwazî cardin biceribînî?" } }, "other" : { "stringUnit" : { "state" : "translated", - "value" : "Terfîkirin têk çû" + "value" : "Terfîkirin nikarin werin tetbîqkirin. Tu dixwazî cardin biceribînî?" + } + } + } + } + } + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Forfremmelsen kunne ikke bli påført. Vil du prøve på nytt?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Forfremmelser kunne ikke bli påført. Vil du prøve på nytt?" + } + } + } + } + } + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "De promotie kon niet toegepast worden. Wilt u het opnieuw proberen?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "De promoties konden niet toegepast worden. Wilt u het opnieuw proberen?" + } + } + } + } + } + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nie można było zastosować promocji. Czy chcesz spróbować ponownie?" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nie można było zastosować promocji. Czy chcesz spróbować ponownie?" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nie udało się zastosować promocji. Czy chcesz spróbować ponownie?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nie można było zastosować promocji. Czy chcesz spróbować ponownie?" + } + } + } + } + } + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Não foi possível aplicar a promoção. Gostaria de tentar novamente?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Não foi possível aplicar as promoções. Gostaria de tentar novamente?" + } + } + } + } + } + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promovările nu au putut fi aplicate. Doriți să încercați din nou?" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promovarea nu a putut fi aplicată. Doriți să încercați din nou?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promovările nu au putut fi aplicate. Doriți să încercați din nou?" + } + } + } + } + } + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Не удалось принять запросы. Повторить попытку?" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Не удалось принять запросы. Повторить попытку?" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Не удалось принять запрос. Повторить попытку?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Не удалось принять запросы. Повторить попытку?" + } + } + } + } + } + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Befordran kunde inte tillämpas. Vill du försöka igen?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Befordringarna kunde inte tillämpas. Vill du försöka igen?" + } + } + } + } + } + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ayrıcalık uygulanamadı. Yeniden denemek ister misiniz?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ayrıcalıklar uygulanamadı. Yeniden denemek ister misiniz?" + } + } + } + } + } + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Підвищення прав не вдалось застосувати. Бажаєте спробувати ще раз?" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Підвищення прав не вдалось застосувати. Бажаєте спробувати ще раз?" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Підвищення прав не вдалось застосувати. Бажаєте спробувати ще раз?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Підвищення прав не вдалось застосувати. Бажаєте спробувати ще раз?" + } + } + } + } + } + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yêu cầu thăng cấp thành viên không thể được thực hiện. Bạn có muốn thử lại?" + } + } + } + } + } + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "无法授权。您想重试吗?" + } + } + } + } + } + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法套用晉升。您想要再試一次嗎?" + } + } + } + } + } + } + } + } + }, + "proNewInstallation" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "With a new installation" + } + } + } + }, + "proNewInstallationDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reinstall {app_name} on this device via the {platform_store}, restore your account with your Recovery Password, and renew your plan in the {app_pro} settings." + } + } + } + }, + "proOptionsRenewalSubtitle" : { + "extractionState" : "manual", + "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nyní jsou k dispozici tři způsoby obnovy:" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Currently, there are three ways to renew:" + } + } + } + }, + "proPercentOff" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{percent}% endirim" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sleva {percent} %" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "{percent}% Off" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "{percent}% de réduction" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "{percent}% korting" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "{percent}% Знижки" + } + } + } + }, + "proPinnedConversations" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} sancılmış danışıq" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} sancılmış danışıq" + } + } + } + } + } + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} připnuté konverzace" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} připnutých konverzací" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} připnutá konverzace" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} připnutých konverzací" + } + } + } + } + } + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} Καρφιτσωμένη Συνομιλία" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} Καρφιτσωμένες Συνομιλίες" + } + } + } + } + } + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} Pinned Conversation" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} Pinned Conversations" + } + } + } + } + } + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} Conversation épinglée" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} Conversations épinglées" + } + } + } + } + } + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} Samtale Festet" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} Samtaler Festet" + } + } + } + } + } + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} przypięte konwersacje" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} przypiętych konwersacji" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} przypięta konwersacja" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "{total} przypiętych konwersacji" } } } } } } + } + } + }, + "proPlanActivatedAuto" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro} planınız aktivdir!

Planınız avtomatik olaraq {date} tarixində başqa bir {current_plan} üçün yenilənəcək. Planınıza edilən güncəlləmələr növbəti {pro} yenilənməsi zamanı qüvvəyə minəcək." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Váš tarif {app_pro} je aktivní!

Váš tarif se automaticky obnoví na další {current_plan} dne {date}. Změny tarifu se projeví při příštím obnovení {pro}." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your {app_pro} plan is active!

Your plan will automatically renew for another {current_plan} on {date}. Updates to your plan take effect when {pro} is next renewed." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Votre formule {app_pro} est active !

Votre formule sera automatiquement renouvelée pour une autre {current_plan} le {date}. Les modifications apportées à votre formule prendront effet lors du prochain renouvellement de {pro}." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je {app_pro} abonnement is actief!

Je abonnement wordt automatisch verlengd voor een nieuw {current_plan} op {date}. Wijzigingen aan je abonnement gaan in wanneer {pro} de volgende keer wordt verlengd." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Twój plan {app_pro} jest aktywny!

Zostanie on automatycznie odnowiony na kolejny {current_plan}, dnia {date}. Zmiany w Twoim planie wejdą w życie przy następnym odnowieniu subskrypcji {pro}." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Для тебе діє підписка {app_pro}.

{date} твою підписку буде самодійно поновлено як {current_plan}. Оновлення підписки настане під час наступного оновлення {pro}." + } + } + } + }, + "proPlanActivatedAutoShort" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro} planınız aktivdir!

Planınız avtomatik olaraq {date} tarixində başqa bir {current_plan} üçün yenilənəcək." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Váš tarif {app_pro} je aktivní!

Tarif se automaticky obnoví na další {current_plan} dne {date}." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your {app_pro} plan is active!

Your plan will automatically renew for another {current_plan} on {date}." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Votre forfait {app_pro} est actif

Votre abonnement se renouvellera automatiquement pour un autre {current_plan} le {date}." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je {app_pro} abonnement is actief!

Je abonnement wordt automatisch verlengd met een {current_plan} op {date}." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Twój plan {app_pro} jest aktywny!

Zostanie on automatycznie odnowiony na kolejny {current_plan}, dnia {date}." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Для тебе діє підписка {app_pro}.

{date} твою підписку буде самодійно поновлено як {current_plan}." + } + } + } + }, + "proPlanActivatedNotAuto" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro} planınızın müddəti {date} tarixində bitir.

Eksklüziv Pro özəlliklərinə kəsintisiz erişimi təmin etmək üçün planınızı indi güncəlləyin." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Váš tarif {app_pro} vyprší dne {date}.

Aktualizujte si tarif nyní, abyste měli i nadále nepřerušený přístup k exkluzivním funkcím Pro." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your {app_pro} plan will expire on {date}.

Update your plan now to ensure uninterrupted access to exclusive Pro features." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Votre offre {app_pro} expirera le {date}.

Mettez votre offre à jour maintenant pour garantir un accès ininterrompu aux fonctionnalités exclusives Pro." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je {app_pro} abonnement verloopt op {date}.

Werk je abonnement nu bij om ononderbroken toegang te behouden tot exclusieve Pro functies." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Twój plan {app_pro} wygasa {date}.

Zaktualizuj swój plan już teraz, by zapewnić sobie nieprzerwany dostęp do ekskluzywnych funkcji Pro." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Твоя підписка {app_pro} спливе {date}.

Для збереження особливих можливостей подовж свою підписку." + } + } + } + }, + "proPlanError" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} planı xətası" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chyba tarifu {pro}" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} Plan Error" + } + } + } + }, + "proPlanExpireDate" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro} planınızın müddəti {date} tarixində bitir." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Váš tarif {app_pro} vyprší dne {date}." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your {app_pro} plan will expire on {date}." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Votre forfait {app_pro} expirera le {date}." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je {app_pro} abonnement verloopt op {date}." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Twój plan {app_pro} wygasa {date}." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Підписка {app_pro} спливе {date}." + } + } + } + }, + "proPlanLoading" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} planı yüklənir" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Načítání tarifu {pro}" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} Plan Loading" + } + } + } + }, + "proPlanLoadingDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} planınız barədə məlumatlar hələ də yüklənir. Bu proses tamamlanana qədər planınızı güncəlləyə bilməzsiniz." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Váš tarif {pro} se stále načítá. Dokud nebude načítání dokončeno, nemůžete tarif změnit." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Information about your {pro} plan is still being loaded. You cannot update your plan until this process is complete." + } + } + } + }, + "proPlanLoadingEllipsis" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} planı yüklənir..." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Načítání tarifu {pro}..." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} plan loading..." + } + } + } + }, + "proPlanNetworkLoadError" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hazırkı planınızı yükləmək üçün şəbəkəyə bağlana bilmir. Bağlantı bərpa olunana qədər {app_name} ilə planınızı güncəlləmək sıradan çıxarılacaq.

Lütfən şəbəkə bağlantınızı yoxlayıb yenidən sınayın." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nelze se připojit k síti a načíst váš aktuální tarif. Aktualizace tarifu prostřednictvím {app_name} bude deaktivována, dokud nebude obnoveno připojení.

Zkontrolujte připojení k síti a zkuste to znovu." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unable to connect to the network to load your current plan. Updating your plan via {app_name} will be disabled until connectivity is restored.

Please check your network connection and retry." + } + } + } + }, + "proPlanNotFound" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} planı tapılmadı" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} nebyl nalezen" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} Plan Not Found" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Forfait {pro} introuvable" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} abonnement niet gevonden" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nie znaleziono planu {pro}" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Передплата {pro} не знайдена" + } + } + } + }, + "proPlanNotFoundDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hesabınız üçün heç bir aktiv plan tapılmadı. Bunun bir səhv olduğunu düşünürsünüzsə, lütfən kömək üçün {app_name} dəstəyi ilə əlaqə saxlayın." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pro váš účet nebyl nalezen žádný aktivní tarif. Pokud si myslíte, že se jedná o chybu, kontaktujte podporu {app_name} a požádejte o pomoc." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No active plan was found for your account. If you believe this is a mistake, please reach out to {app_name} support for assistance." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aucun forfait actif n’a été trouvé pour votre compte. Si vous pensez qu’il s’agit d’une erreur, veuillez contacter l’assistance de {app_name} pour obtenir de l’aide." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Er is geen actief abonnement gevonden voor je account. Als je denkt dat dit een vergissing is, neem dan contact op met de ondersteuning van {app_name} voor hulp." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nie znaleziono aktywnych planów dla Twojego konta. Jeżeli uważasz, że to błąd, prosimy o kontakt z Supportem {app_name}." + } + } + } + }, + "proPlanPlatformRefund" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Because you originally signed up for {app_pro} via the {platform_store}, you'll need to use the same {platform_account} to request a refund." + } + } + } + }, + "proPlanPlatformRefundLong" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Because you originally signed up for {app_pro} via the {platform_store}, your refund request will be processed by {app_name} Support.

Request a refund by hitting the button below and completing the refund request form.

While {app_name} Support strives to process refund requests within 24-72 hours, processing may take longer during times of high request volume." + } + } + } + }, + "proPlanRecover" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} planını geri qaytar" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Znovu nabýt tarif {pro}" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recover {pro} Plan" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Récupérer le forfait {pro}" + } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Promotie mislukt" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Promoties mislukt" - } - } - } - } - } + "value" : "{pro} abonnement herstellen" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "few" : { - "stringUnit" : { - "state" : "translated", - "value" : "Promocje nie powiodły się" - } - }, - "many" : { - "stringUnit" : { - "state" : "translated", - "value" : "Promocje nie powiodły się" - } - }, - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Promocja nie powiodła się" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Promocje nie powiodły się" - } - } - } - } - } + "value" : "Odzyskaj plan {pro}" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Відновити передплату {pro}" + } + } + } + }, + "proPlanRenew" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} planını yenilə" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Obnovit tarif {pro}" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Renew {pro} Plan" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Renouveler l’abonnement {pro}" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} abonnement verlengen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odnów plan {pro}" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Оновити підписку {pro}" + } + } + } + }, + "proPlanRenewDesktop" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Currently, {pro} plans can only be purchased and renewed via the {platform_store} or {platform_store}. Because you are using {app_name} Desktop, you're not able to renew your plan here.

{app_pro} developers are working hard on alternative payment options to allow users to purchase {pro} plans outside of the {platform_store} and {platform_store}. {pro} Roadmap {icon}" + } + } + } + }, + "proPlanRenewDesktopLinked" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Renew your plan in the {app_pro} settings on a linked device with {app_name} installed via the {platform_store} or {platform_store}." + } + } + } + }, + "proPlanRenewPlatformStoreWebsite" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Renew your plan on the {platform_store} website using the {platform_account} you signed up for {pro} with." + } + } + } + }, + "proPlanRenewPlatformWebsite" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Renew your plan on the {platform} website using the {platform_account} you signed up for {pro} with." + } + } + } + }, + "proPlanRenewStart" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Güclü {app_pro} özəlliklərini yenidən istifadə etməyə başlamaq üçün {app_pro} planınızı yeniləyin." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Obnovte váš tarif {app_pro}, abyste mohli znovu využívat užitečné funkce {app_pro}." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Renew your {app_pro} plan to start using powerful {app_pro} Beta features again." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Renouvelez votre abonnement {app_pro} pour recommencer à utiliser les puissantes fonctionnalités bêta de {app_pro}." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verleng je {app_pro} abonnement om opnieuw gebruik te maken van krachtige {app_pro} functies." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odnów swój plan {app_pro}, aby znów używać potężnych funkcji {app_pro} Beta." + } + } + } + }, + "proPlanRenewSupport" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro} planınız yeniləndi! {network_name} dəstək verdiyiniz üçün təşəkkürlər." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Váš tarif {app_pro} byl obnoven! Děkujeme, že podporujete síť {network_name}." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your {app_pro} plan has been renewed! Thank you for supporting the {network_name}." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Votre forfait {app_pro} a été renouvelé ! Merci de soutenir le {network_name}." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je {app_pro} abonnement is verlengd! Bedankt voor je steun aan de {network_name}." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Twój plan {app_pro} został odnowiony! Dziękujemy za wspieranie {network_name}." + } + } + } + }, + "proPlanRestored" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} planı bərpa edildi" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} obnoven" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} Plan Restored" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} Forfait rétabli" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} abonnement hersteld" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Plan {pro} został odzyskany" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "План {pro} відновлено" + } + } + } + }, + "proPlanRestoredDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro} üçün yararlı bir plan aşkarlandı və {pro} statusunuz bərpa edildi!" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Byl rozpoznán platný tarif {app_pro} a váš stav {pro} byl obnoven!" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "A valid plan for {app_pro} was detected and your {pro} status has been restored!" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Un abonnement valide pour {app_pro} a été détecté et votre statut {pro} a été restauré !" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Een geldig abonnement voor {app_pro} is gedetecteerd en je {pro} status is hersteld!" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wykryto ważny plan {app_pro} oraz przywrócono Twój status {pro}!" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Виявлено дійсний план {app_pro}, та ваш статус {pro} було відновлено!" + } + } + } + }, + "proPlanSignUp" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Because you originally signed up for {app_pro} via the {platform_store}, you'll need to use your {platform_account} to update your plan." + } + } + } + }, + "proPriceOneMonth" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "1 ay - {monthly_price}/ay" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "1 měsíc – {monthly_price} / měsíc" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "1 Month - {monthly_price} / Month" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "1 mois – {monthly_price} / mois" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "1 maand - {monthly_price} / maand" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "1 miesiąc - {monthly_price} / miesiąc" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "1 місяць — {monthly_price} / місяць" + } + } + } + }, + "proPriceThreeMonths" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "3 ay - {monthly_price}/ay" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "3 měsíce – {monthly_price} / měsíc" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "3 Months - {monthly_price} / Month" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "3 mois - {monthly_price} / mois" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "3 maanden - {monthly_price} / maand" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "3 miesiące - {monthly_price} / miesiąc" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "3 місяці — {monthly_price} / місяць" + } + } + } + }, + "proPriceTwelveMonths" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "12 ay - {monthly_price}/ay" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "12 měsíců – {monthly_price} / měsíc" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "12 Months - {monthly_price} / Month" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "12 mois - {monthly_price} / mois" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "12 maanden - {monthly_price} / maand" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "12 miesięcy - {monthly_price} / miesiąc" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "12 місяців – {monthly_price} / місяць" + } + } + } + }, + "proRefundAccountDevice" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Open this {app_name} account on an {device_type} device logged into the {platform_account} you originally signed up with. Then, request a refund via the {app_pro} settings." + } + } + } + }, + "proRefundDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Getməyinizə məyus olduq. Geri ödəmə tələb etməzdən əvvəl bilməli olduğunuz şeylər." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mrzí nás, že to rušíte. Než požádáte o vrácení peněz, přečtěte si informace, které byste měli vědět." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "We’re sorry to see you go. Here's what you need to know before requesting a refund." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nous sommes désolés de vous voir partir. Voici ce que vous devez savoir avant de demander un remboursement." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Het spijt ons dat je vertrekt. Dit moet je weten voordat je een terugbetaling aanvraagt." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Przykro nam, że odchodzisz. Tutaj znajdziesz wszystko, co powinieneś wiedzieć przed złożeniem wniosku o zwrot." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Шкода, же ти передумав(ла). Перед вимогою повернення грошей ти мусиш знати ось що." + } + } + } + }, + "proRefunding" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} geri ödəməsi" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vracení peněz za {pro}" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Refunding {pro}" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Remboursement de {pro}" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Terugbetalen {pro}" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zwrot {pro}" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Повернення грошей за {pro}" + } + } + } + }, + "proRefundingDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Refunds for {app_pro} plans are handled exclusively by {platform} through the {platform_store}.

Due to {platform} refund policies, {app_name} developers have no ability to influence the outcome of refund requests. This includes whether the request is approved or denied, as well as whether a full or partial refund is issued." + } + } + } + }, + "proRefundNextSteps" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "{platform} is now processing your refund request. This typically takes 24-48 hours. Depending on their decision, you may see your {pro} status change in {app_name}." + } + } + } + }, + "proRefundRequestSessionSupport" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geri qaytarma tələbiniz {app_name} Dəstək komandası tərəfindən icra olunacaq.

Aşağıdakı düyməyə basaraq və geri ödəniş formunu dolduraraq geri ödəniş tələbinizi göndərin.

{app_name} Dəstək komandası, geri ödəniş tələblərini adətən 24-72 saat ərzində emal edir, yüksək tələb həcminə görə bu proses daha uzun çəkə bilər." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Váš požadavek na vrácení peněz bude zpracován podporou {app_name}.

Požádejte o vrácení peněz kliknutím na tlačítko níže a vyplněním formuláře žádosti o vrácení peněz.

Ačkoliv se podpora {app_name} snaží zpracovat žádosti o vrácení peněz během 24–72 hodin, může zpracování trvat i déle, v případě že je vyřizováno mnoho žádostí." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your refund request will be handled by {app_name} Support.

Request a refund by hitting the button below and completing the refund request form.

While {app_name} Support strives to process refund requests within 24-72 hours, processing may take longer during times of high request volume." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Votre demande de remboursement sera traitée par le service de {app_name}.

Demandez un remboursement en appuyant sur le bouton ci-dessous et en remplissant le formulaire de demande de remboursement.

Bien que le service de {app_name} fait au mieux afin de traiter les demandes de remboursement dans un délai de 24-72 heures, le traitement peut être plus long en période de forte demande." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je restitutieverzoek wordt afgehandeld door {app_name} Support.

Vraag een restitutie aan door op de knop hieronder te drukken en het restitutieformulier in te vullen.

Hoewel {app_name} Support ernaar streeft om restitutieverzoeken binnen 24-72 uur te verwerken, kan het tijdens drukte langer duren." + } + } + } + }, + "proRefundRequestStorePolicies" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your refund request will be handled exclusively by {platform} through the {platform} website.

Due to {platform} refund policies, {app_name} developers have no ability to influence the outcome of refund requests. This includes whether the request is approved or denied, as well as whether a full or partial refund is issued." + } + } + } + }, + "proRefundSupport" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Please contact {platform} for further updates on your refund request. Due to {platform} refund policies, {app_name} developers have no ability to influence the outcome of refund requests.

{platform} Refund Support" + } + } + } + }, + "proRenewBeta" : { + "extractionState" : "manual", + "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Obnovit {pro} Beta" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Renew {pro} Beta" + } + } + } + }, + "proRenewingNoAccessBilling" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Currently, {pro} plans can only be purchased and renewed via the {platform_store} or {platform_store}. Because you installed {app_name} using the {buildVariant}, you're not able to renew your plan here.

{app_pro} developers are working hard on alternative payment options to allow users to purchase {pro} plans outside of the {platform_store} and {platform_store}. {pro} Roadmap {icon}" + } + } + } + }, + "proRequestedRefund" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geri ödəmə tələb edildi" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Žádost o vrácení peněz" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Refund Requested" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Remboursement demandé" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Terugbetaling aangevraagd" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wniosek o zwrot wysłany" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вимогу повернення грошей надіслано" + } + } + } + }, + "proSendMore" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Daha çoxunu göndərmək üçün" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Envia més amb" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Posílejte více se" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mehr senden mit" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Send more with" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Envía más con" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Envía más con" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Envoyez plus avec" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "इसके साथ और भेजें" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Küldjön többet ezzel:" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Invia di più con" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "さらに送信:" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verstuur meer met" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wyślij więcej z" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Envie mais com" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "few" : { - "stringUnit" : { - "state" : "translated", - "value" : "Promovări eșuate" - } - }, - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Promovare eșuată" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Promovări eșuate" - } - } - } - } - } + "value" : "Trimite mai mult cu" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Отправить еще с" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skicka mer med" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "ile daha fazlasını gönderin" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Надсилайте довші повідомлення з" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "发送更多内容,体验" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "升級後可傳送更多內容" + } + } + } + }, + "proSettings" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} ayarları" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nastavení {pro}" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} Settings" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Paramètres {pro}" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} instellingen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ustawienia {pro}" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Налаштування {pro}" + } + } + } + }, + "proStats" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} statistikalarınız" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vaše statistiky {pro}" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deine {pro} Statistik" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your {pro} Stats" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vos statistiques {pro}" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je {pro} statistieken" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Twoje Statystyki {pro}" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ваша статистика {pro}" + } + } + } + }, + "proStatsLoading" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} statistikaları yüklənir" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Načítání statistik {pro}" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} Stats Loading" + } + } + } + }, + "proStatsLoadingDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} statistikalarınız yüklənir, lütfən gözləyin." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vaše statistiky {pro} se načítají, počkejte prosím." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your {pro} stats are loading, please wait." + } + } + } + }, + "proStatsTooltip" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} statistikaları, bu cihazdakı istifadəni əks-etdirir və əlaqələndirilmiş cihazlarda fərqli görünə bilər." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Statistiky {pro} ukazují používání na tomto zařízení a mohou se lišit na jiných propojených zařízeních" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} stats reflect usage on this device and may appear differently on linked devices" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Les statistiques {pro} reflètent l'utilisation sur cet appareil et peuvent apparaître différemment sur les appareils connectés" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} statistieken weerspiegelen het gebruik op dit apparaat en kunnen anders weergegeven worden op gekoppelde apparaten" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Statystyki {pro} pokazują użycie na tym urządzeniu i mogą wyglądać różnie na połączonych urządzeniach" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Звіти підписки {pro} відображають використання лише цього пристрою, тож, мабуть, матимуть иншого вигляду на инших пристроях" + } + } + } + }, + "proStatusError" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} status xətası" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chyba stavu {pro}" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} Status Error" + } + } + } + }, + "proStatusInfoInaccurateNetworkError" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} statusunuzu yoxlamaq üçün şəbəkəyə bağlana bilmir. Bu səhifədə nümayiş olan məlumatlar, bağlantı bərpa olunana qədər qeyri-dəqiq ola bilər.

Lütfən şəbəkə bağlantınızı yoxlayıb yenidən sınayın." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nelze se připojit k síti, aby bylo možné zkontrolovat váš stav {pro}. Informace zobrazené na této stránce mohou být nepřesné, dokud nebude připojení obnoveno.

Zkontrolujte připojení k síti a zkuste to znovu." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unable to connect to the network to check your {pro} status. Information displayed on this page may be inaccurate until connectivity is restored.

Please check your network connection and retry." + } + } + } + }, + "proStatusLoading" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} statusu yüklənir" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Stav načítání {pro}" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} Status Loading" + } + } + } + }, + "proStatusLoadingDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} məlumatlarınız yüklənir. Bu səhifədəki bəzi əməliyyatlar yükləmə tamamlanana qədər əlçatan olmaya bilər." } }, - "ru" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "few" : { - "stringUnit" : { - "state" : "translated", - "value" : "Переводы не удались" - } - }, - "many" : { - "stringUnit" : { - "state" : "translated", - "value" : "Переводы не удались" - } - }, - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Перевод не удался" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Переводы не удались" - } - } - } - } - } + "value" : "Načítají se vaše informace {pro}. Některé akce na této stránce nemusí být dostupné, dokud nebude načítání dokončeno." } }, - "sv-SE" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Befordran Misslyckades" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Befordringar Misslyckades" - } - } - } - } - } + "value" : "Your {pro} information is being loaded. Some actions on this page may be unavailable until loading is complete." + } + } + } + }, + "proStatusLoadingSubtitle" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} status yüklənir" } }, - "tr" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ayrıcalık Başarısız Oldu" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ayrıcalıklar Başarısız Oldu" - } - } - } - } - } + "value" : "stav načítání {pro}" } }, - "uk" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "few" : { - "stringUnit" : { - "state" : "translated", - "value" : "Підвищення прав не відбулось" - } - }, - "many" : { - "stringUnit" : { - "state" : "translated", - "value" : "Підвищення прав не відбулось" - } - }, - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Підвищення прав не відбулось" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Підвищення прав не відбулось" - } - } - } - } - } + "value" : "{pro} status loading" + } + } + } + }, + "proStatusNetworkErrorDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} statusunuzu yoxlamaq üçün şəbəkəyə bağlana bilmir. Bağlantı bərpa olunana qədər {pro} ya yüksəldə bilməyəcəksiniz.

Lütfən şəbəkə bağlantınızı yoxlayıb yenidən sınayın." } }, - "vi" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Yêu cầu thăng cấp cho thành viên đã thất bại" - } - } - } - } - } + "value" : "Nelze se připojit k síti, aby bylo možné zkontrolovat váš stav {pro}. Dokud nebude obnoveno připojení, nelze provést navýšení na {pro}.

Zkontrolujte připojení k síti a zkuste to znovu." } }, - "zh-CN" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "授权失败" - } - } - } - } - } + "value" : "Unable to connect to the network to check your {pro} status. You cannot upgrade to {pro} until connectivity is restored.

Please check your network connection and retry." } } } }, - "promotionFailedDescription" : { + "proStatusRefreshNetworkError" : { "extractionState" : "manual", "localizations" : { "az" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Yüksəltmə tətbiq edilə bilmədi. Təkrar cəhd etmək istəyirsiniz?" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Yüksəltmələr tətbiq edilə bilmədi. Təkrar cəhd etmək istəyirsiniz?" - } - } - } - } - } + "value" : "{pro} statusunuzu təzələmək üçün şəbəkəyə bağlana bilmir. Bu səhifədəki bəzi əməliyyatlar, bağlantı bərpa olunana qədər sıradan çıxarılacaq.

Lütfən şəbəkə bağlantınızı yoxlayıb yenidən sınayın." } }, - "ca" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "La promoció no es va poder aplicar. Vols tornar-ho a provar?" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Les promocions no es va poder aplicar. Vols tornar-ho a provar?" - } - } - } - } - } + "value" : "Nelze se připojit k síti, aby se obnovil váš stav {pro}. Některé akce na této stránce budou deaktivovány, dokud nebude obnoveno připojení.

Zkontrolujte připojení k síti a zkuste to znovu." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unable to connect to the network to refresh your {pro} status. Some actions on this page will be disabled until connectivity is restored.

Please check your network connection and retry." + } + } + } + }, + "proSupportDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{pro} planınızla bağlı kömək lazımdır? Dəstək komandamıza müraciət edin." } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "few" : { - "stringUnit" : { - "state" : "translated", - "value" : "Povýšení nebylo možné uplatnit. Chcete to zkusit znovu?" - } - }, - "many" : { - "stringUnit" : { - "state" : "translated", - "value" : "Povýšení nebylo možné uplatnit. Chcete to zkusit znovu?" - } - }, - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Povýšení nebylo možné uplatnit. Chcete to zkusit znovu?" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Povýšení nebylo možné uplatnit. Chcete to zkusit znovu?" - } - } - } - } - } + "value" : "Potřebujete pomoc se svým tarifem {pro}? Pošlete žádost týmu podpory." } }, - "da" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Forfremmelsen kunne ikke gennemføres. Vil du prøve i gen?" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Forfremmelserne kunne ikke gennemføres. Vil du prøve i gen?" - } - } - } - } - } + "value" : "Need help with your {pro} plan? Submit a request to the support team." } }, - "de" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Die Beförderung konnte nicht durchgeführt werden. Möchtest du es erneut versuchen?" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Die Beförderungen konnten nicht durchgeführt werden. Möchtest du es erneut versuchen?" - } - } - } - } - } + "value" : "Besoin d'aide avec votre forfait {pro} ? Envoyez une demande à l'équipe d'assistance." } }, - "en" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "The promotion could not be applied. Would you like to try again?" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "The promotions could not be applied. Would you like to try again?" - } - } - } - } - } + "value" : "Hulp nodig met je {pro} abonnement? Dien een verzoek in bij het ondersteuningsteam." } }, - "eo" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "La promocio ne povas esti aplikita. Ĉu vi volas reprovi?" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "La promocioj ne povas esti aplikitaj. Ĉu vi volas reprovi?" - } - } - } - } - } + "value" : "Potrzebujesz pomocy z planem {pro}? Wyślij zgłoszenie zespołowi wsparcia." } }, - "es-419" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "La promoción no se ha podido aplicar. ¿Quieres volver a intentarlo?" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Las promociones no se han podido aplicar. ¿Quieres volver a intentarlo?" - } - } - } - } - } + "value" : "Якщо потребуєш допомоги щодо підписки {pro}, надійшли звернення до відділу підтримки." + } + } + } + }, + "proTosPrivacy" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Güncəlləyərək, {app_pro} Xidmət Şərtləri {icon} və Məxfilik Siyasəti {icon} ilə razılaşırsınız" } }, - "es-ES" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "La promoción no se ha podido aplicar. ¿Quieres volver a intentarlo?" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Las promociones no se han podido aplicar. ¿Quieres volver a intentarlo?" - } - } - } - } - } + "value" : "Aktualizací souhlasíte s Podmínkami služby {icon} a Zásadami ochrany osobních údajů {icon} {app_pro}" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Durch die Aktualisierung stimmst du den Nutzungsbedingungen {icon} und der Datenschutzerklärung {icon} von {app_pro} zu" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "By updating, you agree to the {app_pro} Terms of Service {icon} and Privacy Policy {icon}" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "La promotion n'a pas pu être appliquée. Voulez-vous réessayer ?" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Les promotions n'ont pas pu être appliquées. Voulez-vous réessayer ?" - } - } - } - } - } + "value" : "En mettant à jour, vous acceptez les Conditions d'utilisation {icon} et la Politique de confidentialité {icon} de {app_pro}" } }, - "hi" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "प्रमोशन लागू नहीं किया जा सका। क्या आप फिर से प्रयास करना चाहेंगे?" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "प्रमोशन लागू नहीं किए जा सके। क्या आप फिर से प्रयास करना चाहेंगे?" - } - } - } - } - } + "value" : "Door bij te werken ga je akkoord met de {app_pro} Gebruiksvoorwaarden {icon} en het Privacybeleid {icon}" } }, - "hu" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Az előléptetést nem lehetett alkalmazni. Szeretné újra megpróbálni?" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Az előléptetést nem lehetett alkalmazni. Szeretné újra megpróbálni?" - } - } - } - } - } + "value" : "Dokonując zmian wyrażasz zgodę na Warunki Świadczenia Usług {app_pro} {icon} oraz Politykę Prywatności {icon}" } }, - "id" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Promosi tidak dapat diterapkan. Apakah Anda ingin mencoba lagi?" - } - } - } - } - } + "value" : "Цією дією ти надаси згоду щодо дотримання Правил послуги {app_pro} {icon} і Ставлення до особистих відомостей {icon}" + } + } + } + }, + "proUnlimitedPins" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Limitsiz sancma" } }, - "it" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "La promozione non può essere applicata. Vuoi riprovare?" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Le promozioni non possono essere applicate. Vuoi riprovare?" - } - } - } - } - } + "value" : "Neomezený počet připnutí" } }, - "ko" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "승격 시키지 못했습니다. 다시 시도하시겠습니까?" - } - } - } - } - } + "value" : "Unlimited Pins" } }, - "ku-TR" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Terfîkirin nikare were tetbîqkirin. Tu dixwazî cardin biceribînî?" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Terfîkirin nikarin werin tetbîqkirin. Tu dixwazî cardin biceribînî?" - } - } - } - } - } + "value" : "Épingles illimitées" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "De promotie kon niet toegepast worden. Wilt u het opnieuw proberen?" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "De promoties konden niet toegepast worden. Wilt u het opnieuw proberen?" - } - } - } - } - } + "value" : "Onbeperkte Pins" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "few" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nie można było zastosować promocji. Czy chcesz spróbować ponownie?" - } - }, - "many" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nie można było zastosować promocji. Czy chcesz spróbować ponownie?" - } - }, - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nie udało się zastosować promocji. Czy chcesz spróbować ponownie?" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nie można było zastosować promocji. Czy chcesz spróbować ponownie?" - } - } - } - } - } + "value" : "Nielimitowane przypięcia" } }, - "ro" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "few" : { - "stringUnit" : { - "state" : "translated", - "value" : "Promovările nu au putut fi aplicate. Doriți să încercați din nou?" - } - }, - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Promovarea nu a putut fi aplicată. Doriți să încercați din nou?" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Promovările nu au putut fi aplicate. Doriți să încercați din nou?" - } - } - } - } - } + "value" : "Необмежена кількість закріплених бесід" + } + } + } + }, + "proUnlimitedPinsDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Limitsiz sancılmış danışıqla bütün söhbətlərinizi təşkil edin." } }, - "ru" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "few" : { - "stringUnit" : { - "state" : "translated", - "value" : "Не удалось принять запросы. Повторить попытку?" - } - }, - "many" : { - "stringUnit" : { - "state" : "translated", - "value" : "Не удалось принять запросы. Повторить попытку?" - } - }, - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Не удалось принять запрос. Повторить попытку?" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Не удалось принять запросы. Повторить попытку?" - } - } - } - } - } + "value" : "Organizujte si komunikaci pomocí neomezeného počtu připnutých konverzací." } }, - "sv-SE" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Befordran kunde inte tillämpas. Vill du försöka igen?" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Befordringarna kunde inte tillämpas. Vill du försöka igen?" - } - } - } - } - } + "value" : "Organize all your chats with unlimited pinned conversations." } }, - "tr" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ayrıcalık uygulanamadı. Yeniden denemek ister misiniz?" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ayrıcalıklar uygulanamadı. Yeniden denemek ister misiniz?" - } - } - } - } - } + "value" : "Organisez toutes vos discussions avec un nombre illimité de conversations épinglées." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Organiseer al je chats met onbeperkt vastgezette gesprekken." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Organizuj swoje czaty z nielimitowaną możliwością przypinania konwersacji." } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "few" : { - "stringUnit" : { - "state" : "translated", - "value" : "Підвищення прав не вдалось застосувати. Бажаєте спробувати ще раз?" - } - }, - "many" : { - "stringUnit" : { - "state" : "translated", - "value" : "Підвищення прав не вдалось застосувати. Бажаєте спробувати ще раз?" - } - }, - "one" : { - "stringUnit" : { - "state" : "translated", - "value" : "Підвищення прав не вдалось застосувати. Бажаєте спробувати ще раз?" - } - }, - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Підвищення прав не вдалось застосувати. Бажаєте спробувати ще раз?" - } - } - } - } - } + "value" : "Закріплення необмеженої кількості співрозмовників в головному переліку." + } + } + } + }, + "proUpdatePlanDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hazırda {current_plan} Planı üzərindəsiniz. {selected_plan} Planınana keçmək istədiyinizə əminsiniz?

Güncəlləsəniz, planınız {date} tarixində əlavə {selected_plan} {pro} erişimi üçün avtomatik yenilənəcək." } }, - "vi" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "Yêu cầu thăng cấp thành viên không thể được thực hiện. Bạn có muốn thử lại?" - } - } - } - } - } + "value" : "V současné době jste na tarifu {current_plan}. Jste si jisti, že chcete přepnout na tarif {selected_plan}?

Po aktualizaci bude váš tarif automaticky obnoven {date} na další {selected_plan} přístupu {pro}." } }, - "zh-CN" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "%#@arg1@" - }, - "substitutions" : { - "arg1" : { - "argNum" : 1, - "formatSpecifier" : "lld", - "variations" : { - "plural" : { - "other" : { - "stringUnit" : { - "state" : "translated", - "value" : "无法授权。您想重试吗?" - } - } - } - } - } + "value" : "You are currently on the {current_plan} Plan. Are you sure you want to switch to the {selected_plan} Plan?

By updating, your plan will automatically renew on {date} for an additional {selected_plan} of {pro} access." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vous êtes actuellement sur le forfait {current_plan}. Voulez-vous vraiment passer au forfait {selected_plan}?

En mettant à jour, votre forfait sera automatiquement renouvelé le {date} pour {selected_plan} supplémentaire de l'accès {pro}." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je zit momenteel op het {current_plan} abonnement. Weet je zeker dat je wilt overschakelen naar het {selected_plan} abonnement?

Als je dit bijwerkt, wordt je abonnement op {date} automatisch verlengd met {selected_plan} {pro} toegang." } } } }, - "proSendMore" : { + "proUpdatePlanExpireDescription" : { "extractionState" : "manual", "localizations" : { - "ca" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "Envia més amb" + "value" : "Planınız {date} tarixində bitəcək.

Güncəlləsəniz, planınız {date} tarixində əlavə {selected_plan} Pro erişimi üçün avtomatik olaraq yenilənəcək." } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Posílejte více se" + "value" : "Váš tarif vyprší {date}.

Po aktualizaci se váš tarif automaticky obnoví {date} na další {selected_plan} přístupu Pro." } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Send more with" + "value" : "Your plan will expire on {date}.

By updating, your plan will automatically renew on {date} for an additional {selected_plan} of Pro access." } }, - "hu" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Küldjön többet ezzel:" + "value" : "Votre forfait expirera le {date}.

En le mettant à jour, votre forfait sera automatiquement renouvelé le {date} pour {selected_plan} supplémentaires d’accès Pro." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je abonnement verloopt op {date}.

Door bij te werken wordt je abonnement automatisch verlengd op {date} voor een extra {selected_plan} aan Pro-toegang." } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Надсилайте довші повідомлення з" + "value" : "Ваш план завершиться {date}.

Після оновлення налаштувань автоматичної оплати, ваш план автоматично подовжиться {date} на додатковий {selected_plan} доступу до Pro." } } } @@ -355923,10 +366502,130 @@ "proUserProfileModalCallToAction" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} tətbiqindən daha çox faydalanmaq istəyirsiniz? Daha güclü mesajlaşma təcrübəsi üçün {app_pro}-ya yüksəldin." + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vols treure més informació de {app_name}? Actualitzes a {app_pro} per a una experiència de missatgeria més potent." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chcete z {app_name} získat více? Navyštee na {app_pro} pro výkonnější posílání zpráv." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Willst du mehr aus {app_name} herausholen? Upgrade auf {app_pro} für ein leistungsstärkeres Nachrichten-Erlebnis." + } + }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Want to use {app_name} to its fullest potential? Upgrade to {app_pro} to gain access to exclusive perks and features" + "value" : "Want to get more out of {app_name}? Upgrade to {app_pro} for a more powerful messaging experience." + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Quieres sacarle más provecho a {app_name}? Actualiza a {app_pro} para una experiencia de mensajería más potente." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Quieres sacarle más provecho a {app_name}? Actualiza a {app_pro} para una experiencia de mensajería más potente." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vous voulez tirer le meilleur parti de {app_name} ? Passez à {app_pro} pour une expérience de messagerie améliorée." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "क्या आप {app_name} से अधिक प्राप्त करना चाहते हैं? एक अधिक शक्तिशाली संदेश अनुभव के लिए {app_pro} में अपग्रेड करें।" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vuoi ottenere di più da {app_name}? Passa a {app_pro} per un'esperienza di messaggistica più potente." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name}をもっと活用したいですか?より強力なメッセージ体験のために{app_pro}へアップグレードしましょう。" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wil je meer uit {app_name} halen? Upgrade naar {app_pro} voor een krachtigere berichtbeleving." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chcesz więcej z {app_name}? Uaktualnij do {app_pro}, aby uzyskać potężniejsze możliwości wiadomości." + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quer aproveitar mais o {app_name}? Atualize para o {app_pro} e tenha uma experiência de mensagens mais poderosa." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vrei să profiți mai mult de {app_name}? Fă upgrade la {app_pro} pentru o experiență de mesagerie mai puternică." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Хотите больше возможностей от {app_name}? Перейдите на {app_pro}, чтобы получить более мощный опыт обмена сообщениями." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vill du få ut mer av {app_name}? Uppgradera till {app_pro} för en kraftfullare meddelandeupplevelse." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name}'den daha fazla yararlanmak ister misiniz? Daha güçlü bir mesajlaşma deneyimi için {app_pro}'ya yükseltin." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Хочете отримати більше від {app_name}? Оновіться до {app_pro}, щоб мати потужніший досвід обміну повідомленнями." + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "想充分体验 {app_name}?升级到 {app_pro} 享受更强大的消息体验。" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "想要充分利用 {app_name}?升級為 {app_pro},享受更強大的訊息體驗。" } } } @@ -359766,32 +370465,368 @@ "rateSession" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} qiymətləndirilsin?" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ohodnotit {app_name}?" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} bewerten?" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Rate {app_name}?" } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Calificar {app_name}?" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Calificar {app_name}?" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Noter {app_name} ?" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} को रेट करें?" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Valuta {app_name}?" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name}を評価しますか?" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} beoordelen?" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Oceń {app_name}?" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avaliar o {app_name}?" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Evaluezi {app_name}?" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Оцените {app_name}?" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Betygsätt {app_name}?" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Оцінити {app_name}?" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "给 {app_name} 评分?" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "要為 {app_name} 評分嗎?" + } } } }, "rateSessionApp" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tətbiqi qiymətləndir" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Valoris l'aplicació" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ohodnotit aplikaci" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "App bewerten" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Rate App" } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Calificar aplicación" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Calificar aplicación" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Noter l’application" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "ऐप को रेट करें" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Valuta app" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アプリを評価" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "App beoordelen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Oceń aplikację" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avaliar aplicação" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Evaluează aplicația" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Оцените ПО" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Betygsätt appen" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Оцінити застосунок" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "评分应用" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "評分應用程式" + } } } }, "rateSessionModalDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Çox şad olduq ki, {app_name} tətbiqindən həzz alırsınız. Bircə dəqiqəniz varsa, bizi {storevariant} üzərində qiymətləndirin, çünki bu, başqalarının şəxsi və təhlükəsiz mesajlaşmanı kəşf etməsinə kömək edir!" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ens alegrem que gaudiu {app_name}, si teniu un moment, ens classifiqueu a la {storevariant} ajuda els altres a descobrir missatgeria privada i segura!" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Jsme rádi, že se vám {app_name} líbí. Pokud máte chvíli času, ohodnoťte nás na {storevariant} abyste ostatním pomohli objevit soukromou a bezpečnou komunikaci!" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Es freut uns, dass du {app_name} genießt! Wenn du einen Moment Zeit hast, hilft eine Bewertung im {storevariant} anderen dabei, private und sichere Nachrichten zu entdecken." + } + }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "We're glad you're enjoying {app_name}, if
you have a moment, rating us in the
{storevariant} helps others discover
private, secure messaging!" + "value" : "We're glad you're enjoying {app_name}, if you have a moment, rating us in the {storevariant} helps others discover private, secure messaging!" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nos alegra que estés disfrutando de {app_name}. Si tienes un momento, calificarnos en {storevariant} ayuda a otros a descubrir la mensajería privada y segura." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nos alegra que estés disfrutando de {app_name}. Si tienes un momento, calificarnos en {storevariant} ayuda a otros a descubrir la mensajería privada y segura." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nous sommes ravis que vous appréciiez {app_name}. Si vous avez un instant, une évaluation sur {storevariant} aiderait d'autres personnes à découvrir la messagerie privée et sécurisée !" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "हमें खुशी है कि आपको {app_name} पसंद आ रहा है, यदि आपके पास एक क्षण है, तो {storevariant} पर हमारी रेटिंग देने से दूसरों को निजी, सुरक्षित मैसेजिंग खोजने में मदद मिलती है!" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Siamo felici che ti stia piacendo {app_name}. Se hai un momento, lascia una valutazione su {storevariant}, aiuterà altri a scoprire la messaggistica privata e sicura!" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} をご利用いただきありがとうございます。お時間があれば、{storevariant} で評価していただけると、他の方がプライベートで安全なメッセージを見つける手助けになります!" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fijn dat je {app_name} leuk vindt! Als je een momentje hebt, helpt een beoordeling in de {storevariant} anderen om privé en veilig berichten te ontdekken!" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cieszymy się, że podoba Ci się {app_name}. Jeśli masz chwilę, oceń nas w {storevariant}, aby pomóc innym odkryć prywatne i bezpieczne wiadomości!" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ficamos felizes por estar a gostar do {app_name}. Se tiver um momento, dar-nos uma avaliação na {storevariant} ajuda outros a descobrir mensagens privadas e seguras!" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ne bucurăm că îți place {app_name}, dacă ai un minut, o evaluare în {storevariant} îi ajută și pe alții să descopere mesageria privată și sigură!" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Мы рады, что вам нравится {app_name}. Если у вас есть минутка, оцените нас в {storevariant}, это поможет другим открыть для себя конфиденциальный и безопасный обмен сообщениями!" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vi är glada att du gillar {app_name}, om du har en stund hjälper det andra att upptäcka privat och säker meddelandehantering om du betygsätter oss i {storevariant}!" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ми раді, що вам подобається {app_name}. Якщо маєте хвильку, оцініть нас у {storevariant} — це допоможе іншим знайти безпечний та приватний спосіб спілкування!" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "很高兴您喜欢 {app_name},如果方便的话,请在 {storevariant} 上为我们评分,这将帮助他人发现私密、安全的消息方式!" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "很高興您喜歡使用 {app_name},如果有空,請在 {storevariant} 中給我們評分,幫助更多人找到私密、安全的訊息工具!" } } } @@ -361787,12 +372822,30 @@ "value" : "उत्तर प्राप्त हुआ" } }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fogadott válasz" + } + }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Jawaban Diterima" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Risposta ricevuta" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "応答を受信しました" + } + }, "ka" : { "stringUnit" : { "state" : "translated", @@ -361805,6 +372858,18 @@ "value" : "응답 수신됨" } }, + "ku" : { + "stringUnit" : { + "state" : "translated", + "value" : "وەڵامی وەرگیراو" + } + }, + "ku-TR" : { + "stringUnit" : { + "state" : "translated", + "value" : "وەڵامی وەرگیراو" + } + }, "nb" : { "stringUnit" : { "state" : "translated", @@ -361835,6 +372900,18 @@ "value" : "Odebrano odpowiedź" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Resposta recebida" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Răspuns primit" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -361870,6 +372947,12 @@ "state" : "translated", "value" : "已收到应答" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "已接收回應" + } } } }, @@ -361918,6 +373001,18 @@ "value" : "Receiving Call Offer" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recibiendo oferta de llamada" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recibiendo oferta de llamada" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -361930,18 +373025,48 @@ "value" : "कॉल ऑफर प्राप्त हो रहा है" } }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hívás ajánlat fogadása" + } + }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Menerima Penawaran Panggilan" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ricezione offerta di chiamata" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "通話オファーを受信中" + } + }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "통화 제안 받는 중" } }, + "ku" : { + "stringUnit" : { + "state" : "translated", + "value" : "پێشنیاری پەیوەندی بنێرە" + } + }, + "ku-TR" : { + "stringUnit" : { + "state" : "translated", + "value" : "پێشنیاری پەیوەندی بنێرە" + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -361954,6 +373079,18 @@ "value" : "Otrzymanie oferty połączenia" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "A receber oferta de chamada" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se primește oferta de apel" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -361983,6 +373120,12 @@ "state" : "translated", "value" : "正在接收通话邀请" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在接收通話邀請" + } } } }, @@ -362025,6 +373168,18 @@ "value" : "Receiving Pre Offer" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recibiendo oferta previa" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recibiendo oferta previa" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -362037,12 +373192,30 @@ "value" : "प्री ऑफर प्राप्त हो रहा है" } }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Előajánlás beérkeztetése" + } + }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Menerima Pra-Penawaran" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ricezione pre-offerta" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "事前オファーを受信中" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -362061,6 +373234,18 @@ "value" : "Oferta odbioru" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "A receber pré-oferta" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se primește oferta preliminară" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -362090,6 +373275,18 @@ "state" : "translated", "value" : "Đang có cuộc gọi đến" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在接收通话邀请" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在接收通話邀請" + } } } }, @@ -362590,7 +373787,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Hesabınıza müraciəti itirməmək üçün geri qaytarma parolunuzu saxlayın." + "value" : "Hesabınıza erişimi itirməmək üçün geri qaytarma parolunuzu saxlayın." } }, "bal" : { @@ -363533,478 +374730,70 @@ "recoveryPasswordDescription" : { "extractionState" : "manual", "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gebruik jou herstelwagwoord om jou rekening op nuwe toestelle te laai.

Jou rekening kan nie herstel word sonder jou herstelwagwoord nie. Maak seker jy stoor dit iewers veilig en moenie dit met enigiemand deel nie." - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "استخدم كلمة المرور لاستعادة التحميل على أجهزة جديدة.

لا يمكن استعادة الحساب بدون كلمة المرور. تأكد من تخزينها في مكان آمن وسري - ولا تشاركها مع أي أحد." - } - }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "Hesabınızı yeni cihazlara yükləmək üçün geri qaytarma parolunuzu istifadə edin.

Geri qaytarma parolunuz olmadan hesabınız geri qaytarıla bilməz. Təhlükəsiz və etibarlı yerdə saxladığınıza əmin olun və heç kəslə paylaşmayın." - } - }, - "bal" : { - "stringUnit" : { - "state" : "translated", - "value" : "اپنے اکاؤنٹ کو نئے ڈیوائس پر لوڈ کرنے کے لئے اپنا ریکوری پاس ورڈ استعمال کریں۔

آپ کا اکاؤنٹ آپ کے ریکوری پاس ورڈ کے بغیر بازیافت نہیں کیا جا سکتا۔ اسے محفوظ اور محفوظ جگہ پر ذخیرہ کرنا یقینی بنائیں — اور اسے کسی کے ساتھ شیئر نہ کریں۔" - } - }, - "be" : { - "stringUnit" : { - "state" : "translated", - "value" : "Выкарыстоўвайце свой пароль для аднаўлення, каб загрузіць свой уліковы запіс на новых прыладах.

Ваш уліковы запіс не можа быць адноўлены без вашага пароля для аднаўлення. Захоўвайце яго ў бяспечным і надзейным месцы — і не дзяліце з нікога." - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Използвайте вашата парола за възстановяване, за да заредите акаунта си на нови устройства.

Вашият акаунт не може да бъде възстановен без вашата парола за възстановяване. Уверете се, че е съхранена някъде на сигурно място — и не я споделяйте с никого." - } - }, - "bn" : { - "stringUnit" : { - "state" : "translated", - "value" : "নতুন ডিভাইসে আপনার অ্যাকাউন্ট লোড করতে আপনার রিকভারি পাসওয়ার্ড ব্যবহার করুন।

আপনার রিকভারি পাসওয়ার্ড ছাড়া আপনার অ্যাকাউন্ট পুনরুদ্ধার করা যাবে না। নিশ্চিত করুন এটি নিরাপদ এবং সুরক্ষিত জায়গায় সংরক্ষণ করা হয়েছে এবং এটি কারও সাথে শেয়ার করবেন না।" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Utilitza la teva contrasenya de recuperació per carregar el teu compte en nous dispositius.

No es pot recuperar el teu compte sense la teva contrasenya de recuperació. Assegura't que està emmagatzemada en un lloc segur i no la comparteixis amb ningú." + "value" : "Hesabınızı yeni cihazlara yükləmək üçün geri qaytarma parolunuzu istifadə edin.

Geri qaytarma parolunuz olmadan hesabınız geri qaytarıla bilməz. Parolu təhlükəsiz və etibarlı yerdə saxladığınıza əmin olun və heç kəslə paylaşmayın." } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Použijte své heslo pro obnovení pro načtení účtu na nových zařízeních.

Bez hesla pro obnovení nelze obnovit účet. Ujistěte se, že je uložené na bezpečném místě – a nesdílejte ho s nikým." - } - }, - "cy" : { - "stringUnit" : { - "state" : "translated", - "value" : "Defnyddiwch eich chyfrinair adfer i lwytho eich cyfrif ar ddyfeisiau newydd.

Ni ellir adfer eich cyfrif heb eich cyfrinair adfer. Sicrhewch ei fod yn cael ei storio yn rhywle diogel - ac na fyddwch yn ei rannu ag unrhyw un." - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Brug din recovery password til at indlæse din konto på nye enheder.

Din konto kan ikke gendannes uden din recovery password. Sørg for, at den er opbevaret et sikkert sted, og del den ikke med nogen." + "value" : "Použijte své heslo pro obnovení pro načtení účtu na nových zařízeních.

Bez hesla pro obnovení nelze obnovit účet. Ujistěte se, že je uložené na bezpečném místě — a nesdílejte ho s nikým." } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Verwende dein Wiederherstellungspasswort, um deinen Account auf neuen Geräten zu laden.

Dein Account kann ohne dein Wiederherstellungspasswort nicht wiederhergestellt werden. Stelle sicher, dass es an einem sicheren Ort aufbewahrt ist – und teile es niemandem mit." - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Χρησιμοποιήστε τον κωδικό ανάκτησης για να φορτώσετε τον λογαριασμό σας σε νέες συσκευές.

Ο λογαριασμός σας δεν μπορεί να ανακτηθεί χωρίς τον κωδικό ανάκτησης. Βεβαιωθείτε ότι τον αποθηκεύσατε σε ασφαλές μέρος — και μην τον μοιραστείτε με κανέναν." + "value" : "Verwende dein Wiederherstellungspasswort, um deinen Account auf neue Geräten zu laden.

Dein Account kann ohne dein Wiederherstellungspasswort nicht wiederhergestellt werden. Stelle sicher, dass es an einem sicheren Ort aufbewahrt ist – und teile es niemandem mit." } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Use your recovery password to load your account on new devices.

Your account cannot be recovered without your recovery password. Make sure it's stored somewhere safe and secure — and don't share it with anyone." - } - }, - "eo" : { - "stringUnit" : { - "state" : "translated", - "value" : "Uzu vian recuđer passvorton por ŝarĝi vian konton sur novaj aparatoj.

Via konto ne povas esti rekuperita sen via recuđer passvorto. Certigu, ke ĝi estas stokita ien sekura kaj ne dividas ĝin kun iu ajn." - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Utilice su contraseña de recuperación para cargar su cuenta en nuevos dispositivos.

Su cuenta no se puede recuperar sin su contraseña de recuperación. Asegúrese de guardarla en un lugar seguro — y no la comparta con nadie." - } - }, - "es-ES" : { - "stringUnit" : { - "state" : "translated", - "value" : "Usa tu contraseña de recuperación para cargar tu cuenta en nuevos dispositivos.

Tu cuenta no se puede recuperar sin tu contraseña de recuperación. Asegúrate de que esté guardada en un lugar seguro y no la compartas con nadie." - } - }, - "et" : { - "stringUnit" : { - "state" : "translated", - "value" : "Konto laadimiseks kasutage oma taastamissalasõna uutes seadmetes.

Teie kontot ei saa taastada ilma taastamissalasõnata. Veenduge, et see oleks turvaliselt salvestatud ja ärge jagage seda kellelegi." - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Erabili zure berreskurapen pasahitza zure kontua kargatzeko gailu berrietan.

Zure kontua ezin da berreskuratu berreskurapen pasahitzik gabe. Ziurtatu leku seguru batean gordeta dagoela eta ez partekatu inorekin." - } - }, - "fa" : { - "stringUnit" : { - "state" : "translated", - "value" : "از گذرواژه بازیابی خود برای بارگذاری حساب کاربری خود در دستگاه‌های جدید استفاده کنید.

حساب شما بدون گذرواژه بازیابی‌تان بازیابی نخواهد شد. مطمئن شوید که آن را در مکانی امن ذخیره کرده‌اید و با کسی به اشتراک نگذارید." - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Käytä palautussalasanaa ladataksesi tilisi uusiin laitteisiin.

Tiliäsi ei voida palauttaa ilman palautussalasanaa. Varmista, että se on tallennettu turvallisesti ja älä jaa sitä kenellekään." - } - }, - "fil" : { - "stringUnit" : { - "state" : "translated", - "value" : "Use your recovery password to load your account on new devices.

Your account cannot be recovered without your recovery password. Make sure it's stored somewhere safe and secure — and don't share it with anyone." + "value" : "Use your recovery password to load your account on new devices.

Your account cannot be recovered without your recovery password. Make sure it's stored somewhere safe and secure — and don't share it with anyone." } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Utilisez votre mot de passe de récupération pour charger votre compte sur de nouveaux appareils.

Votre compte ne peut pas être récupéré sans votre mot de passe de récupération. Assurez-vous qu'il soit stocké en lieu sûr et sécurisé - et ne le partagez avec personne." - } - }, - "gl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Use your recovery password para cargar a túa conta en novos dispositivos.

A túa conta non se pode recuperar sen a túa recovery password. Asegúrate de que estea gardada nalgún lugar seguro e non a compartas con ninguén." - } - }, - "ha" : { - "stringUnit" : { - "state" : "translated", - "value" : "Yi amfani da kalmar wucewa ta dawo da asusun naka a kan sabbin na'urori.

Ba za a iya dawo da asusunku ba tare da kalmar wucewarku ta dawo ba. Tabbatar an adana ta wani wuri mai tsaro kuma kada ku raba ta da kowa ba." - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "השתמש בסיסמת שחזור שלך לטעינת חשבונך על מכשירים חדשים.

לא ניתן לשחזר שלך חשבון ללא סיסמת השחזור שלך. ודא שהיא שמורה במקום בטוח ומאובטח - אל תשתף אותה עם אחרים." - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "अपना अकाउंट नए डिवाइस पर लोड करने के लिए अपने रिकवरी पासवर्ड का उपयोग करें।

आपका अकाउंट आपके रिकवरी पासवर्ड के बिना पुनर्प्राप्त नहीं किया जा सकता है। सुनिश्चित करें कि इसे कहीं सुरक्षित रखें और इसे किसी के साथ साझा न करें।" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Koristite svoju lozinku za oporavak za učitavanje računa na novim uređajima.

Vaš račun nije moguće oporaviti bez lozinke za oporavak. Osigurajte da je pohranjena na sigurnom mjestu i ne dijelite je s nikim." - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "A visszaállítási jelszavaddal új eszközökön is betöltheted a felhasználódat.

A felhasználód nem állítható vissza a visszaálltási jelszó nélkül. Gondoskodj róla, hogy biztonságos helyen tárolod — és ne oszd meg senkivel." - } - }, - "hy-AM" : { - "stringUnit" : { - "state" : "translated", - "value" : "Օգտագործեք ձեր վերականգնման գաղտնաբառը ձեր հաշիվը նոր սարքերում ներբեռնելու համար.

Ձեր հաշիվը հնարավոր չէ վերականգնել առանց վերականգնման գաղտնաբառի: Համոզվեք, որ այն պահված է անվտանգ տեղում և չկիսեք այն ուրիշների հետ։" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gunakan kata sandi pemulihan Anda untuk memuat akun Anda di perangkat baru.

Akun Anda tidak dapat dipulihkan tanpa kata sandi pemulihan Anda. Pastikan disimpan di tempat yang aman dan terlindungi — dan jangan bagikan kepada siapa pun." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Utilizza la tua password di recupero per caricare il tuo account su nuovi dispositivi.

Il tuo account non può essere ripristinato senza la tua password di recupero. Assicurati di conservarla in un luogo sicuro, riservato e di non condividerla con nessuno." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "リカバリパスワードを使用して、新しいデバイスでアカウントを読み込みます。

リカバリパスワードがないとアカウントを復元できません。 安全な場所に保管し、他の人と共有しないでください。" - } - }, - "ka" : { - "stringUnit" : { - "state" : "translated", - "value" : "გამოიყენეთ თქვენი აღდგენის პაროლი ანგარიშის ახალი მოწყობილობებზე ჩასატვირთად.

თქვენი ანგარიში ვერ აღდგება თქვენი აღდგენის პაროლის გარეშე. დარწმუნდით, რომ ეს მრავალსაფრთილად და უსაფრთხოდ არის შენახული — და არ გააზიაროთ ეს სხვა ადამიანებთან." - } - }, - "km" : { - "stringUnit" : { - "state" : "translated", - "value" : "ប្រើពាក្យសម្ងាត់ដើម្បីទាញយកគណនីរបស់អ្នកលើឧបករណ៍ថ្មីៗ.

គណនីរបស់អ្នកមិនអាចទាញយកកនឡើងវិញដោយគ្មាន Recovery Password។ សូមរក្សាវានៅកន្លែងសុវត្ថិភាព ហើយកុំចែករំលែកវាជាមួយនរណាម្នាក់ទេ។" - } - }, - "kn" : { - "stringUnit" : { - "state" : "translated", - "value" : "ನಿಮ್ಮ ಖಾತೆಯನ್ನು ಹೊಸ ಸಾಧನಗಳಲ್ಲಿ ಲೋಡ್ ಮಾಡಲು ನಿಮ್ಮ ಪುನಃ ಸ್ವಾಸ್ತ್ಯದ ಪಾಸ್‌ವರ್ಡ್ ಅನ್ನು ಬಳಸಿ.

ನಿಮ್ಮ ಪುನಃ ಸ್ವಾಸ್ತ್ಯದ ಪಾಸ್‌ವರ್ಡ್ ಇಲ್ಲದೆ ನಿಮ್ಮ ಖಾತೆಯನ್ನು ಪುನಃ ಪಡೆಯಲು ಸಾಧ್ಯವಿಲ್ಲ. ಇದು ಸುರಕ್ಷಿತವಾಗಿ ಮತ್ತು ಭದ್ರವಾಗಿ ಸಂಗ್ರಹಿಸಿರುವುದನ್ನು ಖಚಿತಪಡಿಸಿಕೊಂಡು—ಯಾರೊಂದಿಗೂ ಅದನ್ನು ಹಂಚಿಕೊಳ್ಳಬೇಡಿ." - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "새 장치에서 계정을 로드하려면 복구 비밀번호를 사용하십시오.

복구 비밀번호 없이는 계정을 복구할 수 없습니다. 안전하게 보관하고 타인과 공유하지 않도록 주의하세요." - } - }, - "ku" : { - "stringUnit" : { - "state" : "translated", - "value" : "بەکارھێنانی تێپەڕەوشەی گەڕاندنەکەت بۆ بارکردنی هەژمارەکەت لەسەر ئامێرە نوێکان.

ئەکاونتەکەت بێ بەکردنی تێپەڕەوشەی گەڕاندنەکەت ناتواندرێت بەسەردەبڕێت. پێویستەوە تەواوەتی پارێزگای دەستیفێڵ بکەیت و وتاپیشت نیبیت." - } - }, - "ku-TR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ji bo barkirina hesabê xwe ya li ser amadekarên nû şîfreya recoverîya xwe bikar bînin.

Hesabên we ne matur nirx bikin jî. Da sûçiyên guman dikin ku ew li ciheke nehiş jî au reş bike û bi kesî re nebibe." - } - }, - "lg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kozesa oba Recovery password yo okuwonya account ku kiga kya njawulo.

Account yo teyeetaagulaamu soola kusasaanya oba Recovery password yo. Kakatila nkumu kakati lwaki tekiriza musajja yenna." - } - }, - "lt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Naudokite savo atkūrimo slaptažodį, kad įkeltumėte savo paskyrą į naujus įrenginius.

Jūsų paskyra negali būti atkurta be jūsų atkūrimo slaptažodžio. Įsitikinkite, kad jis yra saugomas saugioje vietoje — ir nesidalinkite juo su niekuo." - } - }, - "lv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lietojiet savu atjaunošanas paroli, lai ielādētu savu kontu jaunās ierīcēs.

Jūsu kontu nevar atjaunot bez jūsu atjaunošanas paroles. Pārliecinieties, ka tā ir uzglabāta drošā vietā — un nedalieties ar to." - } - }, - "mk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Користете ја вашата лозинка за враќање за да го вчитајте вашиот профил на нови уреди.

Вашиот профил не може да се поврати без вашата лозинка за враќање. Осигурајте се дека е безбедно и сигурно сочувана — и не ја споделувајте со никој." - } - }, - "mn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Таны аккаунтыг шинэ төхөөрөмжүүдэд ачаалахын тулд сэргээх нууц үгийг ашиглана уу.

Таны аккаунтыг сэргээх нууц үггүйгээр сэргээх боломжгүй. Энэ нууц үгийг аюулгүй газар хадгалаарай – хэзээ ч хүнд бүү өгөөрэй." - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gunakan recovery password anda untuk memuatkan akaun anda pada peranti baharu.

Akaun anda tidak boleh dipulihkan tanpa recovery password anda. Pastikan ia disimpan di tempat yang selamat dan selamat — dan jangan kongsikan dengan sesiapa." - } - }, - "my" : { - "stringUnit" : { - "state" : "translated", - "value" : "သင့်အကောင့်ကို နောက်ထပ်စက်ပစ္စည်းများပေါ်တွင် တင်ရန် သင့် recovery password ကို အသုံးပြုပါ။

သင့် recovery password မရှိပဲ သင့်အကောင့်ကို ပြန်ယူ၍ မရနိုင်ပါ။ ၎င်းကို ဘေးကင်းသောနေရာတစ်ခုတွင် သိမ်းဆည်းထားပြီး သက်သေပြရန် မမျှဝေပါနှင့်။" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bruk din Recovery password for å laste inn kontoen din på nye enheter.

Kontot kan ikke gjenopprettes uten din Recovery password. Sørg for at den er lagret et trygt og sikkert sted — og ikke del den med noen." - } - }, - "nb-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bruk recovery password for å laste inn kontoen din på nye enheter.

Kontoen din kan ikke gjenopprettes uten recovery password. Sørg for at det er lagret et trygt og sikkert sted – og del det ikke med noen." - } - }, - "ne-NP" : { - "stringUnit" : { - "state" : "translated", - "value" : "तपाईंको खाता नयाँ उपकरणहरूमा लोड गर्न आफ्नो पुनर्प्राप्ति पासवर्ड प्रयोग गर्नुहोस्।

तपाईँको खाता बिना पुनर्प्राप्ति पासवर्ड पुनः प्राप्त गर्न सकिन्न। यो सुरक्षित र सुरक्षित ठाउँमा राखिएको सुनिश्चित गर्नुहोस् - र यसलाई कसैसँग साझा नगर्नुहोस्।" + "value" : "Utilisez votre mot de passe de récupération pour charger votre compte sur de nouveaux appareils.

Votre compte ne peut pas être récupéré sans votre mot de passe de récupération. Assurez-vous qu'il soit stocké en lieu sûr et sécurisé ;— et ne le partagez avec personne." } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Gebruik uw herstelwachtwoord om uw account op nieuwe apparaten te laden.

Uw account kan niet worden hersteld zonder uw herstelwachtwoord. Zorg ervoor dat het ergens veilig is opgeslagen – en deel het niet met anderen." - } - }, - "nn-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bruk gjenopprettingsløsenordet ditt for å laste kontoen din på nye enheter.

Kontoen din kan ikke gjenopprettes uten gjenopprettingsløsenordet. Sørg for at det er lagret et trygt sted — og del det ikke med noen." - } - }, - "ny" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gwiritsani ntchito chinsinsi chobwezeretsanso akaunti yanu pazida zatsopano.

Akaunti yanu siyingathe kubwezeretsedwa popanda chinsinsicho. Onetsetsani kuti yatero pamtendere ndi chitetezo - ndipo musagawane aliyense." - } - }, - "pa-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "ਆਪਣਾ ਖਾਤਾ ਨਵੇਂ ਜੰਤਰਾਂ ਤੇ ਲੋਡ ਕਰਨ ਲਈ ਆਪਣਾ ਰਿਕਵਰੀ ਪਾਸਵਰਡ ਵਰਤੋ।

ਆਪਣਾ ਖਾਤਾ ਰਿਕਵਰੀ ਪਾਸਵਰਡ ਤੋਂ ਬਿਨਾ ਪੁਨਰ ਪ੍ਰਾਪਤ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਦਾ। ਯਕੀਨੀ ਬਣਾਓ ਕਿ ਇਹ ਕਿਸੇ ਸੁਰੱਖਿਅਤ ਅਤੇ ਸੁਰੱਖਿਅਤ ਸਥਾਨ ਤੇ ਸਟੋਰ ਕੀਤਾ ਗਿਆ ਹੈ - ਅਤੇ ਇਸ ਨੂੰ ਕਿਸੇ ਨਾਲ ਸਾਂਝਾ ਨਾ ਕਰੋ।" + "value" : "Gebruik uw herstelwachtwoord om uw account op nieuwe apparaten te laden.

Uw account kan niet worden hersteld zonder uw herstelwachtwoord. Zorg ervoor dat het ergens veilig is opgeslagen – en deel het met niemand." } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Aby wczytać konto na nowych urządzeniach, użyj hasła odzyskiwania.

Nie można odzyskać konta bez hasła odzyskiwania. Upewnij się, że jest ono przechowywane w bezpiecznym miejscu i nie udostępniaj go nikomu." - } - }, - "ps" : { - "stringUnit" : { - "state" : "translated", - "value" : "د خپل حساب د بارولو لپاره خپل بیا رغونه Password وکاروئ.

پرته له خپل بیا رغونه Password څخه ستاسو حساب بیا رغول نشي. ډاډ ترلاسه کړئ چې دا خوندي او خوند ځای کې ساتل شوی دی - او دا له هیچا سره شریک نه کړئ." - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Use sua senha de recuperação para carregar sua conta em novos dispositivos.

Sua conta não pode ser recuperada sem sua senha de recuperação. Certifique-se de armazená-la em um lugar seguro e não a compartilhe com ninguém." - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Use a sua chave de recuperação para carregar a sua conta em novos dispositivos.

A sua conta não pode ser recuperada sem a sua chave de recuperação. Certifique-se de que está armazenada num lugar seguro — e não a partilhe com ninguém." - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Folosește parola de recuperare pentru a încărca contul pe dispozitive noi.

Contul nu poate fi recuperat fără parola de recuperare. Asigurați-vă că este stocată într-un loc sigur și securizat – și nu o împărtășiți nimănui." + "value" : "Użyj swojego hasła odzyskiwania, by załadować swoje konto na nowych urządzeniach.

Twoje konto nie może być odzyskane bez tego hasła. Upewnij się, że przechowujesz je w bezpiecznym miejscu – i nie ujawniaj go nikomu." } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Используйте пароль восстановления, чтобы загрузить свою учетную запись на новых устройствах.

Ваша учетная запись не может быть восстановлена без пароля восстановления. Убедитесь, что он хранится в безопасном месте, и не передавайте его никому." - } - }, - "sh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Koristite svoju Recovery Password za učitavanje svog naloga na nove uređaje.

Vaš račun ne može biti oporavljen bez vaše Recovery Password. Uverite se da je čuvana negde sigurno i bezbedno – i nemojte je podeliti ni sa kim." - } - }, - "si-LK" : { - "stringUnit" : { - "state" : "translated", - "value" : "ඔබගේ ගිණුම නැවත ආරම්භ කිරීමට නැවත ආරම්භ කරන්න පරිශීලන මුරපදය භාවිතා කරන්න.

ඔබගේ පරිශීලන මුරපදය නැත්තහොත් ඔබේ ගිණුම නැවත ලබා ගැනීමට නොහැක. ආරක්ෂිත සහ ආරක්ෂිත ස්ථානයක එය ගබඩා කර ඇති බවට වග බලා ගන්න — එය කිසිවෙකුට හුවමාරු නොකරන්න." - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Použite svoje recovery password na načítanie účtu na nových zariadeniach.

Bez recovery password váš účet nebude možné obnoviť. Uistite sa, že je uložené na bezpečnom mieste a nezdieľajte ho s nikým." - } - }, - "sl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Uporabite geslo za obnovitev, da naložite svoj račun na novih napravah.

Vašega računa ni mogoče obnoviti brez obnovitvenega gesla. Poskrbite, da bo shranjeno na varnem mestu — in ga ne delite z nikomer." - } - }, - "sq" : { - "stringUnit" : { - "state" : "translated", - "value" : "Përdorni fjalëkalimin e rikuperimit për të ngarkuar llogarinë tuaj në pajisje të reja.

Llogaria juaj nuk mund të rikuperohet pa fjalëkalimin tuaj të rikuperimit. Sigurohuni që ta keni ruajtur në një vend të sigurt dhe mos e ndani me askënd." - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Користите своју Recovery password да учитате свој налог на нове уређаје.

Ваш налог не може бити опорављен без ваше Recovery password. Обавезно га чувајте на сигурном месту и не делите га са никим." - } - }, - "sr-Latn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Koristite svoj recovery password da učitate svoj nalog na novim uređajima.

Vaš nalog se ne može oporaviti bez recovery password-a. Uverite se da je čuvan na sigurnom i ne delite ga ni sa kim." + "value" : "Используйте пароль восстановления, чтобы загрузить свою учетную запись на новых устройствах.

Ваша учетная запись не может быть восстановлена без пароля восстановления. Убедитесь, что он хранится в безопасном месте — и не делитесь им ни с кем." } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Använd din återställningslösenord för att ladda ditt konto på nya enheter.

Ditt konto kan inte återställas utan ditt återställningslösenord. Se till att det lagras säkert och dela det inte med någon." - } - }, - "sw" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tumia nywila ya urejeshaji kufungua akaunti yako kwenye vifaa vipya.

Akaunti yako haiwezi kurejeshwa bila nywila yako ya urejeshaji. Hakikisha umeihifadhi mahali salama na siri — usishiriki na yeyote." - } - }, - "ta" : { - "stringUnit" : { - "state" : "translated", - "value" : "உங்கள் விளம்பரச் சொந்தக் குறியீட்டை பயன்படுத்தி உங்கள் கணக்கை புதிய சாதனங்களில் ஏற்றுக.

உங்கள் விளம்பரச் சொந்தக் குறியீடு இல்லாமல் உங்கள் கணக்கை மீட்டெடுக்க முடியாது. அது பாதுகாப்பான மற்றும் பாதுகாப்பான இடத்தில் சேமிக்கப்படுவதை உறுதிசெய்க - மற்றும் அதைப் பிறரிடம் பகிர வேண்டாம்." - } - }, - "te" : { - "stringUnit" : { - "state" : "translated", - "value" : "మీ ఖాతాను కొత్త పరికరాలపై లోడ్ చేయడానికి మీ రికవరీ పాస్‌వర్డ్‌ని ఉపయోగించండి.

మీ రికవరీ పాస్‌వర్డ్ లేకుండా మీ ఖాతాను పునరుద్ధరించలేరు. అది ఎక్కడైనా సురక్షితంగా నిల్వ చేయబడిందని నిర్ధారించుకోండి – మరియు దానిని ఎవరికీ పంచుకోకండి." - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "ใช้รหัสผ่านการกู้คืนของคุณเพื่อโหลดบัญชีของคุณบนอุปกรณ์ใหม่.

บัญชีของคุณจะไม่สามารถกู้คืนได้หากไม่มีรหัสผ่านการกู้คืนของคุณ. ตรวจสอบให้แน่ใจว่ามีการเก็บไว้อย่างปลอดภัยและไม่แบ่งปันให้ใคร." + "value" : "Använd ditt återställningslösenord för att ladda ditt konto på nya enheter.

Ditt konto kan inte återställas utan ditt återställningslösenord. Se till att det lagras på en säker och trygg plats — och dela det inte med någon." } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Hesabınızı yeni cihazlarda yüklemek için kurtarma şifrenizi kullanın.

Kurtarma şifreniz olmadan hesabınız geri yüklenemez. Güvende olduğundan emin olun ve kimseyle paylaşmayın." + "value" : "Kurtarma şifrenizi kullanarak hesabınızı yeni cihazlara yükleyin.

Kurtarma şifreniz olmadan hesabınız kurtarılamaz. Şifrenizi güvenli bir yerde sakladığınızdan emin olun ve kimseyle paylaşmayın." } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Використовуйте свій пароль для відновлення, щоб завантажити свій обліковий запис на нових пристроях.

Ваш обліковий запис не може бути відновлений без вашого пароля для відновлення. Переконайтеся, що він зберігається десь у надійному місці та не передавайте його нікому." - } - }, - "ur-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "اپنے اکاؤنٹ کو نئے آلات پر لوڈ کرنے کے لیے اپنی بازیابی کا پاس ورڈ استعمال کریں۔

آپ کا اکاؤنٹ آپ کے بازیابی کے پاس ورڈ کے بغیر بازیافت نہیں ہو سکتا۔ یقینی بنائیں کہ یہ کہیں محفوظ اور محفوظ رکھا گیا ہے — اور کسی کے ساتھ اسے شیئر نہ کریں۔" - } - }, - "uz" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hisobingizni yangi qurilmalarda yuklash uchun qayta parolingizdan foydalaning.

Hisobingiz qayta parolsiz tiklanmaydi. Unga xavfsiz va ishonchli joyda saqlanganidan ishonch hosil qiling va hech kimga oshkor qilmang." - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dùng mật khẩu khôi phục của bạn để tải tài khoản của bạn trên các thiết bị mới.

Tài khoản của bạn không thể được khôi phục nếu không có mật khẩu khôi phục của bạn. Hãy chắc chắn rằng nó được lưu trữ ở một nơi an toàn và bảo mật — và đừng chia sẻ nó với bất kỳ ai." - } - }, - "xh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Faka iphasiwedi yakho yokufumana kwakhona ukuze ufake iakhawunti yakho kwizixhobo ezintsha.

Iakhawunti yakho ayinakufumaneka ngaphandle kwephasiwedi yakho yokufumana kwakhona. Qinisekisa ukuba iyonke indawo ekhuselekileyo kwaye ungayabelani nabani na." - } - }, - "zh-CN" : { - "stringUnit" : { - "state" : "translated", - "value" : "使用您的恢复密码在新设备上加载您的帐户。

没有您的恢复密码,您的帐户将无法恢复。请确保将它存储在安全的地方,并且不要与任何人分享。" - } - }, - "zh-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "請使用您的恢復密碼在新裝置上載入您的帳戶。

若沒有恢復密碼將無法恢復您的帳戶。請確保將其儲存於安全的地方—且不要與任何人分享。" + "value" : "Використовуйте пароль для відновлення для завантаження свого облікового запису на нових пристроях.

Ваш обліковий запис не може бути відновлений без пароля для відновлення. Переконайтеся, що він зберігається у надійному місці та не передавайте його нікому." } } } @@ -364575,6 +375364,12 @@ "value" : "Hiba történt a helyreállítási jelszó betöltése közben.

Exportálja a naplófájlokat, majd töltse fel azokat a(z) {app_name} segítségével az ügyfélszolgálatnak a probléma megoldása érdekében." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Si è verificato un errore durante il caricamento della tua password di recupero.

Esporta i log, quindi invia il file tramite il Centro Assistenza di {app_name} per aiutare a risolvere il problema." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -364587,6 +375382,12 @@ "value" : "복구 비밀번호를 불러오는 도중 오류가 발생했습니다.

문제를 해결하기 위해 로그를 내보낸 후 {app_name} 고객 지원 센터에 첨부하여 문의 해주세요." } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "En feil oppstod når ditt gjenopprettingspassord forsøkte å laste.

Vennligst eksporter loggene dine, så opplast filen gjennom Hjelpesenteret til {app_name}." + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -364599,6 +375400,18 @@ "value" : "Wystąpił błąd podczas próby wczytania hasła odzyskiwania.

Wyeksportuj swoje dzienniki, a następnie prześlij plik za pośrednictwem pomocy technicznej aplikacji {app_name}, aby pomóc w rozwiązaniu tego problemu." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ocorreu um erro ao tentar carregar a sua palavra-passe de recuperação.

Por favor exporte os seus logs e depois envie o ficheiro através do Centro de Ajuda do {app_name} para ajudar a resolver este problema." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "A apărut o eroare la încărcarea parolei de recuperare.

Te rugăm să exporți jurnalele, apoi să încarci fișierul prin intermediul Biroului de asistență {app_name} pentru a ajuta la soluționarea acestei probleme." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -364634,6 +375447,12 @@ "state" : "translated", "value" : "尝试加载您的恢复密码时发生错误。

请导出您的日志,然后通过{app_name}帮助服务台上传文件以帮助解决此问题。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "載入您的恢復密碼時發生錯誤。

請匯出您的日誌,然後透過 {app_name} 的協助台上傳檔案以解決此問題。" + } } } }, @@ -367993,484 +378812,76 @@ "recoveryPasswordHidePermanentlyDescription2" : { "extractionState" : "manual", "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Is jy seker jy wil jou herstel wagwoord permanent op hierdie toestel versteek? Dit kan nie ongedaan gemaak word nie." - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "هل أنت متأكد من إخفاء كلمة مرور الاسترداد الخاصة بك على هذا الجهاز نهائيًا؟ لا يمكن التراجع عن هذا." - } - }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "Geri qaytarma parolunuzu bu cihazda həmişəlik gizlətmək istədiyinizə əminsiniz? Bunun geri dönüşü yoxdur." - } - }, - "bal" : { - "stringUnit" : { - "state" : "translated", - "value" : "دم کی لحاظ انت کہ ایی خفیہ استعانت کوڈ ایی ڈیوائیس سرمنداً چھپا بکنی؟ ایی خال ھچگاں نہ بیت." - } - }, - "be" : { - "stringUnit" : { - "state" : "translated", - "value" : "Вы ўпэўненыя, што жадаеце пастаянна схаваць ваш канчатковы пароль аднаўлення на гэтай прыладзе? Гэта немагчыма адмяніць." - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Сигурен ли си, че искаш да скриеш своята възстановителна парола за постоянно на това устройство? Това действие не може да бъде отменено." - } - }, - "bn" : { - "stringUnit" : { - "state" : "translated", - "value" : "আপনি কি এই যন্ত্রে আপনার পুনরুদ্ধার পাসওয়ার্ড স্থায়ীভাবে গোপন করতে নিশ্চিত? এটি পূর্বাবস্থায় ফেরানো যাবে না।" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Esteu segur que voleu amagar permanentment la vostra contrasenya de recuperació en aquest dispositiu? Això no es pot desfer." + "value" : "Geri qaytarma parolunuzu bu cihazdan həmişəlik gizlətmək istədiyinizə əminsiniz?

Bunun geri dönüşü yoxdur." } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Opravdu chcete trvale skrýt heslo pro obnovení na tomto zařízení? Tuto akci nelze vrátit." - } - }, - "cy" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ydych chi'n siŵr eich bod am guddio eich cyfrinair adfer am byth ar y ddyfais hon? Ni ellir dadwneud hyn." - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Er du sikker på, at du permanent vil skjule din gendannelseskode på denne enhed? Dette kan ikke fortrydes." + "value" : "Opravdu chcete trvale skrýt heslo pro obnovení na tomto zařízení?

Tuto akci nelze vrátit." } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Bist du sicher, dass du das Wiederherstellungspasswort auf diesem Gerät dauerhaft ausblenden möchtest? Dies kann nicht mehr rückgängig gemacht werden." - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Είστε βέβαιοι ότι θέλετε να αποκρύψετε μόνιμα τον κωδικό σας ανάκτησης σε αυτήν τη συσκευή; Αυτό δεν μπορεί να αναιρεθεί." + "value" : "Bist du sicher, dass du dein Wiederherstellungspasswort auf diesem Gerät dauerhaft ausblenden möchtest?

Dies kann nicht mehr rückgängig gemacht werden." } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Are you sure you want to permanently hide your recovery password on this device? This cannot be undone." - } - }, - "eo" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ĉu vi certas, ke vi volas porĉiame kaŝi vian reakiraj pasvorton sur ĉi tiu aparato? Ĉi tio ne povas esti malfaro." - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "¿Está seguro que desea ocultar permanentemente su contraseña de recuperación en este dispositivo? Esto no se puede deshacer." - } - }, - "es-ES" : { - "stringUnit" : { - "state" : "translated", - "value" : "¿Está seguro que desea ocultar permanentemente su contraseña de recuperación en este dispositivo? Esto no se puede deshacer." - } - }, - "et" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kas olete kindel, et soovite oma taastamisparooli sellel seadmel jäädavalt peita? Seda ei saa tühistada." - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ziur zaude zure berreskurapen pasahitza gailu honetan betirako ezkutatu nahi duzula? Ezin da desegin." - } - }, - "fa" : { - "stringUnit" : { - "state" : "translated", - "value" : "آیا مطمئن هستید که می‌خواهید گذرواژه بازیابی خود را روی این دستگاه به صورت دائمی پنهان کنید؟ این کار قابل برگشت نیست." - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Haluatko varmasti piilottaa palautussalasanan pysyvästi tässä laitteessa? Tätä ei voi peruuttaa." - } - }, - "fil" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sigurado ka bang gusto mong permanenteng itago ang iyong recovery password sa device na ito? Hindi na ito mababawi." + "value" : "Are you sure you want to permanently hide your recovery password on this device?

This cannot be undone." } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Êtes-vous sûr de vouloir masquer définitivement votre mot de passe de récupération sur cet appareil ? Cela ne peut pas être annulé." - } - }, - "gl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tes a certeza de querer ocultar permanentemente o teu contrasinal de recuperación neste dispositivo? Isto non se pode desfacer." - } - }, - "ha" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ka tabbata kana so ka asirce kalmar dawowa dindindin a wannan na'ura? Wannan ba za a iya warwarewa ba." - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "האם אתה בטוח שברצונך להסתיר את הסיסמה לשחזור שלך לצמיתות במכשיר זה? זה לא ניתן לביטול." - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "क्या आप वाकई अपने रिकवरी पासवर्ड को इस डिवाइस पर स्थायी रूप से छिपाना चाहते हैं? इसे पूर्ववत नहीं किया जा सकता है।" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jeste li sigurni da želite trajno sakriti zaporku za oporavak na ovom uređaju? To se ne može poništiti." - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Biztos, hogy véglegesen el akarod rejteni a visszaállítási jelszavad ezen az eszközön? Ezt nem lehet visszafordítani." - } - }, - "hy-AM" : { - "stringUnit" : { - "state" : "translated", - "value" : "Վստա՞հ եք, որ ուզում եք մշտապես թաքցնել Ձեր վերականգնման գաղտնաբառը այս սարքի վրա: Սա անհնար է հետքը քայլել:" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "Apakah Anda yakin ingin menyembunyikan sandi pemulihan secara permanen di perangkat ini? Hal ini tidak dapat dibatalkan." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sei sicuro di voler nascondere permanentemente la tua password di recupero su questo dispositivo? Questa azione non può essere annullata." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "この端末でリカバリパスワードを永久に非表示にしてもよろしいですか? これは元に戻すことはできません。" - } - }, - "ka" : { - "stringUnit" : { - "state" : "translated", - "value" : "დარწმუნებული ხართ, რომ გსურთ აღდგენის პაროლის ამ მოწყობილობაზე სამუდამოდ დამალვა? ამის დაბრუნება შეუძლებელია." - } - }, - "km" : { - "stringUnit" : { - "state" : "translated", - "value" : "តើអ្នកប្រាកដទេថាចង់លាក់ពាក្យសម្ងាត់សង្គ្រោះរបស់អ្នកដោយស្នាក់នៅលើឧបករណ៍នេះជាអចិន្ត្រៃយ៍? វាមិនអាចត្រូវបានមិនធ្វើវិញបានទេ។" - } - }, - "kn" : { - "stringUnit" : { - "state" : "translated", - "value" : "ನೀವು ಈ ಸಾಧನದಲ್ಲಿ ನಿಮ್ಮ ಪುನಃ ಪಡೆಯುವ ರಹಸ್ಯ ಪದವನ್ನು ಶಾಶ್ವತವಾಗಿ ಮರೆಮಾಡಲು ಖಚಿತವಾಗಿದ್ದೀರಾ? ಇದನ್ನು ರದ್ದುಮಾಡಲಾಗುವುದಿಲ್ಲ." - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "정말 이 장치에서 복구 비밀번호를 영구적으로 숨기겠습니까? 이 작업은 되돌릴 수 없습니다." - } - }, - "ku" : { - "stringUnit" : { - "state" : "translated", - "value" : "دڵنیایت دەتەوێت تەنها نهێنی کلید وشەی گەڕاندنەوە لەسەر ئەم چەشەمەیە بسڕیتەوە؟ ئەمە بشێوی چارنەماوە." - } - }, - "ku-TR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tu piştrast î ku tu dixwazî şîfreya xwe ya rizgarkirinê li ser vê cîhazê bi daîmî veşêrî? Ev nayê vegerandin." - } - }, - "lg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Oli mukakafu nti oyagala okutereka ebisumuluzo by'okuddabiriza ku kidirisa kino emirembe gyonna? Kino tekijja kusoboka okujeemebwa." - } - }, - "lo" : { - "stringUnit" : { - "state" : "translated", - "value" : "ທ່ານຫມັ່ນໃຈບໍ່ວ່າທ່ານຈະເມືອນຊົ່ວຫມົດ Recovery password ຂອງທ່ານເລັວ? ການນັ້ນບໍ່ສາມາດຖືກຄືນໄດ້." - } - }, - "lt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ar tikrai norite visam laikui paslėpti savo atkūrimo slaptažodį šiame įrenginyje? To atšaukti negalima." - } - }, - "lv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vai esat pārliecināts, ka vēlaties pastāvīgi slēpt savu atkopšanas paroli šajā ierīcē? Tas nav atgriezenisks." - } - }, - "mk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Дали сте сигурни дека сакате трајно да ја сокриете вашата лозинка за обновување на овој уред? Ова не може да се поништи." - } - }, - "mn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Та энэхүү нууц үгийг энэ төхөөрөмжөөс нуухдаа итгэлтэй байна уу? Энэ үйлдлийг буцаах боломжгүй." - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "Adakah anda yakin anda mahu menyembunyikan kata laluan pemulihan anda secara kekal pada peranti ini? Ini tidak boleh diundurkan." - } - }, - "my" : { - "stringUnit" : { - "state" : "translated", - "value" : "ဤစက်ကိရိယာတွင် သင့် ပြန်လည်ရယူရေးစကားဝှက်ကို အပြီးဖျောက်လိုသည်မှာ သေချာပါသလား။ ၎င်းကို ပြန်ဆောင်ရွက်၍မရပါ။" + "value" : "Êtes-vous sûr de vouloir cacher définitivement votre mot de passe de récupération sur cet appareil ?

Cette action est irréversible." } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Er du sikker på at du vil permanent skjule ditt Recovery Password på denne enheten? Dette kan ikke angres." - } - }, - "nb-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Er du sikker på at du vil skjule gjenopprettingspassordet ditt permanent på denne enheten? Dette kan ikke angres." - } - }, - "ne-NP" : { - "stringUnit" : { - "state" : "translated", - "value" : "तपाईंले आफ्नो पुनःस्थापना पासवर्ड स्थायी रूपमा यो उपकरणमा लुकाउन निश्चित हुनुहुन्छ? यो पूर्ववत गर्न सकिदैन।" + "value" : "Er du sikker på at du har lyst til å permanent skjule gjenopprettingspassordet ditt?

Dette kan ikke angres." } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Weet u zeker dat u uw herstelwachtwoord permanent wilt verbergen op dit apparaat? Dit kan niet ongedaan gemaakt worden." - } - }, - "nn-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Er du sikker på at du ønskjer å skjule ditt gjenopprettingspassord for godt på denne eininga? Dette kan ikkje angre." - } - }, - "ny" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mukutsimikizika kuti mukufuna kubisitsa chinsinsi chanu chobwezeretsanso pa chipangizo ichi? Izi sizingathe kusinthidwa." - } - }, - "pa-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "ਕੀ ਤੁਸੀਂ ਯਕੀਨਨ ਆਪਣੇ ਮਨੁੱਖੀ ਕਰੋੜੀ ਸੰਕੇਤਾਂ ਨੂੰ ਇਸ ਜੰਤਰ 'ਤੇ ਅਸਥਾਈ ਤੌਰ ਤੇ ਛੁਪਾਉਣਾ ਚਾਹੁੰਦੇ ਹੋ? ਇਹ ਮੁੜ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਦਾ।" + "value" : "Weet u zeker dat u uw herstelwachtwoord permanent wilt verbergen op dit apparaat?

Dit kan niet ongedaan gemaakt worden." } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Czy na pewno chcesz trwale ukryć hasło odzyskiwania na tym urządzeniu? Nie można tego cofnąć." - } - }, - "ps" : { - "stringUnit" : { - "state" : "translated", - "value" : "آیا تاسو ډاډه یاست چې غواړئ خپل recovery password په دې وسیله کې دایمي پټ کړئ؟ دا نشي بیرته اخیستل کیدی." - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tem certeza de que deseja ocultar permanentemente sua senha de recuperação neste dispositivo? Isso não pode ser desfeito." - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tem a certeza de que deseja esconder permanentemente a sua chave de recuperação neste dispositivo? Isso não pode ser desfeito." - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ești sigur/ă că dorești ascunderea definitivă a parolei de recuperare de pe acest dispozitiv? Această acțiune nu poate fi anulată." + "value" : "Czy na pewno chcesz na stałe ukryć swoje hasło odzyskiwania na tym urządzeniu?

Nie można tego cofnąć." } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Вы уверены, что хотите навсегда скрыть ваш пароль восстановления на этом устройстве? Это действие не может быть отменено." - } - }, - "sh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jesi li siguran da želiš trajno sakriti svoju recovery password na ovom uređaju? Ovo se ne može poništiti." - } - }, - "si-LK" : { - "stringUnit" : { - "state" : "translated", - "value" : "ඔබේ ප්‍රතිසාධන මුරපදය මෙම උපාංගයෙන් ස්ථිරවම සඟවීමට අවශ්‍ය බව විශ්වාසද? මෙය හකුලා නොගත හැකි වේ." - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Naozaj chcete trvalo skryť frázu na obnovenie na tomto zariadení? Toto sa nedá vrátiť späť." - } - }, - "sl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ali res želite trajno skriti svoje obnovitveno geslo na tej napravi? Tega ni mogoče razveljaviti." - } - }, - "sq" : { - "stringUnit" : { - "state" : "translated", - "value" : "A jeni të sigurt që doni ta fshini përgjithmonë fjalëkalimin e rikuperimit në këtë pajisje? Kjo nuk mund të zhbëhet." - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Да ли сте сигурни да желите трајно да сакријете вашу Recovery Password на овом уређају? Ово не може бити поништено." - } - }, - "sr-Latn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Da li ste sigurni da želite da trajno sakrijete svoju Recovery password na ovom uređaju? Ovo ne može biti poništeno." + "value" : "Вы уверены, что хотите навсегда скрыть ваш пароль восстановления на этом устройстве?

Это действие не может быть отменено." } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Är du säker på att du vill permanent dölja ditt återställningslösenord på denna enhet? Detta kan inte ångras." - } - }, - "sw" : { - "stringUnit" : { - "state" : "translated", - "value" : "Je, una uhakika unataka kuficha recovery password yako kabisa kwenye kifaa hiki? Hii haiwezi kubatilishwa." - } - }, - "ta" : { - "stringUnit" : { - "state" : "translated", - "value" : "உங்கள் மீட்பு கடவுச்சொல்லை இந்த சாதனத்தில் நிரந்தரமாக மறைக்க விரும்புகிறீர்களா? இது ஆவணப்படுத்த முடியாது." - } - }, - "te" : { - "stringUnit" : { - "state" : "translated", - "value" : "మీరు మీ రికవరీ పాస్వర్డ్‌ను ఈ పరికరంలో శాశ్వతంగా దాచాలనుకుంటున్నారా? ఇది rückgängig చేయడం సాధ్యం కాదు." - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "คุณแน่ใจหรือไม่ว่าต้องการซ่อนไว้รหัสผ่านการกู้คืนบนอุปกรณ์นี้อย่างถาวร? ไม่สามารถย้อนกลับได้" + "value" : "Är du säker på att du vill dölja ditt återställningslösenord permanent på denna enhet?

Detta kan inte ångras." } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Bu cihazdaki kurtarma şifrenizi kalıcı olarak gizlemek istediğinizden emin misiniz? Bu geri alınamaz." + "value" : "Bu cihazda kurtarma şifrenizi kalıcı olarak gizlemek istediğinizden emin misiniz?

Bu işlem geri alınamaz." } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Ви впевнені, що хочете назавжди приховати пароль для відновлення на цьому пристрої? Це не можна буде скасувати." - } - }, - "ur-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "کیا آپ واقعی اپنے ریکوری پاس ورڈ کو اس ڈیوائس پر مستقل طور پر چھپانا چاہتے ہیں؟ یہ رد نہیں ہو سکے گا۔" - } - }, - "uz" : { - "stringUnit" : { - "state" : "translated", - "value" : "Haqiqatan ham tiklash parolingizni ushbu qurilmada doimiy tarzda yashirmoqchimisiz? Bu qaytarib bo'lmaydi." - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bạn có chắc chắc rằng bạn muốn ẩn mật khẩu khôi phục của bạn vĩnh viễn trên thiết bị này? Điều này không thể hồi phục." - } - }, - "xh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Uqinisekile ukuba ufuna ukufihla rhoqo iphasiwedi yakho yokubuyisela kule sixhobo? Oku akunakubuyiselwa." - } - }, - "zh-CN" : { - "stringUnit" : { - "state" : "translated", - "value" : "您确定要在此设备上永久隐藏您的恢复密码吗?该操作无法撤消。" - } - }, - "zh-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "您確定要在此裝置上永久隱藏您的恢復密碼嗎?此操作無法撤銷。" + "value" : "Ви впевнені, що хочете назавжди приховати пароль для відновлення на цьому пристрої?

Цю дію неможливо скасувати." } } } @@ -369915,478 +380326,141 @@ "recoveryPasswordView" : { "extractionState" : "manual", "localizations" : { - "af" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kyk Wagwoord" - } - }, - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "عرض كلمة المرور" - } - }, "az" : { "stringUnit" : { "state" : "translated", - "value" : "Parolu göstər" - } - }, - "bal" : { - "stringUnit" : { - "state" : "translated", - "value" : "پاس ورڈ دیکھیں" - } - }, - "be" : { - "stringUnit" : { - "state" : "translated", - "value" : "Паказаць пароль" - } - }, - "bg" : { - "stringUnit" : { - "state" : "translated", - "value" : "Преглед на паролата" - } - }, - "bn" : { - "stringUnit" : { - "state" : "translated", - "value" : "পাসওয়ার্ড দেখুন" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mostra la contrasenya" + "value" : "Geri qaytarma paroluna bax" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Zobrazit heslo" - } - }, - "cy" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gweld Cyfrinair" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vis adgangskode" + "value" : "Zobrazit heslo pro obnovení" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Passwort anzeigen" - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Προβολή Κωδικού" + "value" : "Wiederherstellungspasswort anzeigen" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "View Password" - } - }, - "eo" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vidi Pasvorton" - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ver contraseña" - } - }, - "es-ES" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ver contraseña" - } - }, - "et" : { - "stringUnit" : { - "state" : "translated", - "value" : "Näita parooli" - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ikusi pasahitza" - } - }, - "fa" : { - "stringUnit" : { - "state" : "translated", - "value" : "نمایش گذرواژه" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Näytä salasana" - } - }, - "fil" : { - "stringUnit" : { - "state" : "translated", - "value" : "View Password" + "value" : "View Recovery Password" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Afficher le mot de passe" + "value" : "Afficher le mot de passe de récupération" } }, - "gl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ver contrasinal" - } - }, - "ha" : { - "stringUnit" : { - "state" : "translated", - "value" : "Duba kalmar wucewa" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "הצג סיסמא" - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "पासवर्ड देखें" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pogledaj lozinku" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jelszó megtekintése" - } - }, - "hy-AM" : { - "stringUnit" : { - "state" : "translated", - "value" : "Դիտել Գաղտնաբառը" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lihat Kata Sandi" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Visualizza password" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "パスワードを見る" - } - }, - "ka" : { - "stringUnit" : { - "state" : "translated", - "value" : "პაროლის პარამეტრების ჩვენება" - } - }, - "km" : { - "stringUnit" : { - "state" : "translated", - "value" : "មើលពាក្យសម្ងាត់" - } - }, - "kn" : { + "nb" : { "stringUnit" : { "state" : "translated", - "value" : "ಹುಡುಕಿ ನೋಡಿ ಪಾಸ್ವರ್ಡ್" + "value" : "Vis Gjenopprettingspassord" } }, - "ko" : { + "nl" : { "stringUnit" : { "state" : "translated", - "value" : "비밀번호 보기" + "value" : "Bekijk Herstelwachtwoord" } }, - "ku" : { + "pl" : { "stringUnit" : { "state" : "translated", - "value" : "بینینی وشەی نهێنی" + "value" : "Pokaż hasło odzyskiwania" } }, - "ku-TR" : { + "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Vaziniya Password" + "value" : "Показать пароль восстановления" } }, - "lg" : { + "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Laba Password" + "value" : "Visa återställningslösenord" } }, - "lt" : { + "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Rodyti slaptažodį" + "value" : "Перегляд паролю для відновлення" } - }, - "lv" : { + } + } + }, + "recoveryPasswordVisibility" : { + "extractionState" : "manual", + "localizations" : { + "az" : { "stringUnit" : { "state" : "translated", - "value" : "Parādīt paroli" + "value" : "Geri qaytarma parolu görünməsi" } }, - "mk" : { + "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Преглед на лозинка" + "value" : "Viditelnost hesla pro obnovení" } }, - "mn" : { + "de" : { "stringUnit" : { "state" : "translated", - "value" : "Нууц үгийг харах" + "value" : "Sichtbarkeit des Wiederherstellungspassworts" } }, - "ms" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Lihat Kata Laluan" + "value" : "Recovery Password Visibility" } }, - "my" : { + "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Password ကြည့်ပါ" + "value" : "Visibilité du mot de passe de récupération" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Vis Password" - } - }, - "nb-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vis passord" - } - }, - "ne-NP" : { - "stringUnit" : { - "state" : "translated", - "value" : "पासवर्ड हेर्नुहोस्" + "value" : "Gjenopprettingspassord Synlighet" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Bekijk wachtwoord" - } - }, - "nn-NO" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vis passord" - } - }, - "ny" : { - "stringUnit" : { - "state" : "translated", - "value" : "Onani Chinsinsi" - } - }, - "pa-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "ਪਾਸਵਰਡ ਵੇਖੋ" + "value" : "Zichtbaarheid herstelwachtwoord" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Zobacz hasło" - } - }, - "ps" : { - "stringUnit" : { - "state" : "translated", - "value" : "پاسورډ وګورئ" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ver Senha" - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ver Palavra-passe" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vizualizați parola" + "value" : "Widoczność hasła odzyskiwania" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Посмотреть пароль" - } - }, - "sh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Prikaži lozinku" - } - }, - "si-LK" : { - "stringUnit" : { - "state" : "translated", - "value" : "මුරපදය පෙන්වන්න" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zobraziť heslo" - } - }, - "sl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ogled gesla" - } - }, - "sq" : { - "stringUnit" : { - "state" : "translated", - "value" : "Shihni Fjalëkalimin" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Погледај лозинку" - } - }, - "sr-Latn" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pregled lozinke" + "value" : "Видимость пароля восстановления" } }, "sv-SE" : { "stringUnit" : { "state" : "translated", - "value" : "Visa lösenord" - } - }, - "sw" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tazama Nywila" - } - }, - "ta" : { - "stringUnit" : { - "state" : "translated", - "value" : "கடவுச்சொல்லைக் காண்பிக்கவும்" - } - }, - "te" : { - "stringUnit" : { - "state" : "translated", - "value" : "పాస్‌వర్డ్ చూడండి" - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "ดูรหัสผ่าน." - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Şifreyi Görüntüle" + "value" : "Synlighet för återställningslösenord" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Показати пароль" - } - }, - "ur-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "پاس ورڈ دیکھیں" - } - }, - "uz" : { - "stringUnit" : { - "state" : "translated", - "value" : "Parolni ko‘rish" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Xem Mật khẩu" - } - }, - "xh" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jonga Iphasiwedi" - } - }, - "zh-CN" : { - "stringUnit" : { - "state" : "translated", - "value" : "查看密码" - } - }, - "zh-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "檢視密碼" + "value" : "Видимість пароля для відновлення" } } } @@ -370409,7 +380483,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Bu, sizin geri qaytarma parolunuzdur. Kiməsə göndərsəniz, hesabınıza tam müraciət edə bilər." + "value" : "Bu, sizin geri qaytarma parolunuzdur. Kiməsə göndərsəniz, hesabınıza tam erişə bilər." } }, "bal" : { @@ -370921,6 +380995,18 @@ "value" : "Rekrei grupon" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Volver a crear grupo" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Volver a crear grupo" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -370951,6 +381037,12 @@ "value" : "Ricrea gruppo" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "グループを再作成" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -370969,6 +381061,18 @@ "value" : "Odtwórz Grupę" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recriar grupo" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recreează grup" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -371010,6 +381114,12 @@ "state" : "translated", "value" : "重新创建群组" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "重新建立群組" + } } } }, @@ -371492,9 +381602,37 @@ } } }, + "refundPlanNonOriginatorApple" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Because you originally signed up for {app_pro} via a different {platform_account}, you'll need to use that {platform_account} to update your plan." + } + } + } + }, + "refundRequestOptions" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Two ways to request a refund:" + } + } + } + }, "remainingCharactersOverTooltip" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesajın uzunluğunu {count} qədər azalt" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -371507,29 +381645,143 @@ "value" : "Zkraťte délku zprávy o {count}" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nachricht um {count} Zeichen kürzen" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Reduce message length by {count}" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reduce la longitud del mensaje en {count}" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reduce la longitud del mensaje en {count}" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Réduis la longueur du message de {count}" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "संदेश की लंबाई {count} तक घटाएं" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Az üzenet hosszának csökkentése ennyivel: {count}" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Riduci la lunghezza del messaggio di {count}" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージをあと{count}字削減してください" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verkort berichtlengte met {count}" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skróć wiadomość o {count} znaków" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reduza o comprimento da mensagem em {count}" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Redu lungimea mesajului cu {count}" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Уменьшить длину на {count}" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Förkorta meddelandet med {count} tecken" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesaj uzunluğunu {count} karakter azaltın" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Скоротіть довжину повідомлення на {count}" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "将消息长度减少 {count} 个字符" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "請減少訊息長度 {count} 個字元" + } } } }, "remainingCharactersTooltip" : { "extractionState" : "manual", "localizations" : { + "az" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld xarakter qaldı" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld xarakter qaldı" + } + } + } + } + }, "ca" : { "variations" : { "plural" : { @@ -371578,6 +381830,42 @@ } } }, + "de" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld Zeichen verbleibt" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld Zeichen verbleiben" + } + } + } + } + }, + "el" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld χαρακτήρας απομένει" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld χαρακτήρες απομένουν" + } + } + } + } + }, "en" : { "variations" : { "plural" : { @@ -371596,6 +381884,96 @@ } } }, + "es-419" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld carácter restante" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld caracteres restantes" + } + } + } + } + }, + "es-ES" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld carácter restante" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld caracteres restantes" + } + } + } + } + }, + "et" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld tähemärk alles" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld tähemärki alles" + } + } + } + } + }, + "fr" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld caractères restants" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld caractères restants" + } + } + } + } + }, + "hi" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld वर्ण शेष" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld वर्ण शेष" + } + } + } + } + }, "hu" : { "variations" : { "plural" : { @@ -371614,6 +381992,24 @@ } } }, + "it" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld carattere rimanente" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld caratteri rimanenti" + } + } + } + } + }, "ja" : { "variations" : { "plural" : { @@ -371632,10 +382028,232 @@ "value" : "{count}자 입력 가능" } }, + "nb" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld tegn igjen" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld tegn igjen" + } + } + } + } + }, + "nl" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld teken resterend" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld tekens resterend" + } + } + } + } + }, + "pl" : { + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pozostały %lld znaki" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pozostało %lld znaków" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pozostał %lld znak" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pozostało %lld znaków" + } + } + } + } + }, + "pt-PT" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld caractere restante" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld caracteres restantes" + } + } + } + } + }, + "ro" : { + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld caractere rămase" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld caracter rămas" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld de caractere rămase" + } + } + } + } + }, + "ru" : { + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld символа осталось" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld символов осталось" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld символ остался" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld символов осталось" + } + } + } + } + }, + "sv-SE" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld tecken kvar" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld tecken kvar" + } + } + } + } + }, + "tr" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld karakter kaldı" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld karakter kaldı" + } + } + } + } + }, "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "{count} символів залишилось" + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld символів залишилось" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld символів залишилось" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld символ залишився" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld символів залишилось" + } + } + } + } + }, + "zh-CN" : { + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "还剩 %lld 个字符" + } + } + } + } + }, + "zh-TW" : { + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "剩餘 %lld 個字元" + } + } + } } } } @@ -372598,6 +383216,129 @@ } } }, + "removePasswordModalDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} üçün hazırkı parolunuzu silin. Daxili olaraq saxlanılmış verilər, cihazınızda saxlanılan təsadüfi yaradılmış açarla təkrar şifrələnəcək." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odstraňte své aktuální heslo pro {app_name}. Lokálně uložená data budou znovu zašifrována pomocí náhodně vygenerovaného klíče uloženého ve vašem zařízení." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Entferne dein aktuelles Passwort für {app_name}. Lokal gespeicherte Daten werden mit einem zufällig generierten Schlüssel, der auf deinem Gerät gespeichert wird, erneut verschlüsselt." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Remove your current password for {app_name}. Locally stored data will be re-encrypted with a randomly generated key, stored on your device." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Supprimez votre mot de passe actuel pour {app_name}. Les données stockées localement seront à nouveau chiffrées à l'aide d'une clé générée aléatoirement, stockée sur votre appareil." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verwijder je huidige wachtwoord voor {app_name}. Lokaal opgeslagen gegevens worden opnieuw versleuteld met een willekeurig gegenereerde sleutel, opgeslagen op je apparaat." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Usuń swoje obecne hasło dla {app_name}. Dane przechowywane lokalnie zostaną ponownie zaszyfrowane losowo wygenerowanym kluczem, przechowywanym na Twoim urządzeniu." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Удалите текущий пароль для {app_name}. Локально сохранённые данные будут повторно зашифрованы с использованием случайно сгенерированного ключа, хранящегося на вашем устройстве." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ta bort ditt nuvarande lösenord för {app_name}. Lokalt lagrad data kommer att krypteras om med en slumpmässigt genererad nyckel som lagras på din enhet." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Видаліть свій поточний пароль для {app_name}. Локально збережені дані буде повторно зашифровано випадково згенерованим ключем, який зберігатиметься на вашому пристрої." + } + } + } + }, + "renew" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yenilə" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Obnovit" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Renew" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Renouveler" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verlengen" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Поновити" + } + } + } + }, + "renewingPro" : { + "extractionState" : "manual", + "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Obnovení Pro" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Renewing Pro" + } + } + } + }, "reply" : { "extractionState" : "manual", "localizations" : { @@ -373077,6 +383818,70 @@ } } }, + "requestRefund" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geri ödəmə tələb et" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Požádat o vrácení platby" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rückerstattung anfordern" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Request Refund" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Demander un remboursement" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Terugbetaling aanvragen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zawnioskuj o zwrot" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Запит на повернення коштів" + } + } + } + }, + "requestRefundPlatformWebsite" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Request a refund on the {platform} website, using the {platform_account} you signed up for {pro} with." + } + } + } + }, "resend" : { "extractionState" : "manual", "localizations" : { @@ -375475,21 +386280,249 @@ "reviewLimit" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rəy limiti" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Límit de revisió" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Omezení hodnocení" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bewertungsgrenze" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Review Limit" } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Límite de reseñas" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Límite de reseñas" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Limite d’évaluations" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "समीक्षा सीमा" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Limite recensioni" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "レビュー制限" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Beoordelingslimiet" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Limit opinii" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Limite de avaliações" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Limită de recenzii" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ограничение на отзыв" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Betygsättningsgräns" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ліміт на відгуки" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "评价次数已达上限" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "評分次數上限" + } } } }, "reviewLimitDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deyəsən, təzəlikcə {app_name} üçün rəy bildirmisiniz, əks-əlaqəniz üçün təşəkkürlər!" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sembla que ja has revisat {app_name} recentment, gràcies pels teus comentaris!" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zdá se, že jste nedávno hodnotili {app_name}. Děkujeme vám za vaši zpětnou vazbu!" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Du hast {app_name} anscheinend kürzlich bewertet – danke für dein Feedback!" + } + }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "It looks like you've already reviewed
{app_name} recently, thanks for your
feedback!" + "value" : "It looks like you've already reviewed {app_name} recently, thanks for your feedback!" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Parece que ya has calificado {app_name} recientemente. ¡Gracias por tus comentarios!" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Parece que ya has calificado {app_name} recientemente. ¡Gracias por tus comentarios!" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Il semble que vous ayez déjà évalué {app_name} récemment. Merci pour vos retours !" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "ऐसा लगता है कि आपने हाल ही में {app_name} की समीक्षा पहले ही कर दी है, आपकी प्रतिक्रिया के लिए धन्यवाद!" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sembra che tu abbia già recensito {app_name} di recente, grazie per il tuo feedback!" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "最近すでに {app_name} を評価いただいているようです。フィードバックありがとうございます!" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Het lijkt erop dat je {app_name} onlangs al hebt beoordeeld, bedankt voor je feedback!" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wygląda na to, że ostatnio już oceniałeś {app_name}, dziękujemy za Twoją opinię!" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Parece que já avaliou recentemente o {app_name}, obrigado pelo seu feedback!" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se pare că ai evaluat deja recent {app_name}, îți mulțumim pentru feedback!" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Похоже, вы недавно уже оставляли отзыв о {app_name}, спасибо за ваш отзыв!" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Det verkar som att du redan betygsatt {app_name} nyligen – tack för din feedback!" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Здається, ви нещодавно вже залишали відгук про {app_name}. Дякуємо за ваш зворотний зв'язок!" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "您最近似乎已经评价过 {app_name},感谢您的反馈!" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "看起來您最近已經對 {app_name} 給予評價,感謝您的回饋!" } } } @@ -378901,7 +389934,7 @@ "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Požadovat upozornění, když kontakt pořídí snímek obrazovky v individuálním chatu." + "value" : "Požadovat upozornění, když kontakt pořídí snímek obrazovky chatu jeden na jednoho." } }, "cy" : { @@ -379326,6 +390359,76 @@ } } }, + "screenshotProtectionDescriptionDesktop" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu cihazda çəkilən ekran şəkillərində {app_name} pəncərəsini gizlət." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skrývat okno {app_name} na snímcích obrazovky pořízených na tomto zařízení." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Conceal the {app_name} window in screenshots taken on this device." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Masquer la fenêtre de {app_name} dans les captures d’écran prises sur cet appareil." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Приховувати вікно {app_name} на знімках екрана, зроблених на цьому пристрої." + } + } + } + }, + "screenshotProtectionDesktop" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ekran şəkli qoruması" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ochrana proti pořizování snímků obrazovky" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Screenshot Protection" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Protection contre les captures d’écran" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Захист від знімків екрана" + } + } + } + }, "screenshotTaken" : { "extractionState" : "manual", "localizations" : { @@ -386157,6 +397260,12 @@ "value" : "حدد أيقونة التطبيق" } }, + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tətbiq ikonunu seç" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -386169,6 +397278,12 @@ "value" : "Vybrat ikonu aplikace" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "App-Symbol auswählen" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -386181,12 +397296,30 @@ "value" : "Elektu piktogramon de aplikaĵo" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seleccionar icono de la aplicación" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seleccionar icono de la aplicación" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Sélectionner l'icône de l'application" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "ऐप आइकन चुनें" + } + }, "hu" : { "stringUnit" : { "state" : "translated", @@ -386199,6 +397332,18 @@ "value" : "Pilih ikon aplikasi" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seleziona icona dell'app" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アプリアイコンを選択" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -386217,11 +397362,53 @@ "value" : "Wybierz ikonę aplikacji" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Selecionar ícone da aplicação" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Selectează pictograma aplicației" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Выбрать иконку приложения" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Välj appikon" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uygulama ikonu seç" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Оберіть значок застосунку" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "选择应用图标" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "選擇應用程式圖示" + } } } }, @@ -387228,6 +398415,18 @@ "value" : "Sending Call Offer" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enviando oferta de llamada" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enviando oferta de llamada" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -387240,12 +398439,30 @@ "value" : "कॉल ऑफर भेजा जा रहा है" } }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hívás ajánlás küldése" + } + }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Mengirim Penawaran Panggilan" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Invio offerta di chiamata" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "通話オファーを送信中" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -387276,6 +398493,18 @@ "value" : "Wysyłanie oferty połączenia" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "A enviar oferta de chamada" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se trimite oferta de apel" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -387305,6 +398534,12 @@ "state" : "translated", "value" : "正在发送通话邀请" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在傳送通話邀請" + } } } }, @@ -387347,6 +398582,18 @@ "value" : "Sending Connection Candidates" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enviando candidatos de conexión" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enviando candidatos de conexión" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -387359,18 +398606,48 @@ "value" : "कनेक्शन उम्मीदवार भेजे जा रहे हैं" } }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kapcsolat jelöltek küldése" + } + }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Mengirim Kandidat Sambungan" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Invio candidati per la connessione" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "接続候補を送信中" + } + }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "연결 후보 전송 중" } }, + "ku" : { + "stringUnit" : { + "state" : "translated", + "value" : "ناردنی کاندیدەکانی پەیوەندی" + } + }, + "ku-TR" : { + "stringUnit" : { + "state" : "translated", + "value" : "ناردنی کاندیدەکانی پەیوەندی" + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -387383,6 +398660,18 @@ "value" : "Wysyłanie kandydatów do połączenia" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "A enviar candidatos de ligação" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se trimit candidații pentru conexiune" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -387412,6 +398701,18 @@ "state" : "translated", "value" : "Đang nhận thông tin các kết nối khả dĩ" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "发送连接候选人" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在傳送連線候選項目" + } } } }, @@ -388397,7 +399698,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Dataları təmizlə" + "value" : "Veriləri təmizlə" } }, "bal" : { @@ -390789,6 +402090,12 @@ "sessionNetworkCurrentPrice" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hazırkı {token_name_short} qiyməti" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -390801,6 +402108,12 @@ "value" : "Aktuální cena {token_name_short}" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktueller {token_name_short}-Preis" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -390813,12 +402126,30 @@ "value" : "Nuna prezo de {token_name_short}" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Precio actual de {token_name_short}" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Precio actual de {token_name_short}" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Prix actuel du {token_name_short}" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "वर्तमान {token_name_short} मूल्य" + } + }, "hu" : { "stringUnit" : { "state" : "translated", @@ -390831,6 +402162,18 @@ "value" : "Harga {token_name_short} saat ini" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Prezzo attuale di {token_name_short}" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "現在の {token_name_short} 価格" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -390849,11 +402192,53 @@ "value" : "Aktualna {token_name_short} cena" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Preço atual de {token_name_short}" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Preț curent {token_name_short}" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Текущая цена {token_name_short}" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nuvarande {token_name_short}-pris" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Güncel {token_name_short} fiyatı" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Поточна ціна {token_name_short}" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "当前 {token_name_short} 价格" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "目前 {token_name_short} 價格" + } } } }, @@ -390866,6 +402251,12 @@ "value" : "يتم إرسال الرسائل باستخدام {network_name}. تتكون الشبكة من عقد محفزة مع {token_name_long}، الذي يحافظ على {app_name} لامركزي وآمن. اعرف المزيد {icon}" } }, + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesajlar {network_name} üzərindən göndərilir. Şəbəkə, {app_name} tətbiqini mərkəzsiz və təhlükəsiz saxlamaq üçün {token_name_long} ilə təşviq olunan node-lardan ibarətdir. Ətraflı öyrən {icon}" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -390878,24 +402269,60 @@ "value" : "Zprávy se odesílají pomocí {network_name}. Síť se skládá ze serverů, jejichž provozovatelé jsou odměňováni pomocí {token_name_long}, což zajišťuje decentralizaci a bezpečnost {app_name}. Další informace {icon}" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nachrichten werden über das {network_name} gesendet. Das Netzwerk besteht aus Knoten, die mit {token_name_long} incentiviert werden, wodurch {app_name} dezentral und sicher bleibt. Mehr erfahren {icon}" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Messages are sent using the {network_name}. The network is comprised of nodes incentivized with {token_name_long}, which keeps {app_name} decentralized and secure. Learn More {icon}" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los mensajes se envían utilizando {network_name}. La red está compuesta por nodos incentivados con {token_name_long}, lo que mantiene a {app_name} descentralizado y seguro. Más información {icon}" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los mensajes se envían utilizando {network_name}. La red está compuesta por nodos incentivados con {token_name_long}, lo que mantiene a {app_name} descentralizado y seguro. Más información {icon}" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Les messages sont envoyés en utilisant l'{network_name}. Le réseau est composé de noeuds stimulés par {token_name_long}, qui permet à {app_name} d'être décentralisée et sécurisée. En savoir plus {icon}" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "संदेश {network_name} का उपयोग करके भेजे जाते हैं। यह नेटवर्क {token_name_long} से प्रेरित नोड्स से बना है, जो {app_name} को विकेंद्रीकृत और सुरक्षित बनाए रखता है। और जानें {icon}" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Az üzenetek küldése a(z) {network_name} hálózaton keresztül történik. A hálózat a(z) {token_name_long} tokennel ösztönzött csomópontokat tartalmaz, amelyek decentralizálttá és biztonságossá teszik a(z) {app_name} alkalmazást.Tudjon meg többet {icon}" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "I messaggi vengono inviati utilizzando la {network_name}. La rete è composta da nodi incentivati con {token_name_long}, che mantengono {app_name} decentralizzata e sicura. Scopri di più {icon}" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メッセージは {network_name} を使用して送信されます。このネットワークは {token_name_long} によってインセンティブを受けたノードで構成されており、{app_name} の分散性と安全性を保っています。詳しくはこちら {icon}" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -390914,17 +402341,65 @@ "value" : "Wiadomości są wysyłane za pośrednictwem sieci {network_name}. Sieć składa się z węzłów, które są motywowane tokenami {token_name_long}, co zapewnia, że {app_name} pozostaje zdecentralizowana i bezpieczna. Dowiedz się więcej {icon}" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "As mensagens são enviadas utilizando a {network_name}. A rede é composta por nós incentivados com {token_name_long}, o que mantém o {app_name} descentralizado e seguro. Saiba mais {icon}" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesajele sunt trimise folosind {network_name}. Rețeaua este alcătuită din noduri stimulate cu {token_name_long}, ceea ce menține {app_name} descentralizat și sigur. Informații suplimentare {icon}" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сообщения отправляются через {network_name}. Сеть состоит из узлов, стимулируемых {token_name_long}, что обеспечивает децентрализацию и безопасность {app_name}. Узнать больше {icon}" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meddelanden skickas via {network_name}. Nätverket består av noder som är incitamenterade med {token_name_long}, vilket håller {app_name} decentraliserad och säker. Läs mer {icon}" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesajlar, {network_name} kullanılarak gönderilir. Ağ, {token_name_long} ile teşvik edilen düğümlerden oluşur; bu da {app_name} uygulamasını merkeziyetsiz ve güvenli tutar. Daha Fazla Bilgi {icon}" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Повідомлення надсилаються за допомогою {network_name}. Мережа складається з вузлів, заохочуваних {token_name_long}, що забезпечує децентралізацію та безпеку {app_name}. Дізнатися більше {icon}" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "消息通过 {network_name} 发送。该网络由通过 {token_name_long} 激励的节点组成,使 {app_name} 实现去中心化并保持安全。了解更多 {icon}" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "訊息是透過 {network_name} 傳送。該網路由透過 {token_name_long} 提供激勵的節點所組成,這確保了 {app_name} 的去中心化與安全性。了解更多 {icon}" + } } } }, "sessionNetworkLearnAboutStaking" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Staking barədə ətraflı öyrən" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -390937,24 +402412,60 @@ "value" : "Přečtěte si o stakingu" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mehr über Staking erfahren" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Learn About Staking" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aprender sobre staking" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aprender sobre staking" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "En savoir plus sur Staking" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "स्टेकिंग के बारे में जानें" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Ismerje meg a lekötést" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Scopri lo staking" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ステーキングについて学ぶ" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -390973,17 +402484,65 @@ "value" : "Dowiedz się więcej o stakingu" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Saiba mais sobre Staking" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Află mai multe despre staking" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подробней о стейкинге" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lär dig mer om staking" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Staking Hakkında Bilgi Edinin" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Дізнайтеся про стейкінг" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "了解 Staking" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "了解質押" + } } } }, "sessionNetworkMarketCap" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bazar dəyəri" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -390996,24 +402555,60 @@ "value" : "Tržní kapitalizace" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Marktkapitalisierung" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Market Cap" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Capitalización de mercado" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Capitalización de mercado" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Capitalisation du marché" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "बाज़ार पूंजीकरण" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Piaci sapka" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Capitalizzazione di mercato" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "時価総額" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -391032,17 +402627,65 @@ "value" : "Kapitalizacja" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Capitalização de Mercado" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Capitalizare de piață" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Рыночная капитализация" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Marknadsvärde" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Piyasa Değeri" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Ринкова капіталізація" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "市值" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "市值" + } } } }, "sessionNetworkNodesSecuring" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} node-ları mesajlarınızın təhlükəsizliyini təmin edir" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -391055,24 +402698,60 @@ "value" : "{app_name} servery zabezpečující vaše zprávy" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name}-Knoten sichern deine Nachrichten" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "{app_name} Nodes securing your messages" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nodos de {app_name} protegiendo tus mensajes" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nodos de {app_name} protegiendo tus mensajes" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "{app_name} Nodes sécurisant vos messages" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "आपके संदेशों की सुरक्षा कर रहे {app_name} नोड्स" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "{app_name} csomópontok védik az Ön üzeneteit" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nodi {app_name} che proteggono i tuoi messaggi" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "自分のメッセージを保護する {app_name} ノード" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -391091,17 +402770,65 @@ "value" : "{app_name} Węzły zabezpieczające twoje wiadomości" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nós do {app_name} a proteger as suas mensagens" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Noduri {app_name} care securizează mesajele tale" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Узлы {app_name} защищающие ваши сообщения" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name}-noder som skyddar dina meddelanden" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mesajlarınızı güvenceye alan {app_name} düğümler" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "{app_name} Вузли, що захищають ваші повідомлення" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "保护你的消息的 {app_name} 节点" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} 節點保護著 您的訊息" + } } } }, "sessionNetworkNodesSwarm" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Swarm-ınızdakı {app_name} node-ları" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -391114,24 +402841,60 @@ "value" : "{app_name} servery ve vašem swarmu" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name}-Knoten in deinem Cluster" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "{app_name} Nodes in your swarm" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nodos de {app_name} en tu swarm" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nodos de {app_name} en tu swarm" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "{app_name} Nodes dans votre essaim" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "आपके स्वार्म में {app_name} नोड्स" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "{app_name} csomópontok vannak a saját bolyban" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nodi {app_name} nel tuo swarm" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "自分のスウォーム内の {app_name} ノード" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -391150,17 +402913,65 @@ "value" : "{app_name} Węzły w twoim roju" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nós do {app_name} no seu enxame" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Noduri {app_name} din roiul tău" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Узлов {app_name} в вашем рою" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name}-noder i din svärm" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Swarmınızdaki {app_name} Nodelar" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "{app_name} Вузли у вашому рої" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "你的 Swarm 中的 {app_name} 节点" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "您的群集 中的 {app_name} 節點" + } } } }, "sessionNetworkNotificationLive" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{token_name_long} artıq aktivdir! Ayarlarda yeni {network_name} bölməsini kəşf edin və {token_name_long} necə Session-u gücləndirdiyini öyrənin." + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -391173,6 +402984,12 @@ "value" : "{token_name_long} funguje! Prozkoumejte novou sekci {network_name} v Nastavení a zjistěte, jak {token_name_long} zabezpečuje funkčnost Session." } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "{token_name_long} ist live! Entdecke den neuen Bereich {network_name} in den Einstellungen und erfahre, wie {token_name_long} Session antreibt." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -391185,18 +403002,48 @@ "value" : "{token_name_long} estas viva! Esploru la novan sekcion {network_name} en Agordoj por lerni kiel {token_name_long} funkciigas Session." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "¡{token_name_long} está activo! Explora la nueva sección {network_name} en Configuración para saber cómo {token_name_long} impulsa Session." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "¡{token_name_long} está activo! Explora la nueva sección {network_name} en Configuración para saber cómo {token_name_long} impulsa Session." + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Le {token_name_long} est en ligne ! Explorez la nouvelle section {network_name} dans les paramètres pour apprendre comment le {token_name_long} alimente Session." } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "{token_name_long} अब लाइव है! यह जानने के लिए सेटिंग्स में नए {network_name} अनुभाग का अन्वेषण करें कि {token_name_long} कैसे Session को शक्ति देता है।" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "{token_name_long} aktív! Fedezze fel az új {network_name} menüpontot a beállításokban, hogy megtudja, hogyan működteti a(z) {token_name_long} a Sessiont." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "{token_name_long} è attivo! Esplora la nuova sezione {network_name} in Impostazioni per scoprire come {token_name_long} alimenta Session." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "{token_name_long} が始動しました!設定内の新しい {network_name} セクションを確認して、{token_name_long} がどのように Session を支えているかを学びましょう。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -391215,17 +403062,65 @@ "value" : "{token_name_long} jest już dostępny! Zapoznaj się z nową sekcją {network_name} w Ustawieniach, aby dowiedzieć się, jak {token_name_long} zasila Session." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "{token_name_long} está ao vivo! Explore a nova seção {network_name} em Definições para saber como {token_name_long} impulsiona o Session." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "{token_name_long} este activ! Explorează noua secțiune {network_name} din Setări pentru a afla cum {token_name_long} alimentează Session." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "{token_name_long} уже доступен! Изучите новый раздел {network_name} в настройках, чтобы узнать, как {token_name_long} обеспечивает работу Session." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "{token_name_long} är live! Utforska det nya avsnittet {network_name} i Inställningar för att lära dig hur {token_name_long} driver Session." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "{token_name_long} yayında! {token_name_long} tokeninin Session'a nasıl güç verdiğini öğrenmek için Ayarlar'daki yeni {network_name} bölümünü keşfedin." + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "{token_name_long} запущено! Ознайомтеся з новим розділом {network_name} у Налаштуваннях, щоб дізнатися, як {token_name_long} забезпечує роботу Session." } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "{token_name_long} 已上线!在设置中探索新的 {network_name} 部分,了解 {token_name_long} 如何为 Session 提供支持。" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "{token_name_long} 現已上線!前往「設定」中的 {network_name} 區段了解 {token_name_long} 如何為 Session 提供支援。" + } } } }, "sessionNetworkSecuredBy" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Şəbəkənin təhlükəsizliyini təmin edən" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -391238,6 +403133,12 @@ "value" : "Síť zabezpečuje" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Netzwerk gesichert durch" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -391250,12 +403151,30 @@ "value" : "Reto sekurigita de" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Red protegida por" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Red protegida por" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Réseau sécurisé par" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "सुरक्षित नेटवर्क" + } + }, "hu" : { "stringUnit" : { "state" : "translated", @@ -391268,6 +403187,18 @@ "value" : "Jaringan aman oleh" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rete protetta da" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ネットワークのセキュリティ提供元" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -391286,17 +403217,65 @@ "value" : "Sieć zabezpieczona przez" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rede protegida por" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rețea securizată de" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сеть защищена" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nätverket skyddas av" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ağın güvenliğini sağlayan" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Мережа захищена" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "网络由以下保障" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "網路保護方式" + } } } }, "sessionNetworkTokenDescription" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Şəbəkənin təhlükəsizliyini təmin etmək üçün {token_name_long} stake etdiyiniz zaman, {staking_reward_pool} fondundan {token_name_short} ilə mükafatlandırılırsınız." + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -391309,24 +403288,60 @@ "value" : "{token_name_long} staking znamená zvyšovat bezpečnost sítě a získávat odměny v podobě {token_name_short} ze {staking_reward_pool}." } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wenn du {token_name_long} stakest, um das Netzwerk zu sichern, erhältst du Belohnungen in {token_name_short} aus dem {staking_reward_pool}." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "When you stake {token_name_long} to secure the network, you earn rewards in {token_name_short} from the {staking_reward_pool}." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cuando haces stake de {token_name_long} para proteger la red, ganas recompensas en {token_name_short} desde el fondo {staking_reward_pool}." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cuando haces stake de {token_name_long} para proteger la red, ganas recompensas en {token_name_short} desde el fondo {staking_reward_pool}." + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Lorsque vous stakez du {token_name_long} pour sécuriser le réseau, vous gagnez des récompenses en {token_name_short} de l’ {staking_reward_pool}." } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "जब आप नेटवर्क को सुरक्षित करने के लिए {token_name_long} स्टेक करते हैं, तो आपको {staking_reward_pool} से {token_name_short} में पुरस्कार मिलते हैं।" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Amikor leköti a(z) {token_name_long} tokent, hogy biztosítsa a hálózatot, akkor {token_name_short} tokenben fog jutalmat kapni a(z) {staking_reward_pool} gyűjtőből." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quando esegui lo staking di {token_name_long} per proteggere la rete, ricevi premi in {token_name_short} dal {staking_reward_pool}." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ネットワークの安全性を確保するために {token_name_long} をステーキングすると、{staking_reward_pool} から {token_name_short} の報酬を獲得できます。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -391345,17 +403360,65 @@ "value" : "Stawiając {token_name_long} w celu zabezpieczenia sieci, otrzymujesz nagrody w {token_name_short} z puli {staking_reward_pool}." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quando deposita {token_name_long} para proteger a rede, ganha recompensas em {token_name_short} do {staking_reward_pool}." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Când stake-uiți {token_name_long} pentru a securiza rețeaua, primiți recompense în {token_name_short} din {staking_reward_pool}." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Когда вы стейкаете {token_name_long} для защиты сети, вы получаете вознаграждение в {token_name_short} из {staking_reward_pool}." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "När du stakar {token_name_long} för att säkra nätverket, tjänar du belöningar i {token_name_short} från {staking_reward_pool}." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ağı güvence altına almak için {token_name_long} stake ettiğinizde, {staking_reward_pool} havuzundan {token_name_short} cinsinden ödüller kazanırsınız." + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Коли ви робите стейкінг {token_name_long}, щоб захистити мережу, ви заробляєте винагороди у {token_name_short} з {staking_reward_pool}." } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "当你质押 {token_name_long} 以保护网络时,你将从 {staking_reward_pool} 中获得以 {token_name_short} 计的奖励。" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "當您質押 {token_name_long} 以保護網路時,您會從 {staking_reward_pool} 獲得 {token_name_short} 獎勵。" + } } } }, "sessionNew" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : " Yeni" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -391368,6 +403431,12 @@ "value" : " Nové" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : " Neu" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -391380,18 +403449,48 @@ "value" : " Nova" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : " Nuevo" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : " Nuevo" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : " Nouveau" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : " नया" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : " Új" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : " Nuovo" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : " 新規" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -391409,6 +403508,54 @@ "state" : "translated", "value" : " Nowy" } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : " Novo" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : " Nou" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : " Новая" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : " Ny" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : " Yeni" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : " Нове" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : " 全新" + } } } }, @@ -392849,6 +404996,59 @@ } } }, + "sessionProBeta" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro} Beta" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro} Beta" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro} Beta" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro} Beta" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro} Bêta" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro} Bèta" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Beta {app_pro}" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_pro} бета" + } + } + } + }, "sessionRecoveryPassword" : { "extractionState" : "manual", "localizations" : { @@ -394313,6 +406513,12 @@ "value" : "Vælg billede til fællesskabet" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Community-Anzeigebild festlegen" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -394325,12 +406531,30 @@ "value" : "Agordi bildon de la komunumo" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Establecer imagen de perfil de la Comunidad" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Establecer imagen de perfil de la Comunidad" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Définir la photo de la communauté" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Community की डिस्प्ले तस्वीर सेट करें" + } + }, "hu" : { "stringUnit" : { "state" : "translated", @@ -394343,6 +406567,18 @@ "value" : "Atur Tampilan Gambar Komunitas" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Imposta immagine della Community" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "コミュニティのプロフィール写真を設定" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -394361,6 +406597,36 @@ "value" : "Ustaw zdjęcie profilowe grupy" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Definir imagem de exibição da Comunidade" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Setează imaginea afișată de Comunitate" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Установить картинку для сообщества" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ange Community-visningsbild" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Topluluk Görünen Resmini Ayarla" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -394372,6 +406638,112 @@ "state" : "translated", "value" : "设置社群头像" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "設定社群顯示圖片" + } + } + } + }, + "setPasswordModalDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} üçün bir parol təyin edin. Daxili olaraq saxlanılmış verilər, bu parolla şifrələnəcək. {app_name} tətbiqini hər başlatdıqda, sizdən bu parolu daxil etməyiniz istənəcək." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nastavte heslo pro {app_name}. Lokálně uložená data budou šifrována tímto heslem. Při každém spuštění {app_name} budete vyzváni k zadání tohoto hesla." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Setze ein Passwort für {app_name}. Lokal gespeicherte Dateien werden mit diesem Passwort verschlüsselt. Du wirst jedes Mal nach diesem Passwort gefragt, wenn du {app_name} startest." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Set a password for {app_name}. Locally stored data will be encrypted with this password. You will be asked to enter this password each time {app_name} starts." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Définissez un mot de passe pour {app_name}. Les données stockées localement seront chiffrées avec ce mot de passe. Il vous sera demandé de saisir ce mot de passe chaque fois que {app_name} sera lancé." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Stel een wachtwoord in voor {app_name}. Lokaal opgeslagen gegevens worden versleuteld met dit wachtwoord. Je wordt gevraagd dit wachtwoord in te voeren telkens wanneer {app_name} wordt gestart." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ustaw hasło dla {app_name}. Dane przechowywane lokalnie będą zaszyfrowane tym hasłem. Będziesz musiał je podać za każdym razem, kiedy uruchamiasz {app_name}." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Установите пароль для {app_name}. Локально сохранённые данные будут зашифрованы с использованием этого пароля. При каждом запуске {app_name} вам потребуется вводить этот пароль." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ställ in ett lösenord för {app_name}. Lokalt lagrade data kommer att krypteras med detta lösenord. Du kommer att bli ombedd att ange detta lösenord varje gång {app_name} startas." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Встановіть пароль для {app_name}. Дані, збережені локально, буде зашифровано цим паролем. Цей пароль запитуватиметься при кожному запуску {app_name}." + } + } + } + }, + "settingsCannotChangeDesktop" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ayar güncəllənə bilmir" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nelze aktualizovat volbu" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cannot Update Setting" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Impossible de mettre à jour les paramètres" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Неможливо оновити налаштування" + } } } }, @@ -394854,6 +407226,520 @@ } } }, + "settingsScreenSecurityDesktop" : { + "extractionState" : "manual", + "localizations" : { + "af" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skermveiligheid" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "أمان الشاشة" + } + }, + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ekran güvənliyi" + } + }, + "bal" : { + "stringUnit" : { + "state" : "translated", + "value" : "سکرین سیکورٹی" + } + }, + "be" : { + "stringUnit" : { + "state" : "translated", + "value" : "Бяспека экрану" + } + }, + "bg" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сигурност на екрана" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "স্ক্রীন সিকিউরিটি" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seguretat de pantalla" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zabezpečení obrazovky" + } + }, + "cy" : { + "stringUnit" : { + "state" : "translated", + "value" : "Diogelu'r sgrin" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skærmsikkerhed" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bildschirmschutz" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ασφάλεια Οθόνης" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Screen Security" + } + }, + "eo" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ekrana sekurigo" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seguridad de pantalla" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seguridad de pantalla" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ekraani turvalisus" + } + }, + "eu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pantailaren Segurtasuna" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "امنیت صفحه نمایش" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Näytön suojaus" + } + }, + "fil" : { + "stringUnit" : { + "state" : "translated", + "value" : "Screen Security" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sécurité d'écran" + } + }, + "gl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seguranza da pantalla" + } + }, + "ha" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tsaron Allo" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "אבטחת מסך" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "स्क्रीन सुरक्षा" + } + }, + "hr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sigurnost zaslona" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Képernyőbiztonság" + } + }, + "hy-AM" : { + "stringUnit" : { + "state" : "translated", + "value" : "Էկրանի անվտանգություն" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keamanan Layar" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sicurezza Schermo" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "スクリーンセキュリティ" + } + }, + "ka" : { + "stringUnit" : { + "state" : "translated", + "value" : "ეკრანის დაცვა" + } + }, + "km" : { + "stringUnit" : { + "state" : "translated", + "value" : "សុវត្ថិភាពអេក្រង់" + } + }, + "kn" : { + "stringUnit" : { + "state" : "translated", + "value" : "ಪರದೆಯ ಭದ್ರತೆ" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "화면 보안" + } + }, + "ku" : { + "stringUnit" : { + "state" : "translated", + "value" : "پاراستنی پردە" + } + }, + "ku-TR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Parastina Ekranê" + } + }, + "lg" : { + "stringUnit" : { + "state" : "translated", + "value" : "Obukuumi bwa ekikola ekiriko akabonero" + } + }, + "lt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ekrano saugumas" + } + }, + "lv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ekrāna drošība" + } + }, + "mk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Екранска Безбедност" + } + }, + "mn" : { + "stringUnit" : { + "state" : "translated", + "value" : "Дэлгэцийн аюулгүй байдал" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keselamatan Skrin" + } + }, + "my" : { + "stringUnit" : { + "state" : "translated", + "value" : "မျက်နှာပြင် လုံခြုံရေး" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skjermsikkerhet" + } + }, + "nb-NO" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skjermsikkerhet" + } + }, + "ne-NP" : { + "stringUnit" : { + "state" : "translated", + "value" : "स्क्रीन सुरक्षा" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Scherm beveiliging" + } + }, + "nn-NO" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skjermtryggleik" + } + }, + "ny" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rikuripa pakallayachina" + } + }, + "pa-IN" : { + "stringUnit" : { + "state" : "translated", + "value" : "ਸਕ੍ਰੀਨ ਸੁਰੱਖਿਆ" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ochrona ekranu" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "د سکرین امنیت" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Segurança de Tela" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Segurança de ecrã" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Securitate ecran" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Защита экрана" + } + }, + "sh" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sigurnost ekrana" + } + }, + "si-LK" : { + "stringUnit" : { + "state" : "translated", + "value" : "තිර ආරක්ෂාව" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zabezpečenie obrazovky" + } + }, + "sl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Varnost zaslona" + } + }, + "sq" : { + "stringUnit" : { + "state" : "translated", + "value" : "Siguri ekrani" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Безбедност екрана" + } + }, + "sr-Latn" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bezbednost ekrana" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skärmsäkerhet" + } + }, + "sw" : { + "stringUnit" : { + "state" : "translated", + "value" : "Usalama wa Skrini" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "திரை பாதுகாப்பு" + } + }, + "te" : { + "stringUnit" : { + "state" : "translated", + "value" : "స్క్రీన్ భద్రత" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "ความปลอดภัยหน้าจอ" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ekran Güvenliği" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Безпека перегляду" + } + }, + "ur-IN" : { + "stringUnit" : { + "state" : "translated", + "value" : "سکرین سیکیورٹی" + } + }, + "uz" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ekran xavfsizligi" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "An ninh màn hình" + } + }, + "xh" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ukhuseleko lweSikrini" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "屏幕安全性" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "螢幕安全性" + } + } + } + }, + "settingsStartCategoryDesktop" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Açılış" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Spuštění" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Startup" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Démarrage" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Автозапуск" + } + } + } + }, "share" : { "extractionState" : "manual", "localizations" : { @@ -396303,7 +409189,7 @@ "az" : { "stringUnit" : { "state" : "translated", - "value" : "Databazanı açarkən bir problem baş verdi. Lütfən, tətbiqi yenidən başladıb bir daha sınayın." + "value" : "Veri bazasını açarkən bir problem baş verdi. Lütfən, tətbiqi yenidən başladıb bir daha sınayın." } }, "bal" : { @@ -396875,6 +409761,18 @@ "value" : "Ups! Wygląda na to, że nie masz konta {app_name}.

Będziesz musiał stworzyć konto w aplikacji {app_name}, zanim będziesz mógł to udostępnić." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ops! Parece que ainda não tem uma conta {app_name}.

Será necessário criar uma na aplicação {app_name} antes de poder partilhar." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ups! Se pare că nu ai încă un cont {app_name}.

Va trebui să creezi unul în aplicația {app_name} înainte de a putea partaja." + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -396910,6 +409808,12 @@ "state" : "translated", "value" : "哎呀!您似乎还没有{app_name}帐户。

您需要先在{app_name}应用中创建一个帐户,然后才能分享。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "哎呀!您似乎尚未擁有 {app_name} 帳號。

您需要先在 {app_name} 應用程式中建立帳號才能進行分享。" + } } } }, @@ -398850,6 +411754,12 @@ "value" : "Egen note" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "»Notiz an mich« anzeigen" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -398862,12 +411772,30 @@ "value" : "Montri noton al mi mem" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mostrar Nota Personal" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mostrar Nota Personal" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Afficher la note pour soi-même" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "अपने लिए नोट दिखाएं" + } + }, "hu" : { "stringUnit" : { "state" : "translated", @@ -398880,6 +411808,18 @@ "value" : "Lihat Catatan Pribadi" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mostra note personali" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "自分用メモを表示" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -398898,11 +411838,53 @@ "value" : "Pokaż moje notatki" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mostrar Nota Pessoal" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Afișează Notă personală" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Показать Заметки для Себя" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Visa Notera till mig själv" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kendime Notu Göster" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Показати нотатку для себе" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "显示备忘录" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "顯示小筆記" + } } } }, @@ -398933,24 +411915,60 @@ "value" : "Er du sikker på, at du vil vise Egen note i din samtaleliste?" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Möchtest du Notiz an mich wirklich in deiner Unterhaltungsliste anzeigen?" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Are you sure you want to show Note to Self in your conversation list?" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Estás seguro de que deseas mostrar Nota Personal en tu lista de conversaciones?" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Estás seguro de que deseas mostrar Nota Personal en tu lista de conversaciones?" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Êtes-vous sûr de vouloir afficher Note pour soi-même dans votre liste de conversations ?" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "क्या आप वाकई अपनी वार्तालाप सूची में अपने लिए नोट दिखाना चाहते हैं?" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Biztosan meg akarja jeleníteni a Jegyzet magamnak jegyzetet a beszélgetési listában?" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sei sicuro di voler mostrare Note to Self nella tua lista di conversazioni?" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "自分用メモを会話リストに表示しますか?" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -398969,11 +411987,118 @@ "value" : "Czy na pewno chcesz wyświetlać Moje notatki na liście konwersacji?" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tem a certeza de que pretende mostrar a Nota Pessoal na sua lista de conversas?" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ești sigur/ă că dorești să afișezi Notă personală în lista de conversații?" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вы уверены, что хотите отобразить Заметку для себя в списке бесед?" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Är du säker på att du vill visa Notera till mig själv i din konversationslista?" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kendime Not'u sohbet listenizde göstermek istediğinizden emin misiniz?" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Ви впевнені, що хочете показувати Нотатку для себе у вашому списку розмов?" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "你确定要在对话列表中显示 Note to Self吗?" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "您確定要在對話列表中顯示 小筆記 嗎?" + } + } + } + }, + "spellChecker" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yazı yoxlanışı" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kontrola pravopisu" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rechtschreibprüfung" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Spell Checker" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Correcteur d'orthographe" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Spellingcontrole" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sprawdzanie pisowni" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Проверка орфографии" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Stavningskontroll" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Перевірка орфографії" + } } } }, @@ -399456,6 +412581,142 @@ } } }, + "strength" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gücü" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Síla" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Passwortstärke" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Strength" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Solidité" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sterkte" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Siła" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Puternic" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Надёжность" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Styrka" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Надійність" + } + } + } + }, + "supportDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Problemlə üzləşmisiniz? Kömək məqalələrini oxuyun, ya da {app_name} Dəstək ilə bir sorğu açın." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Máte problémy? Projděte si články nápovědy nebo kontaktujte podporu {app_name}." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Probleme? Erkunde die Hilfeartikel oder öffne ein Ticket bei dem {app_name} Support." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Having issues? Explore help articles or open a ticket with {app_name} Support." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vous avez des problèmes ? Explorez des articles d'aide ou ouvrez un ticket avec le support {app_name}." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Problemen? Bekijk de hulpartikelen of open een ticket bij {app_name} Support." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Masz problem? Przejrzyj artykuły pomocy lub utwórz zgłoszenie dla Supportu {app_name}." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Возникли проблемы? Ознакомьтесь со статьями в справке или отправьте запрос в службу поддержки {app_name}." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Har du problem? Utforska hjälpartiklar eller öppna en supportförfrågan hos {app_name}." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Виникли проблеми? Перегляньте довідкові статті або створіть запит до служби підтримки {app_name}." + } + } + } + }, "supportGoTo" : { "extractionState" : "manual", "localizations" : { @@ -400459,6 +413720,18 @@ "value" : "Frapetu por reprovi" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toque para reintentar" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toca para reintentar" + } + }, "fr" : { "stringUnit" : { "state" : "translated", @@ -400483,6 +413756,18 @@ "value" : "Ketuk untuk mencoba lagi" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tocca per riprovare" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "タップして再試行" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -400501,6 +413786,18 @@ "value" : "Dotknij aby ponowić" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toque para tentar novamente" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apasă pentru a reîncerca" + } + }, "ru" : { "stringUnit" : { "state" : "translated", @@ -400513,6 +413810,12 @@ "value" : "Tryck för ett nytt försök" } }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tekrar denemek için tıkla" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -400530,6 +413833,12 @@ "state" : "translated", "value" : "点击以重试" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "輕觸以重試" + } } } }, @@ -401988,13 +415297,245 @@ } } }, + "themePreview" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tema önizləməsi" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Náhled motivu" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Design-Vorschau" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Theme Preview" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aperçu du thème" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thema voorbeeld" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Podgląd motywu" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Предпросмотр темы" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Förhandsvisning av tema" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Попередній перегляд теми" + } + } + } + }, + "theReturn" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Qayıt" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zpět" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Return" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Retour" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Terug" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Powrót" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Зворотно" + } + } + } + }, "tooltipAccountIdVisible" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "{name} üçün Hesab ID-si əvvəlki qarşılıqlı əlaqələrə əsasən görünür" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "L'identificador del compte de {name} és visible en funció de les teves interaccions anteriors" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "ID účtu {name} je viditelné
v závislosti na vašich předchozích interakcích" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Die Account-ID von {name} ist basierend auf deinen vorherigen Interaktionen sichtbar" + } + }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "The Account ID of {name} is visible
based on your previous interactions" + "value" : "The Account ID of {name} is visible based on your previous interactions" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "El ID de cuenta de {name} es visible basándose en tus interacciones anteriores" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "El ID de cuenta de {name} es visible basándose en tus interacciones anteriores" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "L’ID de compte de {name} est visible en fonction de vos interactions précédentes" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "{name} का Account ID आपकी पिछली इंटरैक्शन के आधार पर दृश्यमान है" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "L'Account ID di {name} è visibile in base alle interazioni precedenti" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "{name} のAccount IDは、以前のやり取りに基づき表示されます" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "De Account ID van {name} is zichtbaar op basis van je eerdere interacties" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Identyfikator konta {name} jest widoczny na podstawie wcześniejszych interakcji" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "O ID da Conta de {name} está visível com base nas suas interações anteriores" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "ID-ul contului {name} este vizibil în baza interacțiunilor anterioare" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "ID аккаунта {name} виден
на основе ваших предыдущих взаимодействий" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "{name}:s Account ID är synligt baserat på dina tidigare interaktioner" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "{name} adlı kişinin Hesap Kimliği, önceki etkileşimlerinize dayanarak görünür durumdadır" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Account ID {name} видимий через вашу попередню взаємодію" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "基于您与 {name} 之前的互动,其 Account ID 对您可见" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "{name} 的 Account ID 因先前的互動而可見" } } } @@ -402002,10 +415543,248 @@ "tooltipBlindedIdCommunities" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kor ID-lər, spamı azaltmaq və məxfiliyi artırmaq üçün icmalarda istifadə olunur" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Els ID cecs s'utilitzen a les comunitats per reduir el correu brossa i augmentar la privadesa" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Maskovaná ID jsou používána v komunitách
ke snížení spamu a zvýšení soukromí" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verschleierte IDs werden in Communities verwendet, um Spam zu reduzieren und die Privatsphäre zu erhöhen" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Blinded IDs are used in communities to reduce spam and increase privacy" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los ID cegados se utilizan en las comunidades para reducir el spam y aumentar la privacidad" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los ID cegados se utilizan en las comunidades para reducir el spam y aumentar la privacidad" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Les ID aveuglés sont utilisés dans les communautés pour réduire le spam et renforcer la confidentialité" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "ब्लाइंडेड ID समुदायों में स्पैम को कम करने और गोपनीयता बढ़ाने के लिए उपयोग की जाती हैं" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gli ID offuscati vengono utilizzati nelle Community per ridurre lo spam e aumentare la privacy" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ブラインドIDは、スパムを減らしプライバシーを高めるためにCommunityで利用されます" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geblindeerde ID's worden in Community's gebruikt om spam te verminderen en de privacy te vergroten" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zanonimizowane identyfikatory są używane w społecznościach w celu ograniczenia spamu i zwiększenia prywatności" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "IDs Ocultos são usados em Comunidades para reduzir spam e aumentar a privacidade" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "ID-urile cenzurate sunt utilizate în comunități pentru a reduce mesajele spam și a crește confidențialitatea" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Скрытые ID используются в сообществах
для уменьшения спама и повышения конфиденциальности" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Maskerade ID används i Communitys för att minska spam och öka sekretessen" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Körleştirilmiş Kimlikler, istenmeyen mesajları (spam) azaltmak ve gizliliği artırmak için topluluklarda kullanılır" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Знеособлені ID використовуються у спільнотах задля зменшення кількості небажаних повідомлень та підвищення приватності" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "盲化 ID 在社区中用于减少垃圾信息并提高隐私性" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "在 Community 中使用隱藏 ID 可減少垃圾訊息並提升隱私" + } + } + } + }, + "translate" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tərcümə et" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Překlad" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Übersetzen" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Translate" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Traduction" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vertalen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Przetłumacz" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Перевод" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Översätt" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Переклад" + } + } + } + }, + "tray" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sini" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lišta" + } + }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Blinded IDs are used in communities
to reduce spam and increase privacy" + "value" : "Tray" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Barre de tâches" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Systeemvak" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Трей" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktivitetsfält" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Область сповіщень" } } } @@ -403444,6 +417223,12 @@ "unavailable" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Əlçatmazdır" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -403456,6 +417241,12 @@ "value" : "Nedostupné" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nicht verfügbar" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -403468,12 +417259,30 @@ "value" : "Ne disponebla" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "No disponible" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "No disponible" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Indisponible" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "उपलब्ध नहीं है" + } + }, "hu" : { "stringUnit" : { "state" : "translated", @@ -403486,6 +417295,18 @@ "value" : "Tidak tersedia" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Non disponibile" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "利用不可" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -403504,11 +417325,53 @@ "value" : "Niedostępny" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Indisponível" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Indisponibil" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Недоступно" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Otillgänglig" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mevcut Değil" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Недоступно" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "不可用" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法使用" + } } } }, @@ -404470,6 +418333,29 @@ } } }, + "unsupportedCpu" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dəstəklənməyən CPU" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nepodporovaný procesor" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unsupported CPU" + } + } + } + }, "updateApp" : { "extractionState" : "manual", "localizations" : { @@ -404955,9 +418841,515 @@ } } }, + "updateCommunityInformation" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "İcma məlumatlarını güncəllə" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actualitzar la informació de la comunitat" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Upravit informace o komunitě" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Community-Informationen aktualisieren" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Update Community Information" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actualizar la información de la comunidad" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actualizar la información de la comunidad" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mettre à jour les informations de la communauté" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "सामुदायिक जानकारी अपडेट करें" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aggiorna le informazioni della Comunità" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "コミュニティ情報を更新" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Community-informatie bijwerken" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zaktualizuj informacje o społeczności" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Atualizar informações da Comunidade" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actualizează informațiile comunității" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Обновить информацию о сообществе" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uppdatera communityinformation" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Оновити інформацію про спільноту" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "更新社群信息" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "更新社群資訊" + } + } + } + }, + "updateCommunityInformationDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "İcma adı və açıqlaması, bütün icma üzvlərinə görünür" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "El nom i la descripció de la comunitat són visibles per a tots els membres de la comunitat" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Název a popis komunity jsou viditelné pro všechny členy komunity" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Community-Name und -Beschreibung sind für alle Mitglieder der Community sichtbar." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Community name and description are visible to all community members" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "El nombre y la descripción de la comunidad son visibles para todos los miembros de la comunidad" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "El nombre y la descripción de la comunidad son visibles para todos los miembros de la comunidad" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Le nom et la description de la communauté sont visibles par tous les membres" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "सामुदायिक नाम और विवरण सभी सामुदायिक सदस्यों को दिखाई देता है" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Il nome e la descrizione della Comunità sono visibili a tutti i membri" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "コミュニティ名と説明はすべてのメンバーに表示されます" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Communitynaam en beschrijving zijn zichtbaar voor alle communityleden" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nazwa i opis społeczności są widoczne dla wszystkich jej członków" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "O nome e a descrição da Comunidade são visíveis para todos os membros da Comunidade" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Numele și descrierea comunității sunt vizibile pentru toți membrii comunității" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Название и описание Community видны всем участникам Community" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Communitynamn och beskrivning är synliga för alla communitymedlemmar" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Назву та опис спільноти бачать усі учасники" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "社群名称与描述对所有社群成员可见" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "社群名稱和描述對所有社群成員可見" + } + } + } + }, + "updateCommunityInformationEnterShorterDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lütfən, daha qısa icma açıqlaması daxil edin" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Introduïu una descripció de la comunitat més curta" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zadejte prosím kratší popis komunity" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bitte gib eine kürzere Community-Beschreibung ein." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Please enter a shorter community description" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Por favor, introduce una descripción más corta de la comunidad" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Por favor, introduce una descripción más corta de la comunidad" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Veuillez entrer une description de la communauté plus courte" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "कृपया एक छोटा सामुदायिक विवरण दर्ज करें" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inserisci una descrizione della Comunità più breve" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "より短いコミュニティの説明を入力してください" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Voer een kortere communitybeschrijving in" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wprowadź krótszy opis społeczności" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Por favor, insira uma descrição mais curta da Comunidade" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Te rugăm să introduci o descriere a comunității mai scurtă" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Введите более короткое описание сообщества" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vänligen ange en kortare communitybeskrivning" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Будь ласка, введіть коротший опис спільноти" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "请输入更简短的社群描述" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "請輸入較短的社群描述" + } + } + } + }, + "updateCommunityInformationEnterShorterName" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lütfən, daha qısa icma adı daxil edin" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Introduïu un nom de comunitat més curt" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zadejte prosím kratší název komunity" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bitte gib einen kürzeren Community-Namen ein." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Please enter a shorter community name" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Por favor, introduce un nombre de comunidad más corto" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Por favor, introduce un nombre de comunidad más corto" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Veuillez entrer un nom de communauté plus court" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "कृपया एक छोटा सामुदायिक नाम दर्ज करें" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inserisci un nome della Comunità più breve" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "より短いコミュニティ名を入力してください" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Voer een kortere communitynaam in" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wprowadź krótszą nazwę społeczności" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Por favor, insira um nome de Comunidade mais curto" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Te rugăm să introduci un nume al comunității mai scurt" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пожалуйста, введите более короткое название сообщества" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vänligen ange ett kortare communitynamn" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Будь ласка, введіть коротшу назву спільноти" + } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "请输入更简短的社群名称" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "請輸入較短的社群名稱" + } + } + } + }, "updated" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Son güncəlləmə: {relative_time} əvvəl" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -404970,6 +419362,12 @@ "value" : "Naposledy aktualizováno před {relative_time}" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zuletzt aktualisiert vor {relative_time}" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -404982,18 +419380,48 @@ "value" : "Laste ĝisdatigita antaŭ {relative_time}" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Última actualización hace {relative_time}" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Última actualización hace {relative_time}" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Dernière mise à jour il y a {relative_time}" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "अंतिम अद्यतन {relative_time} पहले" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Utoljára frissítve {relative_time}" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ultimo aggiornamento {relative_time} fa" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "最終更新: {relative_time} 前" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -405012,11 +419440,53 @@ "value" : "Ostatnia aktualizacja {relative_time} temu" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Última atualização há {relative_time}" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ultima actualizare acum {relative_time}" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Последнее обновление {relative_time}" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Senast uppdaterad för {relative_time} sedan" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "En son {relative_time} önce güncellendi" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Останнє оновлення {relative_time} тому" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "最近更新于 {relative_time} 前" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "上次更新於 {relative_time} 前" + } } } }, @@ -406969,6 +421439,12 @@ "value" : "Opdatér gruppeoplysninger" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gruppeninformationen aktualisieren" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -406981,12 +421457,30 @@ "value" : "Ĝisdatigi informon de la grupo" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actualizar información del grupo" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actualizar información del grupo" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Mettre à jour les informations du groupe" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "समूह जानकारी अपडेट करें" + } + }, "hu" : { "stringUnit" : { "state" : "translated", @@ -406999,6 +421493,18 @@ "value" : "Perbaharui Informasi Grup" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aggiorna informazioni del gruppo" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "グループ情報を更新" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -407017,6 +421523,36 @@ "value" : "Aktualizuj informacje o grupie" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Atualizar informações do grupo" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actualizează informațiile grupului" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Изменить информацию о группе" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uppdatera gruppinformation" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Grup bilgisini güncelle" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -407028,6 +421564,12 @@ "state" : "translated", "value" : "更新群组信息" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "更新群組資訊" + } } } }, @@ -407058,6 +421600,12 @@ "value" : "Gruppenavn og beskrivelse er synlig for alle gruppemedlemmer." } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gruppenname und Beschreibung sind für alle Gruppenmitglieder sichtbar." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -407070,12 +421618,30 @@ "value" : "Grupa nomo kaj priskribo estas videbla al ĉiuj membroj de la grupo." } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "El nombre y la descripción del grupo son visibles para todos los miembros del grupo." + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "El nombre y la descripción del grupo son visibles para todos los miembros del grupo." + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Le nom du groupe et la description sont visibles par tous les membres du groupe." } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "समूह का नाम और विवरण सभी समूह सदस्यों को दिखाई देता है।" + } + }, "hu" : { "stringUnit" : { "state" : "translated", @@ -407088,6 +421654,18 @@ "value" : "Nama grup dan deskripsi dapat dilihat oleh semua anggota grup." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Il nome e la descrizione del gruppo sono visibili a tutti i membri del gruppo." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "グループ名と説明はすべてのグループメンバーに表示されます。" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -407106,6 +421684,36 @@ "value" : "Nazwa i opis grupy są widoczne dla wszystkich jej członków." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "O nome e a descrição do grupo estão visíveis para todos os membros do grupo." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Numele și descrierea grupului sunt vizibile pentru toți membrii grupului." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Название и описание группы видны всем участникам группы." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gruppnamn och beskrivning är synliga för alla gruppmedlemmar." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Grup adı ve açıklaması tüm grup üyeleri tarafından görülebilir." + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -407117,6 +421725,12 @@ "state" : "translated", "value" : "群组名称与描述对所有群组成员可见。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "群組名稱與描述對所有群組成員可見。" + } } } }, @@ -407147,6 +421761,12 @@ "value" : "Indtast venligst en kortere gruppebeskrivelse" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bitte gib eine kürzere Gruppenbeschreibung ein" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -407159,12 +421779,30 @@ "value" : "Bonvolu enigi pli mallongan priskribon de la grupo" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Por favor, ingresa una descripción del grupo más corta" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Por favor, ingresa una descripción del grupo más corta" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Veuillez entrer une description de groupe plus courte" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "कृपया एक छोटा समूह विवरण दर्ज करें" + } + }, "hu" : { "stringUnit" : { "state" : "translated", @@ -407177,6 +421815,18 @@ "value" : "Masukkan nama grup yang lebih pendek" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inserisci una descrizione del gruppo più breve" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "グループの説明をもっと短く入力してください" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -407195,6 +421845,36 @@ "value" : "Wprowadź krótszy opis grupy" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Por favor, introduza uma descrição mais curta do grupo" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Te rugăm să introduci o descriere mai scurtă a grupului" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Введите более короткое описание группы" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vänligen ange en kortare gruppbeskrivning" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lütfen daha kısa bir grup açıklaması girin" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -407212,6 +421892,12 @@ "state" : "translated", "value" : "请输入更简短的群组描述" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "請輸入一個較短的群組描述" + } } } }, @@ -407739,6 +422425,12 @@ "value" : "Eine neue Version ({version}) von {app_name} ist verfügbar." } }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Μια νέα έκδοση ({version}) της εφαρμογής {app_name} είναι διαθέσιμη." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -407775,12 +422467,24 @@ "value" : "{app_name} का एक नया संस्करण ({version}) उपलब्ध है।" } }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Elérhető a(z) {app_name} új, {version} verziója." + } + }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Versi terbaru ({version}) dari {app_name} telah tersedia." } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "È disponibile una nuova versione ({version}) di {app_name}." + } + }, "ja" : { "stringUnit" : { "state" : "translated", @@ -407805,6 +422509,12 @@ "value" : "Versîyoneke nû ({version}) ya {app_name} berdest e." } }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "En ny versjon ({version}) av {app_name} er tilgjengelig." + } + }, "nl" : { "stringUnit" : { "state" : "translated", @@ -407817,6 +422527,12 @@ "value" : "Dostępna jest nowa wersja ({version}) aplikacji {app_name}." } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uma nova versão ({version}) de {app_name} está disponível." + } + }, "ro" : { "stringUnit" : { "state" : "translated", @@ -407858,6 +422574,260 @@ "state" : "translated", "value" : "{app_name}有新版本({version})可用。" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "{app_name} 有新版本({version})可用。" + } + } + } + }, + "updatePlan" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Planı güncəllə" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktualizovat tarif" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Update Plan" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mettre à jour le forfait" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abonnement bijwerken" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zaktualizuj plan" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Оновити тарифний план" + } + } + } + }, + "updatePlanTwo" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Planınızı güncəlləməyin iki yolu var:" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dva způsoby, jak aktualizovat váš tarif:" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Two ways to update your plan:" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deux façons de mettre à jour votre abonnement :" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Twee manieren om je abonnement bij te werken:" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dwa sposoby na aktualizację planu:" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Два шляхи поновлення твоєї підписки:" + } + } + } + }, + "updateProfileInformation" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profil məlumatlarını güncəllə" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Upravit informace profilu" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profilinformationen aktualisieren" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Update Profile Information" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actualizar información de perfil" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actualizar información de perfil" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mettre à jour les informations du profil" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profielinformatie bijwerken" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zaktualizuj informacje w profilu" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actualizarea informațiilor de profil" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Обновить информацию профиля" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uppdatera profilinformation" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Оновити інформацію облікового запису" + } + } + } + }, + "updateProfileInformationDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ekran adınız və ekran şəkliniz bütün danışıqlarda görünür." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vaše zobrazované jméno a profilová fotka jsou viditelné ve všech konverzacích." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dein Anzeigename und Profilbild sind in allen Unterhaltungen sichtbar." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your display name and display picture are visible in all conversations." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Votre nom d'affichage et votre photo de profil sont visibles dans toutes les conversations." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je weergavenaam en profielfoto zijn zichtbaar in alle gesprekken." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Twoja nazwa wyświetlana i obraz profilu są widoczne we wszystkich konwersacjach." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Numele și poza de profil sunt vizibile în toate conversațiile tale." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ваше отображаемое имя и фотография профиля видны во всех беседах." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ditt visningsnamn och din visningsbild är synliga i alla konversationer." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ваше відображуване ім’я та зображення профілю видимі у всіх розмовах." + } } } }, @@ -408340,6 +423310,71 @@ } } }, + "updates" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Güncəlləmələr" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktualizace" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktualisierungen" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Updates" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mises à jour" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Updates" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktualizacje" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Обновления" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uppdateringar" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Оновлення" + } + } + } + }, "updateSession" : { "extractionState" : "manual", "localizations" : { @@ -409304,9 +424339,97 @@ } } }, + "updating" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Güncəllənir..." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktualizuji..." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wird aktualisiert..." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Updating..." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mise à jour..." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bijwerken..." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktualizowanie..." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actualizare..." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uppdaterar..." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Оновлення..." + } + } + } + }, + "upgradeSession" : { + "extractionState" : "manual", + "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Navýšit {app_name}" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Upgrade {app_name}" + } + } + } + }, "upgradeTo" : { "extractionState" : "manual", "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yüksəlt" + } + }, "ca" : { "stringUnit" : { "state" : "translated", @@ -409319,23 +424442,119 @@ "value" : "Navýšit na" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Upgrade auf" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Upgrade to" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actualizar a" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actualizar a" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mettre à niveau à" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "अपग्रेड करें" + } + }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Frissítés erre:" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Passa a" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アップグレード先:" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Upgraden naar" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uaktualnij do" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Atualizar para" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actualizează la" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Обновить до" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uppgradera till" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yükselt" + } + }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Підвищити до" } + }, + "zh-CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "升级到" + } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "升級為" + } } } }, @@ -411734,6 +426953,53 @@ } } }, + "urlOpenDescriptionAlternative" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keçidlər, brauzerinizdə açılacaq." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odkazy se otevřou ve vašem prohlížeči." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Links will open in your browser." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Les liens s'ouvriront dans votre navigateur." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Links worden in uw browser geopend." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Linki będą otwierane w Twojej przeglądarce." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "За ланкою перейде твоє оглядало мережців за промовчання." + } + } + } + }, "useFastMode" : { "extractionState" : "manual", "localizations" : { @@ -412213,6 +427479,52 @@ } } }, + "viaStoreWebsite" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Via the {platform} website" + } + } + } + }, + "viaStoreWebsiteDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Qeydiyyatdan keçərkən istifadə etdiyiniz {platform_account} hesabı ilə {platform_store} veb saytı üzərindən planınızı dəyişdirin." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Změňte svůj tarif pomocí {platform_account}, se kterým jste se zaregistrovali, prostřednictvím webu {platform_store}." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Change your plan using the {platform_account} you used to sign up with, via the {platform_store} website." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Modifiez votre abonnement en utilisant le compte {platform_account} avec lequel vous vous êtes inscrit, via le site web de {platform_store}." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wijzig je abonnement met het {platform_account} waarmee je je hebt aangemeld, via de {platform_store} website." + } + } + } + }, "video" : { "extractionState" : "manual", "localizations" : { @@ -413677,6 +428989,12 @@ "value" : "Vis mindre" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Weniger anzeigen" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -413689,18 +429007,54 @@ "value" : "Vidi malpli" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ver menos" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ver menos" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Voir Moins" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "कम देखें" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kevesebb" + } + }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tampilkan Ringkas" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mostra meno" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "表示を減らす" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -413719,6 +429073,36 @@ "value" : "Zobacz mniej" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ver menos" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Afișează mai puțin" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Посмотреть меньше" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Visa färre" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Daha az görüntüle" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -413730,6 +429114,12 @@ "state" : "translated", "value" : "查看更少" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "顯示較少" + } } } }, @@ -413760,6 +429150,12 @@ "value" : "Vis mere" } }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mehr anzeigen" + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -413772,18 +429168,54 @@ "value" : "Vidi pli" } }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ver más" + } + }, + "es-ES" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ver más" + } + }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Voir Plus" } }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "और देखें" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Több" + } + }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Lihat Selengkapnya" } }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mostra di più" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "もっと見る" + } + }, "ko" : { "stringUnit" : { "state" : "translated", @@ -413802,6 +429234,36 @@ "value" : "Zobacz więcej" } }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ver mais" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Afișează mai mult" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Посмотреть больше" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Visa mer" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Devamını Görüntüle" + } + }, "uk" : { "stringUnit" : { "state" : "translated", @@ -413813,6 +429275,12 @@ "state" : "translated", "value" : "查看更多" } + }, + "zh-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "顯示更多" + } } } }, @@ -415253,6 +430721,17 @@ } } }, + "warningIosVersionEndingSupport" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Support for iOS 15 has ended. Update to iOS 16 or later to continue receiving app updates." + } + } + } + }, "window" : { "extractionState" : "manual", "localizations" : { @@ -416689,6 +432168,230 @@ } } } + }, + "yourCpuIsUnsupportedSSE42" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "CPU-nuz Linux x64 əməliyyat sistemlərində {app_name}-un təsvirləri emal etməsi üçün tələb olunan SSE 4.2 təlimatlarını dəstəkləmir. Lütfən uyumlu bir CPU-ya keçin və ya fərqli bir əməliyyat sistemi istifadə edin." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Váš procesor nepodporuje instrukce SSE 4.2, které jsou vyžadovány aplikací {app_name} v operačních systémech Linux x64 pro zpracování obrázků. Proveďte upgrade na kompatibilní procesor nebo použijte jiný operační systém." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your CPU does not support SSE 4.2 instructions, which are required by {app_name} on Linux x64 operating systems to process images. Please upgrade to a compatible CPU or use a different operating system." + } + } + } + }, + "yourRecoveryPassword" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geri qaytarma parolunuz" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vaše heslo pro obnovení" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dein Wiederherstellungspasswort" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your Recovery Password" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Votre mot de passe de récupération" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ditt Gjenopprettingspassord" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je herstelwachtwoord" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Twoje hasło odzyskiwania" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ваш Пароль Восстановления" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ditt återställningslösenord" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ваш пароль для відновлення" + } + } + } + }, + "zoomFactor" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Böyütmə amili" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Měřítko přiblížení" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vergrößerungsfaktor" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zoom Factor" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Niveau de Zoom" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zoomfactor" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Współczynnik powiększenia" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Масштабирование приложения" + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zoomfaktor" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Масштаб" + } + } + } + }, + "zoomFactorDescription" : { + "extractionState" : "manual", + "localizations" : { + "az" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mətnin və vizual elementlərin ölçüsünü ayarla." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Upravte velikost textu a vizuálních prvků." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Passe die Größe von Text und visuellen Elementen an." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Adjust the size of text and visual elements." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ajuster la taille du texte et des éléments visuels." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pas de grootte van tekst en visuele elementen aan." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dostosuj wielkość tekstu i elementów wizualnych." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Настройте размер текста и визуальных элементов." + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Justera storleken på text och visuella element." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Налаштування розміру тексту та візуальних елементів." + } + } + } } }, "version" : "1.0" diff --git a/Session/Meta/WebPImages/AnimatedProfileCTA.webp b/Session/Meta/WebPImages/AnimatedProfileCTA.webp new file mode 100644 index 0000000000..9d2ee88e15 Binary files /dev/null and b/Session/Meta/WebPImages/AnimatedProfileCTA.webp differ diff --git a/Session/Meta/WebPImages/AnimatedProfileCTAAnimationCropped.webp b/Session/Meta/WebPImages/AnimatedProfileCTAAnimationCropped.webp new file mode 100644 index 0000000000..099037c065 Binary files /dev/null and b/Session/Meta/WebPImages/AnimatedProfileCTAAnimationCropped.webp differ diff --git a/Session/Meta/WebPImages/GenericCTAAnimation.webp b/Session/Meta/WebPImages/GenericCTAAnimation.webp deleted file mode 100644 index 67ee0987a3..0000000000 Binary files a/Session/Meta/WebPImages/GenericCTAAnimation.webp and /dev/null differ diff --git a/Session/Notifications/NotificationActionHandler.swift b/Session/Notifications/NotificationActionHandler.swift index 5c0999824b..74b18f89f8 100644 --- a/Session/Notifications/NotificationActionHandler.swift +++ b/Session/Notifications/NotificationActionHandler.swift @@ -4,7 +4,7 @@ import Foundation import Combine import GRDB import SignalUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit import SessionMessagingKit import SessionUtilitiesKit @@ -60,17 +60,17 @@ public class NotificationActionHandler { switch response.actionIdentifier { case UNNotificationDefaultActionIdentifier: Log.debug("[NotificationActionHandler] Default action") - switch categoryIdentifier { - case NotificationCategory.info.identifier: - return showPromotedScreen() - .setFailureType(to: Error.self) - .eraseToAnyPublisher() - default: - return showThread(userInfo: userInfo) - .setFailureType(to: Error.self) - .eraseToAnyPublisher() + Task(priority: .userInitiated) { @MainActor [weak self] in + switch categoryIdentifier { + case NotificationCategory.info.identifier: self?.showPromotedScreen() + default: self?.showThread(userInfo: userInfo) + } } + return Just(()) + .setFailureType(to: Error.self) + .eraseToAnyPublisher() + case UNNotificationDismissActionIdentifier: // TODO - mark as read? Log.debug("[NotificationActionHandler] Dismissed notification") @@ -232,7 +232,7 @@ public class NotificationActionHandler { .eraseToAnyPublisher() } - @MainActor func showThread(userInfo: [AnyHashable: Any]) -> AnyPublisher { + @MainActor func showThread(userInfo: [AnyHashable: Any]) { guard let threadId = userInfo[NotificationUserInfoKey.threadId] as? String, let threadVariantRaw = userInfo[NotificationUserInfoKey.threadVariantRaw] as? Int, @@ -249,18 +249,14 @@ public class NotificationActionHandler { dismissing: dependencies[singleton: .app].homePresentedViewController, animated: (UIApplication.shared.applicationState == .active) ) - - return Just(()).eraseToAnyPublisher() } - func showHomeVC() -> AnyPublisher { + @MainActor func showHomeVC() { dependencies[singleton: .app].showHomeView() - return Just(()).eraseToAnyPublisher() } - func showPromotedScreen() -> AnyPublisher { + @MainActor func showPromotedScreen() { dependencies[singleton: .app].showPromotedScreen() - return Just(()).eraseToAnyPublisher() } private func markAsRead(threadId: String) -> AnyPublisher { diff --git a/Session/Notifications/NotificationPresenter.swift b/Session/Notifications/NotificationPresenter.swift index 17ec5ac5ce..c335c898bd 100644 --- a/Session/Notifications/NotificationPresenter.swift +++ b/Session/Notifications/NotificationPresenter.swift @@ -31,6 +31,12 @@ public class NotificationPresenter: NSObject, UNUserNotificationCenterDelegate, /// Populate the notification settings from `libSession` and the database Task.detached(priority: .high) { [weak self] in + do { try await dependencies.waitUntilInitialised(cache: .libSession) } + catch { + Log.error("[NotificationPresenter] Failed to wait until libSession initialised: \(error)") + return + } + typealias GlobalSettings = ( sound: Preferences.Sound, previewType: Preferences.NotificationPreviewType diff --git a/Session/Notifications/SyncPushTokensJob.swift b/Session/Notifications/SyncPushTokensJob.swift index 6a885dfd2c..5d32a61072 100644 --- a/Session/Notifications/SyncPushTokensJob.swift +++ b/Session/Notifications/SyncPushTokensJob.swift @@ -3,7 +3,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionMessagingKit import SessionUtilitiesKit @@ -82,7 +82,7 @@ public enum SyncPushTokensJob: JobExecutor { // Unregister from our server if let existingToken: String = lastRecordedPushToken { Log.info(.syncPushTokensJob, "Unregister using last recorded push token: \(redact(existingToken))") - return PushNotificationAPI + return Network.PushNotification .unsubscribeAll(token: Data(hex: existingToken), using: dependencies) .map { _ in () } .eraseToAnyPublisher() @@ -177,7 +177,7 @@ public enum SyncPushTokensJob: JobExecutor { } Log.info(.syncPushTokensJob, "Sending push token to PN server") - return PushNotificationAPI + return Network.PushNotification .subscribeAll( token: Data(hex: pushToken), isForcedUpdate: true, diff --git a/Session/Onboarding/LoadingScreen.swift b/Session/Onboarding/LoadingScreen.swift index 4fffd9fe46..b731acafbd 100644 --- a/Session/Onboarding/LoadingScreen.swift +++ b/Session/Onboarding/LoadingScreen.swift @@ -3,7 +3,7 @@ import SwiftUI import Combine import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionMessagingKit import SignalUtilitiesKit import SessionUtilitiesKit diff --git a/Session/Onboarding/Onboarding.swift b/Session/Onboarding/Onboarding.swift index 52da3682fa..5dda7496ea 100644 --- a/Session/Onboarding/Onboarding.swift +++ b/Session/Onboarding/Onboarding.swift @@ -5,7 +5,7 @@ import Combine import GRDB import SessionUtilitiesKit import SessionMessagingKit -import SessionSnodeKit +import SessionNetworkingKit // MARK: - Cache @@ -106,17 +106,21 @@ extension Onboarding { self.id = dependencies.randomUUID() self.initialFlow = flow - /// Try to load the users `ed25519KeyPair` from the database and generate the `x25519KeyPair` from it - var ed25519KeyPair: KeyPair = .empty - let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0) - dependencies[singleton: .storage].readAsync( - retrieve: { db in Identity.fetchUserEd25519KeyPair(db) }, - completion: { result in - ed25519KeyPair = ((try? result.successOrThrow()) ?? .empty) - semaphore.signal() - } - ) - semaphore.wait() + /// Try to load the users `ed25519SecretKey` from the general cache and generate the key pairs from it + let ed25519SecretKey: [UInt8] = dependencies[cache: .general].ed25519SecretKey + let ed25519KeyPair: KeyPair = { + guard + !ed25519SecretKey.isEmpty, + let ed25519Seed: Data = dependencies[singleton: .crypto].generate( + .ed25519Seed(ed25519SecretKey: ed25519SecretKey) + ), + let ed25519KeyPair: KeyPair = dependencies[singleton: .crypto].generate( + .ed25519KeyPair(seed: Array(ed25519Seed)) + ) + else { return .empty } + + return ed25519KeyPair + }() let x25519KeyPair: KeyPair = { guard ed25519KeyPair != .empty, @@ -405,13 +409,13 @@ extension Onboarding { .upsert(db) try Profile .filter(id: userSessionId.hexString) - .updateAll(db, Profile.Columns.lastNameUpdate.set(to: nil)) + .updateAll(db, Profile.Columns.profileLastUpdated.set(to: nil)) try Profile.updateIfNeeded( db, publicKey: userSessionId.hexString, displayNameUpdate: .currentUserUpdate(displayName), displayPictureUpdate: .none, - sentTimestamp: dependencies.dateNow.timeIntervalSince1970, + profileUpdateTimestamp: dependencies.dateNow.timeIntervalSince1970, using: dependencies ) diff --git a/Session/Onboarding/PNModeScreen.swift b/Session/Onboarding/PNModeScreen.swift index d4976596a1..40100bf54e 100644 --- a/Session/Onboarding/PNModeScreen.swift +++ b/Session/Onboarding/PNModeScreen.swift @@ -3,7 +3,7 @@ import SwiftUI import Combine import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SignalUtilitiesKit import SessionUtilitiesKit diff --git a/Session/Open Groups/JoinOpenGroupVC.swift b/Session/Open Groups/JoinOpenGroupVC.swift index ace6225588..0c5fa3c2cc 100644 --- a/Session/Open Groups/JoinOpenGroupVC.swift +++ b/Session/Open Groups/JoinOpenGroupVC.swift @@ -6,6 +6,7 @@ import AVFoundation import GRDB import SessionUIKit import SessionMessagingKit +import SessionNetworkingKit import SessionUtilitiesKit import SignalUtilitiesKit @@ -76,7 +77,17 @@ final class JoinOpenGroupVC: BaseVC, UIPageViewControllerDataSource, UIPageViewC setNavBarTitle("communityJoin".localized()) view.themeBackgroundColor = .backgroundSecondary - let navBarHeight: CGFloat = (navigationController?.navigationBar.frame.size.height ?? 0) + + // Only account for navigation header when view controller + // presentation type is `fullScreen` + var navBarHeight: CGFloat { + switch modalPresentationStyle { + case .fullScreen: + return navigationController?.navigationBar.frame.size.height ?? 0 + default: + return 0 + } + } let closeButton = UIBarButtonItem(image: #imageLiteral(resourceName: "X"), style: .plain, target: self, action: #selector(close)) closeButton.themeTintColor = .textPrimary @@ -475,11 +486,11 @@ private final class EnterURLVC: UIViewController, UIGestureRecognizerDelegate, O ) } - func join(_ room: OpenGroupAPI.Room) { + func join(_ room: Network.SOGS.Room) { joinOpenGroupVC?.joinOpenGroup( roomToken: room.token, - server: OpenGroupAPI.defaultServer, - publicKey: OpenGroupAPI.defaultServerPublicKey, + server: Network.SOGS.defaultServer, + publicKey: Network.SOGS.defaultServerPublicKey, shouldOpenCommunity: true, onError: nil ) diff --git a/Session/Open Groups/OpenGroupSuggestionGrid.swift b/Session/Open Groups/OpenGroupSuggestionGrid.swift index 28bbe31ee6..ddc4c72ba9 100644 --- a/Session/Open Groups/OpenGroupSuggestionGrid.swift +++ b/Session/Open Groups/OpenGroupSuggestionGrid.swift @@ -4,6 +4,7 @@ import Foundation import GRDB import Combine import NVActivityIndicatorView +import SessionNetworkingKit import SessionMessagingKit import SessionUIKit import SessionUtilitiesKit @@ -354,7 +355,7 @@ extension OpenGroupSuggestionGrid { snContentView.pin(to: self) } - fileprivate func update(with room: OpenGroupAPI.Room, openGroup: OpenGroup, using dependencies: Dependencies) { + fileprivate func update(with room: Network.SOGS.Room, openGroup: OpenGroup, using dependencies: Dependencies) { label.text = room.name let maybePath: String? = openGroup.displayPictureOriginalUrl @@ -380,7 +381,7 @@ extension OpenGroupSuggestionGrid { // MARK: - Delegate protocol OpenGroupSuggestionGridDelegate { - func join(_ room: OpenGroupAPI.Room) + func join(_ room: Network.SOGS.Room) } // MARK: - LastRowCenteredLayout diff --git a/Session/Path/PathStatusView.swift b/Session/Path/PathStatusView.swift index 907947605f..bd3eefe57b 100644 --- a/Session/Path/PathStatusView.swift +++ b/Session/Path/PathStatusView.swift @@ -3,7 +3,7 @@ import UIKit import Combine import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionMessagingKit import SessionUtilitiesKit @@ -115,10 +115,7 @@ final class PathStatusViewAccessory: UIView, SessionCell.Accessory.CustomView { /// We want the path status to have the same sizing as other list item icons so it needs to be wrapped in /// this contains view - public static let size: SessionCell.Accessory.Size = .fixed( - width: IconSize.medium.size, - height: IconSize.medium.size - ) + public static let size: SessionCell.Accessory.Size = .minWidth(height: IconSize.medium.size) static func create(maxContentWidth: CGFloat, using dependencies: Dependencies) -> PathStatusViewAccessory { return PathStatusViewAccessory(using: dependencies) diff --git a/Session/Path/PathVC.swift b/Session/Path/PathVC.swift index ab37d115f3..2ed2fb61ad 100644 --- a/Session/Path/PathVC.swift +++ b/Session/Path/PathVC.swift @@ -5,7 +5,7 @@ import Combine import NVActivityIndicatorView import SessionMessagingKit import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit final class PathVC: BaseVC { @@ -72,6 +72,10 @@ final class PathVC: BaseVC { setUpNavBar() setUpViewHierarchy() + + if !dependencies[defaults: .standard, key: .hasVisitedPathScreen] { + dependencies[defaults: .standard, key: .hasVisitedPathScreen] = true + } } private func setUpNavBar() { diff --git a/Session/Settings/AppIconViewModel.swift b/Session/Settings/AppIconViewModel.swift index 9e02f0612d..5ebefe1e55 100644 --- a/Session/Settings/AppIconViewModel.swift +++ b/Session/Settings/AppIconViewModel.swift @@ -24,6 +24,26 @@ enum AppIcon: String, CaseIterable { /// additional copies in order to render in the UI var previewImageName: String { "\(rawValue)-Preview" } + // stringlint:ignore_contents + var accessibilityIdentifier: String { + switch self { + case .session: + "Session option" + case .weather: + "Weather option" + case .stocks: + "Stocks option" + case .news: + "News option" + case .notes: + "Notes option" + case .meetings: + "Meetings option" + case .calculator: + "Calculator option" + } + } + // stringlint:ignore_contents init(name: String?) { switch name { @@ -132,9 +152,11 @@ class AppIconViewModel: SessionTableViewModel, NavigatableStateHolder, Observabl oldValue: (previous != nil) ), onTap: { [weak self] in + let lastSelected: String? = dependencies[defaults: .standard, key: .lastSelectedAppIconDisguise] + switch current { case .some: self?.updateAppIcon(nil) - case .none: self?.updateAppIcon(.weather) + case .none: self?.updateAppIcon(lastSelected.map { AppIcon(name: $0) } ?? .weather) } } ) @@ -168,5 +190,11 @@ class AppIconViewModel: SessionTableViewModel, NavigatableStateHolder, Observabl } selectedOptionsSubject.send(icon?.rawValue) + + // Only store custom icons + if let currentIconName = icon?.rawValue { + // Save latest app icon disguise selected + dependencies[defaults: .standard, key: .lastSelectedAppIconDisguise] = currentIconName + } } } diff --git a/Session/Settings/AppearanceViewModel.swift b/Session/Settings/AppearanceViewModel.swift index ab4d1dda1f..075c9390c7 100644 --- a/Session/Settings/AppearanceViewModel.swift +++ b/Session/Settings/AppearanceViewModel.swift @@ -40,7 +40,7 @@ class AppearanceViewModel: SessionTableViewModel, NavigatableStateHolder, Observ case .themes: return "appearanceThemes".localized() case .primaryColor: return "appearancePrimaryColor".localized() case .primaryColorSelection: return nil - case .autoDarkMode: return "appearanceAutoDarkMode".localized() + case .autoDarkMode: return "darkMode".localized() case .appIcon: return "appIcon".localized() } } @@ -167,8 +167,12 @@ class AppearanceViewModel: SessionTableViewModel, NavigatableStateHolder, Observ trailingAccessory: .radio( isSelected: (state.theme == theme) ), - onTap: { + onTap: { [dependencies = viewModel.dependencies] in ThemeManager.updateThemeState(theme: theme) + // Update trigger only if it's not set to true + if !dependencies[defaults: .standard, key: .hasChangedTheme] { + dependencies[defaults: .standard, key: .hasChangedTheme] = true + } } ) } @@ -180,7 +184,8 @@ class AppearanceViewModel: SessionTableViewModel, NavigatableStateHolder, Observ id: .primaryColorPreview, leadingAccessory: .custom( info: ThemeMessagePreviewView.Info() - ) + ), + isEnabled: false ) ] ), @@ -209,16 +214,16 @@ class AppearanceViewModel: SessionTableViewModel, NavigatableStateHolder, Observ elements: [ SessionCell.Info( id: .darkModeMatchSystemSettings, - title: SessionCell.TextInfo( - "followSystemSettings".localized(), - font: .titleRegular - ), + title: "appearanceAutoDarkMode".localized(), + subtitle: "followSystemSettings".localized(), trailingAccessory: .toggle( state.autoDarkModeEnabled, oldValue: previousState.autoDarkModeEnabled ), onTap: { ThemeManager.updateThemeState( + theme: state.theme, /// Keep the current value + primaryColor: state.primaryColor, /// Keep the current value matchSystemNightModeSetting: !state.autoDarkModeEnabled ) } @@ -234,7 +239,10 @@ class AppearanceViewModel: SessionTableViewModel, NavigatableStateHolder, Observ "appIconSelect".localized(), font: .titleRegular ), - trailingAccessory: .icon(.chevronRight), + trailingAccessory: .icon( + .chevronRight, + pinEdges: [.right] + ), onTap: { [weak viewModel, dependencies = viewModel.dependencies] in viewModel?.transitionToScreen( SessionTableViewController( diff --git a/Session/Settings/ConversationSettingsViewModel.swift b/Session/Settings/ConversationSettingsViewModel.swift index 0419aa80fc..5b52afca21 100644 --- a/Session/Settings/ConversationSettingsViewModel.swift +++ b/Session/Settings/ConversationSettingsViewModel.swift @@ -39,16 +39,11 @@ class ConversationSettingsViewModel: SessionTableViewModel, NavigatableStateHold switch self { case .messageTrimming: return "conversationsMessageTrimming".localized() case .audioMessages: return "conversationsAudioMessages".localized() - case .blockedContacts: return nil + case .blockedContacts: return "conversationsBlockedContacts".localized() } } - var style: SessionTableSectionStyle { - switch self { - case .blockedContacts: return .padding - default: return .titleRoundedContent - } - } + var style: SessionTableSectionStyle { .titleRoundedContent } } // MARK: - Content @@ -192,9 +187,13 @@ class ConversationSettingsViewModel: SessionTableViewModel, NavigatableStateHold SessionCell.Info( id: .blockedContacts, title: "conversationsBlockedContacts".localized(), - styling: SessionCell.StyleInfo( - tintColor: .danger, - backgroundStyle: .noBackground + subtitle: "blockedContactsManageDescription".localized(), + trailingAccessory: .icon( + .chevronRight, + pinEdges: [.right] + ), + accessibility: Accessibility( + identifier: "Block contacts - Navigation" ), onTap: { [weak viewModel, dependencies = viewModel.dependencies] in viewModel?.transitionToScreen( diff --git a/Session/Settings/DeveloperSettings/DeveloperSettingsGroupsViewModel.swift b/Session/Settings/DeveloperSettings/DeveloperSettingsGroupsViewModel.swift new file mode 100644 index 0000000000..61d62d1034 --- /dev/null +++ b/Session/Settings/DeveloperSettings/DeveloperSettingsGroupsViewModel.swift @@ -0,0 +1,391 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +import Foundation +import Combine +import GRDB +import DifferenceKit +import SessionUIKit +import SessionNetworkingKit +import SessionMessagingKit +import SessionUtilitiesKit + +class DeveloperSettingsGroupsViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTableSource { + public let dependencies: Dependencies + public let navigatableState: NavigatableState = NavigatableState() + public let state: TableDataState = TableDataState() + public let observableState: ObservableTableSourceState = ObservableTableSourceState() + + /// This value is the current state of the view + @MainActor @Published private(set) var internalState: State + private var observationTask: Task? + + // MARK: - Initialization + + @MainActor init(using dependencies: Dependencies) { + self.dependencies = dependencies + self.internalState = State.initialState(using: dependencies) + + /// Bind the state + self.observationTask = ObservationBuilder + .initialValue(self.internalState) + .debounce(for: .never) + .using(dependencies: dependencies) + .query(DeveloperSettingsGroupsViewModel.queryState) + .assign { [weak self] updatedState in + guard let self = self else { return } + + // FIXME: To slightly reduce the size of the changes this new observation mechanism is currently wired into the old SessionTableViewController observation mechanism, we should refactor it so everything uses the new mechanism + let oldState: State = self.internalState + self.internalState = updatedState + self.pendingTableDataSubject.send(updatedState.sections(viewModel: self, previousState: oldState)) + } + } + + // MARK: - Config + + public enum Section: SessionTableSection { + case general + + var title: String? { + switch self { + case .general: return nil + } + } + + var style: SessionTableSectionStyle { + switch self { + case .general: return .padding + } + } + } + + public enum TableItem: Hashable, Differentiable, CaseIterable { + case updatedGroupsDisableAutoApprove + case updatedGroupsRemoveMessagesOnKick + case updatedGroupsAllowHistoricAccessOnInvite + case updatedGroupsAllowDisplayPicture + case updatedGroupsAllowDescriptionEditing + case updatedGroupsAllowPromotions + case updatedGroupsAllowInviteById + case updatedGroupsDeleteBeforeNow + case updatedGroupsDeleteAttachmentsBeforeNow + + // MARK: - Conformance + + public typealias DifferenceIdentifier = String + + public var differenceIdentifier: String { + switch self { + case .updatedGroupsDisableAutoApprove: return "updatedGroupsDisableAutoApprove" + case .updatedGroupsRemoveMessagesOnKick: return "updatedGroupsRemoveMessagesOnKick" + case .updatedGroupsAllowHistoricAccessOnInvite: return "updatedGroupsAllowHistoricAccessOnInvite" + case .updatedGroupsAllowDisplayPicture: return "updatedGroupsAllowDisplayPicture" + case .updatedGroupsAllowDescriptionEditing: return "updatedGroupsAllowDescriptionEditing" + case .updatedGroupsAllowPromotions: return "updatedGroupsAllowPromotions" + case .updatedGroupsAllowInviteById: return "updatedGroupsAllowInviteById" + case .updatedGroupsDeleteBeforeNow: return "updatedGroupsDeleteBeforeNow" + case .updatedGroupsDeleteAttachmentsBeforeNow: return "updatedGroupsDeleteAttachmentsBeforeNow" + } + } + + public func isContentEqual(to source: TableItem) -> Bool { + self.differenceIdentifier == source.differenceIdentifier + } + + public static var allCases: [TableItem] { + var result: [TableItem] = [] + switch TableItem.updatedGroupsDisableAutoApprove { + case .updatedGroupsDisableAutoApprove: result.append(.updatedGroupsDisableAutoApprove); fallthrough + case .updatedGroupsRemoveMessagesOnKick: result.append(.updatedGroupsRemoveMessagesOnKick); fallthrough + case .updatedGroupsAllowHistoricAccessOnInvite: + result.append(.updatedGroupsAllowHistoricAccessOnInvite); fallthrough + case .updatedGroupsAllowDisplayPicture: result.append(.updatedGroupsAllowDisplayPicture); fallthrough + case .updatedGroupsAllowDescriptionEditing: result.append(.updatedGroupsAllowDescriptionEditing); fallthrough + case .updatedGroupsAllowPromotions: result.append(.updatedGroupsAllowPromotions); fallthrough + case .updatedGroupsAllowInviteById: result.append(.updatedGroupsAllowInviteById); fallthrough + case .updatedGroupsDeleteBeforeNow: result.append(.updatedGroupsDeleteBeforeNow); fallthrough + case .updatedGroupsDeleteAttachmentsBeforeNow: result.append(.updatedGroupsDeleteAttachmentsBeforeNow) + } + + return result + } + } + + // MARK: - Content + + public struct State: Equatable, ObservableKeyProvider { + let updatedGroupsDisableAutoApprove: Bool + let updatedGroupsRemoveMessagesOnKick: Bool + let updatedGroupsAllowHistoricAccessOnInvite: Bool + let updatedGroupsAllowDisplayPicture: Bool + let updatedGroupsAllowDescriptionEditing: Bool + let updatedGroupsAllowPromotions: Bool + let updatedGroupsAllowInviteById: Bool + let updatedGroupsDeleteBeforeNow: Bool + let updatedGroupsDeleteAttachmentsBeforeNow: Bool + + @MainActor public func sections(viewModel: DeveloperSettingsGroupsViewModel, previousState: State) -> [SectionModel] { + DeveloperSettingsGroupsViewModel.sections( + state: self, + previousState: previousState, + viewModel: viewModel + ) + } + + public let observedKeys: Set = [ + .feature(.updatedGroupsDisableAutoApprove), + .feature(.updatedGroupsRemoveMessagesOnKick), + .feature(.updatedGroupsAllowHistoricAccessOnInvite), + .feature(.updatedGroupsAllowDisplayPicture), + .feature(.updatedGroupsAllowDescriptionEditing), + .feature(.updatedGroupsAllowPromotions), + .feature(.updatedGroupsAllowInviteById), + .feature(.updatedGroupsDeleteBeforeNow), + .feature(.updatedGroupsDeleteAttachmentsBeforeNow) + ] + + static func initialState(using dependencies: Dependencies) -> State { + return State( + updatedGroupsDisableAutoApprove: dependencies[feature: .updatedGroupsDisableAutoApprove], + updatedGroupsRemoveMessagesOnKick: dependencies[feature: .updatedGroupsRemoveMessagesOnKick], + updatedGroupsAllowHistoricAccessOnInvite: dependencies[feature: .updatedGroupsAllowHistoricAccessOnInvite], + updatedGroupsAllowDisplayPicture: dependencies[feature: .updatedGroupsAllowDisplayPicture], + updatedGroupsAllowDescriptionEditing: dependencies[feature: .updatedGroupsAllowDescriptionEditing], + updatedGroupsAllowPromotions: dependencies[feature: .updatedGroupsAllowPromotions], + updatedGroupsAllowInviteById: dependencies[feature: .updatedGroupsAllowInviteById], + updatedGroupsDeleteBeforeNow: dependencies[feature: .updatedGroupsDeleteBeforeNow], + updatedGroupsDeleteAttachmentsBeforeNow: dependencies[feature: .updatedGroupsDeleteAttachmentsBeforeNow] + ) + } + } + + let title: String = "Developer Group Settings" + + @Sendable private static func queryState( + previousState: State, + events: [ObservedEvent], + isInitialQuery: Bool, + using dependencies: Dependencies + ) async -> State { + return State( + updatedGroupsDisableAutoApprove: dependencies[feature: .updatedGroupsDisableAutoApprove], + updatedGroupsRemoveMessagesOnKick: dependencies[feature: .updatedGroupsRemoveMessagesOnKick], + updatedGroupsAllowHistoricAccessOnInvite: dependencies[feature: .updatedGroupsAllowHistoricAccessOnInvite], + updatedGroupsAllowDisplayPicture: dependencies[feature: .updatedGroupsAllowDisplayPicture], + updatedGroupsAllowDescriptionEditing: dependencies[feature: .updatedGroupsAllowDescriptionEditing], + updatedGroupsAllowPromotions: dependencies[feature: .updatedGroupsAllowPromotions], + updatedGroupsAllowInviteById: dependencies[feature: .updatedGroupsAllowInviteById], + updatedGroupsDeleteBeforeNow: dependencies[feature: .updatedGroupsDeleteBeforeNow], + updatedGroupsDeleteAttachmentsBeforeNow: dependencies[feature: .updatedGroupsDeleteAttachmentsBeforeNow] + ) + } + + private static func sections( + state: State, + previousState: State, + viewModel: DeveloperSettingsGroupsViewModel + ) -> [SectionModel] { + let general: SectionModel = SectionModel( + model: .general, + elements: [ + SessionCell.Info( + id: .updatedGroupsDisableAutoApprove, + title: "Disable Auto Approve", + subtitle: """ + Prevents a group from automatically getting approved if the admin is already approved. + + Note: The default behaviour is to automatically approve new groups if the admin that sent the invitation is an approved contact. + """, + trailingAccessory: .toggle( + state.updatedGroupsDisableAutoApprove, + oldValue: previousState.updatedGroupsDisableAutoApprove + ), + onTap: { [dependencies = viewModel.dependencies] in + dependencies.set( + feature: .updatedGroupsDisableAutoApprove, + to: !state.updatedGroupsDisableAutoApprove + ) + } + ), + SessionCell.Info( + id: .updatedGroupsRemoveMessagesOnKick, + title: "Remove Messages on Kick", + subtitle: """ + Controls whether a group members messages should be removed when they are kicked from an updated group. + + Note: In a future release we will offer this as an option when removing members but for the initial release it can be controlled via this flag for testing purposes. + """, + trailingAccessory: .toggle( + state.updatedGroupsRemoveMessagesOnKick, + oldValue: previousState.updatedGroupsRemoveMessagesOnKick + ), + onTap: { [dependencies = viewModel.dependencies] in + dependencies.set( + feature: .updatedGroupsRemoveMessagesOnKick, + to: !state.updatedGroupsRemoveMessagesOnKick + ) + } + ), + SessionCell.Info( + id: .updatedGroupsAllowHistoricAccessOnInvite, + title: "Allow Historic Message Access", + subtitle: """ + Controls whether members should be granted access to historic messages when invited to an updated group. + + Note: In a future release we will offer this as an option when inviting members but for the initial release it can be controlled via this flag for testing purposes. + """, + trailingAccessory: .toggle( + state.updatedGroupsAllowHistoricAccessOnInvite, + oldValue: previousState.updatedGroupsAllowHistoricAccessOnInvite + ), + onTap: { [dependencies = viewModel.dependencies] in + dependencies.set( + feature: .updatedGroupsAllowHistoricAccessOnInvite, + to: !state.updatedGroupsAllowHistoricAccessOnInvite + ) + } + ), + SessionCell.Info( + id: .updatedGroupsAllowDisplayPicture, + title: "Custom Display Pictures", + subtitle: """ + Controls whether the UI allows group admins to set a custom display picture for a group. + + Note: In a future release we will offer this functionality but for the initial release it may not be fully supported across platforms so can be controlled via this flag for testing purposes. + """, + trailingAccessory: .toggle( + state.updatedGroupsAllowDisplayPicture, + oldValue: previousState.updatedGroupsAllowDisplayPicture + ), + onTap: { [dependencies = viewModel.dependencies] in + dependencies.set( + feature: .updatedGroupsAllowDisplayPicture, + to: !state.updatedGroupsAllowDisplayPicture + ) + } + ), + SessionCell.Info( + id: .updatedGroupsAllowDescriptionEditing, + title: "Edit Group Descriptions", + subtitle: """ + Controls whether the UI allows group admins to modify the descriptions of updated groups. + + Note: In a future release we will offer this functionality but for the initial release it may not be fully supported across platforms so can be controlled via this flag for testing purposes. + """, + trailingAccessory: .toggle( + state.updatedGroupsAllowDescriptionEditing, + oldValue: previousState.updatedGroupsAllowDescriptionEditing + ), + onTap: { [dependencies = viewModel.dependencies] in + dependencies.set( + feature: .updatedGroupsAllowDescriptionEditing, + to: !state.updatedGroupsAllowDescriptionEditing + ) + } + ), + SessionCell.Info( + id: .updatedGroupsAllowPromotions, + title: "Allow Group Promotions", + subtitle: """ + Controls whether the UI allows group admins to promote other group members to admin within an updated group. + + Note: In a future release we will offer this functionality but for the initial release it may not be fully supported across platforms so can be controlled via this flag for testing purposes. + """, + trailingAccessory: .toggle( + state.updatedGroupsAllowPromotions, + oldValue: previousState.updatedGroupsAllowPromotions + ), + onTap: { [dependencies = viewModel.dependencies] in + dependencies.set( + feature: .updatedGroupsAllowPromotions, + to: !state.updatedGroupsAllowPromotions + ) + } + ), + SessionCell.Info( + id: .updatedGroupsAllowInviteById, + title: "Allow Invite by ID", + subtitle: """ + Controls whether the UI allows group admins to invite other group members directly by their Account ID. + + Note: In a future release we will offer this functionality but it's not included in the initial release. + """, + trailingAccessory: .toggle( + state.updatedGroupsAllowInviteById, + oldValue: previousState.updatedGroupsAllowInviteById + ), + onTap: { [dependencies = viewModel.dependencies] in + dependencies.set( + feature: .updatedGroupsAllowInviteById, + to: !state.updatedGroupsAllowInviteById + ) + } + ), + SessionCell.Info( + id: .updatedGroupsDeleteBeforeNow, + title: "Show button to delete messages before now", + subtitle: """ + Controls whether the UI allows group admins to delete all messages in the group that were sent before the button was pressed. + + Note: In a future release we will offer this functionality but it's not included in the initial release. + """, + trailingAccessory: .toggle( + state.updatedGroupsDeleteBeforeNow, + oldValue: previousState.updatedGroupsDeleteBeforeNow + ), + onTap: { [dependencies = viewModel.dependencies] in + dependencies.set( + feature: .updatedGroupsDeleteBeforeNow, + to: !state.updatedGroupsDeleteBeforeNow + ) + } + ), + SessionCell.Info( + id: .updatedGroupsDeleteAttachmentsBeforeNow, + title: "Show button to delete attachments before now", + subtitle: """ + Controls whether the UI allows group admins to delete all attachments (and their associated messages) in the group that were sent before the button was pressed. + + Note: In a future release we will offer this functionality but it's not included in the initial release. + """, + trailingAccessory: .toggle( + state.updatedGroupsDeleteAttachmentsBeforeNow, + oldValue: previousState.updatedGroupsDeleteAttachmentsBeforeNow + ), + onTap: { [dependencies = viewModel.dependencies] in + dependencies.set( + feature: .updatedGroupsDeleteAttachmentsBeforeNow, + to: !state.updatedGroupsDeleteAttachmentsBeforeNow + ) + } + ) + ] + ) + + return [general] + } + + // MARK: - Functions + + public static func disableDeveloperMode(using dependencies: Dependencies) { + let features: [FeatureConfig] = [ + .updatedGroupsDisableAutoApprove, + .updatedGroupsRemoveMessagesOnKick, + .updatedGroupsAllowHistoricAccessOnInvite, + .updatedGroupsAllowDisplayPicture, + .updatedGroupsAllowDescriptionEditing, + .updatedGroupsAllowPromotions, + .updatedGroupsAllowInviteById, + .updatedGroupsDeleteBeforeNow, + .updatedGroupsDeleteAttachmentsBeforeNow + ] + + features.forEach { feature in + guard dependencies.hasSet(feature: feature) else { return } + + dependencies.set(feature: feature, to: nil) + } + } +} diff --git a/Session/Settings/DeveloperSettings/DeveloperSettingsProViewModel.swift b/Session/Settings/DeveloperSettings/DeveloperSettingsProViewModel.swift new file mode 100644 index 0000000000..0eb37764ab --- /dev/null +++ b/Session/Settings/DeveloperSettings/DeveloperSettingsProViewModel.swift @@ -0,0 +1,439 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +import Foundation +import StoreKit +import Combine +import GRDB +import DifferenceKit +import SessionUIKit +import SessionNetworkingKit +import SessionMessagingKit +import SessionUtilitiesKit + +class DeveloperSettingsProViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTableSource { + public let dependencies: Dependencies + public let navigatableState: NavigatableState = NavigatableState() + public let state: TableDataState = TableDataState() + public let observableState: ObservableTableSourceState = ObservableTableSourceState() + + /// This value is the current state of the view + @MainActor @Published private(set) var internalState: State + private var observationTask: Task? + + // MARK: - Initialization + + @MainActor init(using dependencies: Dependencies) { + self.dependencies = dependencies + self.internalState = State.initialState(using: dependencies) + + /// Bind the state + self.observationTask = ObservationBuilder + .initialValue(self.internalState) + .debounce(for: .never) + .using(dependencies: dependencies) + .query(DeveloperSettingsProViewModel.queryState) + .assign { [weak self] updatedState in + guard let self = self else { return } + + // FIXME: To slightly reduce the size of the changes this new observation mechanism is currently wired into the old SessionTableViewController observation mechanism, we should refactor it so everything uses the new mechanism + let oldState: State = self.internalState + self.internalState = updatedState + self.pendingTableDataSubject.send(updatedState.sections(viewModel: self, previousState: oldState)) + } + } + + // MARK: - Config + + public enum Section: SessionTableSection { + case general + case subscriptions + case features + + var title: String? { + switch self { + case .general: return nil + case .subscriptions: return "Subscriptions" + case .features: return "Features" + } + } + + var style: SessionTableSectionStyle { + switch self { + case .general: return .padding + default: return .titleRoundedContent + } + } + } + + public enum TableItem: Hashable, Differentiable, CaseIterable { + case enableSessionPro + + case purchaseProSubscription + case manageProSubscriptions + case restoreProSubscription + + case proStatus + case proIncomingMessages + + // MARK: - Conformance + + public typealias DifferenceIdentifier = String + + public var differenceIdentifier: String { + switch self { + case .enableSessionPro: return "enableSessionPro" + + case .purchaseProSubscription: return "purchaseProSubscription" + case .manageProSubscriptions: return "manageProSubscriptions" + case .restoreProSubscription: return "restoreProSubscription" + + case .proStatus: return "proStatus" + case .proIncomingMessages: return "proIncomingMessages" + } + } + + public func isContentEqual(to source: TableItem) -> Bool { + self.differenceIdentifier == source.differenceIdentifier + } + + public static var allCases: [TableItem] { + var result: [TableItem] = [] + switch TableItem.enableSessionPro { + case .enableSessionPro: result.append(.enableSessionPro); fallthrough + + case .purchaseProSubscription: result.append(.purchaseProSubscription); fallthrough + case .manageProSubscriptions: result.append(.manageProSubscriptions); fallthrough + case .restoreProSubscription: result.append(.restoreProSubscription); fallthrough + + case .proStatus: result.append(.proStatus); fallthrough + case .proIncomingMessages: result.append(.proIncomingMessages) + } + + return result + } + } + + public enum DeveloperSettingsProEvent: Hashable { + case purchasedProduct([Product], Product?, String?, String?, UInt64?) + } + + // MARK: - Content + + public struct State: Equatable, ObservableKeyProvider { + let sessionProEnabled: Bool + + let products: [Product] + let purchasedProduct: Product? + let purchaseError: String? + let purchaseStatus: String? + let purchaseTransactionId: String? + + let mockCurrentUserSessionPro: Bool + let treatAllIncomingMessagesAsProMessages: Bool + + @MainActor public func sections(viewModel: DeveloperSettingsProViewModel, previousState: State) -> [SectionModel] { + DeveloperSettingsProViewModel.sections( + state: self, + previousState: previousState, + viewModel: viewModel + ) + } + + public let observedKeys: Set = [ + .feature(.sessionProEnabled), + .updateScreen(DeveloperSettingsProViewModel.self), + .feature(.mockCurrentUserSessionPro), + .feature(.treatAllIncomingMessagesAsProMessages) + ] + + static func initialState(using dependencies: Dependencies) -> State { + return State( + sessionProEnabled: dependencies[feature: .sessionProEnabled], + + products: [], + purchasedProduct: nil, + purchaseError: nil, + purchaseStatus: nil, + purchaseTransactionId: nil, + + mockCurrentUserSessionPro: dependencies[feature: .mockCurrentUserSessionPro], + treatAllIncomingMessagesAsProMessages: dependencies[feature: .treatAllIncomingMessagesAsProMessages] + ) + } + } + + let title: String = "Developer Pro Settings" + + @Sendable private static func queryState( + previousState: State, + events: [ObservedEvent], + isInitialQuery: Bool, + using dependencies: Dependencies + ) async -> State { + var products: [Product] = previousState.products + var purchasedProduct: Product? = previousState.purchasedProduct + var purchaseError: String? = previousState.purchaseError + var purchaseStatus: String? = previousState.purchaseStatus + var purchaseTransactionId: String? = previousState.purchaseTransactionId + + events.forEach { event in + guard let eventValue: DeveloperSettingsProEvent = event.value as? DeveloperSettingsProEvent else { return } + + switch eventValue { + case .purchasedProduct(let receivedProducts, let purchased, let error, let status, let id): + products = receivedProducts + purchasedProduct = purchased + purchaseError = error + purchaseStatus = status + purchaseTransactionId = id.map { "\($0)" } + } + } + + return State( + sessionProEnabled: dependencies[feature: .sessionProEnabled], + products: products, + purchasedProduct: purchasedProduct, + purchaseError: purchaseError, + purchaseStatus: purchaseStatus, + purchaseTransactionId: purchaseTransactionId, + mockCurrentUserSessionPro: dependencies[feature: .mockCurrentUserSessionPro], + treatAllIncomingMessagesAsProMessages: dependencies[feature: .treatAllIncomingMessagesAsProMessages] + ) + } + + private static func sections( + state: State, + previousState: State, + viewModel: DeveloperSettingsProViewModel + ) -> [SectionModel] { + let general: SectionModel = SectionModel( + model: .general, + elements: [ + SessionCell.Info( + id: .enableSessionPro, + title: "Enable Session Pro", + subtitle: """ + Enable Post Pro Release mode. + Turning on this Settings will show Pro badge and CTA if needed. + """, + trailingAccessory: .toggle( + state.sessionProEnabled, + oldValue: previousState.sessionProEnabled + ), + onTap: { [weak viewModel] in + viewModel?.updateSessionProEnabled(current: state.sessionProEnabled) + } + ) + ] + ) + + guard state.sessionProEnabled else { return [general] } + + let purchaseStatus: String = { + switch (state.purchaseError, state.purchaseStatus) { + case (.some(let error), _): return "\(error)" + case (_, .some(let status)): return "\(status)" + case (.none, .none): return "None" + } + }() + let productName: String = ( + state.purchasedProduct.map { "\($0.displayName)" } ?? + "N/A" + ) + let transactionId: String = ( + state.purchaseTransactionId.map { "\($0)" } ?? + "N/A" + ) + let subscriptions: SectionModel = SectionModel( + model: .subscriptions, + elements: [ + SessionCell.Info( + id: .purchaseProSubscription, + title: "Purchase Subscription", + subtitle: """ + Purchase Session Pro via the App Store. + + Status: \(purchaseStatus) + Product Name: \(productName) + TransactionId: \(transactionId) + """, + trailingAccessory: .highlightingBackgroundLabel(title: "Purchase"), + onTap: { [weak viewModel] in + Task { await viewModel?.purchaseSubscription() } + } + ), + SessionCell.Info( + id: .manageProSubscriptions, + title: "Manage Subscriptions", + subtitle: """ + Manage subscriptions for Session Pro via the App Store. + + Note: You must purchase a Session Pro subscription before you can manage it. + """, + trailingAccessory: .highlightingBackgroundLabel(title: "Manage"), + onTap: { [weak viewModel] in + Task { await viewModel?.manageSubscriptions() } + } + ), + SessionCell.Info( + id: .restoreProSubscription, + title: "Restore Subscriptions", + subtitle: """ + Restore a Session Pro subscription via the App Store. + """, + trailingAccessory: .highlightingBackgroundLabel(title: "Restore"), + onTap: { [weak viewModel] in + Task { await viewModel?.restoreSubscriptions() } + } + ) + ] + ) + + let features: SectionModel = SectionModel( + model: .features, + elements: [ + SessionCell.Info( + id: .proStatus, + title: "Pro Status", + subtitle: """ + Mock current user a Session Pro user locally. + """, + trailingAccessory: .toggle( + state.mockCurrentUserSessionPro, + oldValue: previousState.mockCurrentUserSessionPro + ), + onTap: { [dependencies = viewModel.dependencies] in + dependencies.set( + feature: .mockCurrentUserSessionPro, + to: !state.mockCurrentUserSessionPro + ) + } + ), + SessionCell.Info( + id: .proIncomingMessages, + title: "All Pro Incoming Messages", + subtitle: """ + Treat all incoming messages as Pro messages. + """, + trailingAccessory: .toggle( + state.treatAllIncomingMessagesAsProMessages, + oldValue: previousState.treatAllIncomingMessagesAsProMessages + ), + onTap: { [dependencies = viewModel.dependencies] in + dependencies.set( + feature: .treatAllIncomingMessagesAsProMessages, + to: !state.treatAllIncomingMessagesAsProMessages + ) + } + ) + ] + ) + + return [general, subscriptions, features] + } + + // MARK: - Functions + + public static func disableDeveloperMode(using dependencies: Dependencies) { + let features: [FeatureConfig] = [ + .sessionProEnabled, + .mockCurrentUserSessionPro, + .treatAllIncomingMessagesAsProMessages + ] + + features.forEach { feature in + guard dependencies.hasSet(feature: feature) else { return } + + dependencies.set(feature: feature, to: nil) + } + } + + private func updateSessionProEnabled(current: Bool) { + dependencies.set(feature: .sessionProEnabled, to: !current) + + if dependencies.hasSet(feature: .mockCurrentUserSessionPro) { + dependencies.set(feature: .mockCurrentUserSessionPro, to: nil) + } + + if dependencies.hasSet(feature: .treatAllIncomingMessagesAsProMessages) { + dependencies.set(feature: .treatAllIncomingMessagesAsProMessages, to: nil) + } + } + + private func purchaseSubscription() async { + do { + let products: [Product] = try await Product.products(for: ["com.getsession.org.pro_sub"]) + + guard let product: Product = products.first else { + Log.error("[DevSettings] Unable to purchase subscription due to error: No products found") + dependencies.notifyAsync( + key: .updateScreen(DeveloperSettingsProViewModel.self), + value: DeveloperSettingsProEvent.purchasedProduct([], nil, "No products found", nil, nil) + ) + return + } + + let result = try await product.purchase() + switch result { + case .success(let verificationResult): + let transaction = try verificationResult.payloadValue + dependencies.notifyAsync( + key: .updateScreen(DeveloperSettingsProViewModel.self), + value: DeveloperSettingsProEvent.purchasedProduct(products, product, nil, "Successful", transaction.id) + ) + await transaction.finish() + + case .pending: + dependencies.notifyAsync( + key: .updateScreen(DeveloperSettingsProViewModel.self), + value: DeveloperSettingsProEvent.purchasedProduct(products, product, nil, "Pending approval", nil) + ) + + case .userCancelled: + dependencies.notifyAsync( + key: .updateScreen(DeveloperSettingsProViewModel.self), + value: DeveloperSettingsProEvent.purchasedProduct(products, product, nil, "User cancelled", nil) + ) + + @unknown default: + dependencies.notifyAsync( + key: .updateScreen(DeveloperSettingsProViewModel.self), + value: DeveloperSettingsProEvent.purchasedProduct(products, product, "Unknown Error", nil, nil) + ) + } + + } + catch { + Log.error("[DevSettings] Unable to purchase subscription due to error: \(error)") + dependencies.notifyAsync( + key: .updateScreen(DeveloperSettingsProViewModel.self), + value: DeveloperSettingsProEvent.purchasedProduct([], nil, "Failed: \(error)", nil, nil) + ) + } + } + + private func manageSubscriptions() async { + guard let scene: UIWindowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else { + return Log.error("[DevSettings] Unable to show manage subscriptions: Unable to get UIWindowScene") + } + + do { + try await AppStore.showManageSubscriptions(in: scene) + print("AS") + } + catch { + Log.error("[DevSettings] Unable to show manage subscriptions: \(error)") + } + } + + private func restoreSubscriptions() async { + do { + try await AppStore.sync() + } + catch { + Log.error("[DevSettings] Unable to show manage subscriptions: \(error)") + } + } +} diff --git a/Session/Settings/DeveloperSettingsViewModel+Testing.swift b/Session/Settings/DeveloperSettings/DeveloperSettingsViewModel+Testing.swift similarity index 87% rename from Session/Settings/DeveloperSettingsViewModel+Testing.swift rename to Session/Settings/DeveloperSettings/DeveloperSettingsViewModel+Testing.swift index 185528af9c..09381d297d 100644 --- a/Session/Settings/DeveloperSettingsViewModel+Testing.swift +++ b/Session/Settings/DeveloperSettings/DeveloperSettingsViewModel+Testing.swift @@ -60,6 +60,11 @@ extension DeveloperSettingsViewModel { /// /// **Value:** `true`/`false` (default: `false`) case debugDisappearingMessageDurations + + /// Controls the number of messages that the CommunityPoller should try to retrieve every time it polls + /// + /// **Value:** `1-256` (default: `100`, a value of `0` will use the default) + case communityPollLimit } ProcessInfo.processInfo.environment.forEach { key, value in @@ -94,6 +99,14 @@ extension DeveloperSettingsViewModel { case .debugDisappearingMessageDurations: dependencies.set(feature: .debugDisappearingMessageDurations, to: (value == "true")) + + case .communityPollLimit: + guard + let intValue: Int = Int(value), + intValue >= 1 && intValue < 256 + else { return } + + dependencies.set(feature: .communityPollLimit, to: intValue) } } #endif diff --git a/Session/Settings/DeveloperSettingsViewModel.swift b/Session/Settings/DeveloperSettings/DeveloperSettingsViewModel.swift similarity index 78% rename from Session/Settings/DeveloperSettingsViewModel.swift rename to Session/Settings/DeveloperSettings/DeveloperSettingsViewModel.swift index e61c5570c0..dd611e73eb 100644 --- a/Session/Settings/DeveloperSettingsViewModel.swift +++ b/Session/Settings/DeveloperSettings/DeveloperSettingsViewModel.swift @@ -8,7 +8,7 @@ import Compression import GRDB import DifferenceKit import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionMessagingKit import SessionUtilitiesKit import SignalUtilitiesKit @@ -35,25 +35,27 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, public enum Section: SessionTableSection { case developerMode - case sessionPro case sessionNetwork + case sessionPro + case groups case general case logging case network case disappearingMessages - case groups + case communities case database var title: String? { switch self { case .developerMode: return nil - case .sessionPro: return "Session Pro" case .sessionNetwork: return "Session Network" + case .sessionPro: return "Session Pro" + case .groups: return "Groups" case .general: return "General" case .logging: return "Logging" case .network: return "Network" case .disappearingMessages: return "Disappearing Messages" - case .groups: return "Groups" + case .communities: return "Communities" case .database: return "Database" } } @@ -69,18 +71,17 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, public enum TableItem: Hashable, Differentiable, CaseIterable { case developerMode - case enableSessionPro - case proStatus - case proIncomingMessages - - case versionBlindedID - case scheduleLocalNotification + case proConfig + case groupConfig + case shortenFileTTL case animationsEnabled case showStringKeys case truncatePubkeysInLogs case copyDocumentsPath case copyAppGroupPath + case resetAppReviewPrompt + case simulateAppReviewLimit case defaultLogLevel case advancedLogging @@ -93,15 +94,10 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, case debugDisappearingMessageDurations - case updatedGroupsDisableAutoApprove - case updatedGroupsRemoveMessagesOnKick - case updatedGroupsAllowHistoricAccessOnInvite - case updatedGroupsAllowDisplayPicture - case updatedGroupsAllowDescriptionEditing - case updatedGroupsAllowPromotions - case updatedGroupsAllowInviteById - case updatedGroupsDeleteBeforeNow - case updatedGroupsDeleteAttachmentsBeforeNow + case communityPollLimit + + case versionBlindedID + case scheduleLocalNotification case createMockContacts case forceSlowDatabaseQueries @@ -115,11 +111,18 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, public var differenceIdentifier: String { switch self { case .developerMode: return "developerMode" + + case .proConfig: return "proConfig" + case .groupConfig: return "groupConfig" + + case .shortenFileTTL: return "shortenFileTTL" case .animationsEnabled: return "animationsEnabled" case .showStringKeys: return "showStringKeys" case .truncatePubkeysInLogs: return "truncatePubkeysInLogs" case .copyDocumentsPath: return "copyDocumentsPath" case .copyAppGroupPath: return "copyAppGroupPath" + case .resetAppReviewPrompt: return "resetAppReviewPrompt" + case .simulateAppReviewLimit: return "simulateAppReviewLimit" case .defaultLogLevel: return "defaultLogLevel" case .advancedLogging: return "advancedLogging" @@ -131,23 +134,11 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, case .pushNotificationService: return "pushNotificationService" case .debugDisappearingMessageDurations: return "debugDisappearingMessageDurations" - - case .updatedGroupsDisableAutoApprove: return "updatedGroupsDisableAutoApprove" - case .updatedGroupsRemoveMessagesOnKick: return "updatedGroupsRemoveMessagesOnKick" - case .updatedGroupsAllowHistoricAccessOnInvite: return "updatedGroupsAllowHistoricAccessOnInvite" - case .updatedGroupsAllowDisplayPicture: return "updatedGroupsAllowDisplayPicture" - case .updatedGroupsAllowDescriptionEditing: return "updatedGroupsAllowDescriptionEditing" - case .updatedGroupsAllowPromotions: return "updatedGroupsAllowPromotions" - case .updatedGroupsAllowInviteById: return "updatedGroupsAllowInviteById" - case .updatedGroupsDeleteBeforeNow: return "updatedGroupsDeleteBeforeNow" - case .updatedGroupsDeleteAttachmentsBeforeNow: return "updatedGroupsDeleteAttachmentsBeforeNow" + + case .communityPollLimit: return "communityPollLimit" case .versionBlindedID: return "versionBlindedID" case .scheduleLocalNotification: return "scheduleLocalNotification" - - case .enableSessionPro: return "enableSessionPro" - case .proStatus: return "proStatus" - case .proIncomingMessages: return "proIncomingMessages" case .createMockContacts: return "createMockContacts" case .forceSlowDatabaseQueries: return "forceSlowDatabaseQueries" @@ -164,11 +155,18 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, var result: [TableItem] = [] switch TableItem.developerMode { case .developerMode: result.append(.developerMode); fallthrough + + case .proConfig: result.append(.proConfig); fallthrough + case .groupConfig: result.append(.groupConfig); fallthrough + + case .shortenFileTTL: result.append(.shortenFileTTL); fallthrough case .animationsEnabled: result.append(.animationsEnabled); fallthrough case .showStringKeys: result.append(.showStringKeys); fallthrough case .truncatePubkeysInLogs: result.append(.truncatePubkeysInLogs); fallthrough case .copyDocumentsPath: result.append(.copyDocumentsPath); fallthrough case .copyAppGroupPath: result.append(.copyAppGroupPath); fallthrough + case .resetAppReviewPrompt: result.append(.resetAppReviewPrompt); fallthrough + case .simulateAppReviewLimit: result.append(.simulateAppReviewLimit); fallthrough case .defaultLogLevel: result.append(.defaultLogLevel); fallthrough case .advancedLogging: result.append(.advancedLogging); fallthrough @@ -181,24 +179,11 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, case .debugDisappearingMessageDurations: result.append(.debugDisappearingMessageDurations); fallthrough - case .updatedGroupsDisableAutoApprove: result.append(.updatedGroupsDisableAutoApprove); fallthrough - case .updatedGroupsRemoveMessagesOnKick: result.append(.updatedGroupsRemoveMessagesOnKick); fallthrough - case .updatedGroupsAllowHistoricAccessOnInvite: - result.append(.updatedGroupsAllowHistoricAccessOnInvite); fallthrough - case .updatedGroupsAllowDisplayPicture: result.append(.updatedGroupsAllowDisplayPicture); fallthrough - case .updatedGroupsAllowDescriptionEditing: result.append(.updatedGroupsAllowDescriptionEditing); fallthrough - case .updatedGroupsAllowPromotions: result.append(.updatedGroupsAllowPromotions); fallthrough - case .updatedGroupsAllowInviteById: result.append(.updatedGroupsAllowInviteById); fallthrough - case .updatedGroupsDeleteBeforeNow: result.append(.updatedGroupsDeleteBeforeNow); fallthrough - case .updatedGroupsDeleteAttachmentsBeforeNow: result.append(.updatedGroupsDeleteAttachmentsBeforeNow); fallthrough + case .communityPollLimit: result.append(.communityPollLimit); fallthrough case .versionBlindedID: result.append(.versionBlindedID); fallthrough case .scheduleLocalNotification: result.append(.scheduleLocalNotification); fallthrough - case .enableSessionPro: result.append(.enableSessionPro); fallthrough - case .proStatus: result.append(.proStatus); fallthrough - case .proIncomingMessages: result.append(.proIncomingMessages); fallthrough - case .createMockContacts: result.append(.createMockContacts); fallthrough case .forceSlowDatabaseQueries: result.append(.forceSlowDatabaseQueries); fallthrough case .exportDatabase: result.append(.exportDatabase); fallthrough @@ -215,6 +200,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, let developerMode: Bool let versionBlindedID: String? + let shortenFileTTL: Bool let animationsEnabled: Bool let showStringKeys: Bool let truncatePubkeysInLogs: Bool @@ -225,25 +211,15 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, let serviceNetwork: ServiceNetwork let forceOffline: Bool - let pushNotificationService: PushNotificationAPI.Service + let pushNotificationService: Network.PushNotification.Service let debugDisappearingMessageDurations: Bool - let updatedGroupsDisableAutoApprove: Bool - let updatedGroupsRemoveMessagesOnKick: Bool - let updatedGroupsAllowHistoricAccessOnInvite: Bool - let updatedGroupsAllowDisplayPicture: Bool - let updatedGroupsAllowDescriptionEditing: Bool - let updatedGroupsAllowPromotions: Bool - let updatedGroupsAllowInviteById: Bool - let updatedGroupsDeleteBeforeNow: Bool - let updatedGroupsDeleteAttachmentsBeforeNow: Bool - - let sessionProEnabled: Bool - let mockCurrentUserSessionPro: Bool - let treatAllIncomingMessagesAsProMessages: Bool + let communityPollLimit: Int let forceSlowDatabaseQueries: Bool + + let updateSimulateAppReviewLimit: Bool } let title: String = "Developer Settings" @@ -268,6 +244,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, cache.get(.developerModeEnabled) }, versionBlindedID: versionBlindedID, + shortenFileTTL: dependencies[feature: .shortenFileTTL], animationsEnabled: dependencies[feature: .animationsEnabled], showStringKeys: dependencies[feature: .showStringKeys], truncatePubkeysInLogs: dependencies[feature: .truncatePubkeysInLogs], @@ -282,21 +259,10 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, debugDisappearingMessageDurations: dependencies[feature: .debugDisappearingMessageDurations], - updatedGroupsDisableAutoApprove: dependencies[feature: .updatedGroupsDisableAutoApprove], - updatedGroupsRemoveMessagesOnKick: dependencies[feature: .updatedGroupsRemoveMessagesOnKick], - updatedGroupsAllowHistoricAccessOnInvite: dependencies[feature: .updatedGroupsAllowHistoricAccessOnInvite], - updatedGroupsAllowDisplayPicture: dependencies[feature: .updatedGroupsAllowDisplayPicture], - updatedGroupsAllowDescriptionEditing: dependencies[feature: .updatedGroupsAllowDescriptionEditing], - updatedGroupsAllowPromotions: dependencies[feature: .updatedGroupsAllowPromotions], - updatedGroupsAllowInviteById: dependencies[feature: .updatedGroupsAllowInviteById], - updatedGroupsDeleteBeforeNow: dependencies[feature: .updatedGroupsDeleteBeforeNow], - updatedGroupsDeleteAttachmentsBeforeNow: dependencies[feature: .updatedGroupsDeleteAttachmentsBeforeNow], - - sessionProEnabled: dependencies[feature: .sessionProEnabled], - mockCurrentUserSessionPro: dependencies[feature: .mockCurrentUserSessionPro], - treatAllIncomingMessagesAsProMessages: dependencies[feature: .treatAllIncomingMessagesAsProMessages], + communityPollLimit: dependencies[feature: .communityPollLimit], - forceSlowDatabaseQueries: dependencies[feature: .forceSlowDatabaseQueries] + forceSlowDatabaseQueries: dependencies[feature: .forceSlowDatabaseQueries], + updateSimulateAppReviewLimit: dependencies[feature: .simulateAppReviewLimit] ) } .compactMapWithPrevious { [weak self] prev, current -> [SectionModel]? in self?.content(prev, current) } @@ -327,9 +293,66 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, ) ] ) + let sessionPro: SectionModel = SectionModel( + model: .sessionPro, + elements: [ + SessionCell.Info( + id: .proConfig, + title: "Session Pro", + subtitle: """ + Configure settings related to Session Pro. + + Session Pro: \(dependencies[feature: .sessionProEnabled] ? "Enabled" : "Disabled") + """, + trailingAccessory: .icon(.chevronRight), + onTap: { [weak self, dependencies] in + self?.transitionToScreen( + SessionTableViewController( + viewModel: DeveloperSettingsProViewModel(using: dependencies) + ) + ) + } + ) + ] + ) + let groups: SectionModel = SectionModel( + model: .groups, + elements: [ + SessionCell.Info( + id: .groupConfig, + title: "Group Configuration", + subtitle: """ + Configure settings related to Groups. + """, + trailingAccessory: .icon(.chevronRight), + onTap: { [weak self, dependencies] in + self?.transitionToScreen( + SessionTableViewController( + viewModel: DeveloperSettingsGroupsViewModel(using: dependencies) + ) + ) + } + ) + ] + ) let general: SectionModel = SectionModel( model: .general, elements: [ + SessionCell.Info( + id: .shortenFileTTL, + title: "Shorten File TTL", + subtitle: "Set the TTL for files in the cache to 1 minute", + trailingAccessory: .toggle( + current.shortenFileTTL, + oldValue: previous?.shortenFileTTL + ), + onTap: { [weak self] in + self?.updateFlag( + for: .shortenFileTTL, + to: !current.shortenFileTTL + ) + } + ), SessionCell.Info( id: .animationsEnabled, title: "Animations Enabled", @@ -408,7 +431,35 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, onTap: { [weak self] in self?.copyAppGroupPath() } - ) + ), + SessionCell.Info( + id: .resetAppReviewPrompt, + title: "Reset App Review Prompt", + subtitle: """ + Clears user default settings for the app review prompt, enabling quicker testing of various display conditions. + """, + trailingAccessory: .highlightingBackgroundLabel(title: "Reset"), + onTap: { [weak self] in + self?.resetAppReviewPrompt() + } + ), + SessionCell.Info( + id: .simulateAppReviewLimit, + title: "Simulate App Review Limit", + subtitle: """ + Controls whether the in-app rating prompt is displayed. This can will simulate a rate limit, preventing the prompt from appearing. + """, + trailingAccessory: .toggle( + current.updateSimulateAppReviewLimit, + oldValue: previous?.updateSimulateAppReviewLimit + ), + onTap: { [weak self] in + self?.updateFlag( + for: .simulateAppReviewLimit, + to: !current.updateSimulateAppReviewLimit + ) + } + ), ] ) let logging: SectionModel = SectionModel( @@ -545,9 +596,9 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, onTap: { [weak self, dependencies] in self?.transitionToScreen( SessionTableViewController( - viewModel: SessionListViewModel( + viewModel: SessionListViewModel( title: "Push Notification Service", - options: PushNotificationAPI.Service.allCases, + options: Network.PushNotification.Service.allCases, behaviour: .autoDismiss( initialSelection: current.pushNotificationService, onOptionSelected: self?.updatePushNotificationService @@ -584,178 +635,28 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, ) ] ) - let groups: SectionModel = SectionModel( - model: .groups, + let communities: SectionModel = SectionModel( + model: .communities, elements: [ SessionCell.Info( - id: .updatedGroupsDisableAutoApprove, - title: "Disable Auto Approve", - subtitle: """ - Prevents a group from automatically getting approved if the admin is already approved. - - Note: The default behaviour is to automatically approve new groups if the admin that sent the invitation is an approved contact. - """, - trailingAccessory: .toggle( - current.updatedGroupsDisableAutoApprove, - oldValue: previous?.updatedGroupsDisableAutoApprove - ), - onTap: { [weak self] in - self?.updateFlag( - for: .updatedGroupsDisableAutoApprove, - to: !current.updatedGroupsDisableAutoApprove - ) - } - ), - SessionCell.Info( - id: .updatedGroupsRemoveMessagesOnKick, - title: "Remove Messages on Kick", - subtitle: """ - Controls whether a group members messages should be removed when they are kicked from an updated group. - - Note: In a future release we will offer this as an option when removing members but for the initial release it can be controlled via this flag for testing purposes. - """, - trailingAccessory: .toggle( - current.updatedGroupsRemoveMessagesOnKick, - oldValue: previous?.updatedGroupsRemoveMessagesOnKick - ), - onTap: { [weak self] in - self?.updateFlag( - for: .updatedGroupsRemoveMessagesOnKick, - to: !current.updatedGroupsRemoveMessagesOnKick - ) - } - ), - SessionCell.Info( - id: .updatedGroupsAllowHistoricAccessOnInvite, - title: "Allow Historic Message Access", + id: .communityPollLimit, + title: "Community Poll Limit", subtitle: """ - Controls whether members should be granted access to historic messages when invited to an updated group. + The number of messages to try to retrieve when polling a community (up to a maximum of 256). - Note: In a future release we will offer this as an option when inviting members but for the initial release it can be controlled via this flag for testing purposes. + Note: An empty value, or a value of 0 will use the default value: \(dependencies.defaultValue(feature: .communityPollLimit).map { "\($0)"} ?? "N/A"). """, - trailingAccessory: .toggle( - current.updatedGroupsAllowHistoricAccessOnInvite, - oldValue: previous?.updatedGroupsAllowHistoricAccessOnInvite - ), - onTap: { [weak self] in - self?.updateFlag( - for: .updatedGroupsAllowHistoricAccessOnInvite, - to: !current.updatedGroupsAllowHistoricAccessOnInvite - ) - } - ), - SessionCell.Info( - id: .updatedGroupsAllowDisplayPicture, - title: "Custom Display Pictures", - subtitle: """ - Controls whether the UI allows group admins to set a custom display picture for a group. - - Note: In a future release we will offer this functionality but for the initial release it may not be fully supported across platforms so can be controlled via this flag for testing purposes. - """, - trailingAccessory: .toggle( - current.updatedGroupsAllowDisplayPicture, - oldValue: previous?.updatedGroupsAllowDisplayPicture - ), - onTap: { [weak self] in - self?.updateFlag( - for: .updatedGroupsAllowDisplayPicture, - to: !current.updatedGroupsAllowDisplayPicture - ) - } - ), - SessionCell.Info( - id: .updatedGroupsAllowDescriptionEditing, - title: "Edit Group Descriptions", - subtitle: """ - Controls whether the UI allows group admins to modify the descriptions of updated groups. - - Note: In a future release we will offer this functionality but for the initial release it may not be fully supported across platforms so can be controlled via this flag for testing purposes. - """, - trailingAccessory: .toggle( - current.updatedGroupsAllowDescriptionEditing, - oldValue: previous?.updatedGroupsAllowDescriptionEditing - ), - onTap: { [weak self] in - self?.updateFlag( - for: .updatedGroupsAllowDescriptionEditing, - to: !current.updatedGroupsAllowDescriptionEditing - ) - } - ), - SessionCell.Info( - id: .updatedGroupsAllowPromotions, - title: "Allow Group Promotions", - subtitle: """ - Controls whether the UI allows group admins to promote other group members to admin within an updated group. - - Note: In a future release we will offer this functionality but for the initial release it may not be fully supported across platforms so can be controlled via this flag for testing purposes. - """, - trailingAccessory: .toggle( - current.updatedGroupsAllowPromotions, - oldValue: previous?.updatedGroupsAllowPromotions - ), - onTap: { [weak self] in - self?.updateFlag( - for: .updatedGroupsAllowPromotions, - to: !current.updatedGroupsAllowPromotions - ) - } - ), - SessionCell.Info( - id: .updatedGroupsAllowInviteById, - title: "Allow Invite by ID", - subtitle: """ - Controls whether the UI allows group admins to invite other group members directly by their Account ID. - - Note: In a future release we will offer this functionality but it's not included in the initial release. - """, - trailingAccessory: .toggle( - current.updatedGroupsAllowInviteById, - oldValue: previous?.updatedGroupsAllowInviteById - ), - onTap: { [weak self] in - self?.updateFlag( - for: .updatedGroupsAllowInviteById, - to: !current.updatedGroupsAllowInviteById - ) - } - ), - SessionCell.Info( - id: .updatedGroupsDeleteBeforeNow, - title: "Show button to delete messages before now", - subtitle: """ - Controls whether the UI allows group admins to delete all messages in the group that were sent before the button was pressed. - - Note: In a future release we will offer this functionality but it's not included in the initial release. - """, - trailingAccessory: .toggle( - current.updatedGroupsDeleteBeforeNow, - oldValue: previous?.updatedGroupsDeleteBeforeNow - ), - onTap: { [weak self] in - self?.updateFlag( - for: .updatedGroupsDeleteBeforeNow, - to: !current.updatedGroupsDeleteBeforeNow - ) - } - ), - SessionCell.Info( - id: .updatedGroupsDeleteAttachmentsBeforeNow, - title: "Show button to delete attachments before now", - subtitle: """ - Controls whether the UI allows group admins to delete all attachments (and their associated messages) in the group that were sent before the button was pressed. - - Note: In a future release we will offer this functionality but it's not included in the initial release. - """, - trailingAccessory: .toggle( - current.updatedGroupsDeleteAttachmentsBeforeNow, - oldValue: previous?.updatedGroupsDeleteAttachmentsBeforeNow - ), - onTap: { [weak self] in - self?.updateFlag( - for: .updatedGroupsDeleteAttachmentsBeforeNow, - to: !current.updatedGroupsDeleteAttachmentsBeforeNow - ) + trailingAccessory: .custom(info: PollLimitInputView.Info( + limit: dependencies[feature: .communityPollLimit], + onChange: { [dependencies] value in + dependencies.set(feature: .communityPollLimit, to: value) + } + )), + onTapView: { view in + view?.subviews + .flatMap { $0.subviews } + .first(where: { $0 is UITextField })? + .becomeFirstResponder() } ) ] @@ -823,63 +724,6 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, ) ] ) - let sessionPro: SectionModel = SectionModel( - model: .sessionPro, - elements: [ - SessionCell.Info( - id: .enableSessionPro, - title: "Enable Session Pro", - subtitle: """ - Enable Post Pro Release mode. - Turning on this Settings will show Pro badge and CTA if needed. - """, - trailingAccessory: .toggle( - current.sessionProEnabled, - oldValue: previous?.sessionProEnabled - ), - onTap: { [weak self] in - self?.updateSessionProEnabled(current: current.sessionProEnabled) - } - ) - ].appending( - contentsOf: current.sessionProEnabled ? [ - SessionCell.Info( - id: .proStatus, - title: "Pro Status", - subtitle: """ - Mock current user a Session Pro user locally. - """, - trailingAccessory: .toggle( - current.mockCurrentUserSessionPro, - oldValue: previous?.mockCurrentUserSessionPro - ), - onTap: { [weak self] in - self?.updateFlag( - for: .mockCurrentUserSessionPro, - to: !current.mockCurrentUserSessionPro - ) - } - ), - SessionCell.Info( - id: .proIncomingMessages, - title: "All Pro Incoming Messages", - subtitle: """ - Treat all incoming messages as Pro messages. - """, - trailingAccessory: .toggle( - current.treatAllIncomingMessagesAsProMessages, - oldValue: previous?.treatAllIncomingMessagesAsProMessages - ), - onTap: { [weak self] in - self?.updateFlag( - for: .treatAllIncomingMessagesAsProMessages, - to: !current.treatAllIncomingMessagesAsProMessages - ) - } - ) - ] : nil - ) - ) let sessionNetwork: SectionModel = SectionModel( model: .sessionNetwork, elements: [ @@ -918,12 +762,13 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, return [ developerMode, + sessionPro, + groups, general, logging, network, disappearingMessages, - groups, - sessionPro, + communities, sessionNetwork, database ] @@ -936,10 +781,19 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, /// then we will get a compile error if it doesn't get resetting instructions added) TableItem.allCases.forEach { item in switch item { - case .developerMode: break // Not a feature - case .versionBlindedID: break // Not a feature - case .scheduleLocalNotification: break // Not a feature - + case .developerMode, .versionBlindedID, .scheduleLocalNotification, .copyDocumentsPath, + .copyAppGroupPath, .resetSnodeCache, .createMockContacts, .exportDatabase, + .importDatabase, .advancedLogging, .resetAppReviewPrompt: + break /// These are actions rather than values stored as "features" so no need to do anything + + case .groupConfig: DeveloperSettingsGroupsViewModel.disableDeveloperMode(using: dependencies) + case .proConfig: DeveloperSettingsProViewModel.disableDeveloperMode(using: dependencies) + + case .shortenFileTTL: + guard dependencies.hasSet(feature: .shortenFileTTL) else { return } + + updateFlag(for: .shortenFileTTL, to: nil) + case .animationsEnabled: guard dependencies.hasSet(feature: .animationsEnabled) else { return } @@ -955,13 +809,10 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, updateFlag(for: .truncatePubkeysInLogs, to: nil) - case .copyDocumentsPath: break // Not a feature - case .copyAppGroupPath: break // Not a feature - case .resetSnodeCache: break // Not a feature - case .createMockContacts: break // Not a feature - case .exportDatabase: break // Not a feature - case .importDatabase: break // Not a feature - case .advancedLogging: break // Not a feature + case .simulateAppReviewLimit: + guard dependencies.hasSet(feature: .simulateAppReviewLimit) else { return } + + updateFlag(for: .simulateAppReviewLimit, to: nil) case .defaultLogLevel: updateDefaulLogLevel(to: nil) // Always reset case .loggingCategory: resetLoggingCategories() // Always reset @@ -986,71 +837,11 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, updateFlag(for: .debugDisappearingMessageDurations, to: nil) - case .updatedGroupsDisableAutoApprove: - guard dependencies.hasSet(feature: .updatedGroupsDisableAutoApprove) else { return } - - updateFlag(for: .updatedGroupsDisableAutoApprove, to: nil) - - case .updatedGroupsRemoveMessagesOnKick: - guard dependencies.hasSet(feature: .updatedGroupsRemoveMessagesOnKick) else { return } - - updateFlag(for: .updatedGroupsRemoveMessagesOnKick, to: nil) - - case .updatedGroupsAllowHistoricAccessOnInvite: - guard dependencies.hasSet(feature: .updatedGroupsAllowHistoricAccessOnInvite) else { - return - } - - updateFlag(for: .updatedGroupsAllowHistoricAccessOnInvite, to: nil) - - case .updatedGroupsAllowDisplayPicture: - guard dependencies.hasSet(feature: .updatedGroupsAllowDisplayPicture) else { return } - - updateFlag(for: .updatedGroupsAllowDisplayPicture, to: nil) - - case .updatedGroupsAllowDescriptionEditing: - guard dependencies.hasSet(feature: .updatedGroupsAllowDescriptionEditing) else { return } - - updateFlag(for: .updatedGroupsAllowDescriptionEditing, to: nil) - - case .updatedGroupsAllowPromotions: - guard dependencies.hasSet(feature: .updatedGroupsAllowPromotions) else { return } - - updateFlag(for: .updatedGroupsAllowPromotions, to: nil) - - case .updatedGroupsAllowInviteById: - guard dependencies.hasSet(feature: .updatedGroupsAllowInviteById) else { return } - - updateFlag(for: .updatedGroupsAllowInviteById, to: nil) - - case .updatedGroupsDeleteBeforeNow: - guard dependencies.hasSet(feature: .updatedGroupsDeleteBeforeNow) else { return } - - updateFlag(for: .updatedGroupsDeleteBeforeNow, to: nil) - - case .updatedGroupsDeleteAttachmentsBeforeNow: - guard dependencies.hasSet(feature: .updatedGroupsDeleteAttachmentsBeforeNow) else { - return - } - - updateFlag(for: .updatedGroupsDeleteAttachmentsBeforeNow, to: nil) - - case .enableSessionPro: - guard dependencies.hasSet(feature: .sessionProEnabled) else { return } + case .communityPollLimit: + guard dependencies.hasSet(feature: .communityPollLimit) else { return } - updateFlag(for: .sessionProEnabled, to: nil) - - case .proStatus: - guard dependencies.hasSet(feature: .mockCurrentUserSessionPro) else { return } - - updateFlag(for: .mockCurrentUserSessionPro, to: nil) - - case .proIncomingMessages: - guard dependencies.hasSet(feature: .treatAllIncomingMessagesAsProMessages) else { - return - } - - updateFlag(for: .treatAllIncomingMessagesAsProMessages, to: nil) + dependencies.set(feature: .communityPollLimit, to: nil) + forceRefresh(type: .databaseQuery) case .forceSlowDatabaseQueries: guard dependencies.hasSet(feature: .forceSlowDatabaseQueries) else { return } @@ -1094,7 +885,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, forceRefresh(type: .databaseQuery) } - private func updatePushNotificationService(to updatedService: PushNotificationAPI.Service?) { + private func updatePushNotificationService(to updatedService: Network.PushNotification.Service?) { guard dependencies[defaults: .standard, key: .isUsingFullAPNs], updatedService != dependencies[feature: .pushNotificationService] @@ -1183,7 +974,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, /// Unsubscribe from push notifications (do this after resetting the network as they are server requests so aren't dependant on a service /// layer and we don't want these to be cancelled) if let existingToken: String = dependencies[singleton: .storage].read({ db in db[.lastRecordedPushToken] }) { - PushNotificationAPI + Network.PushNotification .unsubscribeAll(token: Data(hex: existingToken), using: dependencies) .sinkUntilComplete() } @@ -1260,16 +1051,6 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, forceRefresh(type: .databaseQuery) } - private func updateSessionProEnabled(current: Bool) { - updateFlag(for: .sessionProEnabled, to: !current) - if dependencies.hasSet(feature: .mockCurrentUserSessionPro) { - updateFlag(for: .mockCurrentUserSessionPro, to: nil) - } - if dependencies.hasSet(feature: .treatAllIncomingMessagesAsProMessages) { - updateFlag(for: .treatAllIncomingMessagesAsProMessages, to: nil) - } - } - private func updateForceOffline(current: Bool) { updateFlag(for: .forceOffline, to: !current) @@ -1430,6 +1211,20 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, ) } + private func resetAppReviewPrompt() { + dependencies[defaults: .standard, key: .didShowAppReviewPrompt] = false + dependencies[defaults: .standard, key: .hasVisitedPathScreen] = false + dependencies[defaults: .standard, key: .hasPressedDonateButton] = false + dependencies[defaults: .standard, key: .hasChangedTheme] = false + dependencies[defaults: .standard, key: .rateAppRetryDate] = nil + dependencies[defaults: .standard, key: .rateAppRetryAttemptCount] = 0 + + showToast( + text: "Cleared", + backgroundColor: .backgroundSecondary + ) + } + // MARK: - SESH private func scheduleLocalNotification(button: SessionButton?) { @@ -1908,14 +1703,104 @@ private class DocumentPickerResult: NSObject, UIDocumentPickerDelegate { } } +// MARK: - PollLimitInputView + +final class PollLimitInputView: UIView, UITextFieldDelegate, SessionCell.Accessory.CustomView { + struct Info: Equatable, SessionCell.Accessory.CustomViewInfo { + typealias View = PollLimitInputView + + let limit: Int + let onChange: (Int?) -> Void + + public static func ==(lhs: Info, rhs: Info) -> Bool { + return lhs.limit == rhs.limit + } + + public func hash(into hasher: inout Hasher) { + limit.hash(into: &hasher) + } + } + + static func create(maxContentWidth: CGFloat, using dependencies: Dependencies) -> PollLimitInputView { + return PollLimitInputView() + } + + public static let size: SessionCell.Accessory.Size = .fillWidthWrapHeight + private var onChange: ((Int?) -> Void)? + + // MARK: - Components + + private lazy var textField: UITextField = { + let result = UITextField() + result.font = .systemFont(ofSize: Values.mediumFontSize) + result.textAlignment = .center + result.delegate = self + + return result + }() + + // MARK: - Initializtion + + init() { + super.init(frame: .zero) + + setupUI() + } + + required init?(coder: NSCoder) { + fatalError("Use init(color:) instead") + } + + // MARK: - Layout + + private func setupUI() { + layer.borderWidth = 1 + layer.cornerRadius = 8 + themeBackgroundColor = .backgroundPrimary + themeBorderColor = .borderSeparator + + addSubview(textField) + textField.pin(to: self, withInset: Values.verySmallSpacing) + } + + // MARK: - Content + + func update(with info: Info) { + onChange = info.onChange + textField.text = "\(info.limit)" + } + + // MARK: - UITextFieldDelegate + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + let currentText: String = (textField.text ?? "") + + guard let textRange: Range = Range(range, in: currentText) else { return false } + + let updatedText: String = currentText.replacingCharacters(in: textRange, with: string) + + // Allow an empty string (revert to the default in this case) + guard !updatedText.isEmpty else { + onChange?(nil) + return true + } + guard let value: Int = Int(updatedText) else { return false } + guard value >= 0 && value < 256 else { return false } + + onChange?(value) + return true + } +} + + // MARK: - Listable Conformance extension ServiceNetwork: @retroactive ContentIdentifiable {} extension ServiceNetwork: @retroactive ContentEquatable {} extension ServiceNetwork: Listable {} -extension PushNotificationAPI.Service: @retroactive ContentIdentifiable {} -extension PushNotificationAPI.Service: @retroactive ContentEquatable {} -extension PushNotificationAPI.Service: Listable {} +extension Network.PushNotification.Service: @retroactive ContentIdentifiable {} +extension Network.PushNotification.Service: @retroactive ContentEquatable {} +extension Network.PushNotification.Service: Listable {} extension Log.Level: @retroactive ContentIdentifiable {} extension Log.Level: @retroactive ContentEquatable {} extension Log.Level: Listable {} diff --git a/Session/Settings/HelpViewModel.swift b/Session/Settings/HelpViewModel.swift index ba9d899f8c..4ab69ce0d1 100644 --- a/Session/Settings/HelpViewModel.swift +++ b/Session/Settings/HelpViewModel.swift @@ -76,7 +76,8 @@ class HelpViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTa trailingAccessory: .icon( UIImage(systemName: "arrow.up.forward.app")? .withRenderingMode(.alwaysTemplate), - size: .small + size: .small, + pinEdges: [.right] ), onTap: { guard let url: URL = URL(string: "https://getsession.org/translate") else { @@ -97,7 +98,8 @@ class HelpViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTa trailingAccessory: .icon( UIImage(systemName: "arrow.up.forward.app")? .withRenderingMode(.alwaysTemplate), - size: .small + size: .small, + pinEdges: [.right] ), onTap: { guard let url: URL = URL(string: "https://getsession.org/survey") else { @@ -118,7 +120,8 @@ class HelpViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTa trailingAccessory: .icon( UIImage(systemName: "arrow.up.forward.app")? .withRenderingMode(.alwaysTemplate), - size: .small + size: .small, + pinEdges: [.right] ), onTap: { guard let url: URL = URL(string: "https://getsession.org/faq") else { @@ -139,7 +142,8 @@ class HelpViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTa trailingAccessory: .icon( UIImage(systemName: "arrow.up.forward.app")? .withRenderingMode(.alwaysTemplate), - size: .small + size: .small, + pinEdges: [.right] ), onTap: { guard let url: URL = URL(string: "https://sessionapp.zendesk.com/hc/en-us") else { diff --git a/Session/Settings/NukeDataModal.swift b/Session/Settings/NukeDataModal.swift index 4ade2b621d..ffc90bcd3d 100644 --- a/Session/Settings/NukeDataModal.swift +++ b/Session/Settings/NukeDataModal.swift @@ -4,7 +4,7 @@ import UIKit import Combine import GRDB import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionMessagingKit import SignalUtilitiesKit import SessionUtilitiesKit @@ -201,7 +201,7 @@ final class NukeDataModal: Modal { try communityAuth.compactMap { authMethod in switch authMethod.info { case .community(let server, _, _, _, _): - return try OpenGroupAPI.preparedClearInbox( + return try Network.SOGS.preparedClearInbox( requestAndPathBuildTimeout: Network.defaultTimeout, authMethod: authMethod, using: dependencies @@ -218,7 +218,7 @@ final class NukeDataModal: Modal { .eraseToAnyPublisher() } .tryFlatMap { authMethod, clearedServers in - try SnodeAPI + try Network.SnodeAPI .preparedDeleteAllMessages( namespace: .all, requestAndPathBuildTimeout: Network.defaultTimeout, @@ -296,7 +296,7 @@ final class NukeDataModal: Modal { UIApplication.shared.unregisterForRemoteNotifications() if let deviceToken: String = maybeDeviceToken, dependencies[singleton: .storage].isValid { - PushNotificationAPI + Network.PushNotification .unsubscribeAll(token: Data(hex: deviceToken), using: dependencies) .sinkUntilComplete() } diff --git a/Session/Settings/PrivacySettingsViewModel.swift b/Session/Settings/PrivacySettingsViewModel.swift index 6653336dd1..66eaf0e948 100644 --- a/Session/Settings/PrivacySettingsViewModel.swift +++ b/Session/Settings/PrivacySettingsViewModel.swift @@ -12,7 +12,6 @@ import SessionUtilitiesKit class PrivacySettingsViewModel: SessionTableViewModel, NavigationItemSource, NavigatableStateHolder, ObservableTableSource { public let dependencies: Dependencies public let navigatableState: NavigatableState = NavigatableState() - public let editableState: EditableState = EditableState() public let state: TableDataState = TableDataState() public let observableState: ObservableTableSourceState = ObservableTableSourceState() private let shouldShowCloseButton: Bool @@ -233,7 +232,9 @@ class PrivacySettingsViewModel: SessionTableViewModel, NavigationItemSource, Nav ), confirmationInfo: ConfirmationModal.Info( title: "callsVoiceAndVideoBeta".localized(), - body: .text("callsVoiceAndVideoModalDescription".localized()), + body: .text("callsVoiceAndVideoModalDescription" + .put(key: "session_foundation", value: Constants.session_foundation) + .localized()), showCondition: .disabled, confirmTitle: "theContinue".localized(), confirmStyle: .danger, @@ -457,35 +458,7 @@ class PrivacySettingsViewModel: SessionTableViewModel, NavigationItemSource, Nav subtitle: SessionCell.TextInfo( "typingIndicatorsDescription".localized(), font: .subtitle, - extraViewGenerator: { - let targetHeight: CGFloat = 20 - let targetWidth: CGFloat = ceil(20 * (targetHeight / 12)) - let result: UIView = UIView( - frame: CGRect(x: 0, y: 0, width: targetWidth, height: targetHeight) - ) - result.set(.width, to: targetWidth) - result.set(.height, to: targetHeight) - - // Use a transform scale to reduce the size of the typing indicator to the - // desired size (this way the animation remains intact) - let cell: TypingIndicatorCell = TypingIndicatorCell() - cell.transform = CGAffineTransform( - scaleX: targetHeight / cell.bounds.height, - y: targetHeight / cell.bounds.height - ) - cell.typingIndicatorView.startAnimation() - result.addSubview(cell) - - // Note: Because we are messing with the transform these values don't work - // logically so we inset the positioning to make it look visually centered - // within the layout inspector - cell.center(.vertical, in: result, withInset: -(targetHeight * 0.15)) - cell.center(.horizontal, in: result, withInset: -(targetWidth * 0.35)) - cell.set(.width, to: .width, of: result) - cell.set(.height, to: .height, of: result) - - return result - } + extraViewGenerator: { TypingIndicatorPreviewView() } ), trailingAccessory: .toggle( state.typingIndicatorsEnabled, @@ -532,7 +505,9 @@ class PrivacySettingsViewModel: SessionTableViewModel, NavigationItemSource, Nav let confirmationModal: ConfirmationModal = ConfirmationModal( info: ConfirmationModal.Info( title: "callsVoiceAndVideoBeta".localized(), - body: .text("callsVoiceAndVideoModalDescription".localized()), + body: .text("callsVoiceAndVideoModalDescription" + .put(key: "session_foundation", value: Constants.session_foundation) + .localized()), showCondition: .disabled, confirmTitle: "theContinue".localized(), confirmStyle: .danger, @@ -547,3 +522,67 @@ class PrivacySettingsViewModel: SessionTableViewModel, NavigationItemSource, Nav } } } + +// MARK: - Info + +private final class TypingIndicatorPreviewView: UIView { + static var size: CGSize = CGSize(width: 24, height: 14) + + // MARK: - Components + + private lazy var bubbleView: UIView = { + let result: UIView = UIView() + result.layer.cornerRadius = VisibleMessageCell.smallCornerRadius + result.layer.mask = bubbleViewMaskLayer + result.themeBackgroundColor = .messageBubble_incomingBackground + + return result + }() + + private let bubbleViewMaskLayer: CAShapeLayer = { + let result: CAShapeLayer = CAShapeLayer() + let maskPath: UIBezierPath = UIBezierPath( + roundedRect: CGRect(origin: .zero, size: TypingIndicatorPreviewView.size), + byRoundingCorners: .allCorners, + cornerRadii: CGSize( + width: VisibleMessageCell.largeCornerRadius, + height: VisibleMessageCell.largeCornerRadius + ) + ) + + result.path = maskPath.cgPath + + return result + }() + public lazy var typingIndicatorView: TypingIndicatorView = TypingIndicatorView() + + // MARK: - Initialization + + init() { + super.init(frame: .zero) + + setupLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Layout + + private func setupLayout() { + addSubview(bubbleView) + bubbleView.addSubview(typingIndicatorView) + + set(.width, to: TypingIndicatorPreviewView.size.width) + set(.height, to: TypingIndicatorPreviewView.size.height) + + bubbleView.pin(to: self) + typingIndicatorView.center(in: bubbleView) + + // Use a transform scale to reduce the size of the typing indicator to the + // desired size (this way the animation remains intact) + typingIndicatorView.transform = CGAffineTransform(scaleX: 0.4, y: 0.4) + typingIndicatorView.startAnimation() + } +} diff --git a/Session/Settings/RecoveryPasswordScreen.swift b/Session/Settings/RecoveryPasswordScreen.swift index 603878d164..1fd3034852 100644 --- a/Session/Settings/RecoveryPasswordScreen.swift +++ b/Session/Settings/RecoveryPasswordScreen.swift @@ -66,11 +66,15 @@ struct RecoveryPasswordScreen: View { } .padding(.bottom, Values.smallSpacing) - Text("recoveryPasswordDescription".localized()) - .font(.system(size: Values.smallFontSize)) - .foregroundColor(themeColor: .textPrimary) - .padding(.bottom, Values.mediumSpacing) - .fixedSize(horizontal: false, vertical: true) + AttributedText( + "recoveryPasswordDescription".localizedFormatted( + baseFont: .systemFont(ofSize: Values.smallFontSize) + ) + ) + .font(.system(size: Values.smallFontSize)) + .foregroundColor(themeColor: .textPrimary) + .padding(.bottom, Values.mediumSpacing) + .fixedSize(horizontal: false, vertical: true) if self.showQRCode { QRCodeView( @@ -210,6 +214,8 @@ struct RecoveryPasswordScreen: View { alignment: .leading ) + Spacer() + Button { hideRecoveryPassword() } label: { @@ -218,9 +224,13 @@ struct RecoveryPasswordScreen: View { .font(.system(size: Values.verySmallFontSize)) .foregroundColor(themeColor: .danger) .frame( - width: 55, height: Values.mediumSmallButtonHeight ) + .frame( + minWidth: Values.alertButtonHeight, + alignment: .center + ) + .padding(.horizontal, Values.smallSpacing) .overlay( Capsule() .stroke(themeColor: .danger) diff --git a/Session/Settings/SessionNetworkScreen/SessionNetworkScreen+ViewModel.swift b/Session/Settings/SessionNetworkScreen/SessionNetworkScreen+ViewModel.swift index 2446f852f2..dfefd70e69 100644 --- a/Session/Settings/SessionNetworkScreen/SessionNetworkScreen+ViewModel.swift +++ b/Session/Settings/SessionNetworkScreen/SessionNetworkScreen+ViewModel.swift @@ -5,7 +5,7 @@ import SwiftUI import Combine import GRDB import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit import SessionMessagingKit @@ -78,8 +78,8 @@ extension SessionNetworkScreenContent { self.isRefreshing.toggle() self.lastRefreshWasSuccessful = false - SessionNetworkAPI.client.getInfo(using: dependencies) - .subscribe(on: SessionNetworkAPI.workQueue, using: dependencies) + Network.SessionNetwork.client.getInfo(using: dependencies) + .subscribe(on: Network.SessionNetwork.workQueue, using: dependencies) .receive(on: DispatchQueue.main) .sink( receiveCompletion: { _ in }, diff --git a/Session/Settings/SettingsViewModel.swift b/Session/Settings/SettingsViewModel.swift index d5bd3b0d26..9daf4ee913 100644 --- a/Session/Settings/SettingsViewModel.swift +++ b/Session/Settings/SettingsViewModel.swift @@ -285,8 +285,7 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl title: SessionCell.TextInfo( state.profile.displayName(), font: .titleLarge, - alignment: .center, - interaction: .editable + alignment: .center ), trailingAccessory: .icon( .pencil, @@ -652,59 +651,118 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl private func updateProfilePicture(currentUrl: String?) { let iconName: String = "profile_placeholder" // stringlint:ignore + var hasSetNewProfilePicture: Bool = false + let body: ConfirmationModal.Info.Body = .image( + source: nil, + placeholder: currentUrl + .map { try? dependencies[singleton: .displayPictureManager].path(for: $0) } + .map { ImageDataManager.DataSource.url(URL(fileURLWithPath: $0)) } + .defaulting(to: Lucide.image(icon: .image, size: 40).map { image in + ImageDataManager.DataSource.image( + iconName, + image + .withTintColor(#colorLiteral(red: 0.631372549, green: 0.6352941176, blue: 0.631372549, alpha: 1), renderingMode: .alwaysTemplate) + .withCircularBackground(backgroundColor: #colorLiteral(red: 0.1764705882, green: 0.1764705882, blue: 0.1764705882, alpha: 1)) + ) + }), + icon: (currentUrl != nil ? .pencil : .rightPlus), + style: .circular, + description: { + guard dependencies[feature: .sessionProEnabled] else { return nil } + return dependencies[cache: .libSession].isSessionPro ? + "proAnimatedDisplayPictureModalDescription" + .localized() + .addProBadge( + at: .leading, + font: .systemFont(ofSize: Values.smallFontSize), + textColor: .textSecondary, + proBadgeSize: .small + ): + "proAnimatedDisplayPicturesNonProModalDescription" + .localized() + .addProBadge( + at: .trailing, + font: .systemFont(ofSize: Values.smallFontSize), + textColor: .textSecondary, + proBadgeSize: .small + ) + }(), + accessibility: Accessibility( + identifier: "Upload", + label: "Upload" + ), + dataManager: dependencies[singleton: .imageDataManager], + onProBageTapped: { [weak self] in + self?.showSessionProCTAIfNeeded() + }, + onClick: { [weak self] onDisplayPictureSelected in + self?.onDisplayPictureSelected = { valueUpdate in + onDisplayPictureSelected(valueUpdate) + hasSetNewProfilePicture = true + } + self?.showPhotoLibraryForAvatar() + } + ) self.transitionToScreen( ConfirmationModal( info: ConfirmationModal.Info( title: "profileDisplayPictureSet".localized(), - body: .image( - source: currentUrl - .map { try? dependencies[singleton: .displayPictureManager].path(for: $0) } - .map { ImageDataManager.DataSource.url(URL(fileURLWithPath: $0)) }, - placeholder: UIImage(named: iconName).map { - ImageDataManager.DataSource.image(iconName, $0) - }, - icon: .rightPlus, - style: .circular, - accessibility: Accessibility( - identifier: "Upload", - label: "Upload" - ), - dataManager: dependencies[singleton: .imageDataManager], - onClick: { [weak self] onDisplayPictureSelected in - self?.onDisplayPictureSelected = onDisplayPictureSelected - self?.showPhotoLibraryForAvatar() - } - ), + body: body, confirmTitle: "save".localized(), confirmEnabled: .afterChange { info in switch info.body { - case .image(let source, _, _, _, _, _, _): return (source?.imageData != nil) + case .image(let source, _, _, _, _, _, _, _, _): return (source?.imageData != nil) default: return false } }, cancelTitle: "remove".localized(), - cancelEnabled: .bool(currentUrl != nil), + cancelEnabled: (currentUrl != nil) ? .bool(true) : .afterChange { info in + switch info.body { + case .image(let source, _, _, _, _, _, _, _, _): return (source?.imageData != nil) + default: return false + } + }, hasCloseButton: true, dismissOnConfirm: false, - onConfirm: { [weak self] modal in + onConfirm: { [weak self, dependencies] modal in switch modal.info.body { - case .image(.some(let source), _, _, _, _, _, _): + case .image(.some(let source), _, _, _, _, _, _, _, _): guard let imageData: Data = source.imageData else { return } - + + let isAnimatedImage: Bool = ImageDataManager.isAnimatedImage(imageData) + guard ( + !isAnimatedImage || + dependencies[cache: .libSession].isSessionPro || + !dependencies[feature: .sessionProEnabled] + ) else { + self?.showSessionProCTAIfNeeded() + return + } + self?.updateProfile( - displayPictureUpdate: .currentUserUploadImageData(imageData), + displayPictureUpdate: .currentUserUploadImageData(data: imageData, isReupload: false), onComplete: { [weak modal] in modal?.close() } ) - + default: modal.close() } }, onCancel: { [weak self] modal in - self?.updateProfile( - displayPictureUpdate: .currentUserRemove, - onComplete: { [weak modal] in modal?.close() } - ) + if hasSetNewProfilePicture { + modal.updateContent( + with: modal.info.with( + body: body, + cancelTitle: "remove".localized() + ) + ) + hasSetNewProfilePicture = false + } else { + self?.updateProfile( + displayPictureUpdate: .currentUserRemove, + onComplete: { [weak modal] in modal?.close() } + ) + } } ) ), @@ -712,6 +770,21 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl ) } + @discardableResult func showSessionProCTAIfNeeded() -> Bool { + guard dependencies[feature: .sessionProEnabled] else { return false } + let sessionProModal: ModalHostingViewController = ModalHostingViewController( + modal: ProCTAModal( + delegate: dependencies[singleton: .sessionProState], + variant: .animatedProfileImage( + isSessionProActivated: dependencies[cache: .libSession].isSessionPro + ), + dataManager: dependencies[singleton: .imageDataManager] + ) + ) + self.transitionToScreen(sessionProModal, transitionType: .present) + return true + } + @MainActor private func showPhotoLibraryForAvatar() { Permissions.requestLibraryPermissionIfNeeded(isSavingMedia: false, using: dependencies) { [weak self] in DispatchQueue.main.async { @@ -849,6 +922,11 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl ) self.transitionToScreen(modal, transitionType: .present) + + // Mark app review flag that donate button was tapped + if !dependencies[defaults: .standard, key: .hasPressedDonateButton] { + dependencies[defaults: .standard, key: .hasPressedDonateButton] = true + } } private func openTokenUrl() { diff --git a/Session/Settings/Views/AppIconGridView.swift b/Session/Settings/Views/AppIconGridView.swift index b135e5b13c..d5b9da3d89 100644 --- a/Session/Settings/Views/AppIconGridView.swift +++ b/Session/Settings/Views/AppIconGridView.swift @@ -24,7 +24,11 @@ final class AppIconGridView: UIView { private let contentView: UIView = UIView() private lazy var iconViews: [IconView] = icons.map { icon in - IconView(icon: icon) { [weak self] in self?.onChange?(icon) } + let view = IconView(icon: icon) { [weak self] in self?.onChange?(icon) } + view.accessibilityIdentifier = icon.accessibilityIdentifier + view.isAccessibilityElement = true + + return view } // MARK: - Initializtion diff --git a/Session/Settings/Views/NewTagView.swift b/Session/Settings/Views/NewTagView.swift index f50daca9d8..416f0ae49b 100644 --- a/Session/Settings/Views/NewTagView.swift +++ b/Session/Settings/Views/NewTagView.swift @@ -34,7 +34,7 @@ final class NewTagView: UIView { private func setupUI() { addSubview(newTagLabel) - newTagLabel.pin(.leading, to: .leading, of: self, withInset: -Values.mediumSpacing + Values.verySmallSpacing) + newTagLabel.pin(.leading, to: .leading, of: self, withInset: -Values.mediumSmallSpacing) newTagLabel.pin([ UIView.VerticalEdge.top, UIView.VerticalEdge.bottom, UIView.HorizontalEdge.trailing ], to: self) } diff --git a/Session/Settings/Views/ThemeMessagePreviewView.swift b/Session/Settings/Views/ThemeMessagePreviewView.swift index 166bbab9f7..e82f5b444a 100644 --- a/Session/Settings/Views/ThemeMessagePreviewView.swift +++ b/Session/Settings/Views/ThemeMessagePreviewView.swift @@ -13,9 +13,8 @@ final class ThemeMessagePreviewView: UIView { // MARK: - Components private lazy var incomingMessagePreview: UIView = { - let result: VisibleMessageCell = VisibleMessageCell() - result.translatesAutoresizingMaskIntoConstraints = true - result.update( + let cell: VisibleMessageCell = VisibleMessageCell() + cell.update( with: MessageViewModel( variant: .standardIncoming, body: "appearancePreview2".localized(), @@ -31,20 +30,17 @@ final class ThemeMessagePreviewView: UIView { showExpandedReactions: false, shouldExpanded: false, lastSearchText: nil, + tableSize: UIScreen.main.bounds.size, using: dependencies ) + cell.contentHStack.removeFromSuperview() - // Remove built-in padding - result.authorLabelTopConstraint.constant = 0 - result.contentViewLeadingConstraint1.constant = 0 - - return result + return cell.contentHStack }() private lazy var outgoingMessagePreview: UIView = { - let result: VisibleMessageCell = VisibleMessageCell() - result.translatesAutoresizingMaskIntoConstraints = true - result.update( + let cell: VisibleMessageCell = VisibleMessageCell() + cell.update( with: MessageViewModel( variant: .standardOutgoing, body: "appearancePreview3".localized(), @@ -55,14 +51,12 @@ final class ThemeMessagePreviewView: UIView { showExpandedReactions: false, shouldExpanded: false, lastSearchText: nil, + tableSize: UIScreen.main.bounds.size, using: dependencies ) + cell.contentHStack.removeFromSuperview() - // Remove built-in padding - result.authorLabelTopConstraint.constant = 0 - result.contentViewTrailingConstraint1.constant = 0 - - return result + return cell.contentHStack }() // MARK: - Initializtion @@ -91,8 +85,10 @@ final class ThemeMessagePreviewView: UIView { private func setupLayout() { incomingMessagePreview.pin(.top, to: .top, of: self) incomingMessagePreview.pin(.leading, to: .leading, of: self) + incomingMessagePreview.pin(.trailing, to: .trailing, of: self) outgoingMessagePreview.pin(.top, to: .bottom, of: incomingMessagePreview, withInset: Values.mediumSpacing) + outgoingMessagePreview.pin(.leading, to: .leading, of: self) outgoingMessagePreview.pin(.trailing, to: .trailing, of: self) outgoingMessagePreview.pin(.bottom, to: .bottom, of: self) } diff --git a/Session/Shared/SessionTableViewController.swift b/Session/Shared/SessionTableViewController.swift index 439421c5e3..087091c28f 100644 --- a/Session/Shared/SessionTableViewController.swift +++ b/Session/Shared/SessionTableViewController.swift @@ -95,6 +95,8 @@ class SessionTableViewController: BaseVC, UITableViewDataSource, UITa result.dataSource = self result.delegate = self result.sectionHeaderTopPadding = 0 + result.rowHeight = UITableView.automaticDimension + result.estimatedRowHeight = 56 // Approximate size of an [{Icon} {Text}] SessionCell return result }() @@ -362,34 +364,6 @@ class SessionTableViewController: BaseVC, UITableViewDataSource, UITa disposables: &disposables ) - (viewModel as? ErasedEditableStateHolder)?.isEditing - .receive(on: DispatchQueue.main) - .sink { [weak self, weak tableView] isEditing in - UIView.animate(withDuration: 0.25) { - self?.setEditing(isEditing, animated: true) - - tableView?.visibleCells - .compactMap { $0 as? SessionCell } - .filter { $0.interactionMode == .editable || $0.interactionMode == .alwaysEditing } - .enumerated() - .forEach { index, cell in - cell.update( - isEditing: (isEditing || cell.interactionMode == .alwaysEditing), - becomeFirstResponder: ( - isEditing && - index == 0 && - cell.interactionMode != .alwaysEditing - ), - animated: true - ) - } - - tableView?.beginUpdates() - tableView?.endUpdates() - } - } - .store(in: &disposables) - viewModel.bannerInfo .receive(on: DispatchQueue.main) .sink { [weak self] info in @@ -480,8 +454,7 @@ class SessionTableViewController: BaseVC, UITableViewDataSource, UITa UIView.setAnimationsEnabled(false) cell.setNeedsLayout() cell.layoutIfNeeded() - tableView.beginUpdates() - tableView.endUpdates() + tableView.performBatchUpdates(nil) // Only re-enable animations if the feature flag isn't disabled if dependencies[feature: .animationsEnabled] { UIView.setAnimationsEnabled(true) @@ -489,21 +462,6 @@ class SessionTableViewController: BaseVC, UITableViewDataSource, UITa }, using: viewModel.dependencies ) - cell.update( - isEditing: (self.isEditing || (info.title?.interaction == .alwaysEditing)), - becomeFirstResponder: false, - animated: false - ) - - switch viewModel { - case let editableStateHolder as ErasedEditableStateHolder: - cell.textPublisher - .sink(receiveValue: { [weak editableStateHolder] text in - editableStateHolder?.textChanged(text, for: info.id) - }) - .store(in: &cell.disposables) - default: break - } case (let cell as FullConversationCell, let threadInfo as SessionCell.Info): cell.accessibilityIdentifier = info.accessibility?.identifier @@ -553,14 +511,6 @@ class SessionTableViewController: BaseVC, UITableViewDataSource, UITa return (section.model.footer == nil ? 0 : UITableView.automaticDimension) } - func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { - return UITableView.automaticDimension - } - - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - return UITableView.automaticDimension - } - func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) { guard self.hasLoadedInitialTableData && self.viewHasAppeared && !self.isLoadingMore else { return } diff --git a/Session/Shared/Types/EditableState.swift b/Session/Shared/Types/EditableState.swift deleted file mode 100644 index 873e9da2b5..0000000000 --- a/Session/Shared/Types/EditableState.swift +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. - -import Foundation -import Combine -import DifferenceKit -import SessionUtilitiesKit - -// MARK: - EditableStateHolder - -public protocol EditableStateHolder: AnyObject, TableData, ErasedEditableStateHolder { - var editableState: EditableState { get } -} - -public extension EditableStateHolder { - var textChanged: AnyPublisher<(text: String?, item: TableItem), Never> { editableState.textChanged } - - func setIsEditing(_ isEditing: Bool) { - editableState._isEditing.send(isEditing) - } - - func textChanged(_ text: String?, for item: TableItem) { - editableState._textChanged.send((text, item)) - } -} - -// MARK: - ErasedEditableStateHolder - -public protocol ErasedEditableStateHolder: AnyObject { - var isEditing: AnyPublisher { get } - - func setIsEditing(_ isEditing: Bool) - func textChanged(_ text: String?, for item: Item) -} - -public extension ErasedEditableStateHolder { - var isEditing: AnyPublisher { Just(false).eraseToAnyPublisher() } - - func setIsEditing(_ isEditing: Bool) {} - func textChanged(_ text: String?, for item: Item) {} -} - -public extension ErasedEditableStateHolder where Self: EditableStateHolder { - var isEditing: AnyPublisher { editableState.isEditing } - - func setIsEditing(_ isEditing: Bool) { - editableState._isEditing.send(isEditing) - } - - func textChanged(_ text: String?, for item: Item) { - guard let convertedItem: TableItem = item as? TableItem else { return } - - editableState._textChanged.send((text, convertedItem)) - } -} - -// MARK: - EditableState - -public struct EditableState { - let isEditing: AnyPublisher - let textChanged: AnyPublisher<(text: String?, item: TableItem), Never> - - // MARK: - Internal Variables - - fileprivate let _isEditing: CurrentValueSubject = CurrentValueSubject(false) - fileprivate let _textChanged: PassthroughSubject<(text: String?, item: TableItem), Never> = PassthroughSubject() - - // MARK: - Initialization - - init() { - self.isEditing = _isEditing - .removeDuplicates() - .shareReplay(1) - self.textChanged = _textChanged - .eraseToAnyPublisher() - } -} diff --git a/Session/Shared/Types/SessionCell+Accessory.swift b/Session/Shared/Types/SessionCell+Accessory.swift index ed8d6b0a97..63025785d7 100644 --- a/Session/Shared/Types/SessionCell+Accessory.swift +++ b/Session/Shared/Types/SessionCell+Accessory.swift @@ -40,6 +40,7 @@ public extension SessionCell.Accessory { size: IconSize = .medium, customTint: ThemeValue? = nil, shouldFill: Bool = false, + pinEdges: [UIView.HorizontalEdge] = [.leading, .trailing], accessibility: Accessibility? = nil ) -> SessionCell.Accessory { return SessionCell.AccessoryConfig.Icon( @@ -48,6 +49,7 @@ public extension SessionCell.Accessory { iconSize: size, customTint: customTint, shouldFill: shouldFill, + pinEdges: pinEdges, accessibility: accessibility ) } @@ -57,6 +59,7 @@ public extension SessionCell.Accessory { size: IconSize = .medium, customTint: ThemeValue? = nil, shouldFill: Bool = false, + pinEdges: [UIView.HorizontalEdge] = [.leading, .trailing], accessibility: Accessibility? = nil ) -> SessionCell.Accessory { return SessionCell.AccessoryConfig.Icon( @@ -65,6 +68,7 @@ public extension SessionCell.Accessory { iconSize: size, customTint: customTint, shouldFill: shouldFill, + pinEdges: pinEdges, accessibility: accessibility ) } @@ -74,6 +78,7 @@ public extension SessionCell.Accessory { source: ImageDataManager.DataSource?, customTint: ThemeValue? = nil, shouldFill: Bool = false, + pinEdges: [UIView.HorizontalEdge] = [.leading, .trailing], accessibility: Accessibility? = nil ) -> SessionCell.Accessory { return SessionCell.AccessoryConfig.IconAsync( @@ -81,6 +86,7 @@ public extension SessionCell.Accessory { source: source, customTint: customTint, shouldFill: shouldFill, + pinEdges: pinEdges, accessibility: accessibility ) } @@ -226,6 +232,7 @@ public extension SessionCell.AccessoryConfig { public let iconSize: IconSize public let customTint: ThemeValue? public let shouldFill: Bool + public let pinEdges: [UIView.HorizontalEdge] fileprivate init( icon: Lucide.Icon?, @@ -233,6 +240,7 @@ public extension SessionCell.AccessoryConfig { iconSize: IconSize, customTint: ThemeValue?, shouldFill: Bool, + pinEdges: [UIView.HorizontalEdge], accessibility: Accessibility? ) { self.icon = icon @@ -240,6 +248,7 @@ public extension SessionCell.AccessoryConfig { self.iconSize = iconSize self.customTint = customTint self.shouldFill = shouldFill + self.pinEdges = pinEdges super.init(accessibility: accessibility) } @@ -252,6 +261,7 @@ public extension SessionCell.AccessoryConfig { iconSize.hash(into: &hasher) customTint.hash(into: &hasher) shouldFill.hash(into: &hasher) + pinEdges.hash(into: &hasher) accessibility.hash(into: &hasher) } @@ -264,7 +274,9 @@ public extension SessionCell.AccessoryConfig { iconSize == rhs.iconSize && customTint == rhs.customTint && shouldFill == rhs.shouldFill && + pinEdges == rhs.pinEdges && accessibility == rhs.accessibility + ) } } @@ -278,18 +290,21 @@ public extension SessionCell.AccessoryConfig { public let source: ImageDataManager.DataSource? public let customTint: ThemeValue? public let shouldFill: Bool + public let pinEdges: [UIView.HorizontalEdge] fileprivate init( iconSize: IconSize, source: ImageDataManager.DataSource?, customTint: ThemeValue?, shouldFill: Bool, + pinEdges: [UIView.HorizontalEdge], accessibility: Accessibility? ) { self.iconSize = iconSize self.source = source self.customTint = customTint self.shouldFill = shouldFill + self.pinEdges = pinEdges super.init(accessibility: accessibility) } @@ -301,6 +316,7 @@ public extension SessionCell.AccessoryConfig { source?.hash(into: &hasher) customTint.hash(into: &hasher) shouldFill.hash(into: &hasher) + pinEdges.hash(into: &hasher) accessibility.hash(into: &hasher) } @@ -312,6 +328,7 @@ public extension SessionCell.AccessoryConfig { source == rhs.source && customTint == rhs.customTint && shouldFill == rhs.shouldFill && + pinEdges == rhs.pinEdges && accessibility == rhs.accessibility ) } @@ -563,6 +580,8 @@ public extension SessionCell.AccessoryConfig { public let additionalProfile: Profile? public let additionalProfileIcon: ProfilePictureView.ProfileIcon + override public var shouldFitToEdge: Bool { true } + fileprivate init( id: String, size: ProfilePictureView.Size, @@ -737,6 +756,7 @@ public extension SessionCell.AccessoryConfig { public extension SessionCell.Accessory { enum Size { case fixed(width: CGFloat, height: CGFloat) + case minWidth(height: CGFloat) case fillWidth(height: CGFloat) case fillWidthWrapHeight } @@ -762,19 +782,6 @@ public extension SessionCell.Accessory.CustomViewInfo { let view: View = View.create(maxContentWidth: maxContentWidth, using: dependencies) view.update(with: self) - switch View.size { - case .fixed(let width, let height): - view.set(.width, to: width) - view.set(.height, to: height) - - case .fillWidth(let height): - view.set(.height, to: height) - - case .fillWidthWrapHeight: - view.setContentHugging(.vertical, to: .required) - view.setCompressionResistance(.vertical, to: .required) - } - return view } } diff --git a/Session/Shared/Types/SessionCell+Styling.swift b/Session/Shared/Types/SessionCell+Styling.swift index a83a09a0da..8df10aa317 100644 --- a/Session/Shared/Types/SessionCell+Styling.swift +++ b/Session/Shared/Types/SessionCell+Styling.swift @@ -9,9 +9,7 @@ public extension SessionCell { struct TextInfo: Hashable, Equatable { public enum Interaction: Hashable, Equatable { case none - case editable case copy - case alwaysEditing case expandable } diff --git a/Session/Shared/Views/SessionCell+AccessoryView.swift b/Session/Shared/Views/SessionCell+AccessoryView.swift index 2cae2e3c31..a3815587ae 100644 --- a/Session/Shared/Views/SessionCell+AccessoryView.swift +++ b/Session/Shared/Views/SessionCell+AccessoryView.swift @@ -21,7 +21,9 @@ extension SessionCell { private lazy var minWidthConstraint: NSLayoutConstraint = self.widthAnchor .constraint(greaterThanOrEqualToConstant: AccessoryView.minWidth) - private lazy var fixedWidthConstraint: NSLayoutConstraint = self.set(.width, to: AccessoryView.minWidth) + private lazy var fixedWidthConstraint: NSLayoutConstraint = self + .set(.width, to: AccessoryView.minWidth) + .setting(priority: .defaultHigh) // MARK: - Content @@ -55,7 +57,6 @@ extension SessionCell { accessory: accessory, tintColor: tintColor, isEnabled: isEnabled, - maxContentWidth: maxContentWidth, using: dependencies ) return @@ -73,7 +74,6 @@ extension SessionCell { if let newView: UIView = maybeView { addSubview(newView) - newView.pin(to: self) layout(view: newView, accessory: accessory) } @@ -82,7 +82,6 @@ extension SessionCell { accessory: accessory, tintColor: tintColor, isEnabled: isEnabled, - maxContentWidth: maxContentWidth, using: dependencies ) @@ -163,14 +162,16 @@ extension SessionCell { return createIconView(using: dependencies) case is SessionCell.AccessoryConfig.Toggle: return createToggleView() - case is SessionCell.AccessoryConfig.DropDown: return createDropDownView() + case is SessionCell.AccessoryConfig.DropDown: + return createDropDownView(maxContentWidth: maxContentWidth) + case is SessionCell.AccessoryConfig.Radio: return createRadioView() case is SessionCell.AccessoryConfig.HighlightingBackgroundLabel: - return createHighlightingBackgroundLabelView() + return createHighlightingBackgroundLabelView(maxContentWidth: maxContentWidth) case is SessionCell.AccessoryConfig.HighlightingBackgroundLabelAndRadio: - return createHighlightingBackgroundLabelAndRadioView() + return createHighlightingBackgroundLabelAndRadioView(maxContentWidth: maxContentWidth) case is SessionCell.AccessoryConfig.DisplayPicture: return createDisplayPictureView() case is SessionCell.AccessoryConfig.Search: return createSearchView() @@ -188,14 +189,24 @@ extension SessionCell { return nil } } - + private func layout(view: UIView?, accessory: Accessory) { switch accessory { case let accessory as SessionCell.AccessoryConfig.Icon: - layoutIconView(view, iconSize: accessory.iconSize, shouldFill: accessory.shouldFill) + layoutIconView( + view, + iconSize: accessory.iconSize, + shouldFill: accessory.shouldFill, + pin: accessory.pinEdges + ) case let accessory as SessionCell.AccessoryConfig.IconAsync: - layoutIconView(view, iconSize: accessory.iconSize, shouldFill: accessory.shouldFill) + layoutIconView( + view, + iconSize: accessory.iconSize, + shouldFill: accessory.shouldFill, + pin: accessory.pinEdges + ) case is SessionCell.AccessoryConfig.Toggle: layoutToggleView(view) case is SessionCell.AccessoryConfig.DropDown: layoutDropDownView(view) @@ -227,7 +238,6 @@ extension SessionCell { accessory: Accessory, tintColor: ThemeValue, isEnabled: Bool, - maxContentWidth: CGFloat, using dependencies: Dependencies ) { switch accessory { @@ -288,13 +298,35 @@ extension SessionCell { return result } - private func layoutIconView(_ view: UIView?, iconSize: IconSize, shouldFill: Bool) { + private func layoutIconView(_ view: UIView?, iconSize: IconSize, shouldFill: Bool, pin edges: [UIView.HorizontalEdge]) { guard let imageView: SessionImageView = view as? SessionImageView else { return } imageView.set(.width, to: iconSize.size) imageView.set(.height, to: iconSize.size) - imageView.pin(.leading, to: .leading, of: self, withInset: (shouldFill ? 0 : Values.smallSpacing)) - imageView.pin(.trailing, to: .trailing, of: self, withInset: (shouldFill ? 0 : -Values.smallSpacing)) + imageView.pin(.top, to: .top, of: self) + imageView.pin(.bottom, to: .bottom, of: self).setting(priority: .defaultHigh) + + let edgeSet: Set = Set(edges) + let hasLeadingPin = (edgeSet.contains(.leading) || edgeSet.contains(.left)) + let hasTrailingPin = (edgeSet.contains(.trailing) || edgeSet.contains(.right)) + + /// If we want to pin to both edges then we should actually center instead (otherwise this will cause constraint violations) + if hasLeadingPin && hasTrailingPin { + imageView.center(.horizontal, in: self) + } + else { + let shouldInvertPadding: [UIView.HorizontalEdge] = [.right, .trailing] + + for edge in edges { + let inset: CGFloat = ( + (shouldFill ? 0 : Values.smallSpacing) * + (shouldInvertPadding.contains(edge) ? -1 : 1) + ) + + imageView.pin(edge, to: edge, of: self, withInset: inset) + } + } + fixedWidthConstraint.isActive = (iconSize.size <= fixedWidthConstraint.constant) minWidthConstraint.isActive = !fixedWidthConstraint.isActive } @@ -306,7 +338,7 @@ extension SessionCell { imageView.accessibilityLabel = accessory.accessibility?.label imageView.themeTintColor = (accessory.customTint ?? tintColor) imageView.contentMode = (accessory.shouldFill ? .scaleAspectFill : .scaleAspectFit) - + switch (accessory.icon, accessory.image) { case (.some(let icon), _): imageView.image = Lucide @@ -377,7 +409,7 @@ extension SessionCell { // MARK: -- DropDown - private func createDropDownView() -> UIView { + private func createDropDownView(maxContentWidth: CGFloat) -> UIView { let result: UIStackView = UIStackView() result.translatesAutoresizingMaskIntoConstraints = false result.axis = .horizontal @@ -397,6 +429,8 @@ extension SessionCell { label.themeTextColor = .textPrimary label.setContentHugging(to: .required) label.setCompressionResistance(to: .required) + label.preferredMaxLayoutWidth = (maxContentWidth * 0.4) /// Limit to 40% of content width + label.numberOfLines = 0 result.addArrangedSubview(imageView) result.addArrangedSubview(label) @@ -461,6 +495,7 @@ extension SessionCell { radioBorderView.pin(.leading, to: .leading, of: self, withInset: Values.smallSpacing) radioBorderView.pin(.trailing, to: .trailing, of: self, withInset: -Values.smallSpacing) radioBorderView.pin(.bottom, to: .bottom, of: self) + .setting(priority: .defaultHigh) } private func configureRadioView(_ view: UIView?, _ accessory: SessionCell.AccessoryConfig.Radio, isEnabled: Bool) { @@ -510,8 +545,11 @@ extension SessionCell { // MARK: -- HighlightingBackgroundLabel - private func createHighlightingBackgroundLabelView() -> UIView { - return SessionHighlightingBackgroundLabel() + private func createHighlightingBackgroundLabelView(maxContentWidth: CGFloat) -> UIView { + let result: SessionHighlightingBackgroundLabel = SessionHighlightingBackgroundLabel() + result.preferredMaxLayoutWidth = (maxContentWidth * 0.4) /// Limit to 40% of content width + + return result } private func layoutHighlightingBackgroundLabelView(_ view: UIView?) { @@ -540,10 +578,11 @@ extension SessionCell { // MARK: -- HighlightingBackgroundLabelAndRadio - private func createHighlightingBackgroundLabelAndRadioView() -> UIView { + private func createHighlightingBackgroundLabelAndRadioView(maxContentWidth: CGFloat) -> UIView { let result: UIView = UIView() let label: SessionHighlightingBackgroundLabel = SessionHighlightingBackgroundLabel() let radio: UIView = createRadioView() + label.preferredMaxLayoutWidth = (maxContentWidth * 0.4) /// Limit to 40% of content width result.addSubview(label) result.addSubview(radio) @@ -559,6 +598,8 @@ extension SessionCell { let radioView: UIView = radioBorderView.subviews.first else { return } + label.pin(to: self) + label.pin(.top, to: .top, of: self) label.pin(.leading, to: .leading, of: self, withInset: Values.smallSpacing) label.pin(.trailing, to: .leading, of: radioBorderView, withInset: -Values.smallSpacing) @@ -642,7 +683,11 @@ extension SessionCell { private func layoutDisplayPictureView(_ view: UIView?, size: ProfilePictureView.Size) { guard let profilePictureView: ProfilePictureView = view as? ProfilePictureView else { return } - profilePictureView.pin(to: self) + profilePictureView.size = size + profilePictureView.pin(.top, to: .top, of: self) + profilePictureView.pin(.leading, to: .leading, of: self) + profilePictureView.pin(.trailing, to: .trailing, of: self) + profilePictureView.pin(.bottom, to: .bottom, of: self).setting(priority: .defaultHigh) fixedWidthConstraint.constant = size.viewSize fixedWidthConstraint.isActive = true } @@ -654,12 +699,9 @@ extension SessionCell { ) { guard let profilePictureView: ProfilePictureView = view as? ProfilePictureView else { return } - // Note: We MUST set the 'size' property before triggering the 'update' - // function or the profile picture won't layout correctly profilePictureView.accessibilityIdentifier = accessory.accessibility?.identifier profilePictureView.accessibilityLabel = accessory.accessibility?.label profilePictureView.isAccessibilityElement = (accessory.accessibility != nil) - profilePictureView.size = accessory.size profilePictureView.setDataManager(dependencies[singleton: .imageDataManager]) profilePictureView.update( publicKey: accessory.id, @@ -715,7 +757,10 @@ extension SessionCell { private func layoutButtonView(_ view: UIView?) { guard let button: SessionButton = view as? SessionButton else { return } - button.pin(to: self) + button.pin(.top, to: .top, of: self) + button.pin(.leading, to: .leading, of: self) + button.pin(.trailing, to: .trailing, of: self) + button.pin(.bottom, to: .bottom, of: self).setting(priority: .defaultHigh) minWidthConstraint.isActive = true } @@ -741,6 +786,11 @@ extension SessionCell { view.set(.height, to: height) fixedWidthConstraint.isActive = (width <= fixedWidthConstraint.constant) minWidthConstraint.isActive = !fixedWidthConstraint.isActive + + case .minWidth(let height): + view.set(.width, to: .width, of: self) + view.set(.height, to: height) + fixedWidthConstraint.isActive = true case .fillWidth(let height): view.set(.width, to: .width, of: self) @@ -754,8 +804,10 @@ extension SessionCell { minWidthConstraint.isActive = true } - view.pin(.leading, to: .leading, of: self, withInset: Values.smallSpacing) - view.pin(.trailing, to: .trailing, of: self, withInset: -Values.smallSpacing) + view.pin(.top, to: .top, of: self) + view.pin(.leading, to: .leading, of: self) + view.pin(.trailing, to: .trailing, of: self) + view.pin(.bottom, to: .bottom, of: self) } private func configureCustomView(_ view: UIView?, _ accessory: SessionCell.AccessoryConfig.AnyCustom) { diff --git a/Session/Shared/Views/SessionCell.swift b/Session/Shared/Views/SessionCell.swift index 4bcc1408f3..768773ff44 100644 --- a/Session/Shared/Views/SessionCell.swift +++ b/Session/Shared/Views/SessionCell.swift @@ -10,7 +10,6 @@ import SessionUtilitiesKit public class SessionCell: UITableViewCell { public static let cornerRadius: CGFloat = 17 - private var isEditingTitle = false public private(set) var interactionMode: SessionCell.TextInfo.Interaction = .none public var lastTouchLocation: UITouch? private var shouldHighlightTitle: Bool = true @@ -34,12 +33,10 @@ public class SessionCell: UITableViewCell { private lazy var contentStackViewHorizontalCenterConstraint: NSLayoutConstraint = contentStackView.center(.horizontal, in: cellBackgroundView) private lazy var contentStackViewWidthConstraint: NSLayoutConstraint = contentStackView.set(.width, lessThanOrEqualTo: .width, of: cellBackgroundView) private lazy var leadingAccessoryFillConstraint: NSLayoutConstraint = contentStackView.set(.height, to: .height, of: leadingAccessoryView) - private lazy var titleTextFieldLeadingConstraint: NSLayoutConstraint = titleTextField.pin(.leading, to: .leading, of: cellBackgroundView) - private lazy var titleTextFieldTrailingConstraint: NSLayoutConstraint = titleTextField.pin(.trailing, to: .trailing, of: cellBackgroundView) - private lazy var titleMinHeightConstraint: NSLayoutConstraint = titleStackView.heightAnchor - .constraint(greaterThanOrEqualTo: titleTextField.heightAnchor) private lazy var trailingAccessoryFillConstraint: NSLayoutConstraint = contentStackView.set(.height, to: .height, of: trailingAccessoryView) - private lazy var accessoryWidthMatchConstraint: NSLayoutConstraint = leadingAccessoryView.set(.width, to: .width, of: trailingAccessoryView) + private lazy var accessoryWidthMatchConstraint: NSLayoutConstraint = leadingAccessoryView + .set(.width, to: .width, of: trailingAccessoryView) + .setting(priority: .defaultHigh) private let cellBackgroundView: UIView = { let result: UIView = UIView() @@ -89,10 +86,10 @@ public class SessionCell: UITableViewCell { let result: UIStackView = UIStackView() result.translatesAutoresizingMaskIntoConstraints = false result.axis = .vertical - result.distribution = .equalSpacing + result.distribution = .fill result.alignment = .fill - result.setContentHugging(to: .defaultLow) - result.setCompressionResistance(to: .defaultLow) + result.setContentHugging(.vertical, to: .required) + result.setCompressionResistance(.vertical, to: .required) return result }() @@ -103,23 +100,12 @@ public class SessionCell: UITableViewCell { result.isUserInteractionEnabled = false result.themeTextColor = .textPrimary result.numberOfLines = 0 - result.setContentHugging(to: .defaultLow) - result.setCompressionResistance(to: .defaultLow) + result.setContentHugging(.vertical, to: .required) + result.setCompressionResistance(.vertical, to: .required) return result }() - fileprivate let titleTextField: UITextField = { - let textField: SNTextField = SNTextField(placeholder: "", usesDefaultHeight: false) - textField.translatesAutoresizingMaskIntoConstraints = false - textField.textAlignment = .center - textField.alpha = 0 - textField.isHidden = true - textField.set(.height, to: Values.largeButtonHeight) - - return textField - }() - private let subtitleLabel: SRCopyableLabel = { let result: SRCopyableLabel = SRCopyableLabel() result.translatesAutoresizingMaskIntoConstraints = false @@ -128,8 +114,8 @@ public class SessionCell: UITableViewCell { result.themeTextColor = .textPrimary result.numberOfLines = 0 result.isHidden = true - result.setContentHugging(to: .defaultLow) - result.setCompressionResistance(to: .defaultLow) + result.setContentHugging(.vertical, to: .required) + result.setCompressionResistance(.vertical, to: .required) return result }() @@ -142,8 +128,8 @@ public class SessionCell: UITableViewCell { result.numberOfLines = 0 result.maxNumberOfLines = 3 result.isHidden = true - result.setContentHugging(to: .defaultLow) - result.setCompressionResistance(to: .defaultLow) + result.setContentHugging(.vertical, to: .required) + result.setCompressionResistance(.vertical, to: .required) return result }() @@ -189,13 +175,11 @@ public class SessionCell: UITableViewCell { contentStackView.addArrangedSubview(leadingAccessoryView) contentStackView.addArrangedSubview(titleStackView) - contentStackView.addArrangedSubview(expandableDescriptionLabel) contentStackView.addArrangedSubview(trailingAccessoryView) titleStackView.addArrangedSubview(titleLabel) titleStackView.addArrangedSubview(subtitleLabel) - - cellBackgroundView.addSubview(titleTextField) + titleStackView.addArrangedSubview(expandableDescriptionLabel) setupLayout() } @@ -204,7 +188,9 @@ public class SessionCell: UITableViewCell { cellBackgroundView.pin(.top, to: .top, of: contentView) backgroundLeftConstraint = cellBackgroundView.pin(.leading, to: .leading, of: contentView) backgroundRightConstraint = cellBackgroundView.pin(.trailing, to: .trailing, of: contentView) - cellBackgroundView.pin(.bottom, to: .bottom, of: contentView) + cellBackgroundView + .pin(.bottom, to: .bottom, of: contentView) + .setting(priority: .defaultHigh) cellSelectedBackgroundView.pin(to: cellBackgroundView) @@ -215,12 +201,13 @@ public class SessionCell: UITableViewCell { contentStackViewTopConstraint.isActive = true contentStackViewBottomConstraint.isActive = true - titleTextField.center(.vertical, in: titleLabel) - botSeparatorLeadingConstraint = botSeparator.pin(.leading, to: .leading, of: cellBackgroundView) botSeparatorTrailingConstraint = botSeparator.pin(.trailing, to: .trailing, of: cellBackgroundView) botSeparator.pin(.bottom, to: .bottom, of: cellBackgroundView) + // Limit accessory views horizontal expansion to 40% of the container + trailingAccessoryView.set(.width, lessThanOrEqualTo: .width, of: contentView, multiplier: 0.40) + // Explicitly call this to ensure we have initialised the constraints before we initially // layout (if we don't do this then some constraints get created for the first time when // updating the cell before the `isActive` value gets set, resulting in breaking constriants) @@ -274,6 +261,7 @@ public class SessionCell: UITableViewCell { // Remove and re-add the 'subtitleExtraView' to clear any old constraints targetView.removeFromSuperview() contentView.addSubview(targetView) + targetView.layoutIfNeeded() targetView.pin( .top, @@ -285,7 +273,7 @@ public class SessionCell: UITableViewCell { .leading, to: .leading, of: label, - withInset: lastGlyphRect.maxX + withInset: lastGlyphRect.maxX + 2 // Padding ) } @@ -294,7 +282,6 @@ public class SessionCell: UITableViewCell { public override func prepareForReuse() { super.prepareForReuse() - isEditingTitle = false interactionMode = .none shouldHighlightTitle = true accessibilityIdentifier = nil @@ -312,18 +299,12 @@ public class SessionCell: UITableViewCell { contentStackViewTrailingConstraint.isActive = false contentStackViewHorizontalCenterConstraint.isActive = false contentStackViewWidthConstraint.isActive = false - titleMinHeightConstraint.isActive = false leadingAccessoryView.prepareForReuse() leadingAccessoryView.alpha = 1 leadingAccessoryFillConstraint.isActive = false titleLabel.text = "" titleLabel.themeTextColor = .textPrimary titleLabel.alpha = 1 - titleTextField.text = "" - titleTextField.textAlignment = .center - titleTextField.themeTextColor = .textPrimary - titleTextField.isHidden = true - titleTextField.alpha = 0 subtitleLabel.isUserInteractionEnabled = false subtitleLabel.attributedText = nil subtitleLabel.themeTextColor = .textPrimary @@ -340,10 +321,10 @@ public class SessionCell: UITableViewCell { botSeparator.isHidden = true } - public func update( + @MainActor public func update( with info: Info, tableSize: CGSize, - onToggleExpansion: (() -> Void)? = nil, + onToggleExpansion: (@MainActor () -> Void)? = nil, using dependencies: Dependencies ) { /// Need to do this here as `prepareForReuse` doesn't always seem to get called @@ -366,7 +347,7 @@ public class SessionCell: UITableViewCell { // Layout (do this before setting up the content so we can calculate the expected widths if needed) contentStackViewLeadingConstraint.isActive = (info.styling.alignment == .leading) - contentStackViewTrailingConstraint.isActive = (info.styling.alignment == .leading) + contentStackViewTrailingConstraint.isActive = contentStackViewLeadingConstraint.isActive contentStackViewHorizontalCenterConstraint.constant = ((info.styling.customPadding?.leading ?? 0) + (info.styling.customPadding?.trailing ?? 0)) contentStackViewHorizontalCenterConstraint.isActive = (info.styling.alignment == .centerHugging) contentStackViewWidthConstraint.constant = -(abs((info.styling.customPadding?.leading ?? 0) + (info.styling.customPadding?.trailing ?? 0)) * 2) // Double the center offset to keep within bounds @@ -379,14 +360,6 @@ public class SessionCell: UITableViewCell { default: return false } }() - titleLabel.setContentHuggingPriority( - (info.trailingAccessory != nil ? .defaultLow : .required), - for: .horizontal - ) - titleLabel.setContentCompressionResistancePriority( - (info.trailingAccessory != nil ? .defaultLow : .required), - for: .horizontal - ) contentStackViewTopConstraint.constant = { if let customPadding: CGFloat = info.styling.customPadding?.top { return customPadding @@ -415,16 +388,6 @@ public class SessionCell: UITableViewCell { return -(leadingFitToEdge || trailingFitToEdge ? 0 : Values.mediumSpacing) }() - titleTextFieldLeadingConstraint.constant = { - guard info.styling.backgroundStyle != .noBackground else { return 0 } - - return (leadingFitToEdge ? 0 : Values.mediumSpacing) - }() - titleTextFieldTrailingConstraint.constant = { - guard info.styling.backgroundStyle != .noBackground else { return 0 } - - return -(trailingFitToEdge ? 0 : Values.mediumSpacing) - }() // Styling and positioning let defaultEdgePadding: CGFloat @@ -555,7 +518,7 @@ public class SessionCell: UITableViewCell { maxContentWidth: (tableSize.width - contentStackViewHorizontalInset), using: dependencies ) - titleStackView.isHidden = (info.title == nil && info.subtitle == nil) + titleStackView.isHidden = (info.title == nil && info.subtitle == nil && info.description == nil) titleLabel.isUserInteractionEnabled = (info.title?.interaction == .copy) titleLabel.font = info.title?.font titleLabel.text = info.title?.text @@ -564,32 +527,27 @@ public class SessionCell: UITableViewCell { titleLabel.accessibilityIdentifier = info.title?.accessibility?.identifier titleLabel.accessibilityLabel = info.title?.accessibility?.label titleLabel.isHidden = (info.title == nil) - titleTextField.text = info.title?.text - titleTextField.textAlignment = (info.title?.textAlignment ?? .left) - titleTextField.placeholder = info.title?.editingPlaceholder - titleTextField.isHidden = (info.title == nil) - titleTextField.accessibilityIdentifier = info.title?.accessibility?.identifier - titleTextField.accessibilityLabel = info.title?.accessibility?.label subtitleLabel.isUserInteractionEnabled = (info.subtitle?.interaction == .copy) subtitleLabel.font = info.subtitle?.font + subtitleLabel.themeTextColor = info.styling.subtitleTintColor subtitleLabel.themeAttributedText = info.subtitle.map { subtitle -> ThemedAttributedString? in ThemedAttributedString(stringWithHTMLTags: subtitle.text, font: subtitle.font) } - subtitleLabel.themeTextColor = info.styling.subtitleTintColor subtitleLabel.textAlignment = (info.subtitle?.textAlignment ?? .left) subtitleLabel.accessibilityIdentifier = info.subtitle?.accessibility?.identifier subtitleLabel.accessibilityLabel = info.subtitle?.accessibility?.label subtitleLabel.isHidden = (info.subtitle == nil) expandableDescriptionLabel.font = info.description?.font ?? .systemFont(ofSize: 12) + expandableDescriptionLabel.themeTextColor = info.styling.descriptionTintColor expandableDescriptionLabel.themeAttributedText = info.description.map { description -> ThemedAttributedString? in ThemedAttributedString(stringWithHTMLTags: description.text, font: description.font) } - expandableDescriptionLabel.themeTextColor = info.styling.descriptionTintColor expandableDescriptionLabel.textAlignment = (info.description?.textAlignment ?? .left) expandableDescriptionLabel.accessibilityIdentifier = info.description?.accessibility?.identifier expandableDescriptionLabel.accessibilityLabel = info.description?.accessibility?.label expandableDescriptionLabel.isHidden = (info.description == nil) - expandableDescriptionLabel.onToggleExpansion = (info.description?.interaction == .expandable ? onToggleExpansion : nil) + expandableDescriptionLabel.onToggleExpansion = (info.description?.interaction == .expandable ? + onToggleExpansion : nil) trailingAccessoryView.update( with: info.trailingAccessory, tintColor: info.styling.tintColor, @@ -599,57 +557,17 @@ public class SessionCell: UITableViewCell { ) } - public func update(isEditing: Bool, becomeFirstResponder: Bool, animated: Bool) { - // Note: We set 'isUserInteractionEnabled' based on the 'info.isEditable' flag - // so can use that to determine whether this element can become editable - guard interactionMode == .editable || interactionMode == .alwaysEditing else { return } - - self.isEditingTitle = isEditing - - let changes = { [weak self] in - self?.titleLabel.alpha = (isEditing ? 0 : 1) - self?.titleTextField.alpha = (isEditing ? 1 : 0) - self?.leadingAccessoryView.alpha = (isEditing ? 0 : 1) - self?.trailingAccessoryView.alpha = (isEditing ? 0 : 1) - self?.titleMinHeightConstraint.isActive = isEditing - } - let completion: (Bool) -> Void = { [weak self] complete in - self?.titleTextField.text = self?.originalInputValue - } - - if animated { - UIView.animate(withDuration: 0.25, animations: changes, completion: completion) - } - else { - changes() - completion(true) - } - - if isEditing && becomeFirstResponder { - titleTextField.becomeFirstResponder() - } - else if !isEditing { - titleTextField.resignFirstResponder() - } - } - // MARK: - Interaction public override func setHighlighted(_ highlighted: Bool, animated: Bool) { super.setHighlighted(highlighted, animated: animated) - // When editing disable the highlighted state changes (would result in UI elements - // reappearing otherwise) - guard !self.isEditingTitle else { return } - // If the 'cellSelectedBackgroundView' is hidden then there is no background so we // should update the titleLabel to indicate the highlighted state if cellSelectedBackgroundView.isHidden && shouldHighlightTitle { // Note: We delay the "unhighlight" of the titleLabel so that the transition doesn't // conflict with the transition into edit mode DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(10)) { [weak self] in - guard self?.isEditingTitle == false else { return } - self?.titleLabel.alpha = (highlighted ? 0.8 : 1) } } @@ -672,22 +590,3 @@ public class SessionCell: UITableViewCell { lastTouchLocation = touches.first } } - -// MARK: - Compose - -extension CombineCompatible where Self: SessionCell { - var textPublisher: AnyPublisher { - return self.titleTextField.publisher(for: [.editingChanged, .editingDidEnd]) - .handleEvents( - receiveOutput: { [weak self] textField in - // When editing the text update the 'accessibilityLabel' of the cell to match - // the text - let targetText: String? = (textField.isEditing ? textField.text : self?.titleLabel.text) - self?.accessibilityLabel = (targetText ?? self?.accessibilityLabel) - } - ) - .filter { $0.isEditing } // Don't bother sending events for 'editingDidEnd' - .map { textField -> String in (textField.text ?? "") } - .eraseToAnyPublisher() - } -} diff --git a/Session/Shared/Views/SessionFooterView.swift b/Session/Shared/Views/SessionFooterView.swift index ce4c09f9e7..a5c08c8449 100644 --- a/Session/Shared/Views/SessionFooterView.swift +++ b/Session/Shared/Views/SessionFooterView.swift @@ -4,11 +4,6 @@ import UIKit import SessionUIKit class SessionFooterView: UITableViewHeaderFooterView { - private lazy var emptyHeightConstraint: NSLayoutConstraint = self.heightAnchor - .constraint(equalToConstant: (Values.verySmallSpacing * 2)) - private lazy var filledHeightConstraint: NSLayoutConstraint = self.heightAnchor - .constraint(greaterThanOrEqualToConstant: Values.mediumSpacing) - // MARK: - UI private let stackView: UIStackView = { @@ -53,7 +48,12 @@ class SessionFooterView: UITableViewHeaderFooterView { } private func setupLayout() { - stackView.pin(to: self) + stackView.pin(.top, to: .top, of: self) + stackView.pin(.leading, to: .leading, of: self) + stackView.pin(.trailing, to: .trailing, of: self) + .setting(priority: .defaultHigh) + stackView.pin(.bottom, to: .bottom, of: self) + .setting(priority: .defaultHigh) } // MARK: - Content @@ -81,8 +81,6 @@ class SessionFooterView: UITableViewHeaderFooterView { bottom: (titleIsEmpty ? Values.verySmallSpacing : Values.mediumSpacing), right: edgePadding ) - emptyHeightConstraint.isActive = titleIsEmpty - filledHeightConstraint.isActive = !titleIsEmpty self.layoutIfNeeded() } diff --git a/Session/Shared/Views/SessionHeaderView.swift b/Session/Shared/Views/SessionHeaderView.swift index 899f6b53ee..0cf98554bf 100644 --- a/Session/Shared/Views/SessionHeaderView.swift +++ b/Session/Shared/Views/SessionHeaderView.swift @@ -6,14 +6,10 @@ import SessionUIKit class SessionHeaderView: UITableViewHeaderFooterView { // MARK: - UI - private lazy var titleLabelConstraints: [NSLayoutConstraint] = [ - titleLabel.pin(.top, to: .top, of: self, withInset: Values.mediumSpacing), - titleLabel.pin(.bottom, to: .bottom, of: self, withInset: -Values.mediumSpacing) - ] - private lazy var titleLabelLeadingConstraint: NSLayoutConstraint = titleLabel.pin(.leading, to: .leading, of: self) - private lazy var titleLabelTrailingConstraint: NSLayoutConstraint = titleLabel.pin(.trailing, to: .trailing, of: self) - private lazy var titleSeparatorLeadingConstraint: NSLayoutConstraint = titleSeparator.pin(.leading, to: .leading, of: self) - private lazy var titleSeparatorTrailingConstraint: NSLayoutConstraint = titleSeparator.pin(.trailing, to: .trailing, of: self) + private var titleLabelLeadingConstraint: NSLayoutConstraint? + private var titleLabelTrailingConstraint: NSLayoutConstraint? + private var titleSeparatorLeadingConstraint: NSLayoutConstraint? + private var titleSeparatorTrailingConstraint: NSLayoutConstraint? private let titleLabel: UILabel = { let result: UILabel = UILabel() @@ -34,7 +30,7 @@ class SessionHeaderView: UITableViewHeaderFooterView { private let loadingIndicator: UIActivityIndicatorView = { let result: UIActivityIndicatorView = UIActivityIndicatorView(style: .medium) - result.themeTintColor = .textPrimary + result.themeColor = .textPrimary result.alpha = 0.5 result.startAnimating() result.hidesWhenStopped = true @@ -51,9 +47,9 @@ class SessionHeaderView: UITableViewHeaderFooterView { self.backgroundView = UIView() self.backgroundView?.themeBackgroundColor = .backgroundPrimary - addSubview(titleLabel) - addSubview(titleSeparator) - addSubview(loadingIndicator) + contentView.addSubview(titleLabel) + contentView.addSubview(titleSeparator) + contentView.addSubview(loadingIndicator) setupLayout() } @@ -63,12 +59,22 @@ class SessionHeaderView: UITableViewHeaderFooterView { } private func setupLayout() { - titleLabel.pin(.top, to: .top, of: self, withInset: Values.mediumSpacing) - titleLabel.pin(.bottom, to: .bottom, of: self, withInset: Values.mediumSpacing) - titleLabel.center(.vertical, in: self) + titleLabel.pin(.top, to: .top, of: contentView, withInset: Values.mediumSpacing) + titleLabelLeadingConstraint = titleLabel.pin(.leading, to: .leading, of: contentView) + titleLabelTrailingConstraint = titleLabel + .pin(.trailing, to: .trailing, of: contentView) + .setting(priority: .defaultHigh) + titleLabel + .pin(.bottom, to: .bottom, of: contentView, withInset: -Values.mediumSpacing) + .setting(priority: .defaultHigh) - titleSeparator.center(.vertical, in: self) - loadingIndicator.center(in: self) + titleSeparator.center(.vertical, in: contentView) + titleSeparatorLeadingConstraint = titleSeparator.pin(.leading, to: .leading, of: contentView) + titleSeparatorTrailingConstraint = titleSeparator + .pin(.trailing, to: .trailing, of: contentView) + .setting(priority: .defaultHigh) + + loadingIndicator.center(in: contentView) } // MARK: - Content @@ -79,14 +85,6 @@ class SessionHeaderView: UITableViewHeaderFooterView { titleLabel.isHidden = true titleSeparator.isHidden = true loadingIndicator.isHidden = true - - titleLabelLeadingConstraint.isActive = false - titleLabelTrailingConstraint.isActive = false - titleLabelConstraints.forEach { $0.isActive = false } - - titleSeparator.center(.vertical, in: self) - titleSeparatorLeadingConstraint.isActive = false - titleSeparatorTrailingConstraint.isActive = false } public func update( @@ -94,24 +92,19 @@ class SessionHeaderView: UITableViewHeaderFooterView { style: SessionTableSectionStyle = .titleRoundedContent ) { let titleIsEmpty: Bool = (title ?? "").isEmpty + titleLabelLeadingConstraint?.constant = style.edgePadding + titleLabelTrailingConstraint?.constant = -style.edgePadding + titleSeparatorLeadingConstraint?.constant = style.edgePadding + titleSeparatorTrailingConstraint?.constant = -style.edgePadding switch style { case .titleRoundedContent, .titleEdgeToEdgeContent, .titleNoBackgroundContent: titleLabel.text = title titleLabel.isHidden = titleIsEmpty - titleLabelLeadingConstraint.constant = style.edgePadding - titleLabelTrailingConstraint.constant = -style.edgePadding - titleLabelLeadingConstraint.isActive = !titleIsEmpty - titleLabelTrailingConstraint.isActive = !titleIsEmpty - titleLabelConstraints.forEach { $0.isActive = true } case .titleSeparator: titleSeparator.update(title: title) titleSeparator.isHidden = false - titleSeparatorLeadingConstraint.constant = style.edgePadding - titleSeparatorTrailingConstraint.constant = -style.edgePadding - titleSeparatorLeadingConstraint.isActive = !titleIsEmpty - titleSeparatorTrailingConstraint.isActive = !titleIsEmpty case .none, .padding: break case .loadMore: loadingIndicator.isHidden = false diff --git a/Session/Shared/Views/SessionHighlightingBackgroundLabel.swift b/Session/Shared/Views/SessionHighlightingBackgroundLabel.swift index a4e1972111..6bbe3022cb 100644 --- a/Session/Shared/Views/SessionHighlightingBackgroundLabel.swift +++ b/Session/Shared/Views/SessionHighlightingBackgroundLabel.swift @@ -15,6 +15,11 @@ public class SessionHighlightingBackgroundLabel: UIView { set { label.themeTextColor = newValue } } + var preferredMaxLayoutWidth: CGFloat { + get { label.preferredMaxLayoutWidth - (Values.smallSpacing * 2) } + set { label.preferredMaxLayoutWidth = (newValue - (Values.smallSpacing * 2)) } + } + // MARK: - Components private let label: UILabel = { @@ -24,6 +29,7 @@ public class SessionHighlightingBackgroundLabel: UIView { result.themeTextColor = .textPrimary result.setContentHugging(to: .required) result.setCompressionResistance(to: .required) + result.numberOfLines = 0 return result }() diff --git a/Session/Utilities/BackgroundPoller.swift b/Session/Utilities/BackgroundPoller.swift index bb30cb2599..b03942a719 100644 --- a/Session/Utilities/BackgroundPoller.swift +++ b/Session/Utilities/BackgroundPoller.swift @@ -5,7 +5,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionMessagingKit import SessionUtilitiesKit diff --git a/Session/Utilities/IP2Country.swift b/Session/Utilities/IP2Country.swift index 8ef98ce322..56b9102bba 100644 --- a/Session/Utilities/IP2Country.swift +++ b/Session/Utilities/IP2Country.swift @@ -5,7 +5,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Cache diff --git a/Session/Utilities/ImageLoading+Convenience.swift b/Session/Utilities/ImageLoading+Convenience.swift index 7378040c4a..1b1ef57fd3 100644 --- a/Session/Utilities/ImageLoading+Convenience.swift +++ b/Session/Utilities/ImageLoading+Convenience.swift @@ -69,10 +69,11 @@ public extension ImageDataManager.DataSource { // MARK: - ImageDataManagerType Convenience public extension ImageDataManagerType { + @MainActor func loadImage( attachment: Attachment, using dependencies: Dependencies, - onComplete: @escaping (ImageDataManager.ProcessedImageData?) -> Void = { _ in } + onComplete: @MainActor @escaping (ImageDataManager.ProcessedImageData?) -> Void = { _ in } ) { guard let source: ImageDataManager.DataSource = ImageDataManager.DataSource.from( attachment: attachment, @@ -82,11 +83,12 @@ public extension ImageDataManagerType { load(source, onComplete: onComplete) } + @MainActor func loadThumbnail( size: ImageDataManager.ThumbnailSize, attachment: Attachment, using dependencies: Dependencies, - onComplete: @escaping (ImageDataManager.ProcessedImageData?) -> Void = { _ in } + onComplete: @MainActor @escaping (ImageDataManager.ProcessedImageData?) -> Void = { _ in } ) { guard let source: ImageDataManager.DataSource = ImageDataManager.DataSource.thumbnailFrom( attachment: attachment, @@ -96,41 +98,13 @@ public extension ImageDataManagerType { load(source, onComplete: onComplete) } - // TODO: Is this needeed???? - func cachedImage( - attachment: Attachment, - using dependencies: Dependencies - ) -> UIImage? { - guard let source: ImageDataManager.DataSource = ImageDataManager.DataSource.from( - attachment: attachment, - using: dependencies - ) else { return nil } - - let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0) - var result: ImageDataManager.ProcessedImageData? = nil - - load(source) { imageData in - result = imageData - semaphore.signal() - } - - /// We don't really want to wait at all but it's async logic so give it a very time timeout so it has the chance - /// to deal with other logic running - _ = semaphore.wait(timeout: .now() + .milliseconds(10)) - - switch result?.type { - case .staticImage(let image): return image - case .animatedImage(let frames, _): return frames.first - case .none: return nil - } - } } // MARK: - SessionImageView Convenience public extension SessionImageView { @MainActor - func loadImage(from path: String, onComplete: ((Bool) -> Void)? = nil) { + func loadImage(from path: String, onComplete: (@MainActor (ImageDataManager.ProcessedImageData?) -> Void)? = nil) { loadImage(.url(URL(fileURLWithPath: path)), onComplete: onComplete) } @@ -138,13 +112,13 @@ public extension SessionImageView { func loadImage( attachment: Attachment, using dependencies: Dependencies, - onComplete: ((Bool) -> Void)? = nil + onComplete: (@MainActor (ImageDataManager.ProcessedImageData?) -> Void)? = nil ) { guard let source: ImageDataManager.DataSource = ImageDataManager.DataSource.from( attachment: attachment, using: dependencies ) else { - onComplete?(false) + onComplete?(nil) return } @@ -156,14 +130,14 @@ public extension SessionImageView { size: ImageDataManager.ThumbnailSize, attachment: Attachment, using dependencies: Dependencies, - onComplete: ((Bool) -> Void)? = nil + onComplete: (@MainActor (ImageDataManager.ProcessedImageData?) -> Void)? = nil ) { guard let source: ImageDataManager.DataSource = ImageDataManager.DataSource.thumbnailFrom( attachment: attachment, size: size, using: dependencies ) else { - onComplete?(false) + onComplete?(nil) return } @@ -171,7 +145,7 @@ public extension SessionImageView { } @MainActor - func loadPlaceholder(seed: String, text: String, size: CGFloat, onComplete: ((Bool) -> Void)? = nil) { + func loadPlaceholder(seed: String, text: String, size: CGFloat, onComplete: (@MainActor (ImageDataManager.ProcessedImageData?) -> Void)? = nil) { loadImage(.placeholderIcon(seed: seed, text: text, size: size), onComplete: onComplete) } } diff --git a/Session/Utilities/UIContextualAction+Utilities.swift b/Session/Utilities/UIContextualAction+Utilities.swift index 5b5ce426a4..c0edbc3b20 100644 --- a/Session/Utilities/UIContextualAction+Utilities.swift +++ b/Session/Utilities/UIContextualAction+Utilities.swift @@ -649,7 +649,7 @@ public extension UIContextualAction { guard !isMessageRequest else { switch threadViewModel.threadVariant { case .group: return ThemedAttributedString(string: "groupInviteDelete".localized()) - default: return ThemedAttributedString(string: "messageRequestsDelete".localized()) + default: return ThemedAttributedString(string: "messageRequestsContactDelete".localized()) } } @@ -692,7 +692,11 @@ public extension UIContextualAction { return .deleteGroupAndContent case (.group, _, _): return .leaveGroupAsync - case (.contact, _, _): return .deleteContactConversationAndMarkHidden + case (.contact, true, _): + return .deleteContactConversationAndContact + + case (.contact, false, _): + return .deleteContactConversationAndMarkHidden } }() diff --git a/SessionMessagingKit/Calls/NoopSessionCallManager.swift b/SessionMessagingKit/Calls/NoopSessionCallManager.swift index 7db2b8b1d4..613ece9d01 100644 --- a/SessionMessagingKit/Calls/NoopSessionCallManager.swift +++ b/SessionMessagingKit/Calls/NoopSessionCallManager.swift @@ -2,8 +2,9 @@ import Foundation import CallKit +import SessionUtilitiesKit -internal struct NoopSessionCallManager: CallManagerProtocol { +internal struct NoopSessionCallManager: CallManagerProtocol, NoopDependency { var currentCall: CurrentCallProtocol? func setCurrentCall(_ call: CurrentCallProtocol?) {} diff --git a/SessionMessagingKit/Configuration.swift b/SessionMessagingKit/Configuration.swift index 2f861719a4..f95a65aa42 100644 --- a/SessionMessagingKit/Configuration.swift +++ b/SessionMessagingKit/Configuration.swift @@ -2,58 +2,54 @@ import Foundation import GRDB import SessionUtilitiesKit -public enum SNMessagingKit: MigratableTarget { // Just to make the external API nice - public static func migrations() -> TargetMigrations { - return TargetMigrations( - identifier: .messagingKit, - migrations: [ - [ - _001_InitialSetupMigration.self, - _002_SetupStandardJobs.self - ], // Initial DB Creation - [ - _003_YDBToGRDBMigration.self - ], // YDB to GRDB Migration - [ - _004_RemoveLegacyYDB.self - ], // Legacy DB removal - [ - _005_FixDeletedMessageReadState.self, - _006_FixHiddenModAdminSupport.self, - _007_HomeQueryOptimisationIndexes.self - ], // Add job priorities - [ - _008_EmojiReacts.self, - _009_OpenGroupPermission.self, - _010_AddThreadIdToFTS.self - ], // Fix thread FTS - [ - _011_AddPendingReadReceipts.self, - _012_AddFTSIfNeeded.self, - _013_SessionUtilChanges.self, - _014_GenerateInitialUserConfigDumps.self, - _015_BlockCommunityMessageRequests.self, - _016_MakeBrokenProfileTimestampsNullable.self, - _017_RebuildFTSIfNeeded_2_4_5.self, - _018_DisappearingMessagesConfiguration.self, - _019_ScheduleAppUpdateCheckJob.self, - _020_AddMissingWhisperFlag.self, - _021_ReworkRecipientState.self, - _022_GroupsRebuildChanges.self, - _023_GroupsExpiredFlag.self, - _024_FixBustedInteractionVariant.self, - _025_DropLegacyClosedGroupKeyPairTable.self, - _026_MessageDeduplicationTable.self - ], - [], // Renamed `Setting` to `KeyValueStore` - [ - _027_MoveSettingsToLibSession.self, - _028_RenameAttachments.self, - _029_AddProMessageFlag.self - ] - ] - ) - } +public enum SNMessagingKit { + public static let migrations: [Migration.Type] = [ + _001_SUK_InitialSetupMigration.self, + _002_SUK_SetupStandardJobs.self, + _003_SUK_YDBToGRDBMigration.self, + _004_SNK_InitialSetupMigration.self, + _005_SNK_SetupStandardJobs.self, + _006_SMK_InitialSetupMigration.self, + _007_SMK_SetupStandardJobs.self, + _008_SNK_YDBToGRDBMigration.self, + _009_SMK_YDBToGRDBMigration.self, + _010_FlagMessageHashAsDeletedOrInvalid.self, + _011_RemoveLegacyYDB.self, + _012_AddJobPriority.self, + _013_FixDeletedMessageReadState.self, + _014_FixHiddenModAdminSupport.self, + _015_HomeQueryOptimisationIndexes.self, + _016_ThemePreferences.self, + _017_EmojiReacts.self, + _018_OpenGroupPermission.self, + _019_AddThreadIdToFTS.self, + _020_AddJobUniqueHash.self, + _021_AddSnodeReveivedMessageInfoPrimaryKey.self, + _022_DropSnodeCache.self, + _023_SplitSnodeReceivedMessageInfo.self, + _024_ResetUserConfigLastHashes.self, + _025_AddPendingReadReceipts.self, + _026_AddFTSIfNeeded.self, + _027_SessionUtilChanges.self, + _028_GenerateInitialUserConfigDumps.self, + _029_BlockCommunityMessageRequests.self, + _030_MakeBrokenProfileTimestampsNullable.self, + _031_RebuildFTSIfNeeded_2_4_5.self, + _032_DisappearingMessagesConfiguration.self, + _033_ScheduleAppUpdateCheckJob.self, + _034_AddMissingWhisperFlag.self, + _035_ReworkRecipientState.self, + _036_GroupsRebuildChanges.self, + _037_GroupsExpiredFlag.self, + _038_FixBustedInteractionVariant.self, + _039_DropLegacyClosedGroupKeyPairTable.self, + _040_MessageDeduplicationTable.self, + _041_RenameTableSettingToKeyValueStore.self, + _042_MoveSettingsToLibSession.self, + _043_RenameAttachments.self, + _044_AddProMessageFlag.self, + _045_LastProfileUpdateTimestamp.self + ] public static func configure(using dependencies: Dependencies) { // Configure the job executors diff --git a/SessionMessagingKit/Crypto/Crypto+Attachments.swift b/SessionMessagingKit/Crypto/Crypto+Attachments.swift index 6caca2f459..ceb7cca191 100644 --- a/SessionMessagingKit/Crypto/Crypto+Attachments.swift +++ b/SessionMessagingKit/Crypto/Crypto+Attachments.swift @@ -4,7 +4,7 @@ import Foundation import CommonCrypto -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Encryption diff --git a/SessionMessagingKit/Crypto/Crypto+SessionMessagingKit.swift b/SessionMessagingKit/Crypto/Crypto+SessionMessagingKit.swift index bea6331051..ff92a72277 100644 --- a/SessionMessagingKit/Crypto/Crypto+SessionMessagingKit.swift +++ b/SessionMessagingKit/Crypto/Crypto+SessionMessagingKit.swift @@ -4,7 +4,7 @@ import Foundation import CryptoKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtil import SessionUtilitiesKit @@ -164,38 +164,6 @@ public extension Crypto.Generator { } } - static func plaintextWithPushNotificationPayload( - payload: Data, - encKey: Data - ) -> Crypto.Generator { - return Crypto.Generator( - id: "plaintextWithPushNotificationPayload", - args: [payload, encKey] - ) { - var cPayload: [UInt8] = Array(payload) - var cEncKey: [UInt8] = Array(encKey) - var maybePlaintext: UnsafeMutablePointer? = nil - var plaintextLen: Int = 0 - - guard - cEncKey.count == 32, - session_decrypt_push_notification( - &cPayload, - cPayload.count, - &cEncKey, - &maybePlaintext, - &plaintextLen - ), - plaintextLen > 0, - let plaintext: Data = maybePlaintext.map({ Data(bytes: $0, count: plaintextLen) }) - else { throw MessageReceiverError.decryptionFailed } - - free(UnsafeMutableRawPointer(mutating: maybePlaintext)) - - return plaintext - } - } - static func plaintextWithMultiEncrypt( ciphertext: Data, senderSessionId: SessionId, @@ -231,7 +199,7 @@ public extension Crypto.Generator { static func messageServerHash( swarmPubkey: String, - namespace: SnodeAPI.Namespace, + namespace: Network.SnodeAPI.Namespace, data: Data ) -> Crypto.Generator { return Crypto.Generator( diff --git a/SessionUtilitiesKit/Database/Migrations/_001_InitialSetupMigration.swift b/SessionMessagingKit/Database/Migrations/_001_SUK_InitialSetupMigration.swift similarity index 94% rename from SessionUtilitiesKit/Database/Migrations/_001_InitialSetupMigration.swift rename to SessionMessagingKit/Database/Migrations/_001_SUK_InitialSetupMigration.swift index 038c6de166..d91ffaca7d 100644 --- a/SessionUtilitiesKit/Database/Migrations/_001_InitialSetupMigration.swift +++ b/SessionMessagingKit/Database/Migrations/_001_SUK_InitialSetupMigration.swift @@ -4,10 +4,10 @@ import Foundation import GRDB +import SessionUtilitiesKit -enum _001_InitialSetupMigration: Migration { - static let target: TargetMigrations.Identifier = .utilitiesKit - static let identifier: String = "initialSetup" +enum _001_SUK_InitialSetupMigration: Migration { + static let identifier: String = "utilitiesKit.initialSetup" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [ Identity.self, Job.self, JobDependencies.self diff --git a/SessionUtilitiesKit/Database/Migrations/_002_SetupStandardJobs.swift b/SessionMessagingKit/Database/Migrations/_002_SUK_SetupStandardJobs.swift similarity index 89% rename from SessionUtilitiesKit/Database/Migrations/_002_SetupStandardJobs.swift rename to SessionMessagingKit/Database/Migrations/_002_SUK_SetupStandardJobs.swift index ca787c2c2c..8f27b313d5 100644 --- a/SessionUtilitiesKit/Database/Migrations/_002_SetupStandardJobs.swift +++ b/SessionMessagingKit/Database/Migrations/_002_SUK_SetupStandardJobs.swift @@ -2,12 +2,12 @@ import Foundation import GRDB +import SessionUtilitiesKit /// This migration sets up the standard jobs, since we want these jobs to run before any "once-off" jobs we do this migration /// before running the `YDBToGRDBMigration` -enum _002_SetupStandardJobs: Migration { - static let target: TargetMigrations.Identifier = .utilitiesKit - static let identifier: String = "SetupStandardJobs" +enum _002_SUK_SetupStandardJobs: Migration { + static let identifier: String = "utilitiesKit.SetupStandardJobs" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionSnodeKit/Database/Migrations/_003_YDBToGRDBMigration.swift b/SessionMessagingKit/Database/Migrations/_003_SUK_YDBToGRDBMigration.swift similarity index 71% rename from SessionSnodeKit/Database/Migrations/_003_YDBToGRDBMigration.swift rename to SessionMessagingKit/Database/Migrations/_003_SUK_YDBToGRDBMigration.swift index 4e826bc308..5753532d9a 100644 --- a/SessionSnodeKit/Database/Migrations/_003_YDBToGRDBMigration.swift +++ b/SessionMessagingKit/Database/Migrations/_003_SUK_YDBToGRDBMigration.swift @@ -4,9 +4,8 @@ import Foundation import GRDB import SessionUtilitiesKit -enum _003_YDBToGRDBMigration: Migration { - static let target: TargetMigrations.Identifier = .snodeKit - static let identifier: String = "YDBToGRDBMigration" +enum _003_SUK_YDBToGRDBMigration: Migration { + static let identifier: String = "utilitiesKit.YDBToGRDBMigration" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionSnodeKit/Database/Migrations/_001_InitialSetupMigration.swift b/SessionMessagingKit/Database/Migrations/_004_SNK_InitialSetupMigration.swift similarity index 91% rename from SessionSnodeKit/Database/Migrations/_001_InitialSetupMigration.swift rename to SessionMessagingKit/Database/Migrations/_004_SNK_InitialSetupMigration.swift index 02f160fe0c..9852a0a11f 100644 --- a/SessionSnodeKit/Database/Migrations/_001_InitialSetupMigration.swift +++ b/SessionMessagingKit/Database/Migrations/_004_SNK_InitialSetupMigration.swift @@ -6,9 +6,8 @@ import Foundation import GRDB import SessionUtilitiesKit -enum _001_InitialSetupMigration: Migration { - static let target: TargetMigrations.Identifier = .snodeKit - static let identifier: String = "initialSetup" +enum _004_SNK_InitialSetupMigration: Migration { + static let identifier: String = "snodeKit.initialSetup" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionSnodeKit/Database/Migrations/_002_SetupStandardJobs.swift b/SessionMessagingKit/Database/Migrations/_005_SNK_SetupStandardJobs.swift similarity index 91% rename from SessionSnodeKit/Database/Migrations/_002_SetupStandardJobs.swift rename to SessionMessagingKit/Database/Migrations/_005_SNK_SetupStandardJobs.swift index e92355cc9e..2241868cc8 100644 --- a/SessionSnodeKit/Database/Migrations/_002_SetupStandardJobs.swift +++ b/SessionMessagingKit/Database/Migrations/_005_SNK_SetupStandardJobs.swift @@ -6,9 +6,8 @@ import SessionUtilitiesKit /// This migration sets up the standard jobs, since we want these jobs to run before any "once-off" jobs we do this migration /// before running the `YDBToGRDBMigration` -enum _002_SetupStandardJobs: Migration { - static let target: TargetMigrations.Identifier = .snodeKit - static let identifier: String = "SetupStandardJobs" +enum _005_SNK_SetupStandardJobs: Migration { + static let identifier: String = "snodeKit.SetupStandardJobs" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_001_InitialSetupMigration.swift b/SessionMessagingKit/Database/Migrations/_006_SMK_InitialSetupMigration.swift similarity index 97% rename from SessionMessagingKit/Database/Migrations/_001_InitialSetupMigration.swift rename to SessionMessagingKit/Database/Migrations/_006_SMK_InitialSetupMigration.swift index 2823930730..6c9e861ca9 100644 --- a/SessionMessagingKit/Database/Migrations/_001_InitialSetupMigration.swift +++ b/SessionMessagingKit/Database/Migrations/_006_SMK_InitialSetupMigration.swift @@ -6,9 +6,8 @@ import Foundation import GRDB import SessionUtilitiesKit -enum _001_InitialSetupMigration: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "initialSetup" +enum _006_SMK_InitialSetupMigration: Migration { + static let identifier: String = "messagingKit.initialSetup" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [ Contact.self, Profile.self, SessionThread.self, DisappearingMessagesConfiguration.self, @@ -59,7 +58,7 @@ enum _001_InitialSetupMigration: Migration { /// Create a full-text search table synchronized with the Profile table try db.create(virtualTable: "profile_fts", using: FTS5()) { t in t.synchronize(withTable: "profile") - t.tokenizer = _001_InitialSetupMigration.fullTextSearchTokenizer + t.tokenizer = _006_SMK_InitialSetupMigration.fullTextSearchTokenizer t.column("nickname") t.column("name") @@ -106,7 +105,7 @@ enum _001_InitialSetupMigration: Migration { /// Create a full-text search table synchronized with the ClosedGroup table try db.create(virtualTable: "closedGroup_fts", using: FTS5()) { t in t.synchronize(withTable: "closedGroup") - t.tokenizer = _001_InitialSetupMigration.fullTextSearchTokenizer + t.tokenizer = _006_SMK_InitialSetupMigration.fullTextSearchTokenizer t.column("name") } @@ -157,7 +156,7 @@ enum _001_InitialSetupMigration: Migration { /// Create a full-text search table synchronized with the OpenGroup table try db.create(virtualTable: "openGroup_fts", using: FTS5()) { t in t.synchronize(withTable: "openGroup") - t.tokenizer = _001_InitialSetupMigration.fullTextSearchTokenizer + t.tokenizer = _006_SMK_InitialSetupMigration.fullTextSearchTokenizer t.column("name") } @@ -268,7 +267,7 @@ enum _001_InitialSetupMigration: Migration { /// Create a full-text search table synchronized with the Interaction table try db.create(virtualTable: "interaction_fts", using: FTS5()) { t in t.synchronize(withTable: "interaction") - t.tokenizer = _001_InitialSetupMigration.fullTextSearchTokenizer + t.tokenizer = _006_SMK_InitialSetupMigration.fullTextSearchTokenizer t.column("body") } diff --git a/SessionMessagingKit/Database/Migrations/_002_SetupStandardJobs.swift b/SessionMessagingKit/Database/Migrations/_007_SMK_SetupStandardJobs.swift similarity index 92% rename from SessionMessagingKit/Database/Migrations/_002_SetupStandardJobs.swift rename to SessionMessagingKit/Database/Migrations/_007_SMK_SetupStandardJobs.swift index bfcdbea5d3..f7057035e0 100644 --- a/SessionMessagingKit/Database/Migrations/_002_SetupStandardJobs.swift +++ b/SessionMessagingKit/Database/Migrations/_007_SMK_SetupStandardJobs.swift @@ -3,13 +3,12 @@ import Foundation import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit /// This migration sets up the standard jobs, since we want these jobs to run before any "once-off" jobs we do this migration /// before running the `YDBToGRDBMigration` -enum _002_SetupStandardJobs: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "SetupStandardJobs" +enum _007_SMK_SetupStandardJobs: Migration { + static let identifier: String = "messagingKit.SetupStandardJobs" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionUtilitiesKit/Database/Migrations/_003_YDBToGRDBMigration.swift b/SessionMessagingKit/Database/Migrations/_008_SNK_YDBToGRDBMigration.swift similarity index 69% rename from SessionUtilitiesKit/Database/Migrations/_003_YDBToGRDBMigration.swift rename to SessionMessagingKit/Database/Migrations/_008_SNK_YDBToGRDBMigration.swift index 565fd3e3f2..ac66dd7c0b 100644 --- a/SessionUtilitiesKit/Database/Migrations/_003_YDBToGRDBMigration.swift +++ b/SessionMessagingKit/Database/Migrations/_008_SNK_YDBToGRDBMigration.swift @@ -2,10 +2,10 @@ import Foundation import GRDB +import SessionUtilitiesKit -enum _003_YDBToGRDBMigration: Migration { - static let target: TargetMigrations.Identifier = .utilitiesKit - static let identifier: String = "YDBToGRDBMigration" +enum _008_SNK_YDBToGRDBMigration: Migration { + static let identifier: String = "snodeKit.YDBToGRDBMigration" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift b/SessionMessagingKit/Database/Migrations/_009_SMK_YDBToGRDBMigration.swift similarity index 79% rename from SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift rename to SessionMessagingKit/Database/Migrations/_009_SMK_YDBToGRDBMigration.swift index 13ecc5df76..7851f48d74 100644 --- a/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift +++ b/SessionMessagingKit/Database/Migrations/_009_SMK_YDBToGRDBMigration.swift @@ -4,9 +4,8 @@ import Foundation import GRDB import SessionUtilitiesKit -enum _003_YDBToGRDBMigration: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "YDBToGRDBMigration" +enum _009_SMK_YDBToGRDBMigration: Migration { + static let identifier: String = "messagingKit.YDBToGRDBMigration" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionSnodeKit/Database/Migrations/_004_FlagMessageHashAsDeletedOrInvalid.swift b/SessionMessagingKit/Database/Migrations/_010_FlagMessageHashAsDeletedOrInvalid.swift similarity index 82% rename from SessionSnodeKit/Database/Migrations/_004_FlagMessageHashAsDeletedOrInvalid.swift rename to SessionMessagingKit/Database/Migrations/_010_FlagMessageHashAsDeletedOrInvalid.swift index 486665167c..ff71d5ffbe 100644 --- a/SessionSnodeKit/Database/Migrations/_004_FlagMessageHashAsDeletedOrInvalid.swift +++ b/SessionMessagingKit/Database/Migrations/_010_FlagMessageHashAsDeletedOrInvalid.swift @@ -7,9 +7,8 @@ import SessionUtilitiesKit /// This migration adds a flag to the `SnodeReceivedMessageInfo` so that when deleting interactions we can /// ignore their hashes when subsequently trying to fetch new messages (which results in the storage server returning /// messages from the beginning of time) -enum _004_FlagMessageHashAsDeletedOrInvalid: Migration { - static let target: TargetMigrations.Identifier = .snodeKit - static let identifier: String = "FlagMessageHashAsDeletedOrInvalid" +enum _010_FlagMessageHashAsDeletedOrInvalid: Migration { + static let identifier: String = "snodeKit.FlagMessageHashAsDeletedOrInvalid" static let minExpectedRunDuration: TimeInterval = 0.2 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_004_RemoveLegacyYDB.swift b/SessionMessagingKit/Database/Migrations/_011_RemoveLegacyYDB.swift similarity index 75% rename from SessionMessagingKit/Database/Migrations/_004_RemoveLegacyYDB.swift rename to SessionMessagingKit/Database/Migrations/_011_RemoveLegacyYDB.swift index 07db8962d8..ef8588451f 100644 --- a/SessionMessagingKit/Database/Migrations/_004_RemoveLegacyYDB.swift +++ b/SessionMessagingKit/Database/Migrations/_011_RemoveLegacyYDB.swift @@ -3,12 +3,11 @@ import Foundation import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit /// This migration used to remove the legacy YapDatabase files (the old logic has been removed and is no longer supported so it now does nothing) -enum _004_RemoveLegacyYDB: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "RemoveLegacyYDB" +enum _011_RemoveLegacyYDB: Migration { + static let identifier: String = "messagingKit.RemoveLegacyYDB" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionUtilitiesKit/Database/Migrations/_004_AddJobPriority.swift b/SessionMessagingKit/Database/Migrations/_012_AddJobPriority.swift similarity index 90% rename from SessionUtilitiesKit/Database/Migrations/_004_AddJobPriority.swift rename to SessionMessagingKit/Database/Migrations/_012_AddJobPriority.swift index 852033b582..93a2c68752 100644 --- a/SessionUtilitiesKit/Database/Migrations/_004_AddJobPriority.swift +++ b/SessionMessagingKit/Database/Migrations/_012_AddJobPriority.swift @@ -2,10 +2,10 @@ import Foundation import GRDB +import SessionUtilitiesKit -enum _004_AddJobPriority: Migration { - static let target: TargetMigrations.Identifier = .utilitiesKit - static let identifier: String = "AddJobPriority" +enum _012_AddJobPriority: Migration { + static let identifier: String = "utilitiesKit.AddJobPriority" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_005_FixDeletedMessageReadState.swift b/SessionMessagingKit/Database/Migrations/_013_FixDeletedMessageReadState.swift similarity index 83% rename from SessionMessagingKit/Database/Migrations/_005_FixDeletedMessageReadState.swift rename to SessionMessagingKit/Database/Migrations/_013_FixDeletedMessageReadState.swift index efe33c321d..a49d63d0ca 100644 --- a/SessionMessagingKit/Database/Migrations/_005_FixDeletedMessageReadState.swift +++ b/SessionMessagingKit/Database/Migrations/_013_FixDeletedMessageReadState.swift @@ -5,9 +5,8 @@ import GRDB import SessionUtilitiesKit /// This migration fixes a bug where certain message variants could incorrectly be counted as unread messages -enum _005_FixDeletedMessageReadState: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "FixDeletedMessageReadState" +enum _013_FixDeletedMessageReadState: Migration { + static let identifier: String = "messagingKit.FixDeletedMessageReadState" static let minExpectedRunDuration: TimeInterval = 0.01 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_006_FixHiddenModAdminSupport.swift b/SessionMessagingKit/Database/Migrations/_014_FixHiddenModAdminSupport.swift similarity index 85% rename from SessionMessagingKit/Database/Migrations/_006_FixHiddenModAdminSupport.swift rename to SessionMessagingKit/Database/Migrations/_014_FixHiddenModAdminSupport.swift index 006b04c283..5247ae2d79 100644 --- a/SessionMessagingKit/Database/Migrations/_006_FixHiddenModAdminSupport.swift +++ b/SessionMessagingKit/Database/Migrations/_014_FixHiddenModAdminSupport.swift @@ -6,9 +6,8 @@ import SessionUtilitiesKit /// This migration fixes an issue where hidden mods/admins weren't getting recognised as mods/admins, it reset's the `info_updates` /// for open groups so they will fully re-fetch their mod/admin lists -enum _006_FixHiddenModAdminSupport: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "FixHiddenModAdminSupport" +enum _014_FixHiddenModAdminSupport: Migration { + static let identifier: String = "messagingKit.FixHiddenModAdminSupport" static let minExpectedRunDuration: TimeInterval = 0.01 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_007_HomeQueryOptimisationIndexes.swift b/SessionMessagingKit/Database/Migrations/_015_HomeQueryOptimisationIndexes.swift similarity index 81% rename from SessionMessagingKit/Database/Migrations/_007_HomeQueryOptimisationIndexes.swift rename to SessionMessagingKit/Database/Migrations/_015_HomeQueryOptimisationIndexes.swift index bf8ded493e..3a0a617928 100644 --- a/SessionMessagingKit/Database/Migrations/_007_HomeQueryOptimisationIndexes.swift +++ b/SessionMessagingKit/Database/Migrations/_015_HomeQueryOptimisationIndexes.swift @@ -7,9 +7,8 @@ import GRDB import SessionUtilitiesKit /// This migration adds an index to the interaction table in order to improve the performance of retrieving the number of unread interactions -enum _007_HomeQueryOptimisationIndexes: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "HomeQueryOptimisationIndexes" +enum _015_HomeQueryOptimisationIndexes: Migration { + static let identifier: String = "messagingKit.HomeQueryOptimisationIndexes" static let minExpectedRunDuration: TimeInterval = 0.01 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/Session/Database/Migrations/_001_ThemePreferences.swift b/SessionMessagingKit/Database/Migrations/_016_ThemePreferences.swift similarity index 78% rename from Session/Database/Migrations/_001_ThemePreferences.swift rename to SessionMessagingKit/Database/Migrations/_016_ThemePreferences.swift index 8bb9987f95..f19020e3ed 100644 --- a/Session/Database/Migrations/_001_ThemePreferences.swift +++ b/SessionMessagingKit/Database/Migrations/_016_ThemePreferences.swift @@ -13,9 +13,8 @@ import SessionUtilitiesKit /// **Note:** This migration used to live within `SessionUIKit` but we wanted to isolate it and remove dependencies from it so we /// needed to extract this migration into the `Session` and `SessionShareExtension` targets (since both need theming they both /// need to provide this migration as an option during setup) -enum _001_ThemePreferences: Migration { - static let target: TargetMigrations.Identifier = ._deprecatedUIKit - static let identifier: String = "ThemePreferences" +enum _016_ThemePreferences: Migration { + static let identifier: String = "uiKit.ThemePreferences" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] @@ -86,25 +85,3 @@ private extension Theme.PrimaryColor { } } } - -enum DeprecatedUIKitMigrationTarget: MigratableTarget { - public static func migrations() -> TargetMigrations { - return TargetMigrations( - identifier: ._deprecatedUIKit, - migrations: [ - // Want to ensure the initial DB stuff has been completed before doing any - // SNUIKit migrations - [], // Initial DB Creation - [], // YDB to GRDB Migration - [], // Legacy DB removal - [ - _001_ThemePreferences.self - ], // Add job priorities - [], // Fix thread FTS - [], - [], // Renamed `Setting` to `KeyValueStore` - [] - ] - ) - } -} diff --git a/SessionMessagingKit/Database/Migrations/_008_EmojiReacts.swift b/SessionMessagingKit/Database/Migrations/_017_EmojiReacts.swift similarity index 91% rename from SessionMessagingKit/Database/Migrations/_008_EmojiReacts.swift rename to SessionMessagingKit/Database/Migrations/_017_EmojiReacts.swift index bcd9c2f84b..c102846bad 100644 --- a/SessionMessagingKit/Database/Migrations/_008_EmojiReacts.swift +++ b/SessionMessagingKit/Database/Migrations/_017_EmojiReacts.swift @@ -5,9 +5,8 @@ import GRDB import SessionUtilitiesKit /// This migration adds the new types needed for Emoji Reacts -enum _008_EmojiReacts: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "EmojiReacts" +enum _017_EmojiReacts: Migration { + static let identifier: String = "messagingKit.EmojiReacts" static let minExpectedRunDuration: TimeInterval = 0.01 static let createdTables: [(TableRecord & FetchableRecord).Type] = [Reaction.self] diff --git a/SessionMessagingKit/Database/Migrations/_009_OpenGroupPermission.swift b/SessionMessagingKit/Database/Migrations/_018_OpenGroupPermission.swift similarity index 83% rename from SessionMessagingKit/Database/Migrations/_009_OpenGroupPermission.swift rename to SessionMessagingKit/Database/Migrations/_018_OpenGroupPermission.swift index b8e7c47efb..bf51074058 100644 --- a/SessionMessagingKit/Database/Migrations/_009_OpenGroupPermission.swift +++ b/SessionMessagingKit/Database/Migrations/_018_OpenGroupPermission.swift @@ -4,9 +4,8 @@ import Foundation import GRDB import SessionUtilitiesKit -enum _009_OpenGroupPermission: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "OpenGroupPermission" +enum _018_OpenGroupPermission: Migration { + static let identifier: String = "messagingKit.OpenGroupPermission" static let minExpectedRunDuration: TimeInterval = 0.01 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_010_AddThreadIdToFTS.swift b/SessionMessagingKit/Database/Migrations/_019_AddThreadIdToFTS.swift similarity index 82% rename from SessionMessagingKit/Database/Migrations/_010_AddThreadIdToFTS.swift rename to SessionMessagingKit/Database/Migrations/_019_AddThreadIdToFTS.swift index 9c2aea1207..92dfecc4fd 100644 --- a/SessionMessagingKit/Database/Migrations/_010_AddThreadIdToFTS.swift +++ b/SessionMessagingKit/Database/Migrations/_019_AddThreadIdToFTS.swift @@ -6,9 +6,8 @@ import SessionUtilitiesKit /// This migration recreates the interaction FTS table and adds the threadId so we can do a performant in-conversation /// searh (currently it's much slower than the global search) -enum _010_AddThreadIdToFTS: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "AddThreadIdToFTS" +enum _019_AddThreadIdToFTS: Migration { + static let identifier: String = "messagingKit.AddThreadIdToFTS" static let minExpectedRunDuration: TimeInterval = 3 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] @@ -22,7 +21,7 @@ enum _010_AddThreadIdToFTS: Migration { try db.create(virtualTable: "interaction_fts", using: FTS5()) { t in t.synchronize(withTable: "interaction") - t.tokenizer = _001_InitialSetupMigration.fullTextSearchTokenizer + t.tokenizer = _006_SMK_InitialSetupMigration.fullTextSearchTokenizer t.column("body") t.column("threadId") diff --git a/SessionUtilitiesKit/Database/Migrations/_005_AddJobUniqueHash.swift b/SessionMessagingKit/Database/Migrations/_020_AddJobUniqueHash.swift similarity index 76% rename from SessionUtilitiesKit/Database/Migrations/_005_AddJobUniqueHash.swift rename to SessionMessagingKit/Database/Migrations/_020_AddJobUniqueHash.swift index e4a36701f5..b9bebf7e81 100644 --- a/SessionUtilitiesKit/Database/Migrations/_005_AddJobUniqueHash.swift +++ b/SessionMessagingKit/Database/Migrations/_020_AddJobUniqueHash.swift @@ -2,10 +2,10 @@ import Foundation import GRDB +import SessionUtilitiesKit -enum _005_AddJobUniqueHash: Migration { - static let target: TargetMigrations.Identifier = .utilitiesKit - static let identifier: String = "AddJobUniqueHash" +enum _020_AddJobUniqueHash: Migration { + static let identifier: String = "utilitiesKit.AddJobUniqueHash" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionSnodeKit/Database/Migrations/_005_AddSnodeReveivedMessageInfoPrimaryKey.swift b/SessionMessagingKit/Database/Migrations/_021_AddSnodeReveivedMessageInfoPrimaryKey.swift similarity index 91% rename from SessionSnodeKit/Database/Migrations/_005_AddSnodeReveivedMessageInfoPrimaryKey.swift rename to SessionMessagingKit/Database/Migrations/_021_AddSnodeReveivedMessageInfoPrimaryKey.swift index acc361590d..55e215871e 100644 --- a/SessionSnodeKit/Database/Migrations/_005_AddSnodeReveivedMessageInfoPrimaryKey.swift +++ b/SessionMessagingKit/Database/Migrations/_021_AddSnodeReveivedMessageInfoPrimaryKey.swift @@ -2,12 +2,12 @@ import Foundation import GRDB +import SessionNetworkingKit import SessionUtilitiesKit /// This migration adds a primary key to `SnodeReceivedMessageInfo` based on the key and hash to speed up lookup -enum _005_AddSnodeReveivedMessageInfoPrimaryKey: Migration { - static let target: TargetMigrations.Identifier = .snodeKit - static let identifier: String = "AddSnodeReveivedMessageInfoPrimaryKey" +enum _021_AddSnodeReveivedMessageInfoPrimaryKey: Migration { + static let identifier: String = "snodeKit.AddSnodeReveivedMessageInfoPrimaryKey" static let minExpectedRunDuration: TimeInterval = 0.2 static let createdTables: [(TableRecord & FetchableRecord).Type] = [SnodeReceivedMessageInfo.self] diff --git a/SessionSnodeKit/Database/Migrations/_006_DropSnodeCache.swift b/SessionMessagingKit/Database/Migrations/_022_DropSnodeCache.swift similarity index 86% rename from SessionSnodeKit/Database/Migrations/_006_DropSnodeCache.swift rename to SessionMessagingKit/Database/Migrations/_022_DropSnodeCache.swift index b2a3d41bd2..af5ceaaa5d 100644 --- a/SessionSnodeKit/Database/Migrations/_006_DropSnodeCache.swift +++ b/SessionMessagingKit/Database/Migrations/_022_DropSnodeCache.swift @@ -5,9 +5,8 @@ import GRDB import SessionUtilitiesKit /// This migration drops the current `SnodePool` and `SnodeSet` and their associated jobs as they are handled by `libSession` now -enum _006_DropSnodeCache: Migration { - static let target: TargetMigrations.Identifier = .snodeKit - static let identifier: String = "DropSnodeCache" +enum _022_DropSnodeCache: Migration { + static let identifier: String = "snodeKit.DropSnodeCache" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionSnodeKit/Database/Migrations/_007_SplitSnodeReceivedMessageInfo.swift b/SessionMessagingKit/Database/Migrations/_023_SplitSnodeReceivedMessageInfo.swift similarity index 95% rename from SessionSnodeKit/Database/Migrations/_007_SplitSnodeReceivedMessageInfo.swift rename to SessionMessagingKit/Database/Migrations/_023_SplitSnodeReceivedMessageInfo.swift index aa74f45ff4..91746c8cef 100644 --- a/SessionSnodeKit/Database/Migrations/_007_SplitSnodeReceivedMessageInfo.swift +++ b/SessionMessagingKit/Database/Migrations/_023_SplitSnodeReceivedMessageInfo.swift @@ -2,12 +2,12 @@ import Foundation import GRDB +import SessionNetworkingKit import SessionUtilitiesKit /// This migration splits the old `key` structure used for `SnodeReceivedMessageInfo` into separate columns for more efficient querying -enum _007_SplitSnodeReceivedMessageInfo: Migration { - static let target: TargetMigrations.Identifier = .snodeKit - static let identifier: String = "SplitSnodeReceivedMessageInfo" +enum _023_SplitSnodeReceivedMessageInfo: Migration { + static let identifier: String = "snodeKit.SplitSnodeReceivedMessageInfo" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [SnodeReceivedMessageInfo.self] @@ -91,10 +91,10 @@ enum _007_SplitSnodeReceivedMessageInfo: Migration { let targetNamespace: Int = { guard swarmPublicKeySplitComponents.count == 2 else { - return SnodeAPI.Namespace.default.rawValue + return Network.SnodeAPI.Namespace.default.rawValue } - return (Int(swarmPublicKeySplitComponents[1]) ?? SnodeAPI.Namespace.default.rawValue) + return (Int(swarmPublicKeySplitComponents[1]) ?? Network.SnodeAPI.Namespace.default.rawValue) }() let wasDeletedOrInvalid: Bool? = info["wasDeletedOrInvalid"] diff --git a/SessionSnodeKit/Database/Migrations/_008_ResetUserConfigLastHashes.swift b/SessionMessagingKit/Database/Migrations/_024_ResetUserConfigLastHashes.swift similarity index 63% rename from SessionSnodeKit/Database/Migrations/_008_ResetUserConfigLastHashes.swift rename to SessionMessagingKit/Database/Migrations/_024_ResetUserConfigLastHashes.swift index 468ee6999c..60052a5eda 100644 --- a/SessionSnodeKit/Database/Migrations/_008_ResetUserConfigLastHashes.swift +++ b/SessionMessagingKit/Database/Migrations/_024_ResetUserConfigLastHashes.swift @@ -2,20 +2,20 @@ import Foundation import GRDB +import SessionNetworkingKit import SessionUtilitiesKit /// This migration resets the `lastHash` value for all user config namespaces to force the app to fetch the latest config /// messages in case there are multi-part config message we had previously seen and failed to merge -enum _008_ResetUserConfigLastHashes: Migration { - static let target: TargetMigrations.Identifier = .snodeKit - static let identifier: String = "ResetUserConfigLastHashes" +enum _024_ResetUserConfigLastHashes: Migration { + static let identifier: String = "snodeKit.ResetUserConfigLastHashes" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] static func migrate(_ db: ObservingDatabase, using dependencies: Dependencies) throws { try db.execute(literal: """ DELETE FROM snodeReceivedMessageInfo - WHERE namespace IN (\(SnodeAPI.Namespace.configContacts.rawValue), \(SnodeAPI.Namespace.configUserProfile.rawValue), \(SnodeAPI.Namespace.configUserGroups.rawValue), \(SnodeAPI.Namespace.configConvoInfoVolatile.rawValue)) + WHERE namespace IN (\(Network.SnodeAPI.Namespace.configContacts.rawValue), \(Network.SnodeAPI.Namespace.configUserProfile.rawValue), \(Network.SnodeAPI.Namespace.configUserGroups.rawValue), \(Network.SnodeAPI.Namespace.configConvoInfoVolatile.rawValue)) """) MigrationExecution.updateProgress(1) diff --git a/SessionMessagingKit/Database/Migrations/_011_AddPendingReadReceipts.swift b/SessionMessagingKit/Database/Migrations/_025_AddPendingReadReceipts.swift similarity index 89% rename from SessionMessagingKit/Database/Migrations/_011_AddPendingReadReceipts.swift rename to SessionMessagingKit/Database/Migrations/_025_AddPendingReadReceipts.swift index 5f51432095..0b0d5fec63 100644 --- a/SessionMessagingKit/Database/Migrations/_011_AddPendingReadReceipts.swift +++ b/SessionMessagingKit/Database/Migrations/_025_AddPendingReadReceipts.swift @@ -6,9 +6,8 @@ import SessionUtilitiesKit /// This migration adds a table to track pending read receipts (it's possible to receive a read receipt message before getting the original /// message due to how one-to-one conversations work, by storing pending read receipts we should be able to prevent this case) -enum _011_AddPendingReadReceipts: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "AddPendingReadReceipts" +enum _025_AddPendingReadReceipts: Migration { + static let identifier: String = "messagingKit.AddPendingReadReceipts" static let minExpectedRunDuration: TimeInterval = 0.01 static let createdTables: [(TableRecord & FetchableRecord).Type] = [PendingReadReceipt.self] diff --git a/SessionMessagingKit/Database/Migrations/_012_AddFTSIfNeeded.swift b/SessionMessagingKit/Database/Migrations/_026_AddFTSIfNeeded.swift similarity index 80% rename from SessionMessagingKit/Database/Migrations/_012_AddFTSIfNeeded.swift rename to SessionMessagingKit/Database/Migrations/_026_AddFTSIfNeeded.swift index a030deed3f..b655432c2c 100644 --- a/SessionMessagingKit/Database/Migrations/_012_AddFTSIfNeeded.swift +++ b/SessionMessagingKit/Database/Migrations/_026_AddFTSIfNeeded.swift @@ -5,9 +5,8 @@ import GRDB import SessionUtilitiesKit /// This migration adds the FTS table back for internal test users whose FTS table was removed unintentionally -enum _012_AddFTSIfNeeded: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "AddFTSIfNeeded" +enum _026_AddFTSIfNeeded: Migration { + static let identifier: String = "messagingKit.AddFTSIfNeeded" static let minExpectedRunDuration: TimeInterval = 0.01 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] @@ -17,7 +16,7 @@ enum _012_AddFTSIfNeeded: Migration { if try db.tableExists("interaction_fts") == false { try db.create(virtualTable: "interaction_fts", using: FTS5()) { t in t.synchronize(withTable: "interaction") - t.tokenizer = _001_InitialSetupMigration.fullTextSearchTokenizer + t.tokenizer = _006_SMK_InitialSetupMigration.fullTextSearchTokenizer t.column("body") t.column("threadId") diff --git a/SessionMessagingKit/Database/Migrations/_013_SessionUtilChanges.swift b/SessionMessagingKit/Database/Migrations/_027_SessionUtilChanges.swift similarity index 98% rename from SessionMessagingKit/Database/Migrations/_013_SessionUtilChanges.swift rename to SessionMessagingKit/Database/Migrations/_027_SessionUtilChanges.swift index cb67ad5bf5..57e76b93a9 100644 --- a/SessionMessagingKit/Database/Migrations/_013_SessionUtilChanges.swift +++ b/SessionMessagingKit/Database/Migrations/_027_SessionUtilChanges.swift @@ -9,9 +9,8 @@ import SessionUtil import SessionUtilitiesKit /// This migration makes the neccessary changes to support the updated user config syncing system -enum _013_SessionUtilChanges: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "SessionUtilChanges" +enum _027_SessionUtilChanges: Migration { + static let identifier: String = "messagingKit.SessionUtilChanges" static let minExpectedRunDuration: TimeInterval = 0.4 static let createdTables: [(TableRecord & FetchableRecord).Type] = [ConfigDump.self] @@ -229,7 +228,7 @@ enum _013_SessionUtilChanges: Migration { } } -private extension _013_SessionUtilChanges { +private extension _027_SessionUtilChanges { static func generateLegacyClosedGroupKeyPairHash(threadId: String, publicKey: Data, secretKey: Data) -> String { return Data(Insecure.MD5 .hash(data: threadId.bytes + publicKey.bytes + secretKey.bytes) diff --git a/SessionMessagingKit/Database/Migrations/_014_GenerateInitialUserConfigDumps.swift b/SessionMessagingKit/Database/Migrations/_028_GenerateInitialUserConfigDumps.swift similarity index 98% rename from SessionMessagingKit/Database/Migrations/_014_GenerateInitialUserConfigDumps.swift rename to SessionMessagingKit/Database/Migrations/_028_GenerateInitialUserConfigDumps.swift index c81a813e12..76f27c71dd 100644 --- a/SessionMessagingKit/Database/Migrations/_014_GenerateInitialUserConfigDumps.swift +++ b/SessionMessagingKit/Database/Migrations/_028_GenerateInitialUserConfigDumps.swift @@ -6,9 +6,8 @@ import SessionUtil import SessionUtilitiesKit /// This migration goes through the current state of the database and generates config dumps for the user config types -enum _014_GenerateInitialUserConfigDumps: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "GenerateInitialUserConfigDumps" +enum _028_GenerateInitialUserConfigDumps: Migration { + static let identifier: String = "messagingKit.GenerateInitialUserConfigDumps" static let minExpectedRunDuration: TimeInterval = 4.0 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] @@ -64,7 +63,8 @@ enum _014_GenerateInitialUserConfigDumps: Migration { try cache.updateProfile( displayName: (userProfile?["name"] ?? ""), displayPictureUrl: userProfile?["profilePictureUrl"], - displayPictureEncryptionKey: userProfile?["profileEncryptionKey"] + displayPictureEncryptionKey: userProfile?["profileEncryptionKey"], + isReuploadProfilePicture: false ) try LibSession.updateNoteToSelf( diff --git a/SessionMessagingKit/Database/Migrations/_015_BlockCommunityMessageRequests.swift b/SessionMessagingKit/Database/Migrations/_029_BlockCommunityMessageRequests.swift similarity index 94% rename from SessionMessagingKit/Database/Migrations/_015_BlockCommunityMessageRequests.swift rename to SessionMessagingKit/Database/Migrations/_029_BlockCommunityMessageRequests.swift index dd58e13355..22cb579ef3 100644 --- a/SessionMessagingKit/Database/Migrations/_015_BlockCommunityMessageRequests.swift +++ b/SessionMessagingKit/Database/Migrations/_029_BlockCommunityMessageRequests.swift @@ -5,9 +5,8 @@ import GRDB import SessionUtilitiesKit /// This migration adds a flag indicating whether a profile has indicated it is blocking community message requests -enum _015_BlockCommunityMessageRequests: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "BlockCommunityMessageRequests" +enum _029_BlockCommunityMessageRequests: Migration { + static let identifier: String = "messagingKit.BlockCommunityMessageRequests" static let minExpectedRunDuration: TimeInterval = 0.01 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_016_MakeBrokenProfileTimestampsNullable.swift b/SessionMessagingKit/Database/Migrations/_030_MakeBrokenProfileTimestampsNullable.swift similarity index 89% rename from SessionMessagingKit/Database/Migrations/_016_MakeBrokenProfileTimestampsNullable.swift rename to SessionMessagingKit/Database/Migrations/_030_MakeBrokenProfileTimestampsNullable.swift index 82816602ca..dbbeb35044 100644 --- a/SessionMessagingKit/Database/Migrations/_016_MakeBrokenProfileTimestampsNullable.swift +++ b/SessionMessagingKit/Database/Migrations/_030_MakeBrokenProfileTimestampsNullable.swift @@ -6,9 +6,8 @@ import SessionUtilitiesKit /// This migration updates the tiemstamps added to the `Profile` in earlier migrations to be nullable (having it not null /// results in migration issues when a user jumps between multiple versions) -enum _016_MakeBrokenProfileTimestampsNullable: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "MakeBrokenProfileTimestampsNullable" +enum _030_MakeBrokenProfileTimestampsNullable: Migration { + static let identifier: String = "messagingKit.MakeBrokenProfileTimestampsNullable" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_017_RebuildFTSIfNeeded_2_4_5.swift b/SessionMessagingKit/Database/Migrations/_031_RebuildFTSIfNeeded_2_4_5.swift similarity index 85% rename from SessionMessagingKit/Database/Migrations/_017_RebuildFTSIfNeeded_2_4_5.swift rename to SessionMessagingKit/Database/Migrations/_031_RebuildFTSIfNeeded_2_4_5.swift index c9c9240fde..9660270f21 100644 --- a/SessionMessagingKit/Database/Migrations/_017_RebuildFTSIfNeeded_2_4_5.swift +++ b/SessionMessagingKit/Database/Migrations/_031_RebuildFTSIfNeeded_2_4_5.swift @@ -7,9 +7,8 @@ import GRDB import SessionUtilitiesKit /// This migration adds the FTS table back if either the tables or any of the triggers no longer exist -enum _017_RebuildFTSIfNeeded_2_4_5: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "RebuildFTSIfNeeded_2_4_5" +enum _031_RebuildFTSIfNeeded_2_4_5: Migration { + static let identifier: String = "messagingKit.RebuildFTSIfNeeded_2_4_5" static let minExpectedRunDuration: TimeInterval = 0.01 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] @@ -30,7 +29,7 @@ enum _017_RebuildFTSIfNeeded_2_4_5: Migration { try db.create(virtualTable: "interaction_fts", using: FTS5()) { t in t.synchronize(withTable: "interaction") - t.tokenizer = _001_InitialSetupMigration.fullTextSearchTokenizer + t.tokenizer = _006_SMK_InitialSetupMigration.fullTextSearchTokenizer t.column("body") t.column("threadId") @@ -44,7 +43,7 @@ enum _017_RebuildFTSIfNeeded_2_4_5: Migration { try db.create(virtualTable: "profile_fts", using: FTS5()) { t in t.synchronize(withTable: "profile") - t.tokenizer = _001_InitialSetupMigration.fullTextSearchTokenizer + t.tokenizer = _006_SMK_InitialSetupMigration.fullTextSearchTokenizer t.column("nickname") t.column("name") @@ -58,7 +57,7 @@ enum _017_RebuildFTSIfNeeded_2_4_5: Migration { try db.create(virtualTable: "closedGroup_fts", using: FTS5()) { t in t.synchronize(withTable: "closedGroup") - t.tokenizer = _001_InitialSetupMigration.fullTextSearchTokenizer + t.tokenizer = _006_SMK_InitialSetupMigration.fullTextSearchTokenizer t.column("name") } @@ -71,7 +70,7 @@ enum _017_RebuildFTSIfNeeded_2_4_5: Migration { try db.create(virtualTable: "openGroup_fts", using: FTS5()) { t in t.synchronize(withTable: "openGroup") - t.tokenizer = _001_InitialSetupMigration.fullTextSearchTokenizer + t.tokenizer = _006_SMK_InitialSetupMigration.fullTextSearchTokenizer t.column("name") } diff --git a/SessionMessagingKit/Database/Migrations/_018_DisappearingMessagesConfiguration.swift b/SessionMessagingKit/Database/Migrations/_032_DisappearingMessagesConfiguration.swift similarity index 97% rename from SessionMessagingKit/Database/Migrations/_018_DisappearingMessagesConfiguration.swift rename to SessionMessagingKit/Database/Migrations/_032_DisappearingMessagesConfiguration.swift index 809c426e56..4bf07b018f 100644 --- a/SessionMessagingKit/Database/Migrations/_018_DisappearingMessagesConfiguration.swift +++ b/SessionMessagingKit/Database/Migrations/_032_DisappearingMessagesConfiguration.swift @@ -4,9 +4,8 @@ import Foundation import GRDB import SessionUtilitiesKit -enum _018_DisappearingMessagesConfiguration: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "DisappearingMessagesWithTypes" +enum _032_DisappearingMessagesConfiguration: Migration { + static let identifier: String = "messagingKit.DisappearingMessagesWithTypes" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_019_ScheduleAppUpdateCheckJob.swift b/SessionMessagingKit/Database/Migrations/_033_ScheduleAppUpdateCheckJob.swift similarity index 84% rename from SessionMessagingKit/Database/Migrations/_019_ScheduleAppUpdateCheckJob.swift rename to SessionMessagingKit/Database/Migrations/_033_ScheduleAppUpdateCheckJob.swift index 9f5bd4c724..f0df877278 100644 --- a/SessionMessagingKit/Database/Migrations/_019_ScheduleAppUpdateCheckJob.swift +++ b/SessionMessagingKit/Database/Migrations/_033_ScheduleAppUpdateCheckJob.swift @@ -4,9 +4,8 @@ import Foundation import GRDB import SessionUtilitiesKit -enum _019_ScheduleAppUpdateCheckJob: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "ScheduleAppUpdateCheckJob" +enum _033_ScheduleAppUpdateCheckJob: Migration { + static let identifier: String = "messagingKit.ScheduleAppUpdateCheckJob" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_020_AddMissingWhisperFlag.swift b/SessionMessagingKit/Database/Migrations/_034_AddMissingWhisperFlag.swift similarity index 81% rename from SessionMessagingKit/Database/Migrations/_020_AddMissingWhisperFlag.swift rename to SessionMessagingKit/Database/Migrations/_034_AddMissingWhisperFlag.swift index 90dbfc4fbd..ecd158b8b1 100644 --- a/SessionMessagingKit/Database/Migrations/_020_AddMissingWhisperFlag.swift +++ b/SessionMessagingKit/Database/Migrations/_034_AddMissingWhisperFlag.swift @@ -4,9 +4,8 @@ import Foundation import GRDB import SessionUtilitiesKit -enum _020_AddMissingWhisperFlag: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "AddMissingWhisperFlag" +enum _034_AddMissingWhisperFlag: Migration { + static let identifier: String = "messagingKit.AddMissingWhisperFlag" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_021_ReworkRecipientState.swift b/SessionMessagingKit/Database/Migrations/_035_ReworkRecipientState.swift similarity index 97% rename from SessionMessagingKit/Database/Migrations/_021_ReworkRecipientState.swift rename to SessionMessagingKit/Database/Migrations/_035_ReworkRecipientState.swift index a47d202666..f0ebe35f20 100644 --- a/SessionMessagingKit/Database/Migrations/_021_ReworkRecipientState.swift +++ b/SessionMessagingKit/Database/Migrations/_035_ReworkRecipientState.swift @@ -4,9 +4,8 @@ import Foundation import GRDB import SessionUtilitiesKit -enum _021_ReworkRecipientState: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "ReworkRecipientState" +enum _035_ReworkRecipientState: Migration { + static let identifier: String = "messagingKit.ReworkRecipientState" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] @@ -180,7 +179,7 @@ enum _021_ReworkRecipientState: Migration { } } -private extension _021_ReworkRecipientState { +private extension _035_ReworkRecipientState { enum LegacyState: Int { case sending case failed diff --git a/SessionMessagingKit/Database/Migrations/_022_GroupsRebuildChanges.swift b/SessionMessagingKit/Database/Migrations/_036_GroupsRebuildChanges.swift similarity index 87% rename from SessionMessagingKit/Database/Migrations/_022_GroupsRebuildChanges.swift rename to SessionMessagingKit/Database/Migrations/_036_GroupsRebuildChanges.swift index 3f60a24d4a..55cb03346f 100644 --- a/SessionMessagingKit/Database/Migrations/_022_GroupsRebuildChanges.swift +++ b/SessionMessagingKit/Database/Migrations/_036_GroupsRebuildChanges.swift @@ -5,12 +5,11 @@ import Foundation import UIKit.UIImage import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit -enum _022_GroupsRebuildChanges: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "GroupsRebuildChanges" +enum _036_GroupsRebuildChanges: Migration { + static let identifier: String = "messagingKit.GroupsRebuildChanges" static let minExpectedRunDuration: TimeInterval = 0.1 static var createdTables: [(FetchableRecord & TableRecord).Type] = [] @@ -145,19 +144,24 @@ enum _022_GroupsRebuildChanges: Migration { /// If the group isn't in the invited state then make sure to subscribe for PNs once the migrations are done if !group.invited, let token: String = dependencies[defaults: .standard, key: .deviceToken] { - db.afterCommit { - dependencies[singleton: .storage] - .readPublisher { db in - try PushNotificationAPI.preparedSubscribe( - db, + let maybeAuthMethod: AuthenticationMethod? = try? Authentication.with( + db, + swarmPublicKey: group.groupSessionId, + using: dependencies + ) + + if let authMethod: AuthenticationMethod = maybeAuthMethod { + db.afterCommit { + try? Network.PushNotification + .preparedSubscribe( token: Data(hex: token), - sessionIds: [SessionId(.group, hex: group.groupSessionId)], + swarms: [(SessionId(.group, hex: group.groupSessionId), authMethod)], using: dependencies ) - } - .flatMap { $0.send(using: dependencies) } - .subscribe(on: DispatchQueue.global(qos: .userInitiated), using: dependencies) - .sinkUntilComplete() + .send(using: dependencies) + .subscribe(on: DispatchQueue.global(qos: .userInitiated), using: dependencies) + .sinkUntilComplete() + } } } } @@ -181,7 +185,10 @@ enum _022_GroupsRebuildChanges: Migration { return } - let filename: String = generateFilename(format: imageData.guessedImageFormat, using: dependencies) + let filename: String = generateFilename( + format: MediaUtils.guessedImageFormat(data: imageData), + using: dependencies + ) let filePath: String = URL(fileURLWithPath: dependencies[singleton: .displayPictureManager].sharedDataDisplayPictureDirPath()) .appendingPathComponent(filename) .path @@ -209,7 +216,7 @@ enum _022_GroupsRebuildChanges: Migration { } } -private extension _022_GroupsRebuildChanges { +private extension _036_GroupsRebuildChanges { static func generateFilename(format: ImageFormat = .jpeg, using dependencies: Dependencies) -> String { return dependencies[singleton: .crypto] .generate(.uuid()) diff --git a/SessionMessagingKit/Database/Migrations/_023_GroupsExpiredFlag.swift b/SessionMessagingKit/Database/Migrations/_037_GroupsExpiredFlag.swift similarity index 76% rename from SessionMessagingKit/Database/Migrations/_023_GroupsExpiredFlag.swift rename to SessionMessagingKit/Database/Migrations/_037_GroupsExpiredFlag.swift index 2bffc37639..294efd4846 100644 --- a/SessionMessagingKit/Database/Migrations/_023_GroupsExpiredFlag.swift +++ b/SessionMessagingKit/Database/Migrations/_037_GroupsExpiredFlag.swift @@ -4,9 +4,8 @@ import Foundation import GRDB import SessionUtilitiesKit -enum _023_GroupsExpiredFlag: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "GroupsExpiredFlag" +enum _037_GroupsExpiredFlag: Migration { + static let identifier: String = "messagingKit.GroupsExpiredFlag" static let minExpectedRunDuration: TimeInterval = 0.1 static var createdTables: [(FetchableRecord & TableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_024_FixBustedInteractionVariant.swift b/SessionMessagingKit/Database/Migrations/_038_FixBustedInteractionVariant.swift similarity index 83% rename from SessionMessagingKit/Database/Migrations/_024_FixBustedInteractionVariant.swift rename to SessionMessagingKit/Database/Migrations/_038_FixBustedInteractionVariant.swift index 9b65965362..5929ad6c87 100644 --- a/SessionMessagingKit/Database/Migrations/_024_FixBustedInteractionVariant.swift +++ b/SessionMessagingKit/Database/Migrations/_038_FixBustedInteractionVariant.swift @@ -7,9 +7,8 @@ import SessionUtilitiesKit /// There was a bug with internal releases of the Groups Rebuild feature where we incorrectly assigned an `Interaction.Variant` /// value of `3` to deleted message artifacts when it should have been `2`, this migration updates any interactions with a value of `2` /// to be `3` -enum _024_FixBustedInteractionVariant: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "FixBustedInteractionVariant" +enum _038_FixBustedInteractionVariant: Migration { + static let identifier: String = "messagingKit.FixBustedInteractionVariant" static let minExpectedRunDuration: TimeInterval = 0.1 static var createdTables: [(FetchableRecord & TableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_025_DropLegacyClosedGroupKeyPairTable.swift b/SessionMessagingKit/Database/Migrations/_039_DropLegacyClosedGroupKeyPairTable.swift similarity index 74% rename from SessionMessagingKit/Database/Migrations/_025_DropLegacyClosedGroupKeyPairTable.swift rename to SessionMessagingKit/Database/Migrations/_039_DropLegacyClosedGroupKeyPairTable.swift index afc0dd376d..20111dade4 100644 --- a/SessionMessagingKit/Database/Migrations/_025_DropLegacyClosedGroupKeyPairTable.swift +++ b/SessionMessagingKit/Database/Migrations/_039_DropLegacyClosedGroupKeyPairTable.swift @@ -6,9 +6,8 @@ import SessionUtilitiesKit /// Legacy closed groups are no longer supported so we can drop the `closedGroupKeyPair` table from /// the database -enum _025_DropLegacyClosedGroupKeyPairTable: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "DropLegacyClosedGroupKeyPairTable" +enum _039_DropLegacyClosedGroupKeyPairTable: Migration { + static let identifier: String = "messagingKit.DropLegacyClosedGroupKeyPairTable" static let minExpectedRunDuration: TimeInterval = 0.1 static var createdTables: [(FetchableRecord & TableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_026_MessageDeduplicationTable.swift b/SessionMessagingKit/Database/Migrations/_040_MessageDeduplicationTable.swift similarity index 98% rename from SessionMessagingKit/Database/Migrations/_026_MessageDeduplicationTable.swift rename to SessionMessagingKit/Database/Migrations/_040_MessageDeduplicationTable.swift index fd993ac86b..5431858f19 100644 --- a/SessionMessagingKit/Database/Migrations/_026_MessageDeduplicationTable.swift +++ b/SessionMessagingKit/Database/Migrations/_040_MessageDeduplicationTable.swift @@ -3,14 +3,13 @@ import Foundation import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit /// The different platforms use different approaches for message deduplication but in the future we want to shift the database logic into /// `libSession` so it makes sense to try to define a longer-term deduplication approach we we can use in `libSession`, additonally /// the PN extension will need to replicate this deduplication data so having a single source-of-truth for the data will make things easier -enum _026_MessageDeduplicationTable: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "MessageDeduplicationTable" +enum _040_MessageDeduplicationTable: Migration { + static let identifier: String = "messagingKit.MessageDeduplicationTable" static let minExpectedRunDuration: TimeInterval = 5 static var createdTables: [(FetchableRecord & TableRecord).Type] = [ MessageDeduplication.self @@ -343,7 +342,7 @@ enum _026_MessageDeduplicationTable: Migration { } } -internal extension _026_MessageDeduplicationTable { +internal extension _040_MessageDeduplicationTable { static func legacyDedupeIdentifier( variant: Interaction.Variant, timestampMs: Int64 @@ -372,7 +371,7 @@ internal extension _026_MessageDeduplicationTable { } } -internal extension _026_MessageDeduplicationTable { +internal extension _040_MessageDeduplicationTable { enum ControlMessageProcessRecordVariant: Int { case readReceipt = 1 case typingIndicator = 2 diff --git a/SessionUtilitiesKit/Database/Migrations/_006_RenameTableSettingToKeyValueStore.swift b/SessionMessagingKit/Database/Migrations/_041_RenameTableSettingToKeyValueStore.swift similarity index 68% rename from SessionUtilitiesKit/Database/Migrations/_006_RenameTableSettingToKeyValueStore.swift rename to SessionMessagingKit/Database/Migrations/_041_RenameTableSettingToKeyValueStore.swift index ffad28e128..48a366aecf 100644 --- a/SessionUtilitiesKit/Database/Migrations/_006_RenameTableSettingToKeyValueStore.swift +++ b/SessionMessagingKit/Database/Migrations/_041_RenameTableSettingToKeyValueStore.swift @@ -2,10 +2,10 @@ import Foundation import GRDB +import SessionUtilitiesKit -enum _006_RenameTableSettingToKeyValueStore: Migration { - static let target: TargetMigrations.Identifier = .utilitiesKit - static let identifier: String = "RenameTableSettingToKeyValueStore" // stringlint:disable +enum _041_RenameTableSettingToKeyValueStore: Migration { + static let identifier: String = "utilitiesKit.RenameTableSettingToKeyValueStore" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [ KeyValueStore.self ] diff --git a/SessionMessagingKit/Database/Migrations/_027_MoveSettingsToLibSession.swift b/SessionMessagingKit/Database/Migrations/_042_MoveSettingsToLibSession.swift similarity index 97% rename from SessionMessagingKit/Database/Migrations/_027_MoveSettingsToLibSession.swift rename to SessionMessagingKit/Database/Migrations/_042_MoveSettingsToLibSession.swift index 3f6353e72c..5e63bd1bff 100644 --- a/SessionMessagingKit/Database/Migrations/_027_MoveSettingsToLibSession.swift +++ b/SessionMessagingKit/Database/Migrations/_042_MoveSettingsToLibSession.swift @@ -6,9 +6,8 @@ import SessionUIKit import SessionUtilitiesKit /// This migration extracts an old settings from the database and saves them into libSession -enum _027_MoveSettingsToLibSession: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "MoveSettingsToLibSession" +enum _042_MoveSettingsToLibSession: Migration { + static let identifier: String = "messagingKit.MoveSettingsToLibSession" static let minExpectedRunDuration: TimeInterval = 0.1 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_028_RenameAttachments.swift b/SessionMessagingKit/Database/Migrations/_043_RenameAttachments.swift similarity index 98% rename from SessionMessagingKit/Database/Migrations/_028_RenameAttachments.swift rename to SessionMessagingKit/Database/Migrations/_043_RenameAttachments.swift index 2c92c21d11..ff94b962c2 100644 --- a/SessionMessagingKit/Database/Migrations/_028_RenameAttachments.swift +++ b/SessionMessagingKit/Database/Migrations/_043_RenameAttachments.swift @@ -3,14 +3,13 @@ import Foundation import UniformTypeIdentifiers import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit /// This migration renames all attachments to use a hash of the download url for the filename instead of a random UUID (means we can /// generate the filename just from the URL and don't need to store the filename) -enum _028_RenameAttachments: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "RenameAttachments" +enum _043_RenameAttachments: Migration { + static let identifier: String = "messagingKit.RenameAttachments" static let minExpectedRunDuration: TimeInterval = 3 static let createdTables: [(TableRecord & FetchableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_029_AddProMessageFlag.swift b/SessionMessagingKit/Database/Migrations/_044_AddProMessageFlag.swift similarity index 76% rename from SessionMessagingKit/Database/Migrations/_029_AddProMessageFlag.swift rename to SessionMessagingKit/Database/Migrations/_044_AddProMessageFlag.swift index 0d2751199e..7f51d1ffc2 100644 --- a/SessionMessagingKit/Database/Migrations/_029_AddProMessageFlag.swift +++ b/SessionMessagingKit/Database/Migrations/_044_AddProMessageFlag.swift @@ -4,9 +4,8 @@ import Foundation import GRDB import SessionUtilitiesKit -enum _029_AddProMessageFlag: Migration { - static let target: TargetMigrations.Identifier = .messagingKit - static let identifier: String = "AddProMessageFlag" +enum _044_AddProMessageFlag: Migration { + static let identifier: String = "messagingKit.AddProMessageFlag" static let minExpectedRunDuration: TimeInterval = 0.1 static var createdTables: [(FetchableRecord & TableRecord).Type] = [] diff --git a/SessionMessagingKit/Database/Migrations/_045_LastProfileUpdateTimestamp.swift b/SessionMessagingKit/Database/Migrations/_045_LastProfileUpdateTimestamp.swift new file mode 100644 index 0000000000..89163827fb --- /dev/null +++ b/SessionMessagingKit/Database/Migrations/_045_LastProfileUpdateTimestamp.swift @@ -0,0 +1,21 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import GRDB +import SessionUtilitiesKit + +enum _045_LastProfileUpdateTimestamp: Migration { + static let identifier: String = "LastProfileUpdateTimestamp" + static let minExpectedRunDuration: TimeInterval = 0.1 + static var createdTables: [(FetchableRecord & TableRecord).Type] = [] + + static func migrate(_ db: ObservingDatabase, using dependencies: Dependencies) throws { + try db.alter(table: "Profile") { t in + t.drop(column: "lastNameUpdate") + t.drop(column: "lastBlocksCommunityMessageRequests") + t.rename(column: "displayPictureLastUpdated", to: "profileLastUpdated") + } + + MigrationExecution.updateProgress(1) + } +} diff --git a/SessionMessagingKit/Database/Models/Attachment.swift b/SessionMessagingKit/Database/Models/Attachment.swift index 3f1f5854d9..072306a4d3 100644 --- a/SessionMessagingKit/Database/Models/Attachment.swift +++ b/SessionMessagingKit/Database/Models/Attachment.swift @@ -7,7 +7,7 @@ import Combine import UniformTypeIdentifiers import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUIKit public struct Attachment: Codable, Identifiable, Equatable, Hashable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible { @@ -178,7 +178,7 @@ public struct Attachment: Codable, Identifiable, Equatable, Hashable, FetchableR case .success = Result(try dataSource.write(to: uploadInfo.path)) else { return nil } - let imageSize: CGSize? = Data.mediaSize( + let imageSize: CGSize? = MediaUtils.unrotatedSize( for: uploadInfo.path, type: UTType(sessionMimeType: contentType), mimeType: contentType, @@ -406,7 +406,7 @@ extension Attachment { .path(for: finalDownloadUrl) else { return nil } - return Data.mediaSize( + return MediaUtils.unrotatedSize( for: path, type: UTType(sessionMimeType: contentType), mimeType: contentType, @@ -484,7 +484,13 @@ extension Attachment { } public func buildProto() -> SNProtoAttachmentPointer? { - let builder = SNProtoAttachmentPointer.builder(id: 0) /// `id` is deprecated, rely on `url` instead + /// The `id` value on the protobuf is deprecated, rely on `url` instead + /// + /// **Note:** We need to continue to send this because it seems that the Desktop client _does_ in fact still use this + /// id for downloading attachments. Desktop will be updated to remove it's use but in order to fix attachments for old + /// versions we set this value again + let legacyId: UInt64 = (Network.FileServer.fileId(for: self.downloadUrl).map { UInt64($0) } ?? 0) + let builder = SNProtoAttachmentPointer.builder(id: legacyId) builder.setContentType(contentType) if let sourceFilename: String = sourceFilename, !sourceFilename.isEmpty { @@ -645,7 +651,12 @@ extension Attachment { public var shortDescription: String { if isImage { return "image".localized() } - if isAudio { return "audio".localized() } + if isAudio { + switch variant { + case .voiceMessage: return "messageVoice".localized() + case .standard: return "audio".localized() + } + } if isVideo { return "video".localized() } return "document".localized() } @@ -678,14 +689,4 @@ extension Attachment { return true } - - public static func fileId(for downloadUrl: String?) -> String? { - return downloadUrl - .map { urlString -> String? in - urlString - .split(separator: "/") // stringlint:ignore - .last - .map { String($0) } - } - } } diff --git a/SessionMessagingKit/Database/Models/ClosedGroup.swift b/SessionMessagingKit/Database/Models/ClosedGroup.swift index e525398357..da2655eb59 100644 --- a/SessionMessagingKit/Database/Models/ClosedGroup.swift +++ b/SessionMessagingKit/Database/Models/ClosedGroup.swift @@ -5,7 +5,7 @@ import Combine import GRDB import DifferenceKit import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit public struct ClosedGroup: Codable, Equatable, Hashable, Identifiable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible { @@ -226,16 +226,22 @@ public extension ClosedGroup { /// Subscribe for group push notifications if let token: String = dependencies[defaults: .standard, key: .deviceToken] { - try? PushNotificationAPI - .preparedSubscribe( - db, - token: Data(hex: token), - sessionIds: [SessionId(.group, hex: group.id)], - using: dependencies - ) - .send(using: dependencies) - .subscribe(on: DispatchQueue.global(qos: .userInitiated), using: dependencies) - .sinkUntilComplete() + let maybeAuthMethod: AuthenticationMethod? = try? Authentication.with( + db, + swarmPublicKey: group.id, + using: dependencies + ) + + if let authMethod: AuthenticationMethod = maybeAuthMethod { + try? Network.PushNotification + .preparedSubscribe( + token: Data(hex: token), + swarms: [(SessionId(.group, hex: group.id), authMethod)], + using: dependencies + ) + .send(using: dependencies) + .sinkUntilComplete() + } } } @@ -305,13 +311,20 @@ public extension ClosedGroup { /// Bulk unsubscripe from updated groups being removed if dataToRemove.contains(.pushNotifications) && threadVariants.contains(where: { $0.variant == .group }) { if let token: String = dependencies[defaults: .standard, key: .deviceToken] { - try? PushNotificationAPI + try? Network.PushNotification .preparedUnsubscribe( - db, token: Data(hex: token), - sessionIds: threadVariants + swarms: threadVariants .filter { $0.variant == .group } - .map { SessionId(.group, hex: $0.id) }, + .compactMap { info in + let authMethod: AuthenticationMethod? = try? Authentication.with( + db, + swarmPublicKey: info.id, + using: dependencies + ) + + return authMethod.map { (SessionId(.group, hex: info.id), $0) } + }, using: dependencies ) .send(using: dependencies) @@ -334,19 +347,7 @@ public extension ClosedGroup { } if dataToRemove.contains(.messages) { - struct InteractionThreadInfo: Codable, FetchableRecord, Hashable { - let id: Int64 - let threadId: String - } - - let interactionInfo: Set = try Interaction - .select(.id, .threadId) - .filter(threadIds.contains(Interaction.Columns.threadId)) - .asRequest(of: InteractionThreadInfo.self) - .fetchSet(db) - try Interaction.deleteAll(db, ids: interactionInfo.map { $0.id }) - - interactionInfo.forEach { db.addMessageEvent(id: $0.id, threadId: $0.threadId, type: .deleted) } + try Interaction.deleteWhere(db, .filter(threadIds.contains(Interaction.Columns.threadId))) /// Delete any `MessageDeduplication` entries that we want to reprocess if the member gets /// re-invited to the group with historic access (these are repeatable records so won't cause issues if we re-run them) @@ -381,6 +382,7 @@ public extension ClosedGroup { } if dataToRemove.contains(.thread) { + try Interaction.deleteWhere(db, .filter(threadIds.contains(Interaction.Columns.threadId))) try SessionThread // Intentionally use `deleteAll` here as this gets triggered via `deleteOrLeave` .filter(ids: threadIds) .deleteAll(db) diff --git a/SessionMessagingKit/Database/Models/ConfigDump.swift b/SessionMessagingKit/Database/Models/ConfigDump.swift index b264051429..eed9528aa9 100644 --- a/SessionMessagingKit/Database/Models/ConfigDump.swift +++ b/SessionMessagingKit/Database/Models/ConfigDump.swift @@ -4,7 +4,7 @@ import Foundation import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit public struct ConfigDump: Codable, Equatable, Hashable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible { @@ -86,7 +86,7 @@ public extension ConfigDump.Variant { .groupInfo, .groupMembers, .groupKeys ] - init(namespace: SnodeAPI.Namespace) { + init(namespace: Network.SnodeAPI.Namespace) { switch namespace { case .configUserProfile: self = .userProfile case .configContacts: self = .contacts @@ -104,19 +104,19 @@ public extension ConfigDump.Variant { /// Config messages should last for 30 days rather than the standard 14 var ttl: UInt64 { 30 * 24 * 60 * 60 * 1000 } - var namespace: SnodeAPI.Namespace { + var namespace: Network.SnodeAPI.Namespace { switch self { - case .userProfile: return SnodeAPI.Namespace.configUserProfile - case .contacts: return SnodeAPI.Namespace.configContacts - case .convoInfoVolatile: return SnodeAPI.Namespace.configConvoInfoVolatile - case .userGroups: return SnodeAPI.Namespace.configUserGroups - case .local: return SnodeAPI.Namespace.configLocal + case .userProfile: return Network.SnodeAPI.Namespace.configUserProfile + case .contacts: return Network.SnodeAPI.Namespace.configContacts + case .convoInfoVolatile: return Network.SnodeAPI.Namespace.configConvoInfoVolatile + case .userGroups: return Network.SnodeAPI.Namespace.configUserGroups + case .local: return Network.SnodeAPI.Namespace.configLocal - case .groupInfo: return SnodeAPI.Namespace.configGroupInfo - case .groupMembers: return SnodeAPI.Namespace.configGroupMembers - case .groupKeys: return SnodeAPI.Namespace.configGroupKeys + case .groupInfo: return Network.SnodeAPI.Namespace.configGroupInfo + case .groupMembers: return Network.SnodeAPI.Namespace.configGroupMembers + case .groupKeys: return Network.SnodeAPI.Namespace.configGroupKeys - case .invalid: return SnodeAPI.Namespace.unknown + case .invalid: return Network.SnodeAPI.Namespace.unknown } } diff --git a/SessionMessagingKit/Database/Models/DisappearingMessageConfiguration.swift b/SessionMessagingKit/Database/Models/DisappearingMessageConfiguration.swift index 22c7d9a580..10e9faf495 100644 --- a/SessionMessagingKit/Database/Models/DisappearingMessageConfiguration.swift +++ b/SessionMessagingKit/Database/Models/DisappearingMessageConfiguration.swift @@ -5,7 +5,7 @@ import GRDB import SessionUIKit import SessionUtil import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit public struct DisappearingMessagesConfiguration: Codable, Identifiable, Equatable, Hashable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible { public static var databaseTableName: String { "disappearingMessagesConfiguration" } @@ -248,11 +248,12 @@ public extension DisappearingMessagesConfiguration { using dependencies: Dependencies ) throws { guard threadVariant == .contact else { - try Interaction - .filter(Interaction.Columns.threadId == self.threadId) - .filter(Interaction.Columns.variant == Interaction.Variant.infoDisappearingMessagesUpdate) + try Interaction.deleteWhere( + db, + .filter(Interaction.Columns.threadId == threadId), + .filter(Interaction.Columns.variant == Interaction.Variant.infoDisappearingMessagesUpdate), .filter(Interaction.Columns.expiresInSeconds != self.durationSeconds) - .deleteAll(db) + ) return } @@ -260,28 +261,41 @@ public extension DisappearingMessagesConfiguration { switch (self.isEnabled, self.type) { case (false, _): - try Interaction - .filter(Interaction.Columns.threadId == self.threadId) - .filter(Interaction.Columns.variant == Interaction.Variant.infoDisappearingMessagesUpdate) - .filter(Interaction.Columns.authorId == userSessionId.hexString) + try Interaction.deleteWhere( + db, + .filter(Interaction.Columns.threadId == threadId), + .filter(Interaction.Columns.variant == Interaction.Variant.infoDisappearingMessagesUpdate), + .filter(Interaction.Columns.authorId == userSessionId.hexString), .filter(Interaction.Columns.expiresInSeconds != 0) - .deleteAll(db) + ) case (true, .disappearAfterRead): - try Interaction - .filter(Interaction.Columns.threadId == self.threadId) - .filter(Interaction.Columns.variant == Interaction.Variant.infoDisappearingMessagesUpdate) - .filter(Interaction.Columns.authorId == userSessionId.hexString) - .filter(!(Interaction.Columns.expiresInSeconds == self.durationSeconds && Interaction.Columns.expiresStartedAtMs != Interaction.Columns.timestampMs)) - .deleteAll(db) + try Interaction.deleteWhere( + db, + .filter(Interaction.Columns.threadId == threadId), + .filter(Interaction.Columns.variant == Interaction.Variant.infoDisappearingMessagesUpdate), + .filter(Interaction.Columns.authorId == userSessionId.hexString), + .filter( + !( + Interaction.Columns.expiresInSeconds == self.durationSeconds && + Interaction.Columns.expiresStartedAtMs != Interaction.Columns.timestampMs + ) + ) + ) case (true, .disappearAfterSend): - try Interaction - .filter(Interaction.Columns.threadId == self.threadId) - .filter(Interaction.Columns.variant == Interaction.Variant.infoDisappearingMessagesUpdate) - .filter(Interaction.Columns.authorId == userSessionId.hexString) - .filter(!(Interaction.Columns.expiresInSeconds == self.durationSeconds && Interaction.Columns.expiresStartedAtMs == Interaction.Columns.timestampMs)) - .deleteAll(db) + try Interaction.deleteWhere( + db, + .filter(Interaction.Columns.threadId == threadId), + .filter(Interaction.Columns.variant == Interaction.Variant.infoDisappearingMessagesUpdate), + .filter(Interaction.Columns.authorId == userSessionId.hexString), + .filter( + !( + Interaction.Columns.expiresInSeconds == self.durationSeconds && + Interaction.Columns.expiresStartedAtMs == Interaction.Columns.timestampMs + ) + ) + ) default: break } @@ -298,16 +312,18 @@ public extension DisappearingMessagesConfiguration { ) throws -> MessageReceiver.InsertedInteractionInfo? { switch threadVariant { case .contact: - _ = try Interaction - .filter(Interaction.Columns.threadId == threadId) - .filter(Interaction.Columns.variant == Interaction.Variant.infoDisappearingMessagesUpdate) + try Interaction.deleteWhere( + db, + .filter(Interaction.Columns.threadId == threadId), + .filter(Interaction.Columns.variant == Interaction.Variant.infoDisappearingMessagesUpdate), .filter(Interaction.Columns.authorId == authorId) - .deleteAll(db) + ) case .legacyGroup, .group: - _ = try Interaction - .filter(Interaction.Columns.threadId == threadId) + try Interaction.deleteWhere( + db, + .filter(Interaction.Columns.threadId == threadId), .filter(Interaction.Columns.variant == Interaction.Variant.infoDisappearingMessagesUpdate) - .deleteAll(db) + ) case .community: break } diff --git a/SessionMessagingKit/Database/Models/Interaction.swift b/SessionMessagingKit/Database/Models/Interaction.swift index 59373a3201..43b0dfdcb7 100644 --- a/SessionMessagingKit/Database/Models/Interaction.swift +++ b/SessionMessagingKit/Database/Models/Interaction.swift @@ -3,7 +3,7 @@ import Foundation import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit public struct Interaction: Codable, Identifiable, Equatable, Hashable, FetchableRecord, MutablePersistableRecord, TableRecord, ColumnExpressible { public static var databaseTableName: String { "interaction" } @@ -406,6 +406,26 @@ public struct Interaction: Codable, Identifiable, Equatable, Hashable, Fetchable } } + public func aroundUpdate(_ db: Database, columns: Set, update: () throws -> PersistenceSuccess) throws { + _ = try update() + + // Start the disappearing messages timer if needed + guard columns.contains(Columns.expiresStartedAtMs.name) else { return } + + switch ObservationContext.observingDb { + case .none: Log.error("[Interaction] Could not process 'aroundUpdate' due to missing observingDb.") + case .some(let observingDb): + observingDb.dependencies[singleton: .jobRunner].upsert( + observingDb, + job: DisappearingMessagesJob.updateNextRunIfNeeded( + observingDb, + using: observingDb.dependencies + ), + canStartJob: true + ) + } + } + public mutating func didInsert(_ inserted: InsertionSuccess) { self.id = inserted.rowID } @@ -893,7 +913,7 @@ public extension Interaction { } } - struct ThreadInfo: FetchableRecord, Codable { + struct ThreadInfo: FetchableRecord, Codable, Hashable { public let id: Int64 public let threadId: String @@ -1295,6 +1315,19 @@ public extension Interaction.Variant { // MARK: - Deletion public extension Interaction { + enum Filter { + case filter(SQLSpecificExpressible) + case hasAttachments(Bool) + case deleteAll + + var isDeleteAll: Bool { + switch self { + case .deleteAll: return true + default: return false + } + } + } + private struct InteractionVariantInfo: Codable, FetchableRecord { public typealias Columns = CodingKeys public enum CodingKeys: String, CodingKey, ColumnExpression { @@ -1448,7 +1481,9 @@ public extension Interaction { .filter { $0.variant.isInfoMessage } .compactMap { $0.id } .asSet() - _ = try Interaction.deleteAll(db, ids: infoMessageIds) + try LoggingDatabaseRecordContext.$suppressLogs.withValue(true) { + try Interaction.deleteAll(db, ids: infoMessageIds) + } let localOnly: Bool = (options.contains(.local) && !options.contains(.network)) @@ -1470,9 +1505,11 @@ public extension Interaction { }() if options.contains(.noArtifacts) { - try Interaction - .filter(ids: info.map { $0.id }) - .deleteAll(db) + try LoggingDatabaseRecordContext.$suppressLogs.withValue(true) { + try Interaction + .filter(ids: info.map { $0.id }) + .deleteAll(db) + } } else { try Interaction .filter(ids: info.map { $0.id }) @@ -1506,4 +1543,66 @@ public extension Interaction { } } } + + /// Whenever a message gets deleted we need to send an event to ensure the home screen updates correctly, this function manages + /// that logic so should be used instead of `delete(db)`/`deleteAll(db)` + @discardableResult static func deleteWhere( + _ db: ObservingDatabase, + _ filters: Filter... + ) throws -> Int { + var query: QueryInterfaceRequest = Interaction.select(.id, .threadId) + let shouldDeleteAll: Bool = filters.contains(where: { $0.isDeleteAll }) + var hasAttachmentsFilter: Bool? = nil + + /// Apply each of the filters to the query (unless the filters contains `deleteAll`, in which case ignore all filters) + if !shouldDeleteAll { + for filter in filters { + switch filter { + case .deleteAll: break + case .filter(let expressible): query = query.filter(expressible) + case .hasAttachments(let value): hasAttachmentsFilter = value + } + } + } + + /// Get the `id`/`threadId` combination + var info: Set = try query.asRequest(of: ThreadInfo.self).fetchSet(db) + + /// Since the `hasAttachments` filter is based on another table, we need custom logic for it so fetch all ids with attachments + /// and filter the above result based on the `hasAttachments` value + switch (shouldDeleteAll, hasAttachmentsFilter) { + case (true, _), (_, .none): break + case (_, .some(let requireAttachments)): + let interactionIdsWithAttachments: Set = try InteractionAttachment + .filter(info.map { $0.id }.contains(InteractionAttachment.Columns.interactionId)) + .asRequest(of: Int64.self) + .fetchSet(db) + + info = info.filter { interactionIdsWithAttachments.contains($0.id) == requireAttachments } + } + + /// Actually delete the messages + let numDeleted: Int = try LoggingDatabaseRecordContext.$suppressLogs.withValue(true) { + try Interaction + .filter(info.map { $0.id }.contains(Interaction.Columns.id)) + .deleteAll(db) + } + + /// Notify any observers of message deletion + info.forEach { info in + db.addMessageEvent(id: info.id, threadId: info.threadId, type: .deleted) + } + + return numDeleted + } +} + +extension Interaction: LoggingDatabaseRecord { + public func logDeletion() { Interaction.logDeletion() } + public static func logDeletion() { + Log.critical("Incorrectly deleted interaction directly instead of via `deleteWhere` or `markAsDeleted`.") + #if DEBUG + fatalError("Incorrectly deleted interaction directly instead of via `deleteWhere` or `markAsDeleted`.") + #endif + } } diff --git a/SessionMessagingKit/Database/Models/LinkPreview.swift b/SessionMessagingKit/Database/Models/LinkPreview.swift index faa84197cc..0c8cada73f 100644 --- a/SessionMessagingKit/Database/Models/LinkPreview.swift +++ b/SessionMessagingKit/Database/Models/LinkPreview.swift @@ -7,7 +7,7 @@ import Combine import UniformTypeIdentifiers import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit public struct LinkPreview: Codable, Equatable, Hashable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible { public static var databaseTableName: String { "linkPreview" } @@ -510,7 +510,7 @@ public extension LinkPreview { ) .tryMap { asset, _ -> Data in let type: UTType? = UTType(sessionMimeType: imageMimeType) - let imageSize = Data.mediaSize( + let imageSize = MediaUtils.unrotatedSize( for: asset.filePath, type: type, mimeType: imageMimeType, @@ -522,17 +522,17 @@ public extension LinkPreview { throw LinkPreviewError.invalidContent } + // Loki: If it's a GIF then ensure its validity and don't download it as a JPG + if type == .gif && MediaUtils.isValidImage(at: asset.filePath, type: .gif, using: dependencies) { + return try Data(contentsOf: URL(fileURLWithPath: asset.filePath)) + } + guard let data: Data = try? Data(contentsOf: URL(fileURLWithPath: asset.filePath)) else { throw LinkPreviewError.assertionFailure } guard let srcImage = UIImage(data: data) else { throw LinkPreviewError.invalidContent } - // Loki: If it's a GIF then ensure its validity and don't download it as a JPG - if type == .gif && data.isValidImage(type: .gif) { - return data - } - let maxImageSize: CGFloat = 1024 let shouldResize = imageSize.width > maxImageSize || imageSize.height > maxImageSize diff --git a/SessionMessagingKit/Database/Models/MessageDeduplication.swift b/SessionMessagingKit/Database/Models/MessageDeduplication.swift index 15269d25b2..e8b5a8f531 100644 --- a/SessionMessagingKit/Database/Models/MessageDeduplication.swift +++ b/SessionMessagingKit/Database/Models/MessageDeduplication.swift @@ -3,7 +3,7 @@ import Foundation import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit // MARK: - Log.Category @@ -209,7 +209,7 @@ public extension MessageDeduplication { _ processedMessage: ProcessedMessage, using dependencies: Dependencies ) throws { - typealias Variant = _026_MessageDeduplicationTable.ControlMessageProcessRecordVariant + typealias Variant = _040_MessageDeduplicationTable.ControlMessageProcessRecordVariant try ensureMessageIsNotADuplicate( threadId: processedMessage.threadId, uniqueIdentifier: processedMessage.uniqueIdentifier, @@ -402,12 +402,12 @@ private extension MessageDeduplication { _ db: ObservingDatabase, threadId: String, legacyIdentifier: String?, - legacyVariant: _026_MessageDeduplicationTable.ControlMessageProcessRecordVariant?, + legacyVariant: _040_MessageDeduplicationTable.ControlMessageProcessRecordVariant?, timestampMs: Int64?, serverExpirationTimestamp: TimeInterval?, using dependencies: Dependencies ) throws { - typealias Variant = _026_MessageDeduplicationTable.ControlMessageProcessRecordVariant + typealias Variant = _040_MessageDeduplicationTable.ControlMessageProcessRecordVariant guard let legacyIdentifier: String = legacyIdentifier, let legacyVariant: Variant = legacyVariant, @@ -463,7 +463,7 @@ private extension MessageDeduplication { } @available(*, deprecated, message: "⚠️ Remove this code once once enough time has passed since it's release (at least 1 month)") - static func getLegacyVariant(for variant: Message.Variant?) -> _026_MessageDeduplicationTable.ControlMessageProcessRecordVariant? { + static func getLegacyVariant(for variant: Message.Variant?) -> _040_MessageDeduplicationTable.ControlMessageProcessRecordVariant? { guard let variant: Message.Variant = variant else { return nil } switch variant { @@ -494,7 +494,7 @@ private extension MessageDeduplication { case .standard(_, _, _, let messageInfo, _): guard let timestampMs: UInt64 = messageInfo.message.sentTimestampMs, - let variant: _026_MessageDeduplicationTable.ControlMessageProcessRecordVariant = getLegacyVariant(for: Message.Variant(from: messageInfo.message)) + let variant: _040_MessageDeduplicationTable.ControlMessageProcessRecordVariant = getLegacyVariant(for: Message.Variant(from: messageInfo.message)) else { return nil } return "LegacyRecord-\(variant.rawValue)-\(timestampMs)" // stringlint:ignore diff --git a/SessionMessagingKit/Database/Models/OpenGroup.swift b/SessionMessagingKit/Database/Models/OpenGroup.swift index f6ac676fea..977887c1a1 100644 --- a/SessionMessagingKit/Database/Models/OpenGroup.swift +++ b/SessionMessagingKit/Database/Models/OpenGroup.swift @@ -4,6 +4,7 @@ import Foundation import GRDB +import SessionNetworkingKit import SessionUtilitiesKit public struct OpenGroup: Codable, Equatable, Hashable, Identifiable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible { @@ -40,7 +41,7 @@ public struct OpenGroup: Codable, Equatable, Hashable, Identifiable, FetchableRe self.rawValue = rawValue } - public init(roomInfo: OpenGroupAPI.RoomPollInfo) { + public init(roomInfo: Network.SOGS.RoomPollInfo) { var permissions: Permissions = [] if roomInfo.read { permissions.insert(.read) } diff --git a/SessionMessagingKit/Database/Models/Profile.swift b/SessionMessagingKit/Database/Models/Profile.swift index 26f06fbb0c..d9dd41f57e 100644 --- a/SessionMessagingKit/Database/Models/Profile.swift +++ b/SessionMessagingKit/Database/Models/Profile.swift @@ -21,15 +21,14 @@ public struct Profile: Codable, Sendable, Identifiable, Equatable, Hashable, Fet case id case name - case lastNameUpdate case nickname case displayPictureUrl case displayPictureEncryptionKey - case displayPictureLastUpdated + + case profileLastUpdated case blocksCommunityMessageRequests - case lastBlocksCommunityMessageRequests } /// The id for the user that owns the profile (Note: This could be a sessionId, a blindedId or some future variant) @@ -38,9 +37,6 @@ public struct Profile: Codable, Sendable, Identifiable, Equatable, Hashable, Fet /// The name of the contact. Use this whenever you need the "real", underlying name of a user (e.g. when sending a message). public let name: String - /// The timestamp (in seconds since epoch) that the name was last updated - public let lastNameUpdate: TimeInterval? - /// A custom name for the profile set by the current user public let nickname: String? @@ -52,37 +48,36 @@ public struct Profile: Codable, Sendable, Identifiable, Equatable, Hashable, Fet /// The key with which the profile is encrypted. public let displayPictureEncryptionKey: Data? - /// The timestamp (in seconds since epoch) that the profile picture was last updated - public let displayPictureLastUpdated: TimeInterval? + /// The timestamp (in seconds since epoch) that the profile was last updated + public let profileLastUpdated: TimeInterval? /// A flag indicating whether this profile has reported that it blocks community message requests public let blocksCommunityMessageRequests: Bool? - /// The timestamp (in seconds since epoch) that the `blocksCommunityMessageRequests` setting was last updated - public let lastBlocksCommunityMessageRequests: TimeInterval? + /// The Pro Proof for when this profile is updated + // TODO: Implement this when the structure of Session Pro Proof is determined + public let sessionProProof: String? // MARK: - Initialization public init( id: String, name: String, - lastNameUpdate: TimeInterval? = nil, nickname: String? = nil, displayPictureUrl: String? = nil, displayPictureEncryptionKey: Data? = nil, - displayPictureLastUpdated: TimeInterval? = nil, + profileLastUpdated: TimeInterval? = nil, blocksCommunityMessageRequests: Bool? = nil, - lastBlocksCommunityMessageRequests: TimeInterval? = nil + sessionProProof: String? = nil ) { self.id = id self.name = name - self.lastNameUpdate = lastNameUpdate self.nickname = nickname self.displayPictureUrl = displayPictureUrl self.displayPictureEncryptionKey = displayPictureEncryptionKey - self.displayPictureLastUpdated = displayPictureLastUpdated + self.profileLastUpdated = profileLastUpdated self.blocksCommunityMessageRequests = blocksCommunityMessageRequests - self.lastBlocksCommunityMessageRequests = lastBlocksCommunityMessageRequests + self.sessionProProof = sessionProProof } } @@ -104,13 +99,11 @@ extension Profile: CustomStringConvertible, CustomDebugStringConvertible { Profile( id: \(id), name: \(name), - lastNameUpdate: \(lastNameUpdate.map { "\($0)" } ?? "null"), nickname: \(nickname.map { "\($0)" } ?? "null"), displayPictureUrl: \(displayPictureUrl.map { "\"\($0)\"" } ?? "null"), displayPictureEncryptionKey: \(displayPictureEncryptionKey?.toHexString() ?? "null"), - displayPictureLastUpdated: \(displayPictureLastUpdated.map { "\($0)" } ?? "null"), - blocksCommunityMessageRequests: \(blocksCommunityMessageRequests.map { "\($0)" } ?? "null"), - lastBlocksCommunityMessageRequests: \(lastBlocksCommunityMessageRequests.map { "\($0)" } ?? "null") + profileLastUpdated: \(profileLastUpdated.map { "\($0)" } ?? "null"), + blocksCommunityMessageRequests: \(blocksCommunityMessageRequests.map { "\($0)" } ?? "null") ) """ } @@ -137,13 +130,11 @@ public extension Profile { self = Profile( id: try container.decode(String.self, forKey: .id), name: try container.decode(String.self, forKey: .name), - lastNameUpdate: try? container.decode(TimeInterval?.self, forKey: .lastNameUpdate), nickname: try? container.decode(String?.self, forKey: .nickname), displayPictureUrl: displayPictureUrl, displayPictureEncryptionKey: displayPictureKey, - displayPictureLastUpdated: try? container.decode(TimeInterval?.self, forKey: .displayPictureLastUpdated), - blocksCommunityMessageRequests: try? container.decode(Bool?.self, forKey: .blocksCommunityMessageRequests), - lastBlocksCommunityMessageRequests: try? container.decode(TimeInterval?.self, forKey: .lastBlocksCommunityMessageRequests) + profileLastUpdated: try? container.decode(TimeInterval?.self, forKey: .profileLastUpdated), + blocksCommunityMessageRequests: try? container.decode(Bool?.self, forKey: .blocksCommunityMessageRequests) ) } @@ -152,13 +143,11 @@ public extension Profile { try container.encode(id, forKey: .id) try container.encode(name, forKey: .name) - try container.encodeIfPresent(lastNameUpdate, forKey: .lastNameUpdate) try container.encodeIfPresent(nickname, forKey: .nickname) try container.encodeIfPresent(displayPictureUrl, forKey: .displayPictureUrl) try container.encodeIfPresent(displayPictureEncryptionKey, forKey: .displayPictureEncryptionKey) - try container.encodeIfPresent(displayPictureLastUpdated, forKey: .displayPictureLastUpdated) + try container.encodeIfPresent(profileLastUpdated, forKey: .profileLastUpdated) try container.encodeIfPresent(blocksCommunityMessageRequests, forKey: .blocksCommunityMessageRequests) - try container.encodeIfPresent(lastBlocksCommunityMessageRequests, forKey: .lastBlocksCommunityMessageRequests) } } @@ -172,9 +161,15 @@ public extension Profile { if let displayPictureEncryptionKey: Data = displayPictureEncryptionKey, - let displayPictureUrl: String = displayPictureUrl { + let displayPictureUrl: String = displayPictureUrl + { dataMessageProto.setProfileKey(displayPictureEncryptionKey) profileProto.setProfilePicture(displayPictureUrl) + // TODO: Add ProProof if needed + } + + if let profileLastUpdated: TimeInterval = profileLastUpdated { + profileProto.setLastUpdateSeconds(UInt64(profileLastUpdated)) } do { @@ -218,13 +213,12 @@ public extension Profile { return Profile( id: id, name: "", - lastNameUpdate: nil, nickname: nil, displayPictureUrl: nil, displayPictureEncryptionKey: nil, - displayPictureLastUpdated: nil, + profileLastUpdated: nil, blocksCommunityMessageRequests: nil, - lastBlocksCommunityMessageRequests: nil + sessionProProof: nil ) } @@ -434,13 +428,12 @@ public extension Profile { return Profile( id: id, name: (name ?? self.name), - lastNameUpdate: lastNameUpdate, nickname: (nickname ?? self.nickname), displayPictureUrl: (displayPictureUrl ?? self.displayPictureUrl), displayPictureEncryptionKey: displayPictureEncryptionKey, - displayPictureLastUpdated: displayPictureLastUpdated, + profileLastUpdated: profileLastUpdated, blocksCommunityMessageRequests: blocksCommunityMessageRequests, - lastBlocksCommunityMessageRequests: lastBlocksCommunityMessageRequests + sessionProProof: self.sessionProProof ) } } diff --git a/SessionMessagingKit/Database/Models/SessionThread.swift b/SessionMessagingKit/Database/Models/SessionThread.swift index 3e069e9268..c3893cbeb1 100644 --- a/SessionMessagingKit/Database/Models/SessionThread.swift +++ b/SessionMessagingKit/Database/Models/SessionThread.swift @@ -3,7 +3,7 @@ import Foundation import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit public struct SessionThread: Codable, Identifiable, Equatable, Hashable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible, IdentifiableTableRecord { public static var databaseTableName: String { "thread" } @@ -582,9 +582,10 @@ public extension SessionThread { case .hideContactConversationAndDeleteContentDirectly: // Clear any interactions for the deleted thread - _ = try Interaction + try Interaction.deleteWhere( + db, .filter(threadIds.contains(Interaction.Columns.threadId)) - .deleteAll(db) + ) // Hide the threads try SessionThread.updateVisibility( @@ -598,7 +599,13 @@ public extension SessionThread { try MessageDeduplication.deleteIfNeeded(db, threadIds: threadIds, using: dependencies) case .deleteContactConversationAndMarkHidden: - _ = try SessionThread + // Clear any interactions for the deleted thread + try Interaction.deleteWhere( + db, + .filter(remainingThreadIds.contains(Interaction.Columns.threadId)) + ) + + try SessionThread .filter(ids: remainingThreadIds) .deleteAll(db) @@ -620,9 +627,10 @@ public extension SessionThread { // hidden locally rather than deleted) if threadIds.contains(userSessionId.hexString) { // Clear any interactions for the deleted thread - _ = try Interaction + try Interaction.deleteWhere( + db, .filter(Interaction.Columns.threadId == userSessionId.hexString) - .deleteAll(db) + ) try SessionThread.updateVisibility( db, @@ -643,15 +651,20 @@ public extension SessionThread { // custom data for this contact) try LibSession.remove(db, contactIds: Array(remainingThreadIds), using: dependencies) - _ = try Profile + try Profile .filter(ids: remainingThreadIds) .updateAll(db, Profile.Columns.nickname.set(to: nil)) - _ = try Contact + try Contact .filter(ids: remainingThreadIds) .deleteAll(db) - _ = try SessionThread + try Interaction.deleteWhere( + db, + .filter(remainingThreadIds.contains(Interaction.Columns.threadId)) + ) + + try SessionThread .filter(ids: remainingThreadIds) .deleteAll(db) diff --git a/SessionMessagingKit/Jobs/AttachmentDownloadJob.swift b/SessionMessagingKit/Jobs/AttachmentDownloadJob.swift index 0127ebc746..210d629967 100644 --- a/SessionMessagingKit/Jobs/AttachmentDownloadJob.swift +++ b/SessionMessagingKit/Jobs/AttachmentDownloadJob.swift @@ -3,7 +3,7 @@ import Foundation import Combine import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit public enum AttachmentDownloadJob: JobExecutor { public static var maxFailureCount: Int = 3 @@ -112,7 +112,7 @@ public enum AttachmentDownloadJob: JobExecutor { switch maybeRoomToken { case .some(let roomToken): - return try OpenGroupAPI + return try Network.SOGS .preparedDownload( url: info.downloadUrl, roomToken: roomToken, diff --git a/SessionMessagingKit/Jobs/AttachmentUploadJob.swift b/SessionMessagingKit/Jobs/AttachmentUploadJob.swift index d1dc85ddea..a3cd20c22a 100644 --- a/SessionMessagingKit/Jobs/AttachmentUploadJob.swift +++ b/SessionMessagingKit/Jobs/AttachmentUploadJob.swift @@ -3,7 +3,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Log.Category diff --git a/SessionMessagingKit/Jobs/CheckForAppUpdatesJob.swift b/SessionMessagingKit/Jobs/CheckForAppUpdatesJob.swift index aba703204e..64f9c9f4c7 100644 --- a/SessionMessagingKit/Jobs/CheckForAppUpdatesJob.swift +++ b/SessionMessagingKit/Jobs/CheckForAppUpdatesJob.swift @@ -2,7 +2,7 @@ import Foundation import Combine -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Log.Category @@ -40,7 +40,7 @@ public enum CheckForAppUpdatesJob: JobExecutor { nextRunTimestamp: (dependencies.dateNow.timeIntervalSince1970 + updateCheckFrequency) ) dependencies[singleton: .storage].write { db in - try updatedJob.save(db) + try updatedJob.upsert(db) } Log.info(.cat, "Deferred due to test/simulator build.") @@ -59,7 +59,7 @@ public enum CheckForAppUpdatesJob: JobExecutor { ) dependencies[singleton: .storage].write { db in - try updatedJob.save(db) + try updatedJob.upsert(db) } success(updatedJob, false) diff --git a/SessionMessagingKit/Jobs/ConfigMessageReceiveJob.swift b/SessionMessagingKit/Jobs/ConfigMessageReceiveJob.swift index 5720b9acfb..53cd2a73a5 100644 --- a/SessionMessagingKit/Jobs/ConfigMessageReceiveJob.swift +++ b/SessionMessagingKit/Jobs/ConfigMessageReceiveJob.swift @@ -3,7 +3,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Log.Category @@ -92,13 +92,13 @@ extension ConfigMessageReceiveJob { case data } - public let namespace: SnodeAPI.Namespace + public let namespace: Network.SnodeAPI.Namespace public let serverHash: String public let serverTimestampMs: Int64 public let data: Data public init( - namespace: SnodeAPI.Namespace, + namespace: Network.SnodeAPI.Namespace, serverHash: String, serverTimestampMs: Int64, data: Data diff --git a/SessionMessagingKit/Jobs/ConfigurationSyncJob.swift b/SessionMessagingKit/Jobs/ConfigurationSyncJob.swift index dc37ac0c02..f1266f72a1 100644 --- a/SessionMessagingKit/Jobs/ConfigurationSyncJob.swift +++ b/SessionMessagingKit/Jobs/ConfigurationSyncJob.swift @@ -3,7 +3,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Log.Category @@ -96,14 +96,14 @@ public enum ConfigurationSyncJob: JobExecutor { try Authentication.with(db, swarmPublicKey: swarmPublicKey, using: dependencies) } .tryFlatMap { authMethod -> AnyPublisher<(ResponseInfoType, Network.BatchResponse), Error> in - try SnodeAPI.preparedSequence( + try Network.SnodeAPI.preparedSequence( requests: [] .appending(contentsOf: additionalTransientData?.beforeSequenceRequests) .appending( contentsOf: try pendingPushes.pushData .flatMap { pushData -> [ErasedPreparedRequest] in try pushData.data.map { data -> ErasedPreparedRequest in - try SnodeAPI + try Network.SnodeAPI .preparedSendMessage( message: SnodeMessage( recipient: swarmPublicKey, @@ -121,7 +121,7 @@ public enum ConfigurationSyncJob: JobExecutor { .appending(try { guard !pendingPushes.obsoleteHashes.isEmpty else { return nil } - return try SnodeAPI.preparedDeleteMessages( + return try Network.SnodeAPI.preparedDeleteMessages( serverHashes: Array(pendingPushes.obsoleteHashes), requireSuccessfulDeletion: false, authMethod: authMethod, diff --git a/SessionMessagingKit/Jobs/DisappearingMessagesJob.swift b/SessionMessagingKit/Jobs/DisappearingMessagesJob.swift index 0f7e16f812..e6689d73b0 100644 --- a/SessionMessagingKit/Jobs/DisappearingMessagesJob.swift +++ b/SessionMessagingKit/Jobs/DisappearingMessagesJob.swift @@ -4,7 +4,7 @@ import Foundation import Combine import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit // MARK: - Log.Category @@ -35,19 +35,11 @@ public enum DisappearingMessagesJob: JobExecutor { var numDeleted: Int = -1 let updatedJob: Job? = dependencies[singleton: .storage].write { db in - let interactionInfo: Set = try Interaction - .select(.id, .threadId) - .filter(Interaction.Columns.expiresStartedAtMs != nil) + numDeleted = try Interaction.deleteWhere( + db, + .filter(Interaction.Columns.expiresStartedAtMs != nil), .filter((Interaction.Columns.expiresStartedAtMs + (Interaction.Columns.expiresInSeconds * 1000)) <= timestampNowMs) - .asRequest(of: InteractionThreadInfo.self) - .fetchSet(db) - try Interaction.filter(interactionInfo.map { $0.id }.contains(Interaction.Columns.id)).deleteAll(db) - numDeleted = interactionInfo.count - - // Notify of the deletion - interactionInfo.forEach { info in - db.addMessageEvent(id: info.id, threadId: info.threadId, type: .deleted) - } + ) // Update the next run timestamp for the DisappearingMessagesJob (if the call // to 'updateNextRunIfNeeded' returns 'nil' then it doesn't need to re-run so @@ -73,20 +65,21 @@ private struct InteractionThreadInfo: Codable, FetchableRecord, Hashable { // MARK: - Clean expired messages on app launch public extension DisappearingMessagesJob { - static func cleanExpiredMessagesOnLaunch(using dependencies: Dependencies) { + static func cleanExpiredMessagesOnResume(using dependencies: Dependencies) { guard dependencies[cache: .general].userExists else { return } let timestampNowMs: Double = dependencies[cache: .snodeAPI].currentOffsetTimestampMs() var numDeleted: Int = -1 dependencies[singleton: .storage].write { db in - numDeleted = try Interaction - .filter(Interaction.Columns.expiresStartedAtMs != nil) + numDeleted = try Interaction.deleteWhere( + db, + .filter(Interaction.Columns.expiresStartedAtMs != nil), .filter((Interaction.Columns.expiresStartedAtMs + (Interaction.Columns.expiresInSeconds * 1000)) <= timestampNowMs) - .deleteAll(db) + ) } - Log.info(.cat, "Deleted \(numDeleted) expired messages on app launch.") + Log.info(.cat, "Deleted \(numDeleted) expired messages on app resume.") } } diff --git a/SessionMessagingKit/Jobs/DisplayPictureDownloadJob.swift b/SessionMessagingKit/Jobs/DisplayPictureDownloadJob.swift index ec9ee87bc3..b687c07f66 100644 --- a/SessionMessagingKit/Jobs/DisplayPictureDownloadJob.swift +++ b/SessionMessagingKit/Jobs/DisplayPictureDownloadJob.swift @@ -6,7 +6,7 @@ import Foundation import Combine import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit // MARK: - Log.Category @@ -39,7 +39,7 @@ public enum DisplayPictureDownloadJob: JobExecutor { switch details.target { case .profile(_, let url, _), .group(_, let url, _): guard - let fileId: String = Attachment.fileId(for: url), + let fileId: String = Network.FileServer.fileId(for: url), let downloadUrl: URL = URL(string: Network.FileServer.downloadUrlString(for: url, fileId: fileId)) else { throw NetworkError.invalidURL } @@ -48,21 +48,22 @@ public enum DisplayPictureDownloadJob: JobExecutor { using: dependencies ) - case .community(let fileId, let roomToken, let server): + case .community(let fileId, let roomToken, let server, let skipAuthentication): guard let info: LibSession.OpenGroupCapabilityInfo = try? LibSession.OpenGroupCapabilityInfo .fetchOne(db, id: OpenGroup.idFor(roomToken: roomToken, server: server)) else { throw JobRunnerError.missingRequiredDetails } - return try OpenGroupAPI.preparedDownload( + return try Network.SOGS.preparedDownload( fileId: fileId, roomToken: roomToken, authMethod: Authentication.community(info: info), + skipAuthentication: skipAuthentication, using: dependencies ) } } - .tryMap { (preparedDownload: Network.PreparedRequest) -> Network.PreparedRequest<(Data, String, URL?)> in + .tryMap { (preparedDownload: Network.PreparedRequest) -> Network.PreparedRequest<(Data, String, URL?, Date?)> in guard let filePath: String = try? dependencies[singleton: .displayPictureManager].path( for: (preparedDownload.destination.url?.absoluteString) @@ -74,15 +75,15 @@ public enum DisplayPictureDownloadJob: JobExecutor { throw DisplayPictureError.alreadyDownloaded(preparedDownload.destination.url) } - return preparedDownload.map { _, data in - (data, filePath, preparedDownload.destination.url) + return preparedDownload.map { info, data in + (data, filePath, preparedDownload.destination.url, Date.fromHTTPExpiresHeaders(info.headers["Expires"])) } } .flatMap { $0.send(using: dependencies) } .map { _, result in result } .subscribe(on: scheduler, using: dependencies) .receive(on: scheduler, using: dependencies) - .flatMapStorageReadPublisher(using: dependencies) { (db: ObservingDatabase, result: (Data, String, URL?)) -> (Data, String, URL?) in + .flatMapStorageReadPublisher(using: dependencies) { (db: ObservingDatabase, result: (Data, String, URL?, Date?)) -> (Data, String, URL?, Date?) in /// Check to make sure this download is still a valid update guard details.isValidUpdate(db, using: dependencies) else { throw DisplayPictureError.updateNoLongerValid @@ -90,7 +91,7 @@ public enum DisplayPictureDownloadJob: JobExecutor { return result } - .tryMap { (data: Data, filePath: String, downloadUrl: URL?) -> URL? in + .tryMap { (data: Data, filePath: String, downloadUrl: URL?, expires: Date?) -> (URL?, Date?) in guard let decryptedData: Data = { switch details.target { @@ -118,15 +119,16 @@ public enum DisplayPictureDownloadJob: JobExecutor { ) } - return downloadUrl + return (downloadUrl, expires) } - .flatMapStorageWritePublisher(using: dependencies) { (db: ObservingDatabase, downloadUrl: URL?) in + .flatMapStorageWritePublisher(using: dependencies) { (db: ObservingDatabase, result: (downloadUrl: URL?, expires: Date?)) in /// Store the updated information in the database (this will generally result in the UI refreshing as it'll observe /// the `downloadUrl` changing) try writeChanges( db, details: details, - downloadUrl: downloadUrl, + downloadUrl: result.downloadUrl, + expires: result.expires, using: dependencies ) } @@ -143,6 +145,7 @@ public enum DisplayPictureDownloadJob: JobExecutor { db, details: details, downloadUrl: downloadUrl, + expires: nil, using: dependencies ) }, @@ -179,6 +182,7 @@ public enum DisplayPictureDownloadJob: JobExecutor { _ db: ObservingDatabase, details: Details, downloadUrl: URL?, + expires: Date?, using dependencies: Dependencies ) throws { switch details.target { @@ -189,11 +193,15 @@ public enum DisplayPictureDownloadJob: JobExecutor { db, Profile.Columns.displayPictureUrl.set(to: url), Profile.Columns.displayPictureEncryptionKey.set(to: encryptionKey), - Profile.Columns.displayPictureLastUpdated.set(to: details.timestamp), + Profile.Columns.profileLastUpdated.set(to: details.timestamp), using: dependencies ) db.addProfileEvent(id: id, change: .displayPictureUrl(url)) db.addConversationEvent(id: id, type: .updated(.displayPictureUrl(url))) + + if dependencies[cache: .general].sessionId.hexString == id, let expires: Date = expires { + dependencies[defaults: .standard, key: .profilePictureExpiresDate] = expires + } case .group(let id, let url, let encryptionKey): _ = try? ClosedGroup @@ -206,7 +214,7 @@ public enum DisplayPictureDownloadJob: JobExecutor { ) db.addConversationEvent(id: id, type: .updated(.displayPictureUrl(url))) - case .community(_, let roomToken, let server): + case .community(_, let roomToken, let server, _): _ = try? OpenGroup .filter(id: OpenGroup.idFor(roomToken: roomToken, server: server)) .updateAllAndConfig( @@ -228,18 +236,18 @@ extension DisplayPictureDownloadJob { public enum Target: Codable, Hashable, CustomStringConvertible { case profile(id: String, url: String, encryptionKey: Data) case group(id: String, url: String, encryptionKey: Data) - case community(imageId: String, roomToken: String, server: String) + case community(imageId: String, roomToken: String, server: String, skipAuthentication: Bool = false) var isValid: Bool { switch self { case .profile(_, let url, let encryptionKey), .group(_, let url, let encryptionKey): return ( !url.isEmpty && - Attachment.fileId(for: url) != nil && + Network.FileServer.fileId(for: url) != nil && encryptionKey.count == DisplayPictureManager.aes256KeyByteLength ) - case .community(let imageId, _, _): return !imageId.isEmpty + case .community(let imageId, _, _, _): return !imageId.isEmpty } } @@ -249,14 +257,14 @@ extension DisplayPictureDownloadJob { switch self { case .profile(let id, _, _): return "profile: \(id)" case .group(let id, _, _): return "group: \(id)" - case .community(_, let roomToken, let server): return "room: \(roomToken) on server: \(server)" + case .community(_, let roomToken, let server, _): return "room: \(roomToken) on server: \(server)" } } } public struct Details: Codable, Hashable { public let target: Target - public let timestamp: TimeInterval + public let timestamp: TimeInterval? // MARK: - Hashable @@ -269,16 +277,17 @@ extension DisplayPictureDownloadJob { // MARK: - Initialization - public init?(target: Target, timestamp: TimeInterval) { + public init?(target: Target, timestamp: TimeInterval?) { guard target.isValid else { return nil } self.target = { switch target { - case .community(let imageId, let roomToken, let server): + case .community(let imageId, let roomToken, let server, let skipAuthentication): return .community( imageId: imageId, roomToken: roomToken, - server: server.lowercased() // Always in lowercase on `OpenGroup` + server: server.lowercased(), // Always in lowercase on `OpenGroup` + skipAuthentication: skipAuthentication ) default: return target @@ -295,7 +304,7 @@ extension DisplayPictureDownloadJob { let key: Data = profile.displayPictureEncryptionKey, let details: Details = Details( target: .profile(id: profile.id, url: url, encryptionKey: key), - timestamp: (profile.displayPictureLastUpdated ?? 0) + timestamp: (profile.profileLastUpdated ?? 0) ) else { return nil } @@ -339,11 +348,16 @@ extension DisplayPictureDownloadJob { case .profile(let id, let url, let encryptionKey): guard let latestProfile: Profile = try? Profile.fetchOne(db, id: id) else { return false } + /// If the data matches what is stored in the database then we should be fine to consider it valid (it may be that + /// we are re-downloading a profile due to some invalid state) + let dataMatches: Bool = ( + encryptionKey == latestProfile.displayPictureEncryptionKey && + url == latestProfile.displayPictureUrl + ) + return ( - timestamp >= (latestProfile.displayPictureLastUpdated ?? 0) || ( - encryptionKey == latestProfile.displayPictureEncryptionKey && - url == latestProfile.displayPictureUrl - ) + Profile.shouldUpdateProfile(timestamp, profile: latestProfile, using: dependencies) || + dataMatches ) case .group(let id, let url,_): @@ -358,7 +372,7 @@ extension DisplayPictureDownloadJob { return (url == latestDisplayPictureUrl) - case .community(let imageId, let roomToken, let server): + case .community(let imageId, let roomToken, let server, _): guard let latestImageId: String = try? OpenGroup .select(.imageId) diff --git a/SessionMessagingKit/Jobs/ExpirationUpdateJob.swift b/SessionMessagingKit/Jobs/ExpirationUpdateJob.swift index 0ffd051f5a..ef3cdb9d7a 100644 --- a/SessionMessagingKit/Jobs/ExpirationUpdateJob.swift +++ b/SessionMessagingKit/Jobs/ExpirationUpdateJob.swift @@ -4,7 +4,7 @@ import Foundation import Combine import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit public enum ExpirationUpdateJob: JobExecutor { public static var maxFailureCount: Int = -1 @@ -26,7 +26,7 @@ public enum ExpirationUpdateJob: JobExecutor { dependencies[singleton: .storage] .readPublisher { db in - try SnodeAPI + try Network.SnodeAPI .preparedUpdateExpiry( serverHashes: details.serverHashes, updatedExpiryMs: details.expirationTimestampMs, diff --git a/SessionMessagingKit/Jobs/GarbageCollectionJob.swift b/SessionMessagingKit/Jobs/GarbageCollectionJob.swift index b799931f85..aacaa48e5c 100644 --- a/SessionMessagingKit/Jobs/GarbageCollectionJob.swift +++ b/SessionMessagingKit/Jobs/GarbageCollectionJob.swift @@ -4,7 +4,7 @@ import Foundation import Combine import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit // MARK: - Log.Category @@ -182,7 +182,7 @@ public enum GarbageCollectionJob: JobExecutor { LEFT JOIN \(SessionThread.self) ON \(thread[.id]) = \(openGroup[.threadId]) WHERE ( \(thread[.id]) IS NULL AND - \(SQL("\(openGroup[.server]) != \(OpenGroupAPI.defaultServer.lowercased())")) + \(SQL("\(openGroup[.server]) != \(Network.SOGS.defaultServer.lowercased())")) ) ) """) @@ -309,11 +309,12 @@ public enum GarbageCollectionJob: JobExecutor { /// Remove interactions which should be disappearing after read but never be read within 14 days if finalTypesToCollect.contains(.expiredUnreadDisappearingMessages) { - _ = try Interaction - .filter(Interaction.Columns.expiresInSeconds != 0) - .filter(Interaction.Columns.expiresStartedAtMs == nil) + try Interaction.deleteWhere( + db, + .filter(Interaction.Columns.expiresInSeconds != 0), + .filter(Interaction.Columns.expiresStartedAtMs == nil), .filter(Interaction.Columns.timestampMs < (timestampNow - fourteenDaysInSeconds) * 1000) - .deleteAll(db) + ) } if finalTypesToCollect.contains(.expiredPendingReadReceipts) { diff --git a/SessionMessagingKit/Jobs/GetExpirationJob.swift b/SessionMessagingKit/Jobs/GetExpirationJob.swift index bf15ef97b7..00be1730b7 100644 --- a/SessionMessagingKit/Jobs/GetExpirationJob.swift +++ b/SessionMessagingKit/Jobs/GetExpirationJob.swift @@ -3,7 +3,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit public enum GetExpirationJob: JobExecutor { @@ -39,7 +39,7 @@ public enum GetExpirationJob: JobExecutor { dependencies[singleton: .storage] .readPublisher { db -> Network.PreparedRequest in - try SnodeAPI.preparedGetExpiries( + try Network.SnodeAPI.preparedGetExpiries( of: expirationInfo.map { $0.key }, authMethod: try Authentication.with( db, @@ -90,9 +90,10 @@ public enum GetExpirationJob: JobExecutor { hashesWithNoExiprationInfo = hashesWithNoExiprationInfo.subtracting(inferredExpiredMessageHashes) if !inferredExpiredMessageHashes.isEmpty { - try Interaction + try Interaction.deleteWhere( + db, .filter(inferredExpiredMessageHashes.contains(Interaction.Columns.serverHash)) - .deleteAll(db) + ) } try Interaction diff --git a/SessionMessagingKit/Jobs/GroupInviteMemberJob.swift b/SessionMessagingKit/Jobs/GroupInviteMemberJob.swift index b40827c6e8..e652f75f13 100644 --- a/SessionMessagingKit/Jobs/GroupInviteMemberJob.swift +++ b/SessionMessagingKit/Jobs/GroupInviteMemberJob.swift @@ -5,7 +5,7 @@ import Combine import GRDB import SessionUIKit import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit // MARK: - Log.Category diff --git a/SessionMessagingKit/Jobs/GroupLeavingJob.swift b/SessionMessagingKit/Jobs/GroupLeavingJob.swift index 475a5ea13d..208072da67 100644 --- a/SessionMessagingKit/Jobs/GroupLeavingJob.swift +++ b/SessionMessagingKit/Jobs/GroupLeavingJob.swift @@ -4,7 +4,7 @@ import Foundation import Combine import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit // MARK: - Log.Category @@ -92,7 +92,7 @@ public enum GroupLeavingJob: JobExecutor { .tryFlatMap { requestType -> AnyPublisher in switch requestType { case .sendLeaveMessage(let authMethod, let disappearingConfig): - return try SnodeAPI + return try Network.SnodeAPI .preparedBatch( requests: [ /// Don't expire the `GroupUpdateMemberLeftMessage` as that's not a UI-based diff --git a/SessionMessagingKit/Jobs/GroupPromoteMemberJob.swift b/SessionMessagingKit/Jobs/GroupPromoteMemberJob.swift index 10a934451b..d46639ac78 100644 --- a/SessionMessagingKit/Jobs/GroupPromoteMemberJob.swift +++ b/SessionMessagingKit/Jobs/GroupPromoteMemberJob.swift @@ -5,7 +5,7 @@ import Combine import GRDB import SessionUIKit import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit // MARK: - Log.Category diff --git a/SessionMessagingKit/Jobs/MessageSendJob.swift b/SessionMessagingKit/Jobs/MessageSendJob.swift index 590a660a46..bbb2b1c7fe 100644 --- a/SessionMessagingKit/Jobs/MessageSendJob.swift +++ b/SessionMessagingKit/Jobs/MessageSendJob.swift @@ -4,7 +4,7 @@ import Foundation import Combine import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit // MARK: - Log.Category @@ -352,7 +352,7 @@ public extension MessageSendJob { .compactMap { info in guard let attachment: Attachment = attachments[info.attachmentId], - let fileId: String = Attachment.fileId(for: info.downloadUrl) + let fileId: String = Network.FileServer.fileId(for: info.downloadUrl) else { return nil } return (attachment, fileId) diff --git a/SessionMessagingKit/Jobs/ProcessPendingGroupMemberRemovalsJob.swift b/SessionMessagingKit/Jobs/ProcessPendingGroupMemberRemovalsJob.swift index 3decb55cf9..0253242bc7 100644 --- a/SessionMessagingKit/Jobs/ProcessPendingGroupMemberRemovalsJob.swift +++ b/SessionMessagingKit/Jobs/ProcessPendingGroupMemberRemovalsJob.swift @@ -6,7 +6,7 @@ import GRDB import SessionUtil import SessionUIKit import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit // MARK: - Log.Category @@ -97,7 +97,7 @@ public enum ProcessPendingGroupMemberRemovalsJob: JobExecutor { .tryMap { _ -> Network.PreparedRequest in /// Revoke the members authData from the group so the server rejects API calls from the ex-members (fire-and-forget /// this request, we don't want it to be blocking) - let preparedRevokeSubaccounts: Network.PreparedRequest = try SnodeAPI.preparedRevokeSubaccounts( + let preparedRevokeSubaccounts: Network.PreparedRequest = try Network.SnodeAPI.preparedRevokeSubaccounts( subaccountsToRevoke: try dependencies.mutate(cache: .libSession) { cache in try Array(pendingRemovals.keys).map { memberId in try dependencies[singleton: .crypto].tryGenerate( @@ -131,7 +131,7 @@ public enum ProcessPendingGroupMemberRemovalsJob: JobExecutor { domain: .kickedMessage ) ) - let preparedGroupDeleteMessage: Network.PreparedRequest = try SnodeAPI + let preparedGroupDeleteMessage: Network.PreparedRequest = try Network.SnodeAPI .preparedSendMessage( message: SnodeMessage( recipient: groupSessionId.hexString, @@ -179,7 +179,7 @@ public enum ProcessPendingGroupMemberRemovalsJob: JobExecutor { }() /// Combine the two requests to be sent at the same time - return try SnodeAPI.preparedSequence( + return try Network.SnodeAPI.preparedSequence( requests: [preparedRevokeSubaccounts, preparedGroupDeleteMessage, preparedMemberContentRemovalMessage] .compactMap { $0 }, requireAllBatchResponses: true, @@ -262,7 +262,7 @@ public enum ProcessPendingGroupMemberRemovalsJob: JobExecutor { ) /// Delete the messages from the swarm so users won't download them again - try? SnodeAPI + try? Network.SnodeAPI .preparedDeleteMessages( serverHashes: Array(hashes), requireSuccessfulDeletion: false, diff --git a/SessionMessagingKit/Jobs/RetrieveDefaultOpenGroupRoomsJob.swift b/SessionMessagingKit/Jobs/RetrieveDefaultOpenGroupRoomsJob.swift index a864b6c2fe..e34c48694b 100644 --- a/SessionMessagingKit/Jobs/RetrieveDefaultOpenGroupRoomsJob.swift +++ b/SessionMessagingKit/Jobs/RetrieveDefaultOpenGroupRoomsJob.swift @@ -3,7 +3,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Log.Category @@ -40,17 +40,17 @@ public enum RetrieveDefaultOpenGroupRoomsJob: JobExecutor { .isEmpty else { return deferred(job) } - // The OpenGroupAPI won't make any API calls if there is no entry for an OpenGroup + // The Network.SOGS won't make any API calls if there is no entry for an OpenGroup // in the database so we need to create a dummy one to retrieve the default room data - let defaultGroupId: String = OpenGroup.idFor(roomToken: "", server: OpenGroupAPI.defaultServer) + let defaultGroupId: String = OpenGroup.idFor(roomToken: "", server: Network.SOGS.defaultServer) dependencies[singleton: .storage].write { db in guard try OpenGroup.exists(db, id: defaultGroupId) == false else { return } try OpenGroup( - server: OpenGroupAPI.defaultServer, + server: Network.SOGS.defaultServer, roomToken: "", - publicKey: OpenGroupAPI.defaultServerPublicKey, + publicKey: Network.SOGS.defaultServerPublicKey, isActive: false, name: "", userCount: 0, @@ -64,14 +64,15 @@ public enum RetrieveDefaultOpenGroupRoomsJob: JobExecutor { .readPublisher { [dependencies] db -> AuthenticationMethod in try Authentication.with( db, - server: OpenGroupAPI.defaultServer, + server: Network.SOGS.defaultServer, activeOnly: false, /// The record for the default rooms is inactive using: dependencies ) } - .tryFlatMap { [dependencies] authMethod -> AnyPublisher<(ResponseInfoType, OpenGroupAPI.CapabilitiesAndRoomsResponse), Error> in - try OpenGroupAPI.preparedCapabilitiesAndRooms( + .tryFlatMap { [dependencies] authMethod -> AnyPublisher<(ResponseInfoType, Network.SOGS.CapabilitiesAndRoomsResponse), Error> in + try Network.SOGS.preparedCapabilitiesAndRooms( authMethod: authMethod, + skipAuthentication: true, using: dependencies ).send(using: dependencies) } @@ -96,11 +97,11 @@ public enum RetrieveDefaultOpenGroupRoomsJob: JobExecutor { OpenGroupManager.handleCapabilities( db, capabilities: response.capabilities.data, - on: OpenGroupAPI.defaultServer + on: Network.SOGS.defaultServer ) let existingImageIds: [String: String] = try OpenGroup - .filter(OpenGroup.Columns.server == OpenGroupAPI.defaultServer) + .filter(OpenGroup.Columns.server == Network.SOGS.defaultServer) .filter(OpenGroup.Columns.imageId != nil) .fetchAll(db) .reduce(into: [:]) { result, next in result[next.id] = next.imageId } @@ -112,9 +113,9 @@ public enum RetrieveDefaultOpenGroupRoomsJob: JobExecutor { return ( room, try OpenGroup( - server: OpenGroupAPI.defaultServer, + server: Network.SOGS.defaultServer, roomToken: room.token, - publicKey: OpenGroupAPI.defaultServerPublicKey, + publicKey: Network.SOGS.defaultServerPublicKey, isActive: false, name: room.name, roomDescription: room.roomDescription, @@ -131,7 +132,7 @@ public enum RetrieveDefaultOpenGroupRoomsJob: JobExecutor { db, id: OpenGroup.idFor( roomToken: room.token, - server: OpenGroupAPI.defaultServer + server: Network.SOGS.defaultServer ) ) .map { (room, $0) } @@ -140,7 +141,7 @@ public enum RetrieveDefaultOpenGroupRoomsJob: JobExecutor { /// Schedule the room image download (if it doesn't match out current one) result.forEach { room, openGroup in - let openGroupId: String = OpenGroup.idFor(roomToken: room.token, server: OpenGroupAPI.defaultServer) + let openGroupId: String = OpenGroup.idFor(roomToken: room.token, server: Network.SOGS.defaultServer) guard let imageId: String = room.imageId, @@ -157,7 +158,8 @@ public enum RetrieveDefaultOpenGroupRoomsJob: JobExecutor { target: .community( imageId: imageId, roomToken: room.token, - server: OpenGroupAPI.defaultServer + server: Network.SOGS.defaultServer, + skipAuthentication: true ), timestamp: (dependencies[cache: .snodeAPI].currentOffsetTimestampMs() / 1000) ) diff --git a/SessionMessagingKit/Jobs/SendReadReceiptsJob.swift b/SessionMessagingKit/Jobs/SendReadReceiptsJob.swift index 1da0c5a95c..9196301bfc 100644 --- a/SessionMessagingKit/Jobs/SendReadReceiptsJob.swift +++ b/SessionMessagingKit/Jobs/SendReadReceiptsJob.swift @@ -3,7 +3,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit public enum SendReadReceiptsJob: JobExecutor { diff --git a/SessionMessagingKit/Jobs/UpdateProfilePictureJob.swift b/SessionMessagingKit/Jobs/UpdateProfilePictureJob.swift index cf1f402590..6b1051be05 100644 --- a/SessionMessagingKit/Jobs/UpdateProfilePictureJob.swift +++ b/SessionMessagingKit/Jobs/UpdateProfilePictureJob.swift @@ -4,6 +4,7 @@ import Foundation import Combine import GRDB import SessionUtilitiesKit +import SessionUIKit // MARK: - Log.Category @@ -17,6 +18,7 @@ public enum UpdateProfilePictureJob: JobExecutor { public static let maxFailureCount: Int = -1 public static let requiresThreadId: Bool = false public static let requiresInteractionId: Bool = false + public static let maxTTL: TimeInterval = (14 * 24 * 60 * 60) public static func run( _ job: Job, @@ -31,11 +33,38 @@ public enum UpdateProfilePictureJob: JobExecutor { return deferred(job) // Don't need to do anything if it's not the main app } - // Only re-upload the profile picture if enough time has passed since the last upload - guard - let lastProfilePictureUpload: Date = dependencies[defaults: .standard, key: .lastProfilePictureUpload], - dependencies.dateNow.timeIntervalSince(lastProfilePictureUpload) > (14 * 24 * 60 * 60) - else { + let expirationDate: Date? = dependencies[defaults: .standard, key: .profilePictureExpiresDate] + let lastUploadDate: Date? = dependencies[defaults: .standard, key: .lastProfilePictureUpload] + let expired: Bool = (expirationDate.map({ dependencies.dateNow.timeIntervalSince($0) > 0 }) == true) + let exceededMaxTTL: Bool = (lastUploadDate.map({ dependencies.dateNow.timeIntervalSince($0) > Self.maxTTL }) == true) + + if (expired || exceededMaxTTL) { + /// **Note:** The `lastProfilePictureUpload` value is updated in `DisplayPictureManager` + let profile = dependencies.mutate(cache: .libSession) { $0.profile } + let displayPictureUpdate: DisplayPictureManager.Update = profile.displayPictureUrl + .map { try? dependencies[singleton: .displayPictureManager].path(for: $0) } + .map { dependencies[singleton: .fileManager].contents(atPath: $0) } + .map { .currentUserUploadImageData(data: $0, isReupload: true)} + .defaulting(to: .none) + + Profile + .updateLocal( + displayPictureUpdate: displayPictureUpdate, + using: dependencies + ) + .subscribe(on: scheduler, using: dependencies) + .receive(on: scheduler, using: dependencies) + .sinkUntilComplete( + receiveCompletion: { result in + switch result { + case .failure(let error): failure(job, error, false) + case .finished: + Log.info(.cat, "Profile successfully updated") + success(job, false) + } + } + ) + } else { // Reset the `nextRunTimestamp` value just in case the last run failed so we don't get stuck // in a loop endlessly deferring the job if let jobId: Int64 = job.id { @@ -45,35 +74,14 @@ public enum UpdateProfilePictureJob: JobExecutor { .updateAll(db, Job.Columns.nextRunTimestamp.set(to: 0)) } } + + if expirationDate != nil { + Log.info(.cat, "Deferred as current picture hasn't expired") + } else { + Log.info(.cat, "Deferred as not enough time has passed since the last update") + } - Log.info(.cat, "Deferred as not enough time has passed since the last update") return deferred(job) } - - /// **Note:** The `lastProfilePictureUpload` value is updated in `DisplayPictureManager` - let profile: Profile = dependencies.mutate(cache: .libSession) { $0.profile } - let displayPictureUpdate: DisplayPictureManager.Update = profile.displayPictureUrl - .map { try? dependencies[singleton: .displayPictureManager].path(for: $0) } - .map { dependencies[singleton: .fileManager].contents(atPath: $0) } - .map { .currentUserUploadImageData($0) } - .defaulting(to: .none) - - Profile - .updateLocal( - displayPictureUpdate: displayPictureUpdate, - using: dependencies - ) - .subscribe(on: scheduler, using: dependencies) - .receive(on: scheduler, using: dependencies) - .sinkUntilComplete( - receiveCompletion: { result in - switch result { - case .failure(let error): failure(job, error, false) - case .finished: - Log.info(.cat, "Profile successfully updated") - success(job, false) - } - } - ) } } diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift index 40a00940db..63a69e9de1 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift @@ -24,6 +24,7 @@ internal extension LibSession { Profile.Columns.nickname, Profile.Columns.displayPictureUrl, Profile.Columns.displayPictureEncryptionKey, + Profile.Columns.profileLastUpdated, DisappearingMessagesConfiguration.Columns.isEnabled, DisappearingMessagesConfiguration.Columns.type, DisappearingMessagesConfiguration.Columns.durationSeconds @@ -36,8 +37,7 @@ internal extension LibSessionCacheType { func handleContactsUpdate( _ db: ObservingDatabase, in config: LibSession.Config?, - oldState: [ObservableKey: Any], - serverTimestampMs: Int64 + oldState: [ObservableKey: Any] ) throws { guard configNeedsDump(config) else { return } guard case .contacts(let conf) = config else { @@ -48,7 +48,6 @@ internal extension LibSessionCacheType { // actually a bug) let targetContactData: [String: ContactData] = try LibSession.extractContacts( from: conf, - serverTimestampMs: serverTimestampMs, using: dependencies ).filter { $0.key != userSessionId.hexString } @@ -62,24 +61,18 @@ internal extension LibSessionCacheType { // observation system can't differ between update calls which do and don't change anything) let contact: Contact = Contact.fetchOrCreate(db, id: sessionId, using: dependencies) let profile: Profile = Profile.fetchOrCreate(db, id: sessionId) - let profileNameShouldBeUpdated: Bool = ( - !data.profile.name.isEmpty && - profile.name != data.profile.name && - (profile.lastNameUpdate ?? 0) < (data.profile.lastNameUpdate ?? 0) - ) - let profilePictureShouldBeUpdated: Bool = ( - ( + let profileUpdated: Bool = ((profile.profileLastUpdated ?? 0) < (data.profile.profileLastUpdated ?? 0)) + + if (profileUpdated || (profile.nickname != data.profile.nickname)) { + let profileNameShouldBeUpdated: Bool = ( + !data.profile.name.isEmpty && + profile.name != data.profile.name + ) + let profilePictureShouldBeUpdated: Bool = ( profile.displayPictureUrl != data.profile.displayPictureUrl || profile.displayPictureEncryptionKey != data.profile.displayPictureEncryptionKey - ) && - (profile.displayPictureLastUpdated ?? 0) < (data.profile.displayPictureLastUpdated ?? 0) - ) - - if - profileNameShouldBeUpdated || - profile.nickname != data.profile.nickname || - profilePictureShouldBeUpdated - { + ) + try profile.upsert(db) try Profile .filter(id: sessionId) @@ -89,9 +82,6 @@ internal extension LibSessionCacheType { (!profileNameShouldBeUpdated ? nil : Profile.Columns.name.set(to: data.profile.name) ), - (!profileNameShouldBeUpdated ? nil : - Profile.Columns.lastNameUpdate.set(to: data.profile.lastNameUpdate) - ), (profile.nickname == data.profile.nickname ? nil : Profile.Columns.nickname.set(to: data.profile.nickname) ), @@ -101,8 +91,8 @@ internal extension LibSessionCacheType { (profile.displayPictureEncryptionKey != data.profile.displayPictureEncryptionKey ? nil : Profile.Columns.displayPictureEncryptionKey.set(to: data.profile.displayPictureEncryptionKey) ), - (!profilePictureShouldBeUpdated ? nil : - Profile.Columns.displayPictureLastUpdated.set(to: data.profile.displayPictureLastUpdated) + (!profileUpdated ? nil : + Profile.Columns.profileLastUpdated.set(to: data.profile.profileLastUpdated) ) ].compactMap { $0 }, using: dependencies @@ -343,6 +333,9 @@ public extension LibSession { contact.set(\.nickname, to: info.nickname) contact.set(\.profile_pic.url, to: info.displayPictureUrl) contact.set(\.profile_pic.key, to: info.displayPictureEncryptionKey) + if let profileLastUpdated = info.profileLastUpdated { + contact.set(\.profile_updated, to: profileLastUpdated) + } // Attempts retrieval of the profile picture (will schedule a download if // needed via a throttled subscription on another thread to prevent blocking) @@ -512,19 +505,6 @@ internal extension LibSession { existingContactIds.contains($0.id) } - // Update the user profile first (if needed) - if let updatedUserProfile: Profile = updatedProfiles.first(where: { $0.id == userSessionId.hexString }) { - try dependencies.mutate(cache: .libSession) { cache in - try cache.performAndPushChange(db, for: .userProfile, sessionId: userSessionId) { _ in - try cache.updateProfile( - displayName: updatedUserProfile.name, - displayPictureUrl: updatedUserProfile.displayPictureUrl, - displayPictureEncryptionKey: updatedUserProfile.displayPictureEncryptionKey - ) - } - } - } - try dependencies.mutate(cache: .libSession) { cache in try cache.performAndPushChange(db, for: .contacts, sessionId: userSessionId) { config in try LibSession @@ -740,6 +720,7 @@ extension LibSession { let nickname: String? let displayPictureUrl: String? let displayPictureEncryptionKey: Data? + let profileLastUpdated: Int64? let disappearingMessagesInfo: DisappearingMessageInfo? let priority: Int32? @@ -775,6 +756,7 @@ extension LibSession { nickname: profile?.nickname, displayPictureUrl: profile?.displayPictureUrl, displayPictureEncryptionKey: profile?.displayPictureEncryptionKey, + profileLastUpdated: profile?.profileLastUpdated.map({ Int64($0) }), disappearingMessagesInfo: disappearingMessagesConfig.map { DisappearingMessageInfo( isEnabled: $0.isEnabled, @@ -797,6 +779,7 @@ extension LibSession { nickname: String? = nil, displayPictureUrl: String? = nil, displayPictureEncryptionKey: Data? = nil, + profileLastUpdated: Int64? = nil, disappearingMessagesInfo: DisappearingMessageInfo? = nil, priority: Int32? = nil, created: TimeInterval? = nil @@ -810,6 +793,7 @@ extension LibSession { self.nickname = nickname self.displayPictureUrl = displayPictureUrl self.displayPictureEncryptionKey = displayPictureEncryptionKey + self.profileLastUpdated = profileLastUpdated self.disappearingMessagesInfo = disappearingMessagesInfo self.priority = priority self.created = created @@ -851,7 +835,6 @@ internal struct ContactData { internal extension LibSession { static func extractContacts( from conf: UnsafeMutablePointer?, - serverTimestampMs: Int64, using dependencies: Dependencies ) throws -> [String: ContactData] { var infiniteLoopGuard: Int = 0 @@ -875,11 +858,10 @@ internal extension LibSession { let profileResult: Profile = Profile( id: contactId, name: contact.get(\.name), - lastNameUpdate: (TimeInterval(serverTimestampMs) / 1000), nickname: contact.get(\.nickname, nullIfEmpty: true), displayPictureUrl: displayPictureUrl, displayPictureEncryptionKey: (displayPictureUrl == nil ? nil : contact.get(\.profile_pic.key)), - displayPictureLastUpdated: (TimeInterval(serverTimestampMs) / 1000) + profileLastUpdated: TimeInterval(contact.profile_updated) ) let configResult: DisappearingMessagesConfiguration = DisappearingMessagesConfiguration( threadId: contactId, diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupInfo.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupInfo.swift index ee77869c1e..82cdc5060c 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupInfo.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupInfo.swift @@ -3,7 +3,7 @@ import Foundation import GRDB import SessionUtil -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Size Restrictions @@ -174,7 +174,7 @@ internal extension LibSessionCacheType { if localConfig != updatedConfig { try updatedConfig - .saved(db) + .upserted(db) .clearUnrelatedControlMessages( db, threadVariant: .group, @@ -293,7 +293,7 @@ internal extension LibSessionCacheType { swarmPublicKey: groupSessionId.hexString, using: dependencies )).map { authMethod in - try? SnodeAPI + try? Network.SnodeAPI .preparedDeleteMessages( serverHashes: Array(messageHashesToDelete), requireSuccessfulDeletion: false, diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupMembers.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupMembers.swift index 51215e9344..779b4963c7 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupMembers.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+GroupMembers.swift @@ -3,7 +3,7 @@ import Foundation import GRDB import SessionUtil -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Size Restrictions @@ -134,7 +134,7 @@ internal extension LibSessionCacheType { publicKey: profile.id, displayNameUpdate: .contactUpdate(profile.name), displayPictureUpdate: .from(profile, fallback: .none, using: dependencies), - sentTimestamp: TimeInterval(Double(serverTimestampMs) * 1000), + profileUpdateTimestamp: (profile.profileLastUpdated ?? 0), using: dependencies ) } @@ -522,14 +522,12 @@ internal extension LibSession { Profile( id: member.get(\.session_id), name: member.get(\.name), - lastNameUpdate: TimeInterval(Double(serverTimestampMs) / 1000), nickname: nil, displayPictureUrl: member.get(\.profile_pic.url, nullIfEmpty: true), displayPictureEncryptionKey: (member.get(\.profile_pic.url, nullIfEmpty: true) == nil ? nil : member.get(\.profile_pic.key) ), - displayPictureLastUpdated: TimeInterval(Double(serverTimestampMs) / 1000), - lastBlocksCommunityMessageRequests: nil + profileLastUpdated: TimeInterval(member.profile_updated) ) ) diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+Pro.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+Pro.swift index b67745e7f8..31ba81eb19 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+Pro.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+Pro.swift @@ -22,17 +22,18 @@ public extension LibSession { public extension LibSessionCacheType { var isSessionPro: Bool { - if dependencies.hasSet(feature: .mockCurrentUserSessionPro) { - return dependencies[feature: .mockCurrentUserSessionPro] - } - return false + guard dependencies[feature: .sessionProEnabled] else { return false } + return dependencies[feature: .mockCurrentUserSessionPro] } - func validateProProof(_ proProof: String?) -> Bool { - if dependencies.hasSet(feature: .treatAllIncomingMessagesAsProMessages) { - return dependencies[feature: .treatAllIncomingMessagesAsProMessages] - } - return false + func validateProProof(for message: Message?) -> Bool { + guard let message = message, dependencies[feature: .sessionProEnabled] else { return false } + return dependencies[feature: .treatAllIncomingMessagesAsProMessages] + } + + func validateProProof(for profile: Profile?) -> Bool { + guard let profile = profile, dependencies[feature: .sessionProEnabled] else { return false } + return dependencies[feature: .treatAllIncomingMessagesAsProMessages] } func getProProof() -> String? { diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift index 522957def1..2d69301415 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift @@ -3,7 +3,7 @@ import UIKit import GRDB import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtil import SessionUtilitiesKit @@ -761,6 +761,7 @@ public extension LibSession.Cache { let displayNameInMessage: String? = (visibleMessage?.sender != contactId ? nil : visibleMessage?.profile?.displayName?.nullIfEmpty ) + let profileLastUpdatedInMessage: TimeInterval? = visibleMessage?.profile?.updateTimestampSeconds let fallbackProfile: Profile? = displayNameInMessage.map { Profile(id: contactId, name: $0) } guard var cContactId: [CChar] = contactId.cString(using: .utf8) else { @@ -782,11 +783,10 @@ public extension LibSession.Cache { return Profile( id: contactId, name: String(cString: profileNamePtr), - lastNameUpdate: nil, nickname: nil, displayPictureUrl: displayPictureUrl, displayPictureEncryptionKey: (displayPictureUrl == nil ? nil : displayPic.get(\.key)), - displayPictureLastUpdated: nil + profileLastUpdated: profileLastUpdatedInMessage ) } @@ -812,11 +812,10 @@ public extension LibSession.Cache { return Profile( id: contactId, name: (displayNameInMessage ?? member.get(\.name)), - lastNameUpdate: nil, nickname: nil, displayPictureUrl: displayPictureUrl, displayPictureEncryptionKey: (displayPictureUrl == nil ? nil : member.get(\.profile_pic.key)), - displayPictureLastUpdated: nil + profileLastUpdated: TimeInterval(member.get(\.profile_updated)) ) } @@ -838,11 +837,10 @@ public extension LibSession.Cache { return Profile( id: contactId, name: (displayNameInMessage ?? contact.get(\.name)), - lastNameUpdate: nil, nickname: contact.get(\.nickname, nullIfEmpty: true), displayPictureUrl: displayPictureUrl, displayPictureEncryptionKey: (displayPictureUrl == nil ? nil : contact.get(\.profile_pic.key)), - displayPictureLastUpdated: nil + profileLastUpdated: TimeInterval(contact.get( \.profile_updated)) ) } diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+SharedGroup.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+SharedGroup.swift index 54908b4470..64a1f5260a 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+SharedGroup.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+SharedGroup.swift @@ -3,7 +3,7 @@ import UIKit import GRDB import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtil import SessionUtilitiesKit diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift index 27f7bf7fdf..ea081d9c1b 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift @@ -4,7 +4,7 @@ import Foundation import GRDB import SessionUtil import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit // MARK: - Size Restrictions @@ -39,8 +39,7 @@ internal extension LibSession { internal extension LibSessionCacheType { func handleUserGroupsUpdate( _ db: ObservingDatabase, - in config: LibSession.Config?, - serverTimestampMs: Int64 + in config: LibSession.Config? ) throws { guard configNeedsDump(config) else { return } guard case .userGroups(let conf) = config else { diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift index 8957a66c65..3dae469958 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift @@ -11,7 +11,8 @@ internal extension LibSession { static let columnsRelatedToUserProfile: [Profile.Columns] = [ Profile.Columns.name, Profile.Columns.displayPictureUrl, - Profile.Columns.displayPictureEncryptionKey + Profile.Columns.displayPictureEncryptionKey, + Profile.Columns.profileLastUpdated ] static let syncedSettings: [String] = [ @@ -25,8 +26,7 @@ internal extension LibSessionCacheType { func handleUserProfileUpdate( _ db: ObservingDatabase, in config: LibSession.Config?, - oldState: [ObservableKey: Any], - serverTimestampMs: Int64 + oldState: [ObservableKey: Any] ) throws { guard configNeedsDump(config) else { return } guard case .userProfile(let conf) = config else { @@ -39,10 +39,12 @@ internal extension LibSessionCacheType { let profileName: String = String(cString: profileNamePtr) let displayPic: user_profile_pic = user_profile_get_pic(conf) let displayPictureUrl: String? = displayPic.get(\.url, nullIfEmpty: true) + let profileLastUpdateTimestamp: TimeInterval = TimeInterval(user_profile_get_profile_updated(conf)) let updatedProfile: Profile = Profile( id: userSessionId.hexString, name: profileName, - displayPictureUrl: (oldState[.profile(userSessionId.hexString)] as? Profile)?.displayPictureUrl + displayPictureUrl: (oldState[.profile(userSessionId.hexString)] as? Profile)?.displayPictureUrl, + profileLastUpdated: profileLastUpdateTimestamp ) if let profile: Profile = oldState[.profile(userSessionId.hexString)] as? Profile { @@ -70,10 +72,11 @@ internal extension LibSessionCacheType { return .currentUserUpdateTo( url: displayPictureUrl, key: displayPic.get(\.key), - filePath: filePath + filePath: filePath, + sessionProProof: getProProof() // TODO: double check if this is needed after Pro Proof is implemented ) }(), - sentTimestamp: TimeInterval(Double(serverTimestampMs) / 1000), + profileUpdateTimestamp: profileLastUpdateTimestamp, using: dependencies ) @@ -208,7 +211,8 @@ public extension LibSession.Cache { func updateProfile( displayName: String, displayPictureUrl: String?, - displayPictureEncryptionKey: Data? + displayPictureEncryptionKey: Data?, + isReuploadProfilePicture: Bool ) throws { guard let config: LibSession.Config = config(for: .userProfile, sessionId: userSessionId) else { throw LibSessionError.invalidConfigObject(wanted: .userProfile, got: nil) @@ -233,7 +237,12 @@ public extension LibSession.Cache { var profilePic: user_profile_pic = user_profile_pic() profilePic.set(\.url, to: displayPictureUrl) profilePic.set(\.key, to: displayPictureEncryptionKey) - user_profile_set_pic(conf, profilePic) + if isReuploadProfilePicture { + user_profile_set_reupload_pic(conf, profilePic) + } else { + user_profile_set_pic(conf, profilePic) + } + try LibSessionError.throwIfNeeded(conf) /// Add a pending observation to notify any observers of the change once it's committed diff --git a/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift b/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift index f90e4f37b6..93df1e6c7a 100644 --- a/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift +++ b/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift @@ -2,7 +2,7 @@ import Foundation import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUIKit import SessionUtil import SessionUtilitiesKit @@ -744,7 +744,7 @@ public extension LibSession { case .contacts(let conf): return try LibSession - .extractContacts(from: conf, serverTimestampMs: -1, using: dependencies) + .extractContacts(from: conf, using: dependencies) .reduce(into: [:]) { result, next in result[.contact(next.key)] = next.value.contact result[.profile(next.key)] = next.value.profile @@ -794,16 +794,14 @@ public extension LibSession { try handleUserProfileUpdate( db, in: config, - oldState: oldState, - serverTimestampMs: latestServerTimestampMs + oldState: oldState ) case .contacts: try handleContactsUpdate( db, in: config, - oldState: oldState, - serverTimestampMs: latestServerTimestampMs + oldState: oldState ) case .convoInfoVolatile: @@ -815,8 +813,7 @@ public extension LibSession { case .userGroups: try handleUserGroupsUpdate( db, - in: config, - serverTimestampMs: latestServerTimestampMs + in: config ) case .groupInfo: @@ -1041,10 +1038,12 @@ public protocol LibSessionCacheType: LibSessionImmutableCacheType, MutableCacheT func set(_ key: Setting.EnumKey, _ value: T?) var displayName: String? { get } + func updateProfile( displayName: String, displayPictureUrl: String?, - displayPictureEncryptionKey: Data? + displayPictureEncryptionKey: Data?, + isReuploadProfilePicture: Bool ) throws func canPerformChange( @@ -1184,7 +1183,12 @@ public extension LibSessionCacheType { } func updateProfile(displayName: String) throws { - try updateProfile(displayName: displayName, displayPictureUrl: nil, displayPictureEncryptionKey: nil) + try updateProfile( + displayName: displayName, + displayPictureUrl: nil, + displayPictureEncryptionKey: nil, + isReuploadProfilePicture: false + ) } var profile: Profile { @@ -1197,7 +1201,7 @@ public extension LibSessionCacheType { } } -private final class NoopLibSessionCache: LibSessionCacheType { +private final class NoopLibSessionCache: LibSessionCacheType, NoopDependency { let dependencies: Dependencies let userSessionId: SessionId = .invalid let isEmpty: Bool = true @@ -1319,7 +1323,8 @@ private final class NoopLibSessionCache: LibSessionCacheType { func updateProfile( displayName: String, displayPictureUrl: String?, - displayPictureEncryptionKey: Data? + displayPictureEncryptionKey: Data?, + isReuploadProfilePicture: Bool ) throws {} func canPerformChange( diff --git a/SessionMessagingKit/LibSession/Types/Config.swift b/SessionMessagingKit/LibSession/Types/Config.swift index 52c8057ef5..7eb71d798f 100644 --- a/SessionMessagingKit/LibSession/Types/Config.swift +++ b/SessionMessagingKit/LibSession/Types/Config.swift @@ -4,7 +4,7 @@ import Foundation import SessionUtil -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit public extension LibSession { @@ -324,7 +324,7 @@ public extension LibSession { .sorted() if successfulMergeTimestamps.count != messages.count { - Log.warn(.libSession, "Unable to merge \(SnodeAPI.Namespace.configGroupKeys) messages (\(successfulMergeTimestamps.count)/\(messages.count))") + Log.warn(.libSession, "Unable to merge \(Network.SnodeAPI.Namespace.configGroupKeys) messages (\(successfulMergeTimestamps.count)/\(messages.count))") } return successfulMergeTimestamps.last diff --git a/SessionMessagingKit/Messages/Message+Destination.swift b/SessionMessagingKit/Messages/Message+Destination.swift index b7d393a02c..a61a43ec3d 100644 --- a/SessionMessagingKit/Messages/Message+Destination.swift +++ b/SessionMessagingKit/Messages/Message+Destination.swift @@ -2,7 +2,7 @@ import Foundation import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit public extension Message { @@ -39,7 +39,7 @@ public extension Message { } } - public var defaultNamespace: SnodeAPI.Namespace? { + public var defaultNamespace: Network.SnodeAPI.Namespace? { switch self { case .contact, .syncMessage: return .`default` case .closedGroup(let groupId) where (try? SessionId.Prefix(from: groupId)) == .group: @@ -61,7 +61,7 @@ public extension Message { if prefix == .blinded15 || prefix == .blinded25 { guard let lookup: BlindedIdLookup = try? BlindedIdLookup.fetchOne(db, id: threadId) else { - throw OpenGroupAPIError.blindedLookupMissingCommunityInfo + throw SOGSError.blindedLookupMissingCommunityInfo } return .openGroupInbox( diff --git a/SessionMessagingKit/Messages/Message+DisappearingMessages.swift b/SessionMessagingKit/Messages/Message+DisappearingMessages.swift index d511a374fc..4212f74901 100644 --- a/SessionMessagingKit/Messages/Message+DisappearingMessages.swift +++ b/SessionMessagingKit/Messages/Message+DisappearingMessages.swift @@ -2,7 +2,7 @@ import Foundation import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUIKit import SessionUtilitiesKit diff --git a/SessionMessagingKit/Messages/Message+Origin.swift b/SessionMessagingKit/Messages/Message+Origin.swift index 4da490c326..1c0d5f330a 100644 --- a/SessionMessagingKit/Messages/Message+Origin.swift +++ b/SessionMessagingKit/Messages/Message+Origin.swift @@ -1,14 +1,14 @@ // Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. import Foundation -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit public extension Message { enum Origin: Codable, Hashable { case swarm( publicKey: String, - namespace: SnodeAPI.Namespace, + namespace: Network.SnodeAPI.Namespace, serverHash: String, serverTimestampMs: Int64, serverExpirationTimestamp: TimeInterval @@ -16,7 +16,7 @@ public extension Message { case community( openGroupId: String, sender: String, - timestamp: TimeInterval, + timestamp: TimeInterval?, messageServerId: Int64, whisper: Bool, whisperMods: Bool, diff --git a/SessionMessagingKit/Messages/Message.swift b/SessionMessagingKit/Messages/Message.swift index c640978d0d..2583d84d7c 100644 --- a/SessionMessagingKit/Messages/Message.swift +++ b/SessionMessagingKit/Messages/Message.swift @@ -2,7 +2,7 @@ import Foundation import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit /// Abstract base class for `VisibleMessage` and `ControlMessage`. @@ -83,7 +83,7 @@ public class Message: Codable { case (false, .some(let sigTimestampMs), .some): let delta: TimeInterval = (TimeInterval(max(sigTimestampMs, sentTimestampMs) - min(sigTimestampMs, sentTimestampMs)) / 1000) - return delta < OpenGroupAPI.validTimestampVarianceThreshold + return delta < Network.SOGS.validTimestampVarianceThreshold // FIXME: We want to remove support for this case in a future release case (_, .none, _): return true @@ -174,7 +174,7 @@ public enum ProcessedMessage { ) case config( publicKey: String, - namespace: SnodeAPI.Namespace, + namespace: Network.SnodeAPI.Namespace, serverHash: String, serverTimestampMs: Int64, data: Data, @@ -190,7 +190,7 @@ public enum ProcessedMessage { } } - var namespace: SnodeAPI.Namespace { + var namespace: Network.SnodeAPI.Namespace { switch self { case .standard(_, let threadVariant, _, _, _): switch threadVariant { @@ -432,12 +432,12 @@ public extension Message { static func processRawReceivedReactions( _ db: ObservingDatabase, openGroupId: String, - message: OpenGroupAPI.Message, - associatedPendingChanges: [OpenGroupAPI.PendingChange], + message: Network.SOGS.Message, + associatedPendingChanges: [OpenGroupManager.PendingChange], using dependencies: Dependencies ) -> [Reaction] { guard - let reactions: [String: OpenGroupAPI.Message.Reaction] = message.reactions, + let reactions: [String: Network.SOGS.Message.Reaction] = message.reactions, let openGroupCapabilityInfo: LibSession.OpenGroupCapabilityInfo = try? LibSession.OpenGroupCapabilityInfo .fetchOne(db, id: openGroupId) else { return [] } @@ -483,7 +483,7 @@ public extension Message { let pendingChangeSelfReaction: Bool? = { // Find the newest 'PendingChange' entry with a matching emoji, if one exists, and // set the "self reaction" value based on it's action - let maybePendingChange: OpenGroupAPI.PendingChange? = associatedPendingChanges + let maybePendingChange: OpenGroupManager.PendingChange? = associatedPendingChanges .sorted(by: { lhs, rhs -> Bool in (lhs.seqNo ?? Int64.max) >= (rhs.seqNo ?? Int64.max) }) .first { pendingChange in if case .reaction(_, let emoji, _) = pendingChange.metadata { @@ -495,7 +495,7 @@ public extension Message { // If there is no pending change for this reaction then return nil guard - let pendingChange: OpenGroupAPI.PendingChange = maybePendingChange, + let pendingChange: OpenGroupManager.PendingChange = maybePendingChange, case .reaction(_, _, let action) = pendingChange.metadata else { return nil } diff --git a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Profile.swift b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Profile.swift index 2f3867a06d..d3eccefb6f 100644 --- a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Profile.swift +++ b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Profile.swift @@ -10,7 +10,9 @@ public extension VisibleMessage { public let displayName: String? public let profileKey: Data? public let profilePictureUrl: String? + public let updateTimestampSeconds: TimeInterval? public let blocksCommunityMessageRequests: Bool? + public let sessionProProof: String? // MARK: - Initialization @@ -18,14 +20,18 @@ public extension VisibleMessage { displayName: String, profileKey: Data? = nil, profilePictureUrl: String? = nil, - blocksCommunityMessageRequests: Bool? = nil + updateTimestampSeconds: TimeInterval? = nil, + blocksCommunityMessageRequests: Bool? = nil, + sessionProProof: String? = nil ) { let hasUrlAndKey: Bool = (profileKey != nil && profilePictureUrl != nil) self.displayName = displayName self.profileKey = (hasUrlAndKey ? profileKey : nil) self.profilePictureUrl = (hasUrlAndKey ? profilePictureUrl : nil) + self.updateTimestampSeconds = updateTimestampSeconds self.blocksCommunityMessageRequests = blocksCommunityMessageRequests + self.sessionProProof = sessionProProof } // MARK: - Proto Conversion @@ -40,7 +46,9 @@ public extension VisibleMessage { displayName: displayName, profileKey: proto.profileKey, profilePictureUrl: profileProto.profilePicture, - blocksCommunityMessageRequests: (proto.hasBlocksCommunityMessageRequests ? proto.blocksCommunityMessageRequests : nil) + updateTimestampSeconds: TimeInterval(profileProto.lastUpdateSeconds), + blocksCommunityMessageRequests: (proto.hasBlocksCommunityMessageRequests ? proto.blocksCommunityMessageRequests : nil), + sessionProProof: nil // TODO: Add Session Pro Proof to profile proto ) } @@ -60,6 +68,10 @@ public extension VisibleMessage { profileProto.setProfilePicture(profilePictureUrl) } + if let updateTimestampSeconds: TimeInterval = updateTimestampSeconds { + profileProto.setLastUpdateSeconds(UInt64(updateTimestampSeconds)) + } + dataMessageProto.setProfile(try profileProto.build()) return dataMessageProto } @@ -87,7 +99,9 @@ public extension VisibleMessage { return VMProfile( displayName: displayName, profileKey: proto.profileKey, - profilePictureUrl: profileProto.profilePicture + profilePictureUrl: profileProto.profilePicture, + updateTimestampSeconds: TimeInterval(profileProto.lastUpdateSeconds), + sessionProProof: nil // TODO: Add Session Pro Proof to profile proto ) } @@ -106,6 +120,9 @@ public extension VisibleMessage { messageRequestResponseProto.setProfileKey(profileKey) profileProto.setProfilePicture(profilePictureUrl) } + if let updateTimestampSeconds: TimeInterval = updateTimestampSeconds { + profileProto.setLastUpdateSeconds(UInt64(updateTimestampSeconds)) + } do { messageRequestResponseProto.setProfile(try profileProto.build()) return try messageRequestResponseProto.build() @@ -122,7 +139,8 @@ public extension VisibleMessage { Profile( displayName: \(displayName ?? "null"), profileKey: \(profileKey?.description ?? "null"), - profilePictureUrl: \(profilePictureUrl ?? "null") + profilePictureUrl: \(profilePictureUrl ?? "null"), + UpdateTimestampSeconds: \(updateTimestampSeconds ?? 0) ) """ } diff --git a/SessionMessagingKit/Open Groups/Crypto/Crypto+OpenGroup.swift b/SessionMessagingKit/Open Groups/Crypto/Crypto+OpenGroup.swift new file mode 100644 index 0000000000..2ff8f76fc6 --- /dev/null +++ b/SessionMessagingKit/Open Groups/Crypto/Crypto+OpenGroup.swift @@ -0,0 +1,94 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +import Foundation +import SessionUtil +import SessionUtilitiesKit + +// MARK: - Messages + +public extension Crypto.Generator { + static func ciphertextWithSessionBlindingProtocol( + plaintext: Data, + recipientBlindedId: String, + serverPublicKey: String + ) -> Crypto.Generator { + return Crypto.Generator( + id: "ciphertextWithSessionBlindingProtocol", + args: [plaintext, serverPublicKey] + ) { dependencies in + var cPlaintext: [UInt8] = Array(plaintext) + var cEd25519SecretKey: [UInt8] = dependencies[cache: .general].ed25519SecretKey + var cRecipientBlindedId: [UInt8] = Array(Data(hex: recipientBlindedId)) + var cServerPublicKey: [UInt8] = Array(Data(hex: serverPublicKey)) + var maybeCiphertext: UnsafeMutablePointer? = nil + var ciphertextLen: Int = 0 + + guard !cEd25519SecretKey.isEmpty else { throw MessageSenderError.noUserED25519KeyPair } + guard + cEd25519SecretKey.count == 64, + cServerPublicKey.count == 32, + session_encrypt_for_blinded_recipient( + &cPlaintext, + cPlaintext.count, + &cEd25519SecretKey, + &cServerPublicKey, + &cRecipientBlindedId, + &maybeCiphertext, + &ciphertextLen + ), + ciphertextLen > 0, + let ciphertext: Data = maybeCiphertext.map({ Data(bytes: $0, count: ciphertextLen) }) + else { throw MessageSenderError.encryptionFailed } + + free(UnsafeMutableRawPointer(mutating: maybeCiphertext)) + + return ciphertext + } + } + + static func plaintextWithSessionBlindingProtocol( + ciphertext: Data, + senderId: String, + recipientId: String, + serverPublicKey: String + ) -> Crypto.Generator<(plaintext: Data, senderSessionIdHex: String)> { + return Crypto.Generator( + id: "plaintextWithSessionBlindingProtocol", + args: [ciphertext, senderId, recipientId] + ) { dependencies in + var cCiphertext: [UInt8] = Array(ciphertext) + var cEd25519SecretKey: [UInt8] = dependencies[cache: .general].ed25519SecretKey + var cSenderId: [UInt8] = Array(Data(hex: senderId)) + var cRecipientId: [UInt8] = Array(Data(hex: recipientId)) + var cServerPublicKey: [UInt8] = Array(Data(hex: serverPublicKey)) + var cSenderSessionId: [CChar] = [CChar](repeating: 0, count: 67) + var maybePlaintext: UnsafeMutablePointer? = nil + var plaintextLen: Int = 0 + + guard !cEd25519SecretKey.isEmpty else { throw MessageSenderError.noUserED25519KeyPair } + guard + cEd25519SecretKey.count == 64, + cServerPublicKey.count == 32, + session_decrypt_for_blinded_recipient( + &cCiphertext, + cCiphertext.count, + &cEd25519SecretKey, + &cServerPublicKey, + &cSenderId, + &cRecipientId, + &cSenderSessionId, + &maybePlaintext, + &plaintextLen + ), + plaintextLen > 0, + let plaintext: Data = maybePlaintext.map({ Data(bytes: $0, count: plaintextLen) }) + else { throw MessageReceiverError.decryptionFailed } + + free(UnsafeMutableRawPointer(mutating: maybePlaintext)) + + return (plaintext, String(cString: cSenderSessionId)) + } + } +} diff --git a/SessionMessagingKit/Open Groups/Models/Capabilities.swift b/SessionMessagingKit/Open Groups/Models/Capabilities.swift deleted file mode 100644 index 2947214b80..0000000000 --- a/SessionMessagingKit/Open Groups/Models/Capabilities.swift +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import Foundation - -extension OpenGroupAPI { - public struct Capabilities: Codable, Equatable { - public let capabilities: [Capability.Variant] - public let missing: [Capability.Variant]? - - // MARK: - Initialization - - public init(capabilities: [Capability.Variant], missing: [Capability.Variant]? = nil) { - self.capabilities = capabilities - self.missing = missing - } - } -} diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index 4c6d534abe..e0b7269ffd 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -4,7 +4,7 @@ import Foundation import Combine import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit // MARK: - Singleton @@ -35,7 +35,7 @@ public extension Log.Category { // MARK: - OpenGroupManager public final class OpenGroupManager { - public typealias DefaultRoomInfo = (room: OpenGroupAPI.Room, openGroup: OpenGroup) + public typealias DefaultRoomInfo = (room: Network.SOGS.Room, openGroup: OpenGroup) private let dependencies: Dependencies @@ -79,8 +79,8 @@ public final class OpenGroupManager { .replacingOccurrences(of: serverPort, with: "") ) let options: Set = Set([ - OpenGroupAPI.legacyDefaultServerIP, - OpenGroupAPI.defaultServer + Network.SOGS.legacyDefaultServerIP, + Network.SOGS.defaultServer .replacingOccurrences(of: "http://", with: "") .replacingOccurrences(of: "https://", with: "") ]) @@ -103,7 +103,7 @@ public final class OpenGroupManager { .lowercased() .replacingOccurrences(of: serverPort, with: "") ) - let defaultServerHost: String = OpenGroupAPI.defaultServer + let defaultServerHost: String = Network.SOGS.defaultServer .replacingOccurrences(of: "http://", with: "") .replacingOccurrences(of: "https://", with: "") var serverOptions: Set = Set([ @@ -119,9 +119,9 @@ public final class OpenGroupManager { serverOptions.insert(defaultServerHost) serverOptions.insert("http://\(defaultServerHost)") serverOptions.insert("https://\(defaultServerHost)") - serverOptions.insert(OpenGroupAPI.legacyDefaultServerIP) - serverOptions.insert("http://\(OpenGroupAPI.legacyDefaultServerIP)") - serverOptions.insert("https://\(OpenGroupAPI.legacyDefaultServerIP)") + serverOptions.insert(Network.SOGS.legacyDefaultServerIP) + serverOptions.insert("http://\(Network.SOGS.legacyDefaultServerIP)") + serverOptions.insert("https://\(Network.SOGS.legacyDefaultServerIP)") } // First check if there is no poller for the specified server @@ -161,7 +161,7 @@ public final class OpenGroupManager { return server.lowercased() } - return OpenGroupAPI.defaultServer + return Network.SOGS.defaultServer }() let threadId: String = OpenGroup.idFor(roomToken: roomToken, server: targetServer) @@ -223,11 +223,11 @@ public final class OpenGroupManager { return server.lowercased() } - return OpenGroupAPI.defaultServer + return Network.SOGS.defaultServer }() return Result { - try OpenGroupAPI + try Network.SOGS .preparedCapabilitiesAndRoom( roomToken: roomToken, authMethod: Authentication.community( @@ -243,7 +243,7 @@ public final class OpenGroupManager { } .publisher .flatMap { [dependencies] in $0.send(using: dependencies) } - .flatMapStorageWritePublisher(using: dependencies) { [dependencies] (db: ObservingDatabase, response: (info: ResponseInfoType, value: OpenGroupAPI.CapabilitiesAndRoomResponse)) -> Void in + .flatMapStorageWritePublisher(using: dependencies) { [dependencies] (db: ObservingDatabase, response: (info: ResponseInfoType, value: Network.SOGS.CapabilitiesAndRoomResponse)) -> Void in // Add the new open group to libSession try LibSession.add( db, @@ -263,7 +263,7 @@ public final class OpenGroupManager { // Then the room try OpenGroupManager.handlePollInfo( db, - pollInfo: OpenGroupAPI.RoomPollInfo(room: response.value.room.data), + pollInfo: Network.SOGS.RoomPollInfo(room: response.value.room.data), publicKey: publicKey, for: roomToken, on: targetServer, @@ -323,6 +323,7 @@ public final class OpenGroupManager { } // Remove all the data (everything should cascade delete) + _ = try? Interaction.deleteWhere(db, .filter(Interaction.Columns.threadId == openGroupId)) _ = try? SessionThread .filter(id: openGroupId) .deleteAll(db) @@ -333,7 +334,7 @@ public final class OpenGroupManager { try MessageDeduplication.deleteIfNeeded(db, threadIds: [openGroupId], using: dependencies) // Remove the open group (no foreign key to the thread so it won't auto-delete) - if server?.lowercased() != OpenGroupAPI.defaultServer.lowercased() { + if server?.lowercased() != Network.SOGS.defaultServer.lowercased() { _ = try? OpenGroup .filter(id: openGroupId) .deleteAll(db) @@ -358,7 +359,7 @@ public final class OpenGroupManager { internal static func handleCapabilities( _ db: ObservingDatabase, - capabilities: OpenGroupAPI.Capabilities, + capabilities: Network.SOGS.CapabilitiesResponse, on server: String ) { // Remove old capabilities first @@ -370,7 +371,7 @@ public final class OpenGroupManager { capabilities.capabilities.forEach { capability in try? Capability( openGroupServer: server.lowercased(), - variant: capability, + variant: Capability.Variant(from: capability), isMissing: false ) .upsert(db) @@ -378,7 +379,7 @@ public final class OpenGroupManager { capabilities.missing?.forEach { capability in try? Capability( openGroupServer: server.lowercased(), - variant: capability, + variant: Capability.Variant(from: capability), isMissing: true ) .upsert(db) @@ -387,7 +388,7 @@ public final class OpenGroupManager { internal static func handlePollInfo( _ db: ObservingDatabase, - pollInfo: OpenGroupAPI.RoomPollInfo, + pollInfo: Network.SOGS.RoomPollInfo, publicKey maybePublicKey: String?, for roomToken: String, on server: String, @@ -430,7 +431,7 @@ public final class OpenGroupManager { .updateAllAndConfig(db, changes, using: dependencies) // Update the admin/moderator group members - if let roomDetails: OpenGroupAPI.Room = pollInfo.details { + if let roomDetails: Network.SOGS.Room = pollInfo.details { _ = try? GroupMember .filter(GroupMember.Columns.groupId == threadId) .deleteAll(db) @@ -530,7 +531,7 @@ public final class OpenGroupManager { internal static func handleMessages( _ db: ObservingDatabase, - messages: [OpenGroupAPI.Message], + messages: [Network.SOGS.Message], for roomToken: String, on server: String, using dependencies: Dependencies @@ -542,7 +543,7 @@ public final class OpenGroupManager { // Sorting the messages by server ID before importing them fixes an issue where messages // that quote older messages can't find those older messages - let sortedMessages: [OpenGroupAPI.Message] = messages + let sortedMessages: [Network.SOGS.Message] = messages .filter { $0.deleted != true } .sorted { lhs, rhs in lhs.id < rhs.id } var messageServerInfoToRemove: [(id: Int64, seqNo: Int64)] = messages @@ -656,10 +657,11 @@ public final class OpenGroupManager { // Handle any deletions that are needed if !messageServerInfoToRemove.isEmpty { let messageServerIdsToRemove: [Int64] = messageServerInfoToRemove.map { $0.id } - _ = try? Interaction - .filter(Interaction.Columns.threadId == openGroup.threadId) + _ = try? Interaction.deleteWhere( + db, + .filter(Interaction.Columns.threadId == openGroup.threadId), .filter(messageServerIdsToRemove.contains(Interaction.Columns.openGroupServerMessageId)) - .deleteAll(db) + ) // Update the seqNo for deletions largestValidSeqNo = max(largestValidSeqNo, (messageServerInfoToRemove.map({ $0.seqNo }).max() ?? 0)) @@ -682,7 +684,7 @@ public final class OpenGroupManager { internal static func handleDirectMessages( _ db: ObservingDatabase, - messages: [OpenGroupAPI.DirectMessage], + messages: [Network.SOGS.DirectMessage], fromOutbox: Bool, on server: String, using dependencies: Dependencies @@ -696,7 +698,7 @@ public final class OpenGroupManager { // Sorting the messages by server ID before importing them fixes an issue where messages // that quote older messages can't find those older messages - let sortedMessages: [OpenGroupAPI.DirectMessage] = messages + let sortedMessages: [Network.SOGS.DirectMessage] = messages .sorted { lhs, rhs in lhs.id < rhs.id } let latestMessageId: Int64 = sortedMessages[sortedMessages.count - 1].id var lookupCache: [String: BlindedIdLookup] = [:] // Only want this cache to exist for the current loop @@ -827,9 +829,9 @@ public final class OpenGroupManager { id: Int64, in roomToken: String, on server: String, - type: OpenGroupAPI.PendingChange.ReactAction - ) -> OpenGroupAPI.PendingChange { - let pendingChange = OpenGroupAPI.PendingChange( + type: OpenGroupManager.PendingChange.ReactAction + ) -> OpenGroupManager.PendingChange { + let pendingChange = OpenGroupManager.PendingChange( server: server, room: roomToken, changeType: .reaction, @@ -847,7 +849,7 @@ public final class OpenGroupManager { return pendingChange } - public func updatePendingChange(_ pendingChange: OpenGroupAPI.PendingChange, seqNo: Int64?) { + public func updatePendingChange(_ pendingChange: OpenGroupManager.PendingChange, seqNo: Int64?) { dependencies.mutate(cache: .openGroupManager) { if let index = $0.pendingChanges.firstIndex(of: pendingChange) { $0.pendingChanges[index].seqNo = seqNo @@ -855,7 +857,7 @@ public final class OpenGroupManager { } } - public func removePendingChange(_ pendingChange: OpenGroupAPI.PendingChange) { + public func removePendingChange(_ pendingChange: OpenGroupManager.PendingChange) { dependencies.mutate(cache: .openGroupManager) { if let index = $0.pendingChanges.firstIndex(of: pendingChange) { $0.pendingChanges.remove(at: index) @@ -987,7 +989,7 @@ public extension OpenGroupManager { private let dependencies: Dependencies private let defaultRoomsSubject: CurrentValueSubject<[DefaultRoomInfo], Error> = CurrentValueSubject([]) private var _lastSuccessfulCommunityPollTimestamp: TimeInterval? - public var pendingChanges: [OpenGroupAPI.PendingChange] = [] + public var pendingChanges: [OpenGroupManager.PendingChange] = [] public var defaultRoomsPublisher: AnyPublisher<[DefaultRoomInfo], Error> { defaultRoomsSubject @@ -1042,13 +1044,13 @@ public extension OpenGroupManager { public protocol OGMImmutableCacheType: ImmutableCacheType { var defaultRoomsPublisher: AnyPublisher<[OpenGroupManager.DefaultRoomInfo], Error> { get } - var pendingChanges: [OpenGroupAPI.PendingChange] { get } + var pendingChanges: [OpenGroupManager.PendingChange] { get } } public protocol OGMCacheType: OGMImmutableCacheType, MutableCacheType { var defaultRoomsPublisher: AnyPublisher<[OpenGroupManager.DefaultRoomInfo], Error> { get } - var pendingChanges: [OpenGroupAPI.PendingChange] { get set } + var pendingChanges: [OpenGroupManager.PendingChange] { get set } func getLastSuccessfulCommunityPollTimestamp() -> TimeInterval func setLastSuccessfulCommunityPollTimestamp(_ timestamp: TimeInterval) diff --git a/SessionMessagingKit/Open Groups/Models/PendingChange.swift b/SessionMessagingKit/Open Groups/Types/PendingChange.swift similarity index 67% rename from SessionMessagingKit/Open Groups/Models/PendingChange.swift rename to SessionMessagingKit/Open Groups/Types/PendingChange.swift index dd5af98b5f..6ab6cd2145 100644 --- a/SessionMessagingKit/Open Groups/Models/PendingChange.swift +++ b/SessionMessagingKit/Open Groups/Types/PendingChange.swift @@ -2,7 +2,7 @@ import Foundation -extension OpenGroupAPI { +extension OpenGroupManager { public struct PendingChange: Equatable { public enum ChangeType { case reaction @@ -24,23 +24,23 @@ extension OpenGroupAPI { var seqNo: Int64? let metadata: Metadata - public static func == (lhs: OpenGroupAPI.PendingChange, rhs: OpenGroupAPI.PendingChange) -> Bool { - guard lhs.server == rhs.server && - lhs.room == rhs.room && - lhs.changeType == rhs.changeType && - lhs.seqNo == rhs.seqNo - else { - return false - } + public static func == (lhs: OpenGroupManager.PendingChange, rhs: OpenGroupManager.PendingChange) -> Bool { + guard + lhs.server == rhs.server && + lhs.room == rhs.room && + lhs.changeType == rhs.changeType && + lhs.seqNo == rhs.seqNo + else { return false } switch lhs.changeType { case .reaction: if case .reaction(let lhsMessageId, let lhsEmoji, let lhsAction) = lhs.metadata, - case .reaction(let rhsMessageId, let rhsEmoji, let rhsAction) = rhs.metadata { + case .reaction(let rhsMessageId, let rhsEmoji, let rhsAction) = rhs.metadata + { return lhsMessageId == rhsMessageId && lhsEmoji == rhsEmoji && lhsAction == rhsAction - } else { - return false } + + return false } } } diff --git a/SessionMessagingKit/Protos/Generated/SNProto.swift b/SessionMessagingKit/Protos/Generated/SNProto.swift index 14aaa60e99..633e60c3a7 100644 --- a/SessionMessagingKit/Protos/Generated/SNProto.swift +++ b/SessionMessagingKit/Protos/Generated/SNProto.swift @@ -1296,6 +1296,9 @@ extension SNProtoDataExtractionNotification.SNProtoDataExtractionNotificationBui if let _value = profilePicture { builder.setProfilePicture(_value) } + if hasLastUpdateSeconds { + builder.setLastUpdateSeconds(lastUpdateSeconds) + } return builder } @@ -1313,6 +1316,10 @@ extension SNProtoDataExtractionNotification.SNProtoDataExtractionNotificationBui proto.profilePicture = valueParam } + @objc public func setLastUpdateSeconds(_ valueParam: UInt64) { + proto.lastUpdateSeconds = valueParam + } + @objc public func build() throws -> SNProtoLokiProfile { return try SNProtoLokiProfile.parseProto(proto) } @@ -1344,6 +1351,13 @@ extension SNProtoDataExtractionNotification.SNProtoDataExtractionNotificationBui return proto.hasProfilePicture } + @objc public var lastUpdateSeconds: UInt64 { + return proto.lastUpdateSeconds + } + @objc public var hasLastUpdateSeconds: Bool { + return proto.hasLastUpdateSeconds + } + private init(proto: SessionProtos_LokiProfile) { self.proto = proto } diff --git a/SessionMessagingKit/Protos/Generated/SessionProtos.pb.swift b/SessionMessagingKit/Protos/Generated/SessionProtos.pb.swift index b6fc8ff3bd..40f49dead2 100644 --- a/SessionMessagingKit/Protos/Generated/SessionProtos.pb.swift +++ b/SessionMessagingKit/Protos/Generated/SessionProtos.pb.swift @@ -619,12 +619,23 @@ struct SessionProtos_LokiProfile { /// Clears the value of `profilePicture`. Subsequent reads from it will return its default value. mutating func clearProfilePicture() {self._profilePicture = nil} + /// Timestamp of the last profile update + var lastUpdateSeconds: UInt64 { + get {return _lastUpdateSeconds ?? 0} + set {_lastUpdateSeconds = newValue} + } + /// Returns true if `lastUpdateSeconds` has been explicitly set. + var hasLastUpdateSeconds: Bool {return self._lastUpdateSeconds != nil} + /// Clears the value of `lastUpdateSeconds`. Subsequent reads from it will return its default value. + mutating func clearLastUpdateSeconds() {self._lastUpdateSeconds = nil} + var unknownFields = SwiftProtobuf.UnknownStorage() init() {} fileprivate var _displayName: String? = nil fileprivate var _profilePicture: String? = nil + fileprivate var _lastUpdateSeconds: UInt64? = nil } struct SessionProtos_DataMessage { @@ -2321,6 +2332,7 @@ extension SessionProtos_LokiProfile: SwiftProtobuf.Message, SwiftProtobuf._Messa static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .same(proto: "displayName"), 2: .same(proto: "profilePicture"), + 3: .same(proto: "lastUpdateSeconds"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -2331,6 +2343,7 @@ extension SessionProtos_LokiProfile: SwiftProtobuf.Message, SwiftProtobuf._Messa switch fieldNumber { case 1: try { try decoder.decodeSingularStringField(value: &self._displayName) }() case 2: try { try decoder.decodeSingularStringField(value: &self._profilePicture) }() + case 3: try { try decoder.decodeSingularUInt64Field(value: &self._lastUpdateSeconds) }() default: break } } @@ -2347,12 +2360,16 @@ extension SessionProtos_LokiProfile: SwiftProtobuf.Message, SwiftProtobuf._Messa try { if let v = self._profilePicture { try visitor.visitSingularStringField(value: v, fieldNumber: 2) } }() + try { if let v = self._lastUpdateSeconds { + try visitor.visitSingularUInt64Field(value: v, fieldNumber: 3) + } }() try unknownFields.traverse(visitor: &visitor) } static func ==(lhs: SessionProtos_LokiProfile, rhs: SessionProtos_LokiProfile) -> Bool { if lhs._displayName != rhs._displayName {return false} if lhs._profilePicture != rhs._profilePicture {return false} + if lhs._lastUpdateSeconds != rhs._lastUpdateSeconds {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/SessionMessagingKit/Protos/SessionProtos.proto b/SessionMessagingKit/Protos/SessionProtos.proto index 13ca774aec..a25e477759 100644 --- a/SessionMessagingKit/Protos/SessionProtos.proto +++ b/SessionMessagingKit/Protos/SessionProtos.proto @@ -115,6 +115,8 @@ message DataExtractionNotification { message LokiProfile { optional string displayName = 1; optional string profilePicture = 2; + + optional uint64 lastUpdateSeconds = 3; // Timestamp of the last profile update } message DataMessage { diff --git a/SessionMessagingKit/Sending & Receiving/AttachmentUploader.swift b/SessionMessagingKit/Sending & Receiving/AttachmentUploader.swift index 532c5fe5ec..8c85b12bd5 100644 --- a/SessionMessagingKit/Sending & Receiving/AttachmentUploader.swift +++ b/SessionMessagingKit/Sending & Receiving/AttachmentUploader.swift @@ -3,7 +3,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - AttachmentUploader @@ -11,7 +11,7 @@ import SessionUtilitiesKit public final class AttachmentUploader { private enum Destination { case fileServer - case community(LibSession.OpenGroupCapabilityInfo) + case community(roomToken: String, server: String) var shouldEncrypt: Bool { switch self { @@ -78,7 +78,9 @@ public final class AttachmentUploader { // Generate the correct upload info based on the state of the attachment let destination: AttachmentUploader.Destination = { switch authMethod { - case let auth as Authentication.community: return .community(auth.openGroupCapabilityInfo) + case let auth as Authentication.community: + return .community(roomToken: auth.roomToken, server: auth.server) + default: return .fileServer } }() @@ -86,18 +88,18 @@ public final class AttachmentUploader { let endpoint: (any EndpointType) = { switch destination { case .fileServer: return Network.FileServer.Endpoint.file - case .community(let info): return OpenGroupAPI.Endpoint.roomFile(info.roomToken) + case .community(let roomToken, _): return Network.SOGS.Endpoint.roomFile(roomToken) } }() // This can occur if an AttachmentUploadJob was explicitly created for a message // dependant on the attachment being uploaded (in this case the attachment has // already been uploaded so just succeed) - if attachment.state == .uploaded, let fileId: String = Attachment.fileId(for: attachment.downloadUrl) { + if attachment.state == .uploaded, let fileId: String = Network.FileServer.fileId(for: attachment.downloadUrl) { return ( attachment, try Network.PreparedRequest.cached( - FileUploadResponse(id: fileId), + FileUploadResponse(id: fileId, expires: nil), endpoint: endpoint, using: dependencies ), @@ -114,7 +116,7 @@ public final class AttachmentUploader { // Note: The most common cases for this will be for LinkPreviews or Quotes if attachment.state == .downloaded, - let fileId: String = Attachment.fileId(for: attachment.downloadUrl), + let fileId: String = Network.FileServer.fileId(for: attachment.downloadUrl), ( !destination.shouldEncrypt || ( attachment.encryptionKey != nil && @@ -125,7 +127,7 @@ public final class AttachmentUploader { return ( attachment, try Network.PreparedRequest.cached( - FileUploadResponse(id: fileId), + FileUploadResponse(id: fileId, expires: nil), endpoint: endpoint, using: dependencies ), @@ -174,13 +176,13 @@ public final class AttachmentUploader { digest ) - case .community(let info): + case .community(let roomToken, _): return ( attachment, - try OpenGroupAPI.preparedUpload( + try Network.SOGS.preparedUpload( data: finalData, - roomToken: info.roomToken, - authMethod: Authentication.community(info: info), + roomToken: roomToken, + authMethod: authMethod, using: dependencies ), encryptionKey, @@ -211,11 +213,11 @@ public final class AttachmentUploader { case (_, _, .fileServer): return Network.FileServer.downloadUrlString(for: response.id) - case (_, _, .community(let info)): - return OpenGroupAPI.downloadUrlString( + case (_, _, .community(let roomToken, let server)): + return Network.SOGS.downloadUrlString( for: response.id, - server: info.server, - roomToken: info.roomToken + server: server, + roomToken: roomToken ) } }(), diff --git a/SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift b/SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift index 152819c82f..adbc379e5c 100644 --- a/SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift +++ b/SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift @@ -111,6 +111,7 @@ public class SignalAttachment: Equatable { public var sourceFilename: String? { return dataSource.sourceFilename?.filteredFilename } public var isValidImage: Bool { return dataSource.isValidImage } public var isValidVideo: Bool { return dataSource.isValidVideo } + public var imageSize: CGSize? { return dataSource.imageSize } // This flag should be set for text attachments that can be sent as text messages. public var isConvertibleToTextMessage = false @@ -180,71 +181,6 @@ public class SignalAttachment: Equatable { return errorDescription } - - public func staticThumbnail(using dependencies: Dependencies) -> UIImage? { - if isAnimatedImage { - return image() - } - else if isImage { - return image() - } - else if isVideo { - return videoPreview(using: dependencies) - } - else if isAudio { - return nil - } - - return nil - } - - public func image() -> UIImage? { - if let cachedImage = cachedImage { - return cachedImage - } - guard let image = UIImage(data: dataSource.data) else { - return nil - } - - cachedImage = image - return image - } - - public func videoPreview(using dependencies: Dependencies) -> UIImage? { - if let cachedVideoPreview = cachedVideoPreview { - return cachedVideoPreview - } - - guard let mediaUrl = dataUrl else { - return nil - } - - do { - let filePath = mediaUrl.path - guard - dependencies[singleton: .fileManager].fileExists(atPath: filePath), - let mimeType: String = dataType.sessionMimeType, - let assetInfo: (asset: AVURLAsset, cleanup: () -> Void) = AVURLAsset.asset( - for: filePath, - mimeType: mimeType, - sourceFilename: sourceFilename, - using: dependencies - ) - else { return nil } - - let generator = AVAssetImageGenerator(asset: assetInfo.asset) - generator.appliesPreferredTrackTransform = true - let cgImage = try generator.copyCGImage(at: CMTimeMake(value: 0, timescale: 1), actualTime: nil) - let image = UIImage(cgImage: cgImage) - assetInfo.cleanup() - - cachedVideoPreview = image - return image - - } catch { - return nil - } - } public func text() -> String? { guard let text = String(data: dataSource.data, encoding: .utf8) else { @@ -462,7 +398,7 @@ public class SignalAttachment: Equatable { } if UTType.supportedAnimatedImageTypes.contains(type) { - guard dataSource.dataLength <= MediaUtils.maxFileSizeAnimatedImage else { + guard dataSource.dataLength <= SNUtilitiesKit.maxFileSize else { attachment.error = .fileSizeTooLarge return attachment } @@ -515,7 +451,7 @@ public class SignalAttachment: Equatable { return ( doesImageHaveAcceptableFileSize(dataSource: dataSource, imageQuality: imageQuality) && - dataSource.dataLength <= MediaUtils.maxFileSizeImage + dataSource.dataLength <= SNUtilitiesKit.maxFileSize ) } @@ -545,7 +481,7 @@ public class SignalAttachment: Equatable { assert(attachment.error == nil) if imageQuality == .original && - attachment.dataLength < MediaUtils.maxFileSizeGeneric && + attachment.dataLength < SNUtilitiesKit.maxFileSize && UTType.supportedOutputImageTypes.contains(attachment.dataType) { // We should avoid resizing images attached "as documents" if possible. return attachment @@ -575,7 +511,7 @@ public class SignalAttachment: Equatable { dataSource.sourceFilename = jpgFilename if doesImageHaveAcceptableFileSize(dataSource: dataSource, imageQuality: imageQuality) && - dataSource.dataLength <= MediaUtils.maxFileSizeImage { + dataSource.dataLength <= SNUtilitiesKit.maxFileSize { let recompressedAttachment = SignalAttachment(dataSource: dataSource, dataType: .jpeg) recompressedAttachment.cachedImage = dstImage return recompressedAttachment @@ -759,7 +695,7 @@ public class SignalAttachment: Equatable { dataSource: dataSource, type: type, validTypes: UTType.supportedVideoTypes, - maxFileSize: MediaUtils.maxFileSizeVideo, + maxFileSize: SNUtilitiesKit.maxFileSize, using: dependencies ) } @@ -878,7 +814,7 @@ public class SignalAttachment: Equatable { guard let dataSource = dataSource, UTType.supportedOutputVideoTypes.contains(type), - dataSource.dataLength <= MediaUtils.maxFileSizeVideo + dataSource.dataLength <= SNUtilitiesKit.maxFileSize else { return false } return false @@ -895,7 +831,7 @@ public class SignalAttachment: Equatable { dataSource: dataSource, type: type, validTypes: UTType.supportedAudioTypes, - maxFileSize: MediaUtils.maxFileSizeAudio, + maxFileSize: SNUtilitiesKit.maxFileSize, using: dependencies ) } @@ -911,7 +847,7 @@ public class SignalAttachment: Equatable { dataSource: dataSource, type: type, validTypes: nil, - maxFileSize: MediaUtils.maxFileSizeGeneric, + maxFileSize: SNUtilitiesKit.maxFileSize, using: dependencies ) } diff --git a/SessionMessagingKit/Sending & Receiving/Errors/MessageReceiverError.swift b/SessionMessagingKit/Sending & Receiving/Errors/MessageReceiverError.swift index af8255fbce..15d5488927 100644 --- a/SessionMessagingKit/Sending & Receiving/Errors/MessageReceiverError.swift +++ b/SessionMessagingKit/Sending & Receiving/Errors/MessageReceiverError.swift @@ -26,13 +26,14 @@ public enum MessageReceiverError: Error, CustomStringConvertible { case duplicatedCall case missingRequiredAdminPrivileges case deprecatedMessage + case failedToProcess public var isRetryable: Bool { switch self { case .duplicateMessage, .invalidMessage, .unknownMessage, .unknownEnvelopeType, .invalidSignature, .noData, .senderBlocked, .noThread, .selfSend, .decryptionFailed, .invalidConfigMessageHandling, .outdatedMessage, .ignorableMessage, .ignorableMessageRequestMessage, - .missingRequiredAdminPrivileges: + .missingRequiredAdminPrivileges, .failedToProcess: return false default: return true @@ -112,6 +113,7 @@ public enum MessageReceiverError: Error, CustomStringConvertible { case .duplicatedCall: return "Duplicate call." case .missingRequiredAdminPrivileges: return "Handling this message requires admin privileges which the current user does not have." case .deprecatedMessage: return "This message type has been deprecated." + case .failedToProcess: return "Failed to process." } } } diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift index 9670210fdc..cdd2921bcf 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift @@ -5,7 +5,7 @@ import AVFAudio import GRDB import WebRTC import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit // MARK: - Log.Category diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+DataExtractionNotification.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+DataExtractionNotification.swift index 4186c65e0b..4c5f597204 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+DataExtractionNotification.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+DataExtractionNotification.swift @@ -2,7 +2,7 @@ import Foundation import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit extension MessageReceiver { diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift index 95a548578b..4de5e3bcb9 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift @@ -3,7 +3,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit extension MessageReceiver { @@ -146,7 +146,7 @@ extension MessageReceiver { displayNameUpdate: .contactUpdate(profile.displayName), displayPictureUpdate: .from(profile, fallback: .contactRemove, using: dependencies), blocksCommunityMessageRequests: profile.blocksCommunityMessageRequests, - sentTimestamp: TimeInterval(Double(sentTimestampMs) / 1000), + profileUpdateTimestamp: profile.updateTimestampSeconds, using: dependencies ) } @@ -250,7 +250,7 @@ extension MessageReceiver { displayNameUpdate: .contactUpdate(profile.displayName), displayPictureUpdate: .from(profile, fallback: .contactRemove, using: dependencies), blocksCommunityMessageRequests: profile.blocksCommunityMessageRequests, - sentTimestamp: TimeInterval(Double(sentTimestampMs) / 1000), + profileUpdateTimestamp: profile.updateTimestampSeconds, using: dependencies ) } @@ -307,7 +307,7 @@ extension MessageReceiver { // devices that had the group before they were promoted try SnodeReceivedMessageInfo .filter(SnodeReceivedMessageInfo.Columns.swarmPublicKey == groupSessionId.hexString) - .filter(SnodeReceivedMessageInfo.Columns.namespace == SnodeAPI.Namespace.groupMessages.rawValue) + .filter(SnodeReceivedMessageInfo.Columns.namespace == Network.SnodeAPI.Namespace.groupMessages.rawValue) .updateAllAndConfig( db, SnodeReceivedMessageInfo.Columns.wasDeletedOrInvalid.set(to: true), @@ -472,10 +472,11 @@ extension MessageReceiver { /// If the message is about adding the current user then we should remove any existing `infoGroupInfoInvited` interactions /// from the group (don't want to have two different messages indicating the current user was added to the group) if messageContainsCurrentUser && message.changeType == .added { - _ = try Interaction - .filter(Interaction.Columns.threadId == groupSessionId.hexString) + try Interaction.deleteWhere( + db, + .filter(Interaction.Columns.threadId == groupSessionId.hexString), .filter(Interaction.Columns.variant == Interaction.Variant.infoGroupInfoInvited) - .deleteAll(db) + ) } switch messageInfo.infoString(using: dependencies) { @@ -610,7 +611,7 @@ extension MessageReceiver { displayNameUpdate: .contactUpdate(profile.displayName), displayPictureUpdate: .from(profile, fallback: .contactRemove, using: dependencies), blocksCommunityMessageRequests: profile.blocksCommunityMessageRequests, - sentTimestamp: TimeInterval(Double(sentTimestampMs) / 1000), + profileUpdateTimestamp: profile.updateTimestampSeconds, using: dependencies ) } @@ -627,7 +628,7 @@ extension MessageReceiver { name: $0, displayPictureUrl: profile.profilePictureUrl, displayPictureEncryptionKey: profile.profileKey, - displayPictureLastUpdated: (Double(sentTimestampMs) / 1000) + profileLastUpdated: (Double(sentTimestampMs) / 1000) ) } }, @@ -752,7 +753,7 @@ extension MessageReceiver { ) else { return } - try? SnodeAPI + try? Network.SnodeAPI .preparedDeleteMessages( serverHashes: Array(hashes), requireSuccessfulDeletion: false, @@ -920,7 +921,7 @@ extension MessageReceiver { db.afterCommit { dependencies[singleton: .storage] .readPublisher { db in - try SnodeAPI.preparedDeleteMessages( + try Network.SnodeAPI.preparedDeleteMessages( serverHashes: [serverHash], requireSuccessfulDeletion: false, authMethod: try Authentication.with( @@ -943,10 +944,11 @@ extension MessageReceiver { /// Remove any existing `infoGroupInfoInvited` interactions from the group (don't want to have a duplicate one in case /// the group was created via a `USER_GROUPS` config when syncing a new device) - _ = try Interaction - .filter(Interaction.Columns.threadId == groupSessionId.hexString) + try Interaction.deleteWhere( + db, + .filter(Interaction.Columns.threadId == groupSessionId.hexString), .filter(Interaction.Columns.variant == Interaction.Variant.infoGroupInfoInvited) - .deleteAll(db) + ) /// Unline most control messages we don't bother setting expiration values for this message, this is because we won't actually /// have the current disappearing messages config as we won't have polled the group yet (and the settings are stored in the diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+LegacyClosedGroups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+LegacyClosedGroups.swift index 8396652b28..ec2ecadc46 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+LegacyClosedGroups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+LegacyClosedGroups.swift @@ -4,7 +4,7 @@ import Foundation import Combine import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit extension MessageReceiver { internal static func handleNewLegacyClosedGroup( diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+LibSession.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+LibSession.swift index acfe478541..f209de30ed 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+LibSession.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+LibSession.swift @@ -3,7 +3,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit extension MessageReceiver { diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift index b5d2893a4d..aa966d9d39 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift @@ -6,7 +6,7 @@ import Foundation import Combine import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit extension MessageReceiver { internal static func handleMessageRequestResponse( @@ -26,14 +26,12 @@ extension MessageReceiver { // Update profile if needed (want to do this regardless of whether the message exists or // not to ensure the profile info gets sync between a users devices at every chance) if let profile = message.profile { - let messageSentTimestamp: TimeInterval = TimeInterval(Double(message.sentTimestampMs ?? 0) / 1000) - try Profile.updateIfNeeded( db, publicKey: senderId, displayNameUpdate: .contactUpdate(profile.displayName), displayPictureUpdate: .from(profile, fallback: .none, using: dependencies), - sentTimestamp: messageSentTimestamp, + profileUpdateTimestamp: profile.updateTimestampSeconds, using: dependencies ) } diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+UnsendRequests.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+UnsendRequests.swift index ae75d068a5..c10460d2f4 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+UnsendRequests.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+UnsendRequests.swift @@ -2,7 +2,7 @@ import Foundation import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit extension MessageReceiver { @@ -52,7 +52,7 @@ extension MessageReceiver { ) try Interaction.markAsDeleted( db, - threadId: threadId, + threadId: interactionInfo.threadId, /// Can't use `threadId` as that may be the current users threadVariant: threadVariant, interactionIds: [interactionInfo.id], options: [.local, .network], @@ -70,7 +70,7 @@ extension MessageReceiver { case .contact: dependencies[singleton: .storage] .readPublisher { db in - try SnodeAPI.preparedDeleteMessages( + try Network.SnodeAPI.preparedDeleteMessages( serverHashes: Array(hashes), requireSuccessfulDeletion: false, authMethod: try Authentication.with( diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift index e096a96a9d..430b38e325 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift @@ -2,7 +2,7 @@ import Foundation import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit extension MessageReceiver { @@ -31,7 +31,8 @@ extension MessageReceiver { // Note: `message.sentTimestamp` is in ms (convert to TimeInterval before converting to // seconds to maintain the accuracy) - let messageSentTimestamp: TimeInterval = TimeInterval(Double(message.sentTimestampMs ?? 0) / 1000) + let messageSentTimestampMs: UInt64 = message.sentTimestampMs ?? 0 + let messageSentTimestamp: TimeInterval = TimeInterval(Double(messageSentTimestampMs) / 1000) let isMainAppActive: Bool = dependencies[defaults: .appGroup, key: .isMainAppActive] // Update profile if needed (want to do this regardless of whether the message exists or @@ -43,7 +44,7 @@ extension MessageReceiver { displayNameUpdate: .contactUpdate(profile.displayName), displayPictureUpdate: .from(profile, fallback: .contactRemove, using: dependencies), blocksCommunityMessageRequests: profile.blocksCommunityMessageRequests, - sentTimestamp: messageSentTimestamp, + profileUpdateTimestamp: profile.updateTimestampSeconds, using: dependencies ) } @@ -186,7 +187,7 @@ extension MessageReceiver { using: dependencies ) do { - let isProMessage: Bool = dependencies.mutate(cache: .libSession, { $0.validateProProof(message.proProof) }) + let isProMessage: Bool = dependencies.mutate(cache: .libSession, { $0.validateProProof(for: message) }) let processedMessageBody: String? = Self.truncateMessageTextIfNeeded( message.text, isProMessage: isProMessage, @@ -516,7 +517,7 @@ extension MessageReceiver { using dependencies: Dependencies ) throws -> Int64? { guard - let reaction: VisibleMessage.VMReaction = message.reaction, + let vmReaction: VisibleMessage.VMReaction = message.reaction, proto.dataMessage?.reaction != nil else { return nil } @@ -525,8 +526,8 @@ extension MessageReceiver { let maybeInteractionId: Int64? = try? Interaction .select(.id) .filter(Interaction.Columns.threadId == thread.id) - .filter(Interaction.Columns.timestampMs == reaction.timestamp) - .filter(Interaction.Columns.authorId == reaction.publicKey) + .filter(Interaction.Columns.timestampMs == vmReaction.timestamp) + .filter(Interaction.Columns.authorId == vmReaction.publicKey) .filter(Interaction.Columns.variant != Interaction.Variant.standardIncomingDeleted) .filter(Interaction.Columns.state != Interaction.State.deleted) .asRequest(of: Int64.self) @@ -539,10 +540,10 @@ extension MessageReceiver { let sortId = Reaction.getSortId( db, interactionId: interactionId, - emoji: reaction.emoji + emoji: vmReaction.emoji ) - switch reaction.kind { + switch vmReaction.kind { case .react: // Determine whether the app is active based on the prefs rather than the UIApplication state to avoid // requiring main-thread execution @@ -554,7 +555,7 @@ extension MessageReceiver { serverHash: message.serverHash, timestampMs: timestampMs, authorId: sender, - emoji: reaction.emoji, + emoji: vmReaction.emoji, count: 1, sortId: sortId ).inserted(db) @@ -568,11 +569,12 @@ extension MessageReceiver { } // Don't notify if the reaction was added before the lastest read timestamp for - // the conversation + // the conversation or the reaction is for the sender's own message if !suppressNotifications && sender != userSessionId.hexString && - !timestampAlreadyRead + !timestampAlreadyRead && + vmReaction.publicKey != sender { try? dependencies[singleton: .notificationsManager].notifyUser( cat: .messageReceiver, @@ -619,7 +621,7 @@ extension MessageReceiver { try Reaction .filter(Reaction.Columns.interactionId == interactionId) .filter(Reaction.Columns.authorId == sender) - .filter(Reaction.Columns.emoji == reaction.emoji) + .filter(Reaction.Columns.emoji == vmReaction.emoji) .deleteAll(db) } diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+Groups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+Groups.swift index 58caa31955..a0129669ac 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+Groups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+Groups.swift @@ -4,7 +4,7 @@ import Foundation import Combine import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit extension MessageSender { private typealias PreparedGroupData = ( @@ -13,7 +13,7 @@ extension MessageSender { thread: SessionThread, group: ClosedGroup, members: [GroupMember], - preparedNotificationsSubscription: Network.PreparedRequest? + preparedNotificationsSubscription: Network.PreparedRequest? ) public static func createGroup( @@ -38,7 +38,7 @@ extension MessageSender { } return dependencies[singleton: .displayPictureManager] - .prepareAndUploadDisplayPicture(imageData: displayPictureData) + .prepareAndUploadDisplayPicture(imageData: displayPictureData, compression: true) .mapError { error -> Error in error } .map { Optional($0) } .eraseToAnyPublisher() @@ -119,14 +119,19 @@ extension MessageSender { ) // Prepare the notification subscription - var preparedNotificationSubscription: Network.PreparedRequest? + var preparedNotificationSubscription: Network.PreparedRequest? if let token: String = dependencies[defaults: .standard, key: .deviceToken] { - preparedNotificationSubscription = try? PushNotificationAPI + preparedNotificationSubscription = try? Network.PushNotification .preparedSubscribe( - db, token: Data(hex: token), - sessionIds: [createdInfo.groupSessionId], + swarms: [( + createdInfo.groupSessionId, + Authentication.groupAdmin( + groupSessionId: createdInfo.groupSessionId, + ed25519SecretKey: createdInfo.identityKeyPair.secretKey + ) + )], using: dependencies ) } @@ -479,7 +484,7 @@ extension MessageSender { /// Add a record of the change to the conversation _ = try updatedConfig - .saved(db) + .upserted(db) .insertControlMessage( db, threadVariant: .group, @@ -593,7 +598,7 @@ extension MessageSender { using: dependencies ) - maybeSupplementalKeyRequest = try SnodeAPI.preparedSendMessage( + maybeSupplementalKeyRequest = try Network.SnodeAPI.preparedSendMessage( message: SnodeMessage( recipient: sessionId.hexString, data: supplementData, @@ -677,7 +682,7 @@ extension MessageSender { /// Unrevoke the newly added members just in case they had previously gotten their access to the group /// revoked (fire-and-forget this request, we don't want it to be blocking - if the invited user still can't access /// the group the admin can resend their invitation which will also attempt to unrevoke their subaccount) - let unrevokeRequest: Network.PreparedRequest = try SnodeAPI.preparedUnrevokeSubaccounts( + let unrevokeRequest: Network.PreparedRequest = try Network.SnodeAPI.preparedUnrevokeSubaccounts( subaccountsToUnrevoke: memberJobData.map { _, _, _, subaccountToken in subaccountToken }, authMethod: Authentication.groupAdmin( groupSessionId: sessionId, @@ -856,7 +861,7 @@ extension MessageSender { using: dependencies ) - maybeSupplementalKeyRequest = try SnodeAPI.preparedSendMessage( + maybeSupplementalKeyRequest = try Network.SnodeAPI.preparedSendMessage( message: SnodeMessage( recipient: sessionId.hexString, data: supplementData, @@ -902,7 +907,7 @@ extension MessageSender { /// Unrevoke the member just in case they had previously gotten their access to the group revoked and the /// unrevoke request when initially added them failed (fire-and-forget this request, we don't want it to be blocking) - let unrevokeRequest: Network.PreparedRequest = try SnodeAPI + let unrevokeRequest: Network.PreparedRequest = try Network.SnodeAPI .preparedUnrevokeSubaccounts( subaccountsToUnrevoke: memberInfo.map { token, _ in token }, authMethod: Authentication.groupAdmin( diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift index 059032f9c6..91ce5e83db 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift @@ -4,7 +4,7 @@ import Foundation import GRDB import SessionUIKit import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit // MARK: - Log.Category @@ -28,7 +28,7 @@ public enum MessageReceiver { var customProto: SNProtoContent? = nil var customMessage: Message? = nil let sender: String - let sentTimestampMs: UInt64 + let sentTimestampMs: UInt64? let serverHash: String? let openGroupServerMessageId: UInt64? let openGroupWhisper: Bool @@ -53,7 +53,7 @@ public enum MessageReceiver { uniqueIdentifier = "\(messageServerId)" plaintext = data.removePadding() // Remove the padding sender = messageSender - sentTimestampMs = UInt64(floor(timestamp * 1000)) // Convert to ms for database consistency + sentTimestampMs = timestamp.map { UInt64(floor($0 * 1000)) } // Convert to ms for database consistency serverHash = nil openGroupServerMessageId = UInt64(messageServerId) openGroupWhisper = messageWhisper @@ -400,9 +400,9 @@ public enum MessageReceiver { // visible (the only other spot this flag gets set is when sending messages) let shouldBecomeVisible: Bool = { switch message { - case is ReadReceipt: return true - case is TypingIndicator: return true - case is UnsendRequest: return true + case is ReadReceipt: return false + case is TypingIndicator: return false + case is UnsendRequest: return false case is CallMessage: return (threadId != dependencies[cache: .general].sessionId.hexString) /// These are sent to the one-to-one conversation so they shouldn't make that visible diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift b/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift index bb681dd1b5..f10e1dca4c 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift @@ -3,7 +3,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit extension MessageSender { @@ -199,17 +199,27 @@ extension MessageSender { } else { // Otherwise we do want to try and update the referenced interaction - let interaction: Interaction? = try interaction(db, for: message, interactionId: interactionId) + let maybeInteraction: Interaction? = try interaction(db, for: message, interactionId: interactionId) // Get the visible message if possible - if let interaction: Interaction = interaction { + if var interaction: Interaction = maybeInteraction { // Only store the server hash of a sync message if the message is self send valid switch (message.isSelfSendValid, destination) { case (false, .syncMessage): try interaction.with(state: .sent).update(db) case (true, .syncMessage), (_, .contact), (_, .closedGroup), (_, .openGroup), (_, .openGroupInbox): - try interaction.with( + // The timestamp to use for scheduling message deletion. This is generated + // when the message is successfully sent to ensure the deletion timer starts + // from the correct time. + var scheduledTimestampForDeletion: Double? { + guard interaction.isExpiringMessage else { return nil } + let sentTimestampMs: Double = dependencies[cache: .snodeAPI].currentOffsetTimestampMs() + return sentTimestampMs + } + + // Update the interaction so we have the correct `expiresStartedAtMs` value + interaction = interaction.with( serverHash: message.serverHash, // Track the open group server message ID and update server timestamp (use server // timestamp for open group messages otherwise the quote messages may not be able @@ -218,9 +228,11 @@ extension MessageSender { nil : serverTimestampMs.map { Int64($0) } ), + expiresStartedAtMs: scheduledTimestampForDeletion, // Updates the expiresStartedAtMs value when message is marked as sent openGroupServerMessageId: message.openGroupServerMessageId.map { Int64($0) }, state: .sent - ).update(db) + ) + try interaction.update(db) if interaction.isExpiringMessage { // Start disappearing messages job after a message is successfully sent. @@ -240,7 +252,7 @@ extension MessageSender { if case .syncMessage = destination, - let startedAtMs: Double = interaction.expiresStartedAtMs, + let startedAtMs: Double = scheduledTimestampForDeletion, let expiresInSeconds: TimeInterval = interaction.expiresInSeconds, let serverHash: String = message.serverHash { diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender.swift b/SessionMessagingKit/Sending & Receiving/MessageSender.swift index 510819e3f4..8752d80003 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender.swift @@ -3,7 +3,7 @@ // stringlint:disable import Foundation -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Log.Category @@ -43,7 +43,7 @@ public final class MessageSender { public static func preparedSend( message: Message, to destination: Message.Destination, - namespace: SnodeAPI.Namespace?, + namespace: Network.SnodeAPI.Namespace?, interactionId: Int64?, attachments: [(attachment: Attachment, fileId: String)]?, authMethod: AuthenticationMethod, @@ -138,7 +138,7 @@ public final class MessageSender { private static func preparedSendToSnodeDestination( message: Message, to destination: Message.Destination, - namespace: SnodeAPI.Namespace?, + namespace: Network.SnodeAPI.Namespace?, interactionId: Int64?, attachments: [(attachment: Attachment, fileId: String)]?, messageSendTimestampMs: Int64, @@ -146,7 +146,9 @@ public final class MessageSender { onEvent: ((Event) -> Void)?, using dependencies: Dependencies ) throws -> Network.PreparedRequest { - guard let namespace: SnodeAPI.Namespace = namespace else { throw MessageSenderError.invalidMessage } + guard let namespace: Network.SnodeAPI.Namespace = namespace else { + throw MessageSenderError.invalidMessage + } /// Set the sender/recipient info (needed to be valid) /// @@ -170,7 +172,8 @@ public final class MessageSender { VisibleMessage.VMProfile( displayName: profile.name, profileKey: profile.displayPictureEncryptionKey, - profilePictureUrl: profile.displayPictureUrl + profilePictureUrl: profile.displayPictureUrl, + updateTimestampSeconds: profile.profileLastUpdated ) } } @@ -201,7 +204,7 @@ public final class MessageSender { // Perform any pre-send actions onEvent?(.willSend(message, destination, interactionId: interactionId)) - return try SnodeAPI + return try Network.SnodeAPI .preparedSendMessage( message: snodeMessage, in: namespace, @@ -269,6 +272,7 @@ public final class MessageSender { displayName: profile.name, profileKey: profile.displayPictureEncryptionKey, profilePictureUrl: profile.displayPictureUrl, + updateTimestampSeconds: profile.profileLastUpdated, blocksCommunityMessageRequests: !checkForCommunityMessageRequests ) } @@ -287,7 +291,7 @@ public final class MessageSender { // Perform any pre-send actions onEvent?(.willSend(message, destination, interactionId: interactionId)) - return try OpenGroupAPI + return try Network.SOGS .preparedSend( plaintext: plaintext, roomToken: roomToken, @@ -300,9 +304,9 @@ public final class MessageSender { .map { _, response in let updatedMessage: Message = message updatedMessage.openGroupServerMessageId = UInt64(response.id) - updatedMessage.sentTimestampMs = UInt64(floor(response.posted * 1000)) + updatedMessage.sentTimestampMs = response.posted.map { UInt64(floor($0 * 1000)) } - return (updatedMessage, Int64(floor(response.posted * 1000)), nil) + return (updatedMessage, response.posted.map { Int64(floor($0 * 1000)) }, nil) } } @@ -334,7 +338,8 @@ public final class MessageSender { VisibleMessage.VMProfile( displayName: profile.name, profileKey: profile.displayPictureEncryptionKey, - profilePictureUrl: profile.displayPictureUrl + profilePictureUrl: profile.displayPictureUrl, + updateTimestampSeconds: profile.profileLastUpdated ) } @@ -352,7 +357,7 @@ public final class MessageSender { // Perform any pre-send actions onEvent?(.willSend(message, destination, interactionId: interactionId)) - return try OpenGroupAPI + return try Network.SOGS .preparedSend( ciphertext: ciphertext, toInboxFor: recipientBlindedPublicKey, @@ -371,7 +376,7 @@ public final class MessageSender { // MARK: - Message Wrapping public static func encodeMessageForSending( - namespace: SnodeAPI.Namespace, + namespace: Network.SnodeAPI.Namespace, destination: Message.Destination, message: Message, attachments: [(attachment: Attachment, fileId: String)]?, diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyGroupOnlyRequest.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyGroupOnlyRequest.swift deleted file mode 100644 index 1a87dcf8e5..0000000000 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyGroupOnlyRequest.swift +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. - -import Foundation - -extension PushNotificationAPI { - struct LegacyGroupOnlyRequest: Codable { - let token: String - let pubKey: String - let device: String - let legacyGroupPublicKeys: Set - } -} diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyGroupRequest.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyGroupRequest.swift deleted file mode 100644 index 962011dfd4..0000000000 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyGroupRequest.swift +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. - -import Foundation - -extension PushNotificationAPI { - struct LegacyGroupRequest: Codable { - let pubKey: String - let closedGroupPublicKey: String - } -} diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyNotifyRequest.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyNotifyRequest.swift deleted file mode 100644 index 491fa77570..0000000000 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyNotifyRequest.swift +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. - -import Foundation - -extension PushNotificationAPI { - struct LegacyNotifyRequest: Codable { - enum CodingKeys: String, CodingKey { - case data - case sendTo = "send_to" - } - - let data: String - let sendTo: String - } -} diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyPushServerResponse.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyPushServerResponse.swift deleted file mode 100644 index bd412f24e7..0000000000 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyPushServerResponse.swift +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import Foundation - -public extension PushNotificationAPI { - struct LegacyPushServerResponse: Codable { - let code: Int - let message: String? - } -} diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyUnsubscribeRequest.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyUnsubscribeRequest.swift deleted file mode 100644 index 663bafb174..0000000000 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyUnsubscribeRequest.swift +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. - -import Foundation -import SessionSnodeKit - -extension PushNotificationAPI { - struct LegacyUnsubscribeRequest: Codable { - private let token: String - - init(token: String) { - self.token = token - } - } -} diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/NotificationsManagerType.swift b/SessionMessagingKit/Sending & Receiving/Notifications/NotificationsManagerType.swift index aa0cdcefa0..b844d336dc 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/NotificationsManagerType.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/NotificationsManagerType.swift @@ -95,10 +95,16 @@ public extension NotificationsManagerType { ) else { throw MessageReceiverError.ignorableMessage } - /// If the message is a reaction then we only want to show notifications for `contact` conversations + /// If the message is a reaction then we only want to show notifications for `contact` conversations, any only if the + /// reaction isn't added to a message sent by the reactor if visibleMessage.reaction != nil { switch threadVariant { - case .contact: break + case .contact: + guard visibleMessage.reaction?.publicKey != sender else { + throw MessageReceiverError.ignorableMessage + } + break + case .legacyGroup, .group, .community: throw MessageReceiverError.ignorableMessage } } @@ -245,7 +251,6 @@ public extension NotificationsManagerType { ) }? .filteredForDisplay - .filteredForNotification .nullIfEmpty? .replacingMentions( currentUserSessionIds: currentUserSessionIds, @@ -413,7 +418,7 @@ public extension NotificationsManagerType { // MARK: - NoopNotificationsManager -public struct NoopNotificationsManager: NotificationsManagerType { +public struct NoopNotificationsManager: NotificationsManagerType, NoopDependency { public let dependencies: Dependencies public init(using dependencies: Dependencies) { diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI+SessionMessagingKit.swift b/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI+SessionMessagingKit.swift new file mode 100644 index 0000000000..dda9ad50dd --- /dev/null +++ b/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI+SessionMessagingKit.swift @@ -0,0 +1,127 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import Combine +import GRDB +import SessionNetworkingKit +import SessionUtilitiesKit + +public extension Network.PushNotification { + static func subscribeAll( + token: Data, + isForcedUpdate: Bool, + using dependencies: Dependencies + ) -> AnyPublisher { + let hexEncodedToken: String = token.toHexString() + let oldToken: String? = dependencies[defaults: .standard, key: .deviceToken] + let lastUploadTime: Double = dependencies[defaults: .standard, key: .lastDeviceTokenUpload] + let now: TimeInterval = dependencies.dateNow.timeIntervalSince1970 + + guard isForcedUpdate || hexEncodedToken != oldToken || now - lastUploadTime > tokenExpirationInterval else { + Log.info(.pushNotificationAPI, "Device token hasn't changed or expired; no need to re-upload.") + return Just(()) + .setFailureType(to: Error.self) + .eraseToAnyPublisher() + } + + return dependencies[singleton: .storage] + .readPublisher { db -> Network.PreparedRequest in + let userSessionId: SessionId = dependencies[cache: .general].sessionId + let userAuthMethod: AuthenticationMethod = try Authentication.with( + db, + swarmPublicKey: userSessionId.hexString, + using: dependencies + ) + + return try Network.PushNotification + .preparedSubscribe( + token: token, + swarms: [(userSessionId, userAuthMethod)] + .appending(contentsOf: try ClosedGroup + .select(.threadId) + .filter( + ClosedGroup.Columns.threadId > SessionId.Prefix.group.rawValue && + ClosedGroup.Columns.threadId < SessionId.Prefix.group.endOfRangeString + ) + .filter(ClosedGroup.Columns.shouldPoll) + .asRequest(of: String.self) + .fetchSet(db) + .map { threadId in + ( + SessionId(.group, hex: threadId), + try Authentication.with( + db, + swarmPublicKey: threadId, + using: dependencies + ) + ) + } + ), + using: dependencies + ) + .handleEvents( + receiveOutput: { _, response in + guard response.subResponses.first?.success == true else { return } + + dependencies[defaults: .standard, key: .deviceToken] = hexEncodedToken + dependencies[defaults: .standard, key: .lastDeviceTokenUpload] = now + dependencies[defaults: .standard, key: .isUsingFullAPNs] = true + } + ) + } + .flatMap { $0.send(using: dependencies) } + .map { _ in () } + .eraseToAnyPublisher() + } + + public static func unsubscribeAll( + token: Data, + using dependencies: Dependencies + ) -> AnyPublisher { + return dependencies[singleton: .storage] + .readPublisher { db -> Network.PreparedRequest in + let userSessionId: SessionId = dependencies[cache: .general].sessionId + let userAuthMethod: AuthenticationMethod = try Authentication.with( + db, + swarmPublicKey: userSessionId.hexString, + using: dependencies + ) + + return try Network.PushNotification + .preparedUnsubscribe( + token: token, + swarms: [(userSessionId, userAuthMethod)] + .appending(contentsOf: (try? ClosedGroup + .select(.threadId) + .filter( + ClosedGroup.Columns.threadId > SessionId.Prefix.group.rawValue && + ClosedGroup.Columns.threadId < SessionId.Prefix.group.endOfRangeString + ) + .asRequest(of: String.self) + .fetchSet(db)) + .defaulting(to: []) + .map { threadId in + ( + SessionId(.group, hex: threadId), + try Authentication.with( + db, + swarmPublicKey: threadId, + using: dependencies + ) + ) + }), + using: dependencies + ) + .handleEvents( + receiveOutput: { _, response in + guard response.subResponses.first?.success == true else { return } + + dependencies[defaults: .standard, key: .deviceToken] = nil + } + ) + } + .flatMap { $0.send(using: dependencies) } + .map { _ in () } + .eraseToAnyPublisher() + } +} diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift b/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift deleted file mode 100644 index 5c4bcd9c7c..0000000000 --- a/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift +++ /dev/null @@ -1,320 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. -// -// stringlint:disable - -import Foundation -import Combine -import GRDB -import SessionSnodeKit -import SessionUtilitiesKit - -// MARK: - KeychainStorage - -public extension KeychainStorage.DataKey { static let pushNotificationEncryptionKey: Self = "PNEncryptionKeyKey" } - -// MARK: - Log.Category - -private extension Log.Category { - static let cat: Log.Category = .create("PushNotificationAPI", defaultLevel: .info) -} - -// MARK: - PushNotificationAPI - -public enum PushNotificationAPI { - internal static let encryptionKeyLength: Int = 32 - private static let maxRetryCount: Int = 4 - private static let tokenExpirationInterval: TimeInterval = (12 * 60 * 60) - - public static let server: String = "https://push.getsession.org" - public static let serverPublicKey = "d7557fe563e2610de876c0ac7341b62f3c82d5eea4b62c702392ea4368f51b3b" - - // MARK: - Batch Requests - - public static func subscribeAll( - token: Data, - isForcedUpdate: Bool, - using dependencies: Dependencies - ) -> AnyPublisher { - let hexEncodedToken: String = token.toHexString() - let oldToken: String? = dependencies[defaults: .standard, key: .deviceToken] - let lastUploadTime: Double = dependencies[defaults: .standard, key: .lastDeviceTokenUpload] - let now: TimeInterval = dependencies.dateNow.timeIntervalSince1970 - - guard isForcedUpdate || hexEncodedToken != oldToken || now - lastUploadTime > tokenExpirationInterval else { - Log.info(.cat, "Device token hasn't changed or expired; no need to re-upload.") - return Just(()) - .setFailureType(to: Error.self) - .eraseToAnyPublisher() - } - - return dependencies[singleton: .storage] - .readPublisher { db -> Network.PreparedRequest in - let userSessionId: SessionId = dependencies[cache: .general].sessionId - - return try PushNotificationAPI - .preparedSubscribe( - db, - token: token, - sessionIds: [userSessionId] - .appending(contentsOf: try ClosedGroup - .select(.threadId) - .filter( - ClosedGroup.Columns.threadId > SessionId.Prefix.group.rawValue && - ClosedGroup.Columns.threadId < SessionId.Prefix.group.endOfRangeString - ) - .filter(ClosedGroup.Columns.shouldPoll) - .asRequest(of: String.self) - .fetchSet(db) - .map { SessionId(.group, hex: $0) } - ), - using: dependencies - ) - .handleEvents( - receiveOutput: { _, response in - guard response.subResponses.first?.success == true else { return } - - dependencies[defaults: .standard, key: .deviceToken] = hexEncodedToken - dependencies[defaults: .standard, key: .lastDeviceTokenUpload] = now - dependencies[defaults: .standard, key: .isUsingFullAPNs] = true - } - ) - } - .flatMap { $0.send(using: dependencies) } - .map { _ in () } - .eraseToAnyPublisher() - } - - public static func unsubscribeAll( - token: Data, - using dependencies: Dependencies - ) -> AnyPublisher { - return dependencies[singleton: .storage] - .readPublisher { db -> Network.PreparedRequest in - let userSessionId: SessionId = dependencies[cache: .general].sessionId - - return try PushNotificationAPI - .preparedUnsubscribe( - db, - token: token, - sessionIds: [userSessionId] - .appending(contentsOf: (try? ClosedGroup - .select(.threadId) - .filter( - ClosedGroup.Columns.threadId > SessionId.Prefix.group.rawValue && - ClosedGroup.Columns.threadId < SessionId.Prefix.group.endOfRangeString - ) - .asRequest(of: String.self) - .fetchSet(db)) - .defaulting(to: []) - .map { SessionId(.group, hex: $0) }), - using: dependencies - ) - .handleEvents( - receiveOutput: { _, response in - guard response.subResponses.first?.success == true else { return } - - dependencies[defaults: .standard, key: .deviceToken] = nil - } - ) - } - .flatMap { $0.send(using: dependencies) } - .map { _ in () } - .eraseToAnyPublisher() - } - - // MARK: - Prepared Requests - - public static func preparedSubscribe( - _ db: ObservingDatabase, - token: Data, - sessionIds: [SessionId], - using dependencies: Dependencies - ) throws -> Network.PreparedRequest { - guard dependencies[defaults: .standard, key: .isUsingFullAPNs] else { - throw NetworkError.invalidPreparedRequest - } - - guard let notificationsEncryptionKey: Data = try? dependencies[singleton: .keychain].getOrGenerateEncryptionKey( - forKey: .pushNotificationEncryptionKey, - length: encryptionKeyLength, - cat: .cat, - legacyKey: "PNEncryptionKeyKey", - legacyService: "PNKeyChainService" - ) else { - Log.error(.cat, "Unable to retrieve PN encryption key.") - throw KeychainStorageError.keySpecInvalid - } - - return try Network.PreparedRequest( - request: Request( - method: .post, - endpoint: Endpoint.subscribe, - body: SubscribeRequest( - subscriptions: sessionIds.map { sessionId -> SubscribeRequest.Subscription in - SubscribeRequest.Subscription( - namespaces: { - switch sessionId.prefix { - case .group: return [ - .groupMessages, - .configGroupKeys, - .configGroupInfo, - .configGroupMembers, - .revokedRetrievableGroupMessages - ] - default: return [ - .default, - .configUserProfile, - .configContacts, - .configConvoInfoVolatile, - .configUserGroups - ] - } - }(), - /// Note: Unfortunately we always need the message content because without the content - /// control messages can't be distinguished from visible messages which results in the - /// 'generic' notification being shown when receiving things like typing indicator updates - includeMessageData: true, - serviceInfo: ServiceInfo( - token: token.toHexString() - ), - notificationsEncryptionKey: notificationsEncryptionKey, - authMethod: try Authentication.with( - db, - swarmPublicKey: sessionId.hexString, - using: dependencies - ), - timestamp: (dependencies[cache: .snodeAPI].currentOffsetTimestampMs() / 1000) // Seconds - ) - } - ) - ), - responseType: SubscribeResponse.self, - retryCount: PushNotificationAPI.maxRetryCount, - using: dependencies - ) - .handleEvents( - receiveOutput: { _, response in - zip(response.subResponses, sessionIds).forEach { subResponse, sessionId in - guard subResponse.success != true else { return } - - Log.error(.cat, "Couldn't subscribe for push notifications for: \(sessionId) due to error (\(subResponse.error ?? -1)): \(subResponse.message ?? "nil").") - } - }, - receiveCompletion: { result in - switch result { - case .finished: break - case .failure(let error): Log.error(.cat, "Couldn't subscribe for push notifications due to error: \(error).") - } - } - ) - } - - public static func preparedUnsubscribe( - _ db: ObservingDatabase, - token: Data, - sessionIds: [SessionId], - using dependencies: Dependencies - ) throws -> Network.PreparedRequest { - return try Network.PreparedRequest( - request: Request( - method: .post, - endpoint: Endpoint.unsubscribe, - body: UnsubscribeRequest( - subscriptions: sessionIds.map { sessionId -> UnsubscribeRequest.Subscription in - UnsubscribeRequest.Subscription( - serviceInfo: ServiceInfo( - token: token.toHexString() - ), - authMethod: try Authentication.with( - db, - swarmPublicKey: sessionId.hexString, - using: dependencies - ), - timestamp: (dependencies[cache: .snodeAPI].currentOffsetTimestampMs() / 1000) // Seconds - ) - } - ) - ), - responseType: UnsubscribeResponse.self, - retryCount: PushNotificationAPI.maxRetryCount, - using: dependencies - ) - .handleEvents( - receiveOutput: { _, response in - zip(response.subResponses, sessionIds).forEach { subResponse, sessionId in - guard subResponse.success != true else { return } - - Log.error(.cat, "Couldn't unsubscribe for push notifications for: \(sessionId) due to error (\(subResponse.error ?? -1)): \(subResponse.message ?? "nil").") - } - }, - receiveCompletion: { result in - switch result { - case .finished: break - case .failure(let error): Log.error(.cat, "Couldn't unsubscribe for push notifications due to error: \(error).") - } - } - ) - } - - // MARK: - Notification Handling - - public static func processNotification( - notificationContent: UNNotificationContent, - using dependencies: Dependencies - ) -> (data: Data?, metadata: NotificationMetadata, result: ProcessResult) { - // Make sure the notification is from the updated push server - guard notificationContent.userInfo["spns"] != nil else { - return (nil, .invalid, .legacyFailure) - } - - guard let base64EncodedEncString: String = notificationContent.userInfo["enc_payload"] as? String else { - return (nil, .invalid, .failureNoContent) - } - - // Decrypt and decode the payload - let notification: BencodeResponse - - do { - guard let encryptedData: Data = Data(base64Encoded: base64EncodedEncString) else { - throw CryptoError.invalidBase64EncodedData - } - - let notificationsEncryptionKey: Data = try dependencies[singleton: .keychain].getOrGenerateEncryptionKey( - forKey: .pushNotificationEncryptionKey, - length: encryptionKeyLength, - cat: .cat, - legacyKey: "PNEncryptionKeyKey", - legacyService: "PNKeyChainService" - ) - let decryptedData: Data = try dependencies[singleton: .crypto].tryGenerate( - .plaintextWithPushNotificationPayload( - payload: encryptedData, - encKey: notificationsEncryptionKey - ) - ) - notification = try BencodeDecoder(using: dependencies) - .decode(BencodeResponse.self, from: decryptedData) - } - catch { - Log.error(.cat, "Failed to decrypt or decode notification due to error: \(error)") - return (nil, .invalid, .failure) - } - - // If the metadata says that the message was too large then we should show the generic - // notification (this is a valid case) - guard !notification.info.dataTooLong else { return (nil, notification.info, .successTooLong) } - - // Check that the body we were given is valid and not empty - guard - let notificationData: Data = notification.data, - notification.info.dataLength == notificationData.count, - !notificationData.isEmpty - else { - Log.error(.cat, "Get notification data failed") - return (nil, notification.info, .failureNoContent) - } - - // Success, we have the notification content - return (notificationData, notification.info, .success) - } -} diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/CommunityPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/CommunityPoller.swift index 835329ec9c..40477f59f3 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/CommunityPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/CommunityPoller.swift @@ -3,7 +3,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Cache @@ -20,7 +20,7 @@ public extension Cache { // MARK: - CommunityPollerType public protocol CommunityPollerType { - typealias PollResponse = (info: ResponseInfoType, data: Network.BatchResponseMap) + typealias PollResponse = (info: ResponseInfoType, data: Network.BatchResponseMap) var isPolling: Bool { get } var receivedPollResponse: AnyPublisher { get } @@ -31,14 +31,13 @@ public protocol CommunityPollerType { // MARK: - CommunityPoller -private typealias Capabilities = OpenGroupAPI.Capabilities +private typealias Capabilities = Network.SOGS.CapabilitiesResponse public final class CommunityPoller: CommunityPollerType & PollerType { // MARK: - Settings private static let minPollInterval: TimeInterval = 3 private static let maxPollInterval: TimeInterval = (60 * 60) - internal static let maxInactivityPeriod: TimeInterval = (14 * 24 * 60 * 60) /// If there are hidden rooms that we poll and they fail too many times we want to prune them (as it likely means they no longer /// exist, and since they are already hidden it's unlikely that the user will notice that we stopped polling for them) @@ -75,7 +74,7 @@ public final class CommunityPoller: CommunityPollerType & PollerType { pollerQueue: DispatchQueue, pollerDestination: PollerDestination, pollerDrainBehaviour: ThreadSafeObject = .alwaysRandom, - namespaces: [SnodeAPI.Namespace] = [], + namespaces: [Network.SnodeAPI.Namespace] = [], failureCount: Int, shouldStoreMessages: Bool, logStartAndStopCalls: Bool, @@ -217,13 +216,13 @@ public final class CommunityPoller: CommunityPollerType & PollerType { .subscribe(on: pollerQueue, using: dependencies) .receive(on: pollerQueue, using: dependencies) .tryMap { [dependencies] authMethod in - try OpenGroupAPI.preparedCapabilities( + try Network.SOGS.preparedCapabilities( authMethod: authMethod, using: dependencies ) } .flatMap { [dependencies] in $0.send(using: dependencies) } - .flatMapStorageWritePublisher(using: dependencies) { [pollerDestination] (db: ObservingDatabase, response: (info: ResponseInfoType, data: OpenGroupAPI.Capabilities)) in + .flatMapStorageWritePublisher(using: dependencies) { [pollerDestination] (db: ObservingDatabase, response: (info: ResponseInfoType, data: Network.SOGS.CapabilitiesResponse)) in OpenGroupManager.handleCapabilities( db, capabilities: response.data, @@ -260,7 +259,7 @@ public final class CommunityPoller: CommunityPollerType & PollerType { /// for cases where we need explicit/custom behaviours to occur (eg. Onboarding) public func poll(forceSynchronousProcessing: Bool = false) -> AnyPublisher { typealias PollInfo = ( - roomInfo: [OpenGroupAPI.RoomInfo], + roomInfo: [Network.SOGS.PollRoomInfo], lastInboxMessageId: Int64, lastOutboxMessageId: Int64, authMethod: AuthenticationMethod @@ -276,15 +275,15 @@ public final class CommunityPoller: CommunityPollerType & PollerType { .readPublisher { [pollerDestination, dependencies] db -> PollInfo in /// **Note:** The `OpenGroup` type converts to lowercase in init let server: String = pollerDestination.target.lowercased() - let roomInfo: [OpenGroupAPI.RoomInfo] = try OpenGroup + let roomInfo: [Network.SOGS.PollRoomInfo] = try OpenGroup .select(.roomToken, .infoUpdates, .sequenceNumber) .filter(OpenGroup.Columns.server == server) .filter(OpenGroup.Columns.isActive == true) .filter(OpenGroup.Columns.roomToken != "") - .asRequest(of: OpenGroupAPI.RoomInfo.self) + .asRequest(of: Network.SOGS.PollRoomInfo.self) .fetchAll(db) - guard !roomInfo.isEmpty else { throw OpenGroupAPIError.invalidPoll } + guard !roomInfo.isEmpty else { throw SOGSError.invalidPoll } return ( roomInfo, @@ -303,12 +302,15 @@ public final class CommunityPoller: CommunityPollerType & PollerType { try Authentication.with(db, server: server, using: dependencies) ) } - .tryFlatMap { [pollCount, dependencies] pollInfo -> AnyPublisher<(ResponseInfoType, Network.BatchResponseMap), Error> in - try OpenGroupAPI + .tryFlatMap { [pollCount, dependencies] pollInfo -> AnyPublisher<(ResponseInfoType, Network.BatchResponseMap), Error> in + try Network.SOGS .preparedPoll( roomInfo: pollInfo.roomInfo, lastInboxMessageId: pollInfo.lastInboxMessageId, lastOutboxMessageId: pollInfo.lastOutboxMessageId, + checkForCommunityMessageRequests: dependencies.mutate(cache: .libSession) { + $0.get(.checkForCommunityMessageRequests) + }, hasPerformedInitialPoll: (pollCount > 0), timeSinceLastPoll: (dependencies.dateNow.timeIntervalSince1970 - lastSuccessfulPollTimestamp), authMethod: pollInfo.authMethod, @@ -340,16 +342,16 @@ public final class CommunityPoller: CommunityPollerType & PollerType { private func handlePollResponse( info: ResponseInfoType, - response: Network.BatchResponseMap, + response: Network.BatchResponseMap, failureCount: Int, using dependencies: Dependencies ) -> AnyPublisher { var rawMessageCount: Int = 0 - let validResponses: [OpenGroupAPI.Endpoint: Any] = response.data + let validResponses: [Network.SOGS.Endpoint: Any] = response.data .filter { endpoint, data in switch endpoint { case .capabilities: - guard (data as? Network.BatchSubResponse)?.body != nil else { + guard (data as? Network.BatchSubResponse)?.body != nil else { Log.error(.poller, "\(pollerName) failed due to invalid capability data.") return false } @@ -357,8 +359,8 @@ public final class CommunityPoller: CommunityPollerType & PollerType { return true case .roomPollInfo(let roomToken, _): - guard (data as? Network.BatchSubResponse)?.body != nil else { - switch (data as? Network.BatchSubResponse)?.code { + guard (data as? Network.BatchSubResponse)?.body != nil else { + switch (data as? Network.BatchSubResponse)?.code { case 404: Log.error(.poller, "\(pollerName) failed to retrieve info for unknown room '\(roomToken)'.") default: Log.error(.poller, "\(pollerName) failed due to invalid room info data.") } @@ -369,17 +371,17 @@ public final class CommunityPoller: CommunityPollerType & PollerType { case .roomMessagesRecent(let roomToken), .roomMessagesBefore(let roomToken, _), .roomMessagesSince(let roomToken, _): guard - let responseData: Network.BatchSubResponse<[Failable]> = data as? Network.BatchSubResponse<[Failable]>, - let responseBody: [Failable] = responseData.body + let responseData: Network.BatchSubResponse<[Failable]> = data as? Network.BatchSubResponse<[Failable]>, + let responseBody: [Failable] = responseData.body else { - switch (data as? Network.BatchSubResponse<[Failable]>)?.code { + switch (data as? Network.BatchSubResponse<[Failable]>)?.code { case 404: Log.error(.poller, "\(pollerName) failed to retrieve messages for unknown room '\(roomToken)'.") default: Log.error(.poller, "\(pollerName) failed due to invalid messages data.") } return false } - let successfulMessages: [OpenGroupAPI.Message] = responseBody.compactMap { $0.value } + let successfulMessages: [Network.SOGS.Message] = responseBody.compactMap { $0.value } rawMessageCount += successfulMessages.count if successfulMessages.count != responseBody.count { @@ -392,7 +394,7 @@ public final class CommunityPoller: CommunityPollerType & PollerType { case .inbox, .inboxSince, .outbox, .outboxSince: guard - let responseData: Network.BatchSubResponse<[OpenGroupAPI.DirectMessage]?> = data as? Network.BatchSubResponse<[OpenGroupAPI.DirectMessage]?>, + let responseData: Network.BatchSubResponse<[Network.SOGS.DirectMessage]?> = data as? Network.BatchSubResponse<[Network.SOGS.DirectMessage]?>, !responseData.failedToParseBody else { Log.error(.poller, "\(pollerName) failed due to invalid inbox/outbox data.") @@ -400,7 +402,7 @@ public final class CommunityPoller: CommunityPollerType & PollerType { } // Double optional because the server can return a `304` with an empty body - let messages: [OpenGroupAPI.DirectMessage] = ((responseData.body ?? []) ?? []) + let messages: [Network.SOGS.DirectMessage] = ((responseData.body ?? []) ?? []) rawMessageCount += messages.count return !messages.isEmpty @@ -428,18 +430,18 @@ public final class CommunityPoller: CommunityPollerType & PollerType { } return dependencies[singleton: .storage] - .readPublisher { [pollerDestination] db -> (capabilities: OpenGroupAPI.Capabilities, groups: [OpenGroup]) in + .readPublisher { [pollerDestination] db -> (capabilities: Network.SOGS.CapabilitiesResponse, groups: [OpenGroup]) in let allCapabilities: [Capability] = try Capability .filter(Capability.Columns.openGroupServer == pollerDestination.target) .fetchAll(db) - let capabilities: OpenGroupAPI.Capabilities = OpenGroupAPI.Capabilities( + let capabilities: Network.SOGS.CapabilitiesResponse = Network.SOGS.CapabilitiesResponse( capabilities: allCapabilities .filter { !$0.isMissing } - .map { $0.variant }, + .map { $0.variant.rawValue }, missing: { - let missingCapabilities: [Capability.Variant] = allCapabilities + let missingCapabilities: [String] = allCapabilities .filter { $0.isMissing } - .map { $0.variant } + .map { $0.variant.rawValue } return (missingCapabilities.isEmpty ? nil : missingCapabilities) }() @@ -452,22 +454,22 @@ public final class CommunityPoller: CommunityPollerType & PollerType { return (capabilities, groups) } - .flatMap { [pollerDestination, dependencies] (capabilities: OpenGroupAPI.Capabilities, groups: [OpenGroup]) -> AnyPublisher in - let changedResponses: [OpenGroupAPI.Endpoint: Any] = validResponses + .flatMap { [pollerDestination, dependencies] (capabilities: Network.SOGS.CapabilitiesResponse, groups: [OpenGroup]) -> AnyPublisher in + let changedResponses: [Network.SOGS.Endpoint: Any] = validResponses .filter { endpoint, data in switch endpoint { case .capabilities: guard - let responseData: Network.BatchSubResponse = data as? Network.BatchSubResponse, - let responseBody: OpenGroupAPI.Capabilities = responseData.body + let responseData: Network.BatchSubResponse = data as? Network.BatchSubResponse, + let responseBody: Network.SOGS.CapabilitiesResponse = responseData.body else { return false } return (responseBody != capabilities) case .roomPollInfo(let roomToken, _): guard - let responseData: Network.BatchSubResponse = data as? Network.BatchSubResponse, - let responseBody: OpenGroupAPI.RoomPollInfo = responseData.body + let responseData: Network.BatchSubResponse = data as? Network.BatchSubResponse, + let responseBody: Network.SOGS.RoomPollInfo = responseData.body else { return false } guard let existingOpenGroup: OpenGroup = groups.first(where: { $0.roomToken == roomToken }) else { return true @@ -507,8 +509,8 @@ public final class CommunityPoller: CommunityPollerType & PollerType { switch endpoint { case .capabilities: guard - let responseData: Network.BatchSubResponse = data as? Network.BatchSubResponse, - let responseBody: OpenGroupAPI.Capabilities = responseData.body + let responseData: Network.BatchSubResponse = data as? Network.BatchSubResponse, + let responseBody: Network.SOGS.CapabilitiesResponse = responseData.body else { return } OpenGroupManager.handleCapabilities( @@ -519,8 +521,8 @@ public final class CommunityPoller: CommunityPollerType & PollerType { case .roomPollInfo(let roomToken, _): guard - let responseData: Network.BatchSubResponse = data as? Network.BatchSubResponse, - let responseBody: OpenGroupAPI.RoomPollInfo = responseData.body + let responseData: Network.BatchSubResponse = data as? Network.BatchSubResponse, + let responseBody: Network.SOGS.RoomPollInfo = responseData.body else { return } try OpenGroupManager.handlePollInfo( @@ -534,8 +536,8 @@ public final class CommunityPoller: CommunityPollerType & PollerType { case .roomMessagesRecent(let roomToken), .roomMessagesBefore(let roomToken, _), .roomMessagesSince(let roomToken, _): guard - let responseData: Network.BatchSubResponse<[Failable]> = data as? Network.BatchSubResponse<[Failable]>, - let responseBody: [Failable] = responseData.body + let responseData: Network.BatchSubResponse<[Failable]> = data as? Network.BatchSubResponse<[Failable]>, + let responseBody: [Failable] = responseData.body else { return } interactionInfo.append( @@ -550,12 +552,12 @@ public final class CommunityPoller: CommunityPollerType & PollerType { case .inbox, .inboxSince, .outbox, .outboxSince: guard - let responseData: Network.BatchSubResponse<[OpenGroupAPI.DirectMessage]?> = data as? Network.BatchSubResponse<[OpenGroupAPI.DirectMessage]?>, + let responseData: Network.BatchSubResponse<[Network.SOGS.DirectMessage]?> = data as? Network.BatchSubResponse<[Network.SOGS.DirectMessage]?>, !responseData.failedToParseBody else { return } // Double optional because the server can return a `304` with an empty body - let messages: [OpenGroupAPI.DirectMessage] = ((responseData.body ?? []) ?? []) + let messages: [Network.SOGS.DirectMessage] = ((responseData.body ?? []) ?? []) let fromOutbox: Bool = { switch endpoint { case .outbox, .outboxSince: return true @@ -734,4 +736,4 @@ public extension CommunityPollerCacheType { // MARK: - Conformance -extension OpenGroupAPI.RoomInfo: FetchableRecord {} +extension Network.SOGS.PollRoomInfo: @retroactive FetchableRecord {} diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift index e9290f1800..57c63b8be8 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift @@ -3,7 +3,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Singleton @@ -33,7 +33,7 @@ public extension Singleton { // MARK: - CurrentUserPoller public final class CurrentUserPoller: SwarmPoller { - public static let namespaces: [SnodeAPI.Namespace] = [ + public static let namespaces: [Network.SnodeAPI.Namespace] = [ .default, .configUserProfile, .configContacts, .configConvoInfoVolatile, .configUserGroups ] private let pollInterval: TimeInterval = 1.5 diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/GroupPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/GroupPoller.swift index 667ec2909c..7e816d4a4f 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/GroupPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/GroupPoller.swift @@ -3,7 +3,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Cache @@ -23,7 +23,7 @@ public final class GroupPoller: SwarmPoller { private let minPollInterval: Double = 3 private let maxPollInterval: Double = 30 - public static func namespaces(swarmPublicKey: String) -> [SnodeAPI.Namespace] { + public static func namespaces(swarmPublicKey: String) -> [Network.SnodeAPI.Namespace] { guard (try? SessionId.Prefix(from: swarmPublicKey)) == .group else { return [.legacyClosedGroup] } @@ -62,7 +62,7 @@ public final class GroupPoller: SwarmPoller { .flatMap { [receivedPollResponse] _ in receivedPollResponse } .first() .map { $0.filter { $0.isConfigMessage } } - .filter { !$0.contains(where: { $0.namespace == SnodeAPI.Namespace.configGroupKeys }) } + .filter { !$0.contains(where: { $0.namespace == Network.SnodeAPI.Namespace.configGroupKeys }) } .sinkUntilComplete( receiveValue: { [pollerDestination, pollerName, dependencies] configMessages in Log.error(.poller, "\(pollerName) received no config messages in it's first poll, flagging as expired.") diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/PollerType.swift b/SessionMessagingKit/Sending & Receiving/Pollers/PollerType.swift index 8cdacd5c99..c3e16e5fc5 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/PollerType.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/PollerType.swift @@ -4,7 +4,7 @@ import Foundation import Combine -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Log.Category @@ -64,7 +64,7 @@ public protocol PollerType: AnyObject { pollerQueue: DispatchQueue, pollerDestination: PollerDestination, pollerDrainBehaviour: ThreadSafeObject, - namespaces: [SnodeAPI.Namespace], + namespaces: [Network.SnodeAPI.Namespace], failureCount: Int, shouldStoreMessages: Bool, logStartAndStopCalls: Bool, diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/SwarmPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/SwarmPoller.swift index b632d6d87d..ab0851838b 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/SwarmPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/SwarmPoller.swift @@ -3,7 +3,7 @@ import Foundation import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - SwarmPollerType @@ -41,7 +41,7 @@ public class SwarmPoller: SwarmPollerType & PollerType { public var lastPollStart: TimeInterval = 0 public var cancellable: AnyCancellable? - private let namespaces: [SnodeAPI.Namespace] + private let namespaces: [Network.SnodeAPI.Namespace] private let customAuthMethod: AuthenticationMethod? private let shouldStoreMessages: Bool private let receivedPollResponseSubject: PassthroughSubject = PassthroughSubject() @@ -53,7 +53,7 @@ public class SwarmPoller: SwarmPollerType & PollerType { pollerQueue: DispatchQueue, pollerDestination: PollerDestination, pollerDrainBehaviour: ThreadSafeObject, - namespaces: [SnodeAPI.Namespace], + namespaces: [Network.SnodeAPI.Namespace], failureCount: Int = 0, shouldStoreMessages: Bool, logStartAndStopCalls: Bool, @@ -108,8 +108,8 @@ public class SwarmPoller: SwarmPollerType & PollerType { /// Fetch the messages return dependencies[singleton: .network] .getSwarm(for: pollerDestination.target) - .tryFlatMapWithRandomSnode(drainBehaviour: _pollerDrainBehaviour, using: dependencies) { [pollerDestination, customAuthMethod, namespaces, dependencies] snode -> AnyPublisher<(LibSession.Snode, Network.PreparedRequest), Error> in - dependencies[singleton: .storage].readPublisher { db -> (LibSession.Snode, Network.PreparedRequest) in + .tryFlatMapWithRandomSnode(drainBehaviour: _pollerDrainBehaviour, using: dependencies) { [pollerDestination, customAuthMethod, namespaces, dependencies] snode -> AnyPublisher<(LibSession.Snode, Network.PreparedRequest), Error> in + dependencies[singleton: .storage].readPublisher { db -> (LibSession.Snode, Network.PreparedRequest) in let authMethod: AuthenticationMethod = try (customAuthMethod ?? Authentication.with( db, swarmPublicKey: pollerDestination.target, @@ -118,7 +118,7 @@ public class SwarmPoller: SwarmPollerType & PollerType { return ( snode, - try SnodeAPI.preparedPoll( + try Network.SnodeAPI.preparedPoll( db, namespaces: namespaces, refreshingConfigHashes: activeHashes, @@ -134,10 +134,10 @@ public class SwarmPoller: SwarmPollerType & PollerType { .map { _, response in (snode, response) } } .flatMapStorageWritePublisher(using: dependencies, updates: { [pollerDestination, shouldStoreMessages, forceSynchronousProcessing, dependencies] db, info -> ([Job], [Job], PollResult) in - let (snode, namespacedResults): (LibSession.Snode, SnodeAPI.PollResponse) = info + let (snode, namespacedResults): (LibSession.Snode, Network.SnodeAPI.PollResponse) = info /// Get all of the messages and sort them by their required `processingOrder` - typealias MessageData = (namespace: SnodeAPI.Namespace, messages: [SnodeReceivedMessage], lastHash: String?) + typealias MessageData = (namespace: Network.SnodeAPI.Namespace, messages: [SnodeReceivedMessage], lastHash: String?) let sortedMessages: [MessageData] = namespacedResults .compactMap { namespace, result -> MessageData? in (result.data?.messages).map { (namespace, $0, result.data?.lastHash) } @@ -233,7 +233,7 @@ public class SwarmPoller: SwarmPollerType & PollerType { shouldStoreMessages: Bool, ignoreDedupeFiles: Bool, forceSynchronousProcessing: Bool, - sortedMessages: [(namespace: SnodeAPI.Namespace, messages: [SnodeReceivedMessage], lastHash: String?)], + sortedMessages: [(namespace: Network.SnodeAPI.Namespace, messages: [SnodeReceivedMessage], lastHash: String?)], using dependencies: Dependencies ) -> ([Job], [Job], PollResult) { /// No need to do anything if there are no messages @@ -386,16 +386,17 @@ public class SwarmPoller: SwarmPollerType & PollerType { } } - /// Make sure to add any synchronously processed messages to the `finalProcessedMessages` - /// as otherwise they wouldn't be emitted by the `receivedPollResponseSubject` + /// Make sure to add any synchronously processed messages to the `finalProcessedMessages` as otherwise + /// they wouldn't be emitted by the `receivedPollResponseSubject`, also need to add the count to + /// `messageCount` to ensure it's not incorrect finalProcessedMessages += processedMessages + messageCount += processedMessages.count return nil } .flatMap { $0 } /// If we don't want to store the messages then no need to continue (don't want to create message receive jobs or mess with cached hashes) guard shouldStoreMessages && !forceSynchronousProcessing else { - messageCount += allProcessedMessages.count finalProcessedMessages += allProcessedMessages return ([], [], (finalProcessedMessages, rawMessageCount, messageCount, hadValidHashUpdate)) } diff --git a/SessionMessagingKit/Sending & Receiving/Typing Indicators/TypingIndicators.swift b/SessionMessagingKit/Sending & Receiving/Typing Indicators/TypingIndicators.swift index 3f5da3a998..3486db29e6 100644 --- a/SessionMessagingKit/Sending & Receiving/Typing Indicators/TypingIndicators.swift +++ b/SessionMessagingKit/Sending & Receiving/Typing Indicators/TypingIndicators.swift @@ -3,7 +3,7 @@ import Foundation import GRDB import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit // MARK: - Singleton diff --git a/SessionMessagingKit/Shared Models/MessageViewModel+DeletionActions.swift b/SessionMessagingKit/Shared Models/MessageViewModel+DeletionActions.swift index 05553088ed..67fd9318a2 100644 --- a/SessionMessagingKit/Shared Models/MessageViewModel+DeletionActions.swift +++ b/SessionMessagingKit/Shared Models/MessageViewModel+DeletionActions.swift @@ -4,14 +4,14 @@ import Foundation import Combine import GRDB import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit public extension MessageViewModel { struct DeletionBehaviours { public enum Behaviour { case markAsDeleted(ids: [Int64], options: Interaction.DeletionOption, threadId: String, threadVariant: SessionThread.Variant) - case deleteFromDatabase(ids: [Int64], threadId: String) + case deleteFromDatabase([Int64]) case cancelPendingSendJobs([Int64]) case preparedRequest(Network.PreparedRequest) } @@ -97,14 +97,9 @@ public extension MessageViewModel { ) } - case .deleteFromDatabase(let ids, let threadId): + case .deleteFromDatabase(let ids): result = result.flatMapStorageWritePublisher(using: dependencies) { db, _ in - _ = try Interaction - .filter(ids: ids) - .deleteAll(db) - ids.forEach { id in - db.addMessageEvent(id: id, threadId: threadId, type: .deleted) - } + try Interaction.deleteWhere(db, .filter(ids.contains(Interaction.Columns.id))) } case .preparedRequest(let preparedRequest): @@ -129,7 +124,7 @@ public extension MessageViewModel.DeletionBehaviours { enum SelectedMessageState { case outgoingOnly case containsIncoming - case containsDeletedOrControlMessages + case containsLocalOnlyMessages /// Control, pending or deleted messages } /// If it's a legacy group and they have been deprecated then the user shouldn't be able to delete messages @@ -139,8 +134,9 @@ public extension MessageViewModel.DeletionBehaviours { let state: SelectedMessageState = { guard !cellViewModels.contains(where: { $0.variant.isDeletedMessage }) && - !cellViewModels.contains(where: { $0.variant.isInfoMessage }) - else { return .containsDeletedOrControlMessages } + !cellViewModels.contains(where: { $0.variant.isInfoMessage }) && + !cellViewModels.contains(where: { $0.state == .sending || $0.state == .failed }) + else { return .containsLocalOnlyMessages } return (cellViewModels.contains(where: { $0.variant == .standardIncoming }) ? .containsIncoming : @@ -176,8 +172,8 @@ public extension MessageViewModel.DeletionBehaviours { }() switch (state, isAdmin) { - /// User selects messages including a control message or “deleted” message - case (.containsDeletedOrControlMessages, _): + /// User selects messages including a control, pending or “deleted” message + case (.containsLocalOnlyMessages, _): return MessageViewModel.DeletionBehaviours( title: "deleteMessage" .putNumber(cellViewModels.count) @@ -203,13 +199,12 @@ public extension MessageViewModel.DeletionBehaviours { /// Control messages and deleted messages should be immediately deleted from the database .deleteFromDatabase( - ids: cellViewModels + cellViewModels .filter { viewModel in viewModel.variant.isInfoMessage || viewModel.variant.isDeletedMessage } - .map { $0.id }, - threadId: threadData.threadId + .map { $0.id } ), /// Other message types should only be marked as deleted @@ -421,7 +416,7 @@ public extension MessageViewModel.DeletionBehaviours { .chunked(by: Network.BatchRequest.childRequestLimit) .map { unsendRequestChunk in .preparedRequest( - try SnodeAPI.preparedBatch( + try Network.SnodeAPI.preparedBatch( requests: unsendRequestChunk, requireAllBatchResponses: false, swarmPublicKey: threadData.threadId, @@ -432,7 +427,7 @@ public extension MessageViewModel.DeletionBehaviours { ) .appending(serverHashes.isEmpty ? nil : .preparedRequest( - try SnodeAPI.preparedDeleteMessages( + try Network.SnodeAPI.preparedDeleteMessages( serverHashes: Array(serverHashes), requireSuccessfulDeletion: false, authMethod: try Authentication.with( @@ -447,10 +442,7 @@ public extension MessageViewModel.DeletionBehaviours { ) .appending(threadData.threadIsNoteToSelf ? /// If it's the `Note to Self`conversation then we want to just delete the interaction - .deleteFromDatabase( - ids: cellViewModels.map { $0.id }, - threadId: threadData.threadId - ) : + .deleteFromDatabase(cellViewModels.map { $0.id }) : .markAsDeleted( ids: targetViewModels.map { $0.id }, options: [.local, .network], @@ -505,7 +497,7 @@ public extension MessageViewModel.DeletionBehaviours { .chunked(by: Network.BatchRequest.childRequestLimit) .map { unsendRequestChunk in .preparedRequest( - try SnodeAPI.preparedBatch( + try Network.SnodeAPI.preparedBatch( requests: unsendRequestChunk, requireAllBatchResponses: false, swarmPublicKey: threadData.threadId, @@ -625,7 +617,7 @@ public extension MessageViewModel.DeletionBehaviours { ) ) .appending(serverHashes.isEmpty ? nil : - .preparedRequest(try SnodeAPI + .preparedRequest(try Network.SnodeAPI .preparedDeleteMessages( serverHashes: Array(serverHashes), requireSuccessfulDeletion: false, @@ -667,7 +659,7 @@ public extension MessageViewModel.DeletionBehaviours { let deleteRequests: [Network.PreparedRequest] = try cellViewModels .compactMap { $0.openGroupServerMessageId } .map { messageId in - try OpenGroupAPI.preparedMessageDelete( + try Network.SOGS.preparedMessageDelete( id: messageId, roomToken: roomToken, authMethod: authMethod, @@ -683,7 +675,7 @@ public extension MessageViewModel.DeletionBehaviours { .chunked(by: Network.BatchRequest.childRequestLimit) .map { deleteRequestsChunk in .preparedRequest( - try OpenGroupAPI.preparedBatch( + try Network.SOGS.preparedBatch( requests: deleteRequestsChunk, authMethod: authMethod, using: dependencies @@ -693,10 +685,7 @@ public extension MessageViewModel.DeletionBehaviours { } ) .appending( - .deleteFromDatabase( - ids: cellViewModels.map { $0.id }, - threadId: threadData.threadId - ) + .deleteFromDatabase(cellViewModels.map { $0.id }) ) } } diff --git a/SessionMessagingKit/Utilities/AttachmentManager.swift b/SessionMessagingKit/Utilities/AttachmentManager.swift index 33f95cdb4c..394cb1fef0 100644 --- a/SessionMessagingKit/Utilities/AttachmentManager.swift +++ b/SessionMessagingKit/Utilities/AttachmentManager.swift @@ -7,7 +7,7 @@ import Combine import UniformTypeIdentifiers import GRDB import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Singleton @@ -210,7 +210,7 @@ public final class AttachmentManager: Sendable, ThumbnailManager { // Process image attachments if UTType.isImage(contentType) || UTType.isAnimated(contentType) { return ( - Data.isValidImage(at: path, type: UTType(sessionMimeType: contentType), using: dependencies), + MediaUtils.isValidImage(at: path, type: UTType(sessionMimeType: contentType), using: dependencies), nil ) } diff --git a/SessionMessagingKit/Utilities/Authentication+SessionMessagingKit.swift b/SessionMessagingKit/Utilities/Authentication+SessionMessagingKit.swift index 4a1069c1ca..d51fb9fd78 100644 --- a/SessionMessagingKit/Utilities/Authentication+SessionMessagingKit.swift +++ b/SessionMessagingKit/Utilities/Authentication+SessionMessagingKit.swift @@ -2,7 +2,7 @@ import Foundation import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Authentication Types @@ -78,38 +78,6 @@ public extension Authentication { } } } - - /// Used when interacting with a community - struct community: AuthenticationMethod { - public let openGroupCapabilityInfo: LibSession.OpenGroupCapabilityInfo - public let forceBlinded: Bool - - public var server: String { openGroupCapabilityInfo.server } - public var publicKey: String { openGroupCapabilityInfo.publicKey } - public var hasCapabilities: Bool { !openGroupCapabilityInfo.capabilities.isEmpty } - public var supportsBlinding: Bool { openGroupCapabilityInfo.capabilities.contains(.blind) } - - public var info: Info { - .community( - server: server, - publicKey: publicKey, - hasCapabilities: hasCapabilities, - supportsBlinding: supportsBlinding, - forceBlinded: forceBlinded - ) - } - - public init(info: LibSession.OpenGroupCapabilityInfo, forceBlinded: Bool = false) { - self.openGroupCapabilityInfo = info - self.forceBlinded = forceBlinded - } - - // MARK: - SignatureGenerator - - public func generateSignature(with verificationBytes: [UInt8], using dependencies: Dependencies) throws -> Authentication.Signature { - throw CryptoError.signatureGenerationFailed - } - } } // MARK: - Convenience @@ -119,6 +87,19 @@ fileprivate struct GroupAuthData: Codable, FetchableRecord { let authData: Data? } +public extension Authentication.community { + init(info: LibSession.OpenGroupCapabilityInfo, forceBlinded: Bool = false) { + self.init( + roomToken: info.roomToken, + server: info.server, + publicKey: info.publicKey, + hasCapabilities: !info.capabilities.isEmpty, + supportsBlinding: info.capabilities.contains(.blind), + forceBlinded: forceBlinded + ) + } +} + public extension Authentication { static func with( _ db: ObservingDatabase, diff --git a/SessionMessagingKit/Utilities/DisplayPictureManager.swift b/SessionMessagingKit/Utilities/DisplayPictureManager.swift index f544f6eb3e..31f849baaa 100644 --- a/SessionMessagingKit/Utilities/DisplayPictureManager.swift +++ b/SessionMessagingKit/Utilities/DisplayPictureManager.swift @@ -4,7 +4,7 @@ import UIKit import Combine import GRDB import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Singleton @@ -25,38 +25,38 @@ public extension Log.Category { // MARK: - DisplayPictureManager public class DisplayPictureManager { - public typealias UploadResult = (downloadUrl: String, filePath: String, encryptionKey: Data) + public typealias UploadResult = (downloadUrl: String, filePath: String, encryptionKey: Data, expries: Date?) public enum Update { case none case contactRemove - case contactUpdateTo(url: String, key: Data, filePath: String) + case contactUpdateTo(url: String, key: Data, filePath: String, contactProProof: String?) case currentUserRemove - case currentUserUploadImageData(Data) - case currentUserUpdateTo(url: String, key: Data, filePath: String) + case currentUserUploadImageData(data: Data, isReupload: Bool) + case currentUserUpdateTo(url: String, key: Data, filePath: String, sessionProProof: String?) case groupRemove case groupUploadImageData(Data) case groupUpdateTo(url: String, key: Data, filePath: String) static func from(_ profile: VisibleMessage.VMProfile, fallback: Update, using dependencies: Dependencies) -> Update { - return from(profile.profilePictureUrl, key: profile.profileKey, fallback: fallback, using: dependencies) + return from(profile.profilePictureUrl, key: profile.profileKey, contactProProof: profile.sessionProProof, fallback: fallback, using: dependencies) } public static func from(_ profile: Profile, fallback: Update, using dependencies: Dependencies) -> Update { - return from(profile.displayPictureUrl, key: profile.displayPictureEncryptionKey, fallback: fallback, using: dependencies) + return from(profile.displayPictureUrl, key: profile.displayPictureEncryptionKey, contactProProof: profile.sessionProProof, fallback: fallback, using: dependencies) } - static func from(_ url: String?, key: Data?, fallback: Update, using dependencies: Dependencies) -> Update { + static func from(_ url: String?, key: Data?, contactProProof: String?, fallback: Update, using dependencies: Dependencies) -> Update { guard let url: String = url, let key: Data = key, let filePath: String = try? dependencies[singleton: .displayPictureManager].path(for: url) else { return fallback } - return .contactUpdateTo(url: url, key: key, filePath: filePath) + return .contactUpdateTo(url: url, key: key, filePath: filePath, contactProProof: contactProProof) } } @@ -182,7 +182,7 @@ public class DisplayPictureManager { // MARK: - Uploading - public func prepareAndUploadDisplayPicture(imageData: Data) -> AnyPublisher { + public func prepareAndUploadDisplayPicture(imageData: Data, compression: Bool) -> AnyPublisher { return Just(()) .setFailureType(to: DisplayPictureError.self) .tryMap { [dependencies] _ -> (Network.PreparedRequest, String, Data) in @@ -191,7 +191,7 @@ public class DisplayPictureManager { let newEncryptionKey: Data let finalImageData: Data let fileExtension: String - let guessedFormat: ImageFormat = imageData.guessedImageFormat + let guessedFormat: ImageFormat = MediaUtils.guessedImageFormat(data: imageData) finalImageData = try { switch guessedFormat { @@ -223,7 +223,7 @@ public class DisplayPictureManager { image = image.resized(toFillPixelSize: CGSize(width: DisplayPictureManager.maxDiameter, height: DisplayPictureManager.maxDiameter)) } - guard let data: Data = image.jpegData(compressionQuality: 0.95) else { + guard let data: Data = image.jpegData(compressionQuality: (compression ? 0.95 : 1.0)) else { Log.error(.displayPictureManager, "Updating service with profile failed.") throw DisplayPictureError.writeFailed } @@ -298,12 +298,13 @@ public class DisplayPictureManager { } .eraseToAnyPublisher() } - .tryMap { [dependencies] fileUploadResponse, temporaryFilePath, newEncryptionKey -> (String, String, Data) in + .tryMap { [dependencies] fileUploadResponse, temporaryFilePath, newEncryptionKey -> (String, Date?, String, Data) in let downloadUrl: String = Network.FileServer.downloadUrlString(for: fileUploadResponse.id) + let expries: Date? = fileUploadResponse.expires.map { Date(timeIntervalSince1970: $0)} let finalFilePath: String = try dependencies[singleton: .displayPictureManager].path(for: downloadUrl) try dependencies[singleton: .fileManager].moveItem(atPath: temporaryFilePath, toPath: finalFilePath) - return (downloadUrl, finalFilePath, newEncryptionKey) + return (downloadUrl, expries, finalFilePath, newEncryptionKey) } .mapError { error in Log.error(.displayPictureManager, "Updating service with profile failed with error: \(error).") @@ -314,7 +315,7 @@ public class DisplayPictureManager { default: return DisplayPictureError.uploadFailed } } - .map { [dependencies] downloadUrl, finalFilePath, newEncryptionKey -> UploadResult in + .map { [dependencies] downloadUrl, expires, finalFilePath, newEncryptionKey -> UploadResult in /// Load the data into the `imageDataManager` (assuming we will use it elsewhere in the UI) Task(priority: .userInitiated) { await dependencies[singleton: .imageDataManager].load( @@ -323,7 +324,7 @@ public class DisplayPictureManager { } Log.verbose(.displayPictureManager, "Successfully uploaded avatar image.") - return (downloadUrl, finalFilePath, newEncryptionKey) + return (downloadUrl, finalFilePath, newEncryptionKey, expires) } .eraseToAnyPublisher() } diff --git a/SessionMessagingKit/Utilities/ExtensionHelper.swift b/SessionMessagingKit/Utilities/ExtensionHelper.swift index 0b04520659..0075b91d78 100644 --- a/SessionMessagingKit/Utilities/ExtensionHelper.swift +++ b/SessionMessagingKit/Utilities/ExtensionHelper.swift @@ -1,7 +1,7 @@ // Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. import Foundation -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Singleton @@ -738,7 +738,7 @@ public class ExtensionHelper: ExtensionHelperType { } public func loadMessages() async throws { - typealias MessageData = (namespace: SnodeAPI.Namespace, messages: [SnodeReceivedMessage], lastHash: String?) + typealias MessageData = (namespace: Network.SnodeAPI.Namespace, messages: [SnodeReceivedMessage], lastHash: String?) /// Retrieve all conversation file paths /// @@ -781,7 +781,7 @@ public class ExtensionHelper: ExtensionHelperType { do { let sortedMessages: [MessageData] = try configMessageHashes - .reduce([SnodeAPI.Namespace: [SnodeReceivedMessage]]()) { (result: [SnodeAPI.Namespace: [SnodeReceivedMessage]], hash: String) in + .reduce([Network.SnodeAPI.Namespace: [SnodeReceivedMessage]]()) { (result: [Network.SnodeAPI.Namespace: [SnodeReceivedMessage]], hash: String) in let path: String = URL(fileURLWithPath: this.conversationsPath) .appendingPathComponent(conversationHash) .appendingPathComponent(this.conversationConfigDir) @@ -865,29 +865,58 @@ public class ExtensionHelper: ExtensionHelperType { } ) - allMessagePaths.forEach { path in - do { - let plaintext: Data = try this.read(from: path) - let message: SnodeReceivedMessage = try JSONDecoder(using: dependencies) - .decode(SnodeReceivedMessage.self, from: plaintext) - - SwarmPoller.processPollResponse( + let sortedMessages: [MessageData] = allMessagePaths + .reduce([Network.SnodeAPI.Namespace: [SnodeReceivedMessage]]()) { (result: [Network.SnodeAPI.Namespace: [SnodeReceivedMessage]], path: String) in + do { + let plaintext: Data = try this.read(from: path) + let message: SnodeReceivedMessage = try JSONDecoder(using: dependencies) + .decode(SnodeReceivedMessage.self, from: plaintext) + + return result.appending(message, toArrayOn: message.namespace) + } + catch { + failureStandardCount += 1 + Log.error(.cat, "Discarding standard message due to error: \(error)") + return result + } + } + .map { namespace, messages -> MessageData in + /// We need to sort the messages as we don't know what order they were read from disk in and some + /// messages (eg. a `VisibleMessage` and it's corresponding `UnsendRequest`) need to be + /// processed in a particular order or they won't behave correctly, luckily the `SnodeReceivedMessage.timestampMs` + /// is the "network offset" timestamp when the message was sent to the storage server (rather than the + /// "sent timestamp" on the message, which for an `UnsendRequest` will match it's associate message) + /// so we can just sort by that + ( + namespace, + messages.sorted { $0.timestampMs < $1.timestampMs }, + nil + ) + } + .sorted { lhs, rhs in lhs.namespace.processingOrder < rhs.namespace.processingOrder } + + /// Process the message (inserting into the database if needed (messages are processed per conversaiton so + /// all have the same `swarmPublicKey`) + switch sortedMessages.first?.messages.first?.swarmPublicKey { + case .none: break + case .some(let swarmPublicKey): + let (_, _, result) = SwarmPoller.processPollResponse( db, cat: .cat, source: .pushNotification, - swarmPublicKey: message.swarmPublicKey, + swarmPublicKey: swarmPublicKey, shouldStoreMessages: true, ignoreDedupeFiles: true, forceSynchronousProcessing: true, - sortedMessages: [(message.namespace, [message], nil)], + sortedMessages: sortedMessages, using: dependencies ) - successStandardCount += 1 - } - catch { - failureStandardCount += 1 - Log.error(.cat, "Discarding standard message due to error: \(error)") - } + successStandardCount += result.validMessageCount + + if result.validMessageCount != result.rawMessageCount { + failureStandardCount += (result.rawMessageCount - result.validMessageCount) + Log.error(.cat, "Discarding some standard messages due to error: \(MessageReceiverError.failedToProcess)") + } } /// Remove the standard message files now that they are processed diff --git a/SessionMessagingKit/Utilities/MessageWrapper.swift b/SessionMessagingKit/Utilities/MessageWrapper.swift index 1e7f97ba3c..3ff9796b7b 100644 --- a/SessionMessagingKit/Utilities/MessageWrapper.swift +++ b/SessionMessagingKit/Utilities/MessageWrapper.swift @@ -1,7 +1,7 @@ // stringlint:disable import Foundation -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit public enum MessageWrapper { diff --git a/SessionMessagingKit/Utilities/Profile+CurrentUser.swift b/SessionMessagingKit/Utilities/Profile+Updating.swift similarity index 70% rename from SessionMessagingKit/Utilities/Profile+CurrentUser.swift rename to SessionMessagingKit/Utilities/Profile+Updating.swift index 0796e4f761..8baf719cdd 100644 --- a/SessionMessagingKit/Utilities/Profile+CurrentUser.swift +++ b/SessionMessagingKit/Utilities/Profile+Updating.swift @@ -78,12 +78,13 @@ public extension Profile { } } + let profileUpdateTimestampMs: Double = dependencies[cache: .snodeAPI].currentOffsetTimestampMs() try Profile.updateIfNeeded( db, publicKey: userSessionId.hexString, displayNameUpdate: displayNameUpdate, displayPictureUpdate: displayPictureUpdate, - sentTimestamp: dependencies.dateNow.timeIntervalSince1970, + profileUpdateTimestamp: TimeInterval(profileUpdateTimestampMs / 1000), using: dependencies ) Log.info(.profile, "Successfully updated user profile.") @@ -91,11 +92,12 @@ public extension Profile { .mapError { _ in DisplayPictureError.databaseChangesFailed } .eraseToAnyPublisher() - case .currentUserUploadImageData(let data): + case .currentUserUploadImageData(let data, let isReupload): return dependencies[singleton: .displayPictureManager] - .prepareAndUploadDisplayPicture(imageData: data) + .prepareAndUploadDisplayPicture(imageData: data, compression: !isReupload) .mapError { $0 as Error } .flatMapStorageWritePublisher(using: dependencies, updates: { db, result in + let profileUpdateTimestamp: TimeInterval = dependencies[cache: .snodeAPI].currentOffsetTimestampMs() / 1000 try Profile.updateIfNeeded( db, publicKey: userSessionId.hexString, @@ -103,12 +105,15 @@ public extension Profile { displayPictureUpdate: .currentUserUpdateTo( url: result.downloadUrl, key: result.encryptionKey, - filePath: result.filePath + filePath: result.filePath, + sessionProProof: dependencies.mutate(cache: .libSession) { $0.getProProof() } ), - sentTimestamp: dependencies.dateNow.timeIntervalSince1970, + profileUpdateTimestamp: profileUpdateTimestamp, + isReuploadCurrentUserProfilePicture: isReupload, using: dependencies ) + dependencies[defaults: .standard, key: .profilePictureExpiresDate] = result.expries dependencies[defaults: .standard, key: .lastProfilePictureUpload] = dependencies.dateNow Log.info(.profile, "Successfully updated user profile.") }) @@ -120,7 +125,36 @@ public extension Profile { } .eraseToAnyPublisher() } - } + } + + /// To try to maintain backwards compatibility with profile changes we want to continue to accept profile changes from old clients if + /// we haven't received a profile update from a new client yet otherwise, if we have, then we should only accept profile changes if + /// they are newer that our cached version of the profile data + static func shouldUpdateProfile( + _ profileUpdateTimestamp: TimeInterval?, + profile: Profile, + using dependencies: Dependencies + ) -> Bool { + /// We should consider `libSession` the source-of-truth for profile data for contacts so try to retrieve the profile data from + /// there before falling back to the one fetched from the database + let targetProfile: Profile = ( + dependencies.mutate(cache: .libSession) { $0.profile(contactId: profile.id) } ?? + profile + ) + let finalProfileUpdateTimestamp: TimeInterval = (profileUpdateTimestamp ?? 0) + let finalCachedProfileUpdateTimestamp: TimeInterval = (targetProfile.profileLastUpdated ?? 0) + + /// If neither the profile update or the cached profile have a timestamp then we should just always accept the update + /// + /// **Note:** We check if they are equal to `0` here because the default value from `libSession` will be `0` + /// rather than `null` + guard finalProfileUpdateTimestamp != 0 || finalCachedProfileUpdateTimestamp != 0 else { + return true + } + + /// Otherwise we should only accept the update if it's newer than our cached value + return (finalProfileUpdateTimestamp > finalCachedProfileUpdateTimestamp) + } static func updateIfNeeded( _ db: ObservingDatabase, @@ -128,36 +162,24 @@ public extension Profile { displayNameUpdate: DisplayNameUpdate = .none, displayPictureUpdate: DisplayPictureManager.Update, blocksCommunityMessageRequests: Bool? = nil, - sentTimestamp: TimeInterval, + profileUpdateTimestamp: TimeInterval?, + isReuploadCurrentUserProfilePicture: Bool = false, using dependencies: Dependencies ) throws { let isCurrentUser = (publicKey == dependencies[cache: .general].sessionId.hexString) let profile: Profile = Profile.fetchOrCreate(db, id: publicKey) var profileChanges: [ConfigColumnAssignment] = [] - /// There were some bugs (somewhere) where some of these timestamps valid could be in seconds or milliseconds so we need to try to - /// detect this and convert it to proper seconds (if we don't then we will never update the profile) - func convertToSections(_ maybeValue: Double?) -> TimeInterval { - guard let value: Double = maybeValue else { return 0 } - - if value > 9_000_000_000_000 { // Microseconds - return (value / 1_000_000) - } else if value > 9_000_000_000 { // Milliseconds - return (value / 1000) - } - - return TimeInterval(value) // Seconds + guard shouldUpdateProfile(profileUpdateTimestamp, profile: profile, using: dependencies) else { + return } // Name - // FIXME: This 'lastNameUpdate' approach is buggy - we should have a timestamp on the ConvoInfoVolatile - switch (displayNameUpdate, isCurrentUser, (sentTimestamp > convertToSections(profile.lastNameUpdate))) { - case (.none, _, _): break - case (.currentUserUpdate(let name), true, _), (.contactUpdate(let name), false, true): + switch (displayNameUpdate, isCurrentUser) { + case (.none, _): break + case (.currentUserUpdate(let name), true), (.contactUpdate(let name), false): guard let name: String = name, !name.isEmpty, name != profile.name else { break } - profileChanges.append(Profile.Columns.lastNameUpdate.set(to: sentTimestamp)) - if profile.name != name { profileChanges.append(Profile.Columns.name.set(to: name)) db.addProfileEvent(id: publicKey, change: .name(name)) @@ -168,9 +190,8 @@ public extension Profile { } // Blocks community message requests flag - if let blocksCommunityMessageRequests: Bool = blocksCommunityMessageRequests, sentTimestamp > convertToSections(profile.lastBlocksCommunityMessageRequests) { + if let blocksCommunityMessageRequests: Bool = blocksCommunityMessageRequests { profileChanges.append(Profile.Columns.blocksCommunityMessageRequests.set(to: blocksCommunityMessageRequests)) - profileChanges.append(Profile.Columns.lastBlocksCommunityMessageRequests.set(to: sentTimestamp)) } // Profile picture & profile key @@ -180,8 +201,6 @@ public extension Profile { preconditionFailure("Invalid options for this function") case (.contactRemove, false), (.currentUserRemove, true): - profileChanges.append(Profile.Columns.displayPictureLastUpdated.set(to: sentTimestamp)) - if profile.displayPictureEncryptionKey != nil { profileChanges.append(Profile.Columns.displayPictureEncryptionKey.set(to: nil)) } @@ -191,8 +210,8 @@ public extension Profile { db.addProfileEvent(id: publicKey, change: .displayPictureUrl(nil)) } - case (.contactUpdateTo(let url, let key, let filePath), false), - (.currentUserUpdateTo(let url, let key, let filePath), true): + case (.contactUpdateTo(let url, let key, let filePath, let proProof), false), + (.currentUserUpdateTo(let url, let key, let filePath, let proProof), true): /// If we have already downloaded the image then no need to download it again (the database records will be updated /// once the download completes) if !dependencies[singleton: .fileManager].fileExists(atPath: filePath) { @@ -203,7 +222,7 @@ public extension Profile { shouldBeUnique: true, details: DisplayPictureDownloadJob.Details( target: .profile(id: profile.id, url: url, encryptionKey: key), - timestamp: sentTimestamp + timestamp: profileUpdateTimestamp ) ), canStartJob: dependencies[singleton: .appContext].isMainApp @@ -218,16 +237,18 @@ public extension Profile { if key != profile.displayPictureEncryptionKey && key.count == DisplayPictureManager.aes256KeyByteLength { profileChanges.append(Profile.Columns.displayPictureEncryptionKey.set(to: key)) } - - profileChanges.append(Profile.Columns.displayPictureLastUpdated.set(to: sentTimestamp)) } + // TODO: Handle Pro Proof update + /// Don't want profiles in messages to modify the current users profile info so ignore those cases default: break } - // Persist any changes + /// Persist any changes if !profileChanges.isEmpty { + profileChanges.append(Profile.Columns.profileLastUpdated.set(to: profileUpdateTimestamp)) + try profile.upsert(db) try Profile @@ -237,6 +258,21 @@ public extension Profile { profileChanges, using: dependencies ) + + /// We don't automatically update the current users profile data when changed in the database so need to manually + /// trigger the update + if isCurrentUser, let updatedProfile = try? Profile.fetchOne(db, id: publicKey) { + try dependencies.mutate(cache: .libSession) { cache in + try cache.performAndPushChange(db, for: .userProfile, sessionId: dependencies[cache: .general].sessionId) { _ in + try cache.updateProfile( + displayName: updatedProfile.name, + displayPictureUrl: updatedProfile.displayPictureUrl, + displayPictureEncryptionKey: updatedProfile.displayPictureEncryptionKey, + isReuploadProfilePicture: isReuploadCurrentUserProfilePicture + ) + } + } + } } } } diff --git a/SessionMessagingKit/Utilities/ProfilePictureView+Convenience.swift b/SessionMessagingKit/Utilities/ProfilePictureView+Convenience.swift index fff1736d22..6a0ddff983 100644 --- a/SessionMessagingKit/Utilities/ProfilePictureView+Convenience.swift +++ b/SessionMessagingKit/Utilities/ProfilePictureView+Convenience.swift @@ -48,13 +48,27 @@ public extension ProfilePictureView { ) switch (explicitPath, publicKey.isEmpty, threadVariant) { - case (.some(let path), _, _): + // TODO: Deal with this case later when implement group related Pro features + case (.some(let path), _, .legacyGroup), (.some(let path), _, .group): fallthrough + case (.some(let path), _, .community): /// If we are given an explicit `displayPictureUrl` then only use that return (Info( source: .url(URL(fileURLWithPath: path)), + animationBehaviour: .generic(true), icon: profileIcon ), nil) + case (.some(let path), _, _): + /// If we are given an explicit `displayPictureUrl` then only use that + return ( + Info( + source: .url(URL(fileURLWithPath: path)), + animationBehaviour: ProfilePictureView.animationBehaviour(from: profile, using: dependencies), + icon: profileIcon + ), + nil + ) + case (_, _, .community): return ( Info( @@ -62,9 +76,10 @@ public extension ProfilePictureView { switch size { case .navigation, .message: return .image("SessionWhite16", #imageLiteral(resourceName: "SessionWhite16")) case .list: return .image("SessionWhite24", #imageLiteral(resourceName: "SessionWhite24")) - case .hero: return .image("SessionWhite40", #imageLiteral(resourceName: "SessionWhite40")) + case .hero, .modal: return .image("SessionWhite40", #imageLiteral(resourceName: "SessionWhite40")) } }(), + animationBehaviour: .generic(true), inset: UIEdgeInsets( top: 12, left: 12, @@ -101,7 +116,11 @@ public extension ProfilePictureView { }() return ( - Info(source: source, icon: profileIcon), + Info( + source: source, + animationBehaviour: ProfilePictureView.animationBehaviour(from: profile, using: dependencies), + icon: profileIcon + ), additionalProfile .map { other in let source: ImageDataManager.DataSource = { @@ -120,11 +139,16 @@ public extension ProfilePictureView { return ImageDataManager.DataSource.url(URL(fileURLWithPath: path)) }() - return Info(source: source, icon: additionalProfileIcon) + return Info( + source: source, + animationBehaviour: ProfilePictureView.animationBehaviour(from: other, using: dependencies), + icon: additionalProfileIcon + ) } .defaulting( to: Info( source: .image("ic_user_round_fill", UIImage(named: "ic_user_round_fill")), + animationBehaviour: .generic(false), renderingMode: .alwaysTemplate, themeTintColor: .white, inset: UIEdgeInsets( @@ -156,7 +180,29 @@ public extension ProfilePictureView { return ImageDataManager.DataSource.url(URL(fileURLWithPath: path)) }() - return (Info(source: source, icon: profileIcon), nil) + return ( + Info( + source: source, + animationBehaviour: ProfilePictureView.animationBehaviour(from: profile, using: dependencies), + icon: profileIcon), + nil + ) + } + } +} + +public extension ProfilePictureView { + static func animationBehaviour(from profile: Profile?, using dependencies: Dependencies) -> Info.AnimationBehaviour { + guard dependencies[feature: .sessionProEnabled] else { return .generic(true) } + + switch profile { + case .none: return .generic(false) + + case .some(let profile) where profile.id == dependencies[cache: .general].sessionId.hexString: + return .currentUser(dependencies[singleton: .sessionProState]) + + case .some(let profile): + return .contact(dependencies.mutate(cache: .libSession, { $0.validateProProof(for: profile) })) } } } diff --git a/SessionMessagingKit/Utilities/SNProtoEnvelope+Conversion.swift b/SessionMessagingKit/Utilities/SNProtoEnvelope+Conversion.swift index 98d331cd1d..ed46bc066c 100644 --- a/SessionMessagingKit/Utilities/SNProtoEnvelope+Conversion.swift +++ b/SessionMessagingKit/Utilities/SNProtoEnvelope+Conversion.swift @@ -1,7 +1,7 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit public extension SNProtoEnvelope { diff --git a/SessionMessagingKit/Utilities/Threading+SMK.swift b/SessionMessagingKit/Utilities/Threading+SessionMessagingKit.swift similarity index 100% rename from SessionMessagingKit/Utilities/Threading+SMK.swift rename to SessionMessagingKit/Utilities/Threading+SessionMessagingKit.swift diff --git a/SessionMessagingKitTests/Database/Models/MessageDeduplicationSpec.swift b/SessionMessagingKitTests/Database/Models/MessageDeduplicationSpec.swift index 458c7cc357..22fb802b43 100644 --- a/SessionMessagingKitTests/Database/Models/MessageDeduplicationSpec.swift +++ b/SessionMessagingKitTests/Database/Models/MessageDeduplicationSpec.swift @@ -2,7 +2,7 @@ import Foundation import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit import Quick @@ -18,9 +18,7 @@ class MessageDeduplicationSpec: AsyncSpec { @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNMessagingKit.self - ], + migrations: SNMessagingKit.migrations, using: dependencies ) @TestState(singleton: .extensionHelper, in: dependencies) var mockExtensionHelper: MockExtensionHelper! = MockExtensionHelper( diff --git a/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift b/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift index 0ca3ed5345..1389d05588 100644 --- a/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift +++ b/SessionMessagingKitTests/Jobs/DisplayPictureDownloadJobSpec.swift @@ -6,7 +6,7 @@ import GRDB import Quick import Nimble -@testable import SessionSnodeKit +@testable import SessionNetworkingKit @testable import SessionMessagingKit @testable import SessionUtilitiesKit @@ -23,14 +23,15 @@ class DisplayPictureDownloadJobSpec: QuickSpec { dependencies.dateNow = Date(timeIntervalSince1970: 1234567890) } @TestState(cache: .libSession, in: dependencies) var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache( - initialSetup: { $0.defaultInitialSetup() } + initialSetup: { + $0.defaultInitialSetup() + $0.when { $0.profile(contactId: .any, threadId: .any, threadVariant: .any, visibleMessage: .any) } + .thenReturn(nil) + } ) @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNMessagingKit.self - ], + migrations: SNMessagingKit.migrations, using: dependencies, initialData: { db in try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) @@ -544,7 +545,7 @@ class DisplayPictureDownloadJobSpec: QuickSpec { ) ) let expectedRequest: Network.PreparedRequest = mockStorage.read { db in - try OpenGroupAPI.preparedDownload( + try Network.SOGS.preparedDownload( fileId: "12", roomToken: "testRoom", authMethod: Authentication.community( @@ -590,7 +591,7 @@ class DisplayPictureDownloadJobSpec: QuickSpec { name: "test", displayPictureUrl: nil, displayPictureEncryptionKey: nil, - displayPictureLastUpdated: nil + profileLastUpdated: nil ) mockStorage.write { db in try profile.insert(db) } job = Job( @@ -708,7 +709,7 @@ class DisplayPictureDownloadJobSpec: QuickSpec { name: "test", displayPictureUrl: "http://oxen.io/100/", displayPictureEncryptionKey: encryptionKey, - displayPictureLastUpdated: 1234567890 + profileLastUpdated: 1234567890 ) mockStorage.write { db in _ = try Profile.deleteAll(db) @@ -757,7 +758,7 @@ class DisplayPictureDownloadJobSpec: QuickSpec { .updateAll( db, Profile.Columns.displayPictureEncryptionKey.set(to: Data([1, 2, 3])), - Profile.Columns.displayPictureLastUpdated.set(to: 9999999999) + Profile.Columns.profileLastUpdated.set(to: 9999999999) ) } } @@ -780,7 +781,7 @@ class DisplayPictureDownloadJobSpec: QuickSpec { name: "test", displayPictureUrl: "http://oxen.io/100/", displayPictureEncryptionKey: encryptionKey, - displayPictureLastUpdated: 1234567891 + profileLastUpdated: 1234567891 ) )) } @@ -794,7 +795,7 @@ class DisplayPictureDownloadJobSpec: QuickSpec { .updateAll( db, Profile.Columns.displayPictureUrl.set(to: "testUrl"), - Profile.Columns.displayPictureLastUpdated.set(to: 9999999999) + Profile.Columns.profileLastUpdated.set(to: 9999999999) ) } } @@ -817,7 +818,7 @@ class DisplayPictureDownloadJobSpec: QuickSpec { name: "test", displayPictureUrl: "http://oxen.io/100/", displayPictureEncryptionKey: encryptionKey, - displayPictureLastUpdated: 1234567891 + profileLastUpdated: 1234567891 ) )) } @@ -830,7 +831,7 @@ class DisplayPictureDownloadJobSpec: QuickSpec { try Profile .updateAll( db, - Profile.Columns.displayPictureLastUpdated.set(to: 9999999999) + Profile.Columns.profileLastUpdated.set(to: 9999999999) ) } } @@ -862,7 +863,7 @@ class DisplayPictureDownloadJobSpec: QuickSpec { name: "test", displayPictureUrl: "http://oxen.io/100/", displayPictureEncryptionKey: encryptionKey, - displayPictureLastUpdated: 1234567891 + profileLastUpdated: 1234567891 ) )) } @@ -877,7 +878,7 @@ class DisplayPictureDownloadJobSpec: QuickSpec { name: "test", displayPictureUrl: "http://oxen.io/100/", displayPictureEncryptionKey: encryptionKey, - displayPictureLastUpdated: 1234567891 + profileLastUpdated: 1234567891 ) )) } diff --git a/SessionMessagingKitTests/Jobs/MessageSendJobSpec.swift b/SessionMessagingKitTests/Jobs/MessageSendJobSpec.swift index 15012b5318..e549e2c221 100644 --- a/SessionMessagingKitTests/Jobs/MessageSendJobSpec.swift +++ b/SessionMessagingKitTests/Jobs/MessageSendJobSpec.swift @@ -35,10 +35,7 @@ class MessageSendJobSpec: QuickSpec { ) @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNMessagingKit.self - ], + migrations: SNMessagingKit.migrations, using: dependencies, initialData: { db in try SessionThread.upsert( diff --git a/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift b/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift index 39d2bff89d..42ff8e59cc 100644 --- a/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift +++ b/SessionMessagingKitTests/Jobs/RetrieveDefaultOpenGroupRoomsJobSpec.swift @@ -6,7 +6,7 @@ import GRDB import Quick import Nimble -@testable import SessionSnodeKit +@testable import SessionNetworkingKit @testable import SessionMessagingKit @testable import SessionUtilitiesKit @@ -20,10 +20,7 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { } @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNMessagingKit.self - ], + migrations: SNMessagingKit.migrations, using: dependencies, initialData: { db in try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) @@ -45,17 +42,22 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { MockNetwork.batchResponseData( with: [ ( - OpenGroupAPI.Endpoint.capabilities, - OpenGroupAPI.Capabilities(capabilities: [.blind, .reactions]).batchSubResponse() + Network.SOGS.Endpoint.capabilities, + Network.SOGS.CapabilitiesResponse( + capabilities: [ + Capability.Variant.blind.rawValue, + Capability.Variant.reactions.rawValue + ] + ).batchSubResponse() ), ( - OpenGroupAPI.Endpoint.rooms, + Network.SOGS.Endpoint.rooms, [ - OpenGroupAPI.Room.mock.with( + Network.SOGS.Room.mock.with( token: "testRoom", name: "TestRoomName" ), - OpenGroupAPI.Room.mock.with( + Network.SOGS.Room.mock.with( token: "testRoom2", name: "TestRoomName2", infoUpdates: 12, @@ -194,9 +196,9 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { let openGroups: [OpenGroup]? = mockStorage.read { db in try OpenGroup.fetchAll(db) } expect(openGroups?.count).to(equal(1)) - expect(openGroups?.map { $0.server }).to(equal([OpenGroupAPI.defaultServer])) + expect(openGroups?.map { $0.server }).to(equal([Network.SOGS.defaultServer])) expect(openGroups?.map { $0.roomToken }).to(equal([""])) - expect(openGroups?.map { $0.publicKey }).to(equal([OpenGroupAPI.defaultServerPublicKey])) + expect(openGroups?.map { $0.publicKey }).to(equal([Network.SOGS.defaultServerPublicKey])) expect(openGroups?.map { $0.isActive }).to(equal([false])) expect(openGroups?.map { $0.name }).to(equal([""])) } @@ -209,9 +211,9 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { mockStorage.write { db in try OpenGroup( - server: OpenGroupAPI.defaultServer, + server: Network.SOGS.defaultServer, roomToken: "", - publicKey: OpenGroupAPI.defaultServerPublicKey, + publicKey: Network.SOGS.defaultServerPublicKey, isActive: false, name: "TestExisting", userCount: 0, @@ -231,9 +233,9 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { let openGroups: [OpenGroup]? = mockStorage.read { db in try OpenGroup.fetchAll(db) } expect(openGroups?.count).to(equal(1)) - expect(openGroups?.map { $0.server }).to(equal([OpenGroupAPI.defaultServer])) + expect(openGroups?.map { $0.server }).to(equal([Network.SOGS.defaultServer])) expect(openGroups?.map { $0.roomToken }).to(equal([""])) - expect(openGroups?.map { $0.publicKey }).to(equal([OpenGroupAPI.defaultServerPublicKey])) + expect(openGroups?.map { $0.publicKey }).to(equal([Network.SOGS.defaultServerPublicKey])) expect(openGroups?.map { $0.isActive }).to(equal([false])) expect(openGroups?.map { $0.name }).to(equal(["TestExisting"])) } @@ -242,9 +244,9 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { it("sends the correct request") { mockStorage.write { db in try OpenGroup( - server: OpenGroupAPI.defaultServer, + server: Network.SOGS.defaultServer, roomToken: "", - publicKey: OpenGroupAPI.defaultServerPublicKey, + publicKey: Network.SOGS.defaultServerPublicKey, isActive: false, name: "TestExisting", userCount: 0, @@ -252,17 +254,18 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { ) .insert(db) } - let expectedRequest: Network.PreparedRequest! = mockStorage.read { db in - try OpenGroupAPI.preparedCapabilitiesAndRooms( + let expectedRequest: Network.PreparedRequest! = mockStorage.read { db in + try Network.SOGS.preparedCapabilitiesAndRooms( authMethod: Authentication.community( info: LibSession.OpenGroupCapabilityInfo( roomToken: "", - server: OpenGroupAPI.defaultServer, - publicKey: OpenGroupAPI.defaultServerPublicKey, + server: Network.SOGS.defaultServer, + publicKey: Network.SOGS.defaultServerPublicKey, capabilities: [] ), forceBlinded: false ), + skipAuthentication: true, using: dependencies ) } @@ -284,6 +287,8 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { requestAndPathBuildTimeout: expectedRequest.requestAndPathBuildTimeout ) }) + + expect(expectedRequest?.headers).to(beEmpty()) } // MARK: -- will retry 8 times before it fails @@ -325,7 +330,7 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { let capabilities: [Capability]? = mockStorage.read { db in try Capability.fetchAll(db) } expect(capabilities?.count).to(equal(2)) expect(capabilities?.map { $0.openGroupServer }) - .to(equal([OpenGroupAPI.defaultServer, OpenGroupAPI.defaultServer])) + .to(equal([Network.SOGS.defaultServer, Network.SOGS.defaultServer])) expect(capabilities?.map { $0.variant }).to(equal([.blind, .reactions])) expect(capabilities?.map { $0.isMissing }).to(equal([false, false])) } @@ -344,13 +349,13 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { let openGroups: [OpenGroup]? = mockStorage.read { db in try OpenGroup.fetchAll(db) } expect(openGroups?.count).to(equal(3)) // 1 for the entry used to fetch the default rooms expect(openGroups?.map { $0.server }) - .to(equal([OpenGroupAPI.defaultServer, OpenGroupAPI.defaultServer, OpenGroupAPI.defaultServer])) + .to(equal([Network.SOGS.defaultServer, Network.SOGS.defaultServer, Network.SOGS.defaultServer])) expect(openGroups?.map { $0.roomToken }).to(equal(["", "testRoom", "testRoom2"])) expect(openGroups?.map { $0.publicKey }) .to(equal([ - OpenGroupAPI.defaultServerPublicKey, - OpenGroupAPI.defaultServerPublicKey, - OpenGroupAPI.defaultServerPublicKey + Network.SOGS.defaultServerPublicKey, + Network.SOGS.defaultServerPublicKey, + Network.SOGS.defaultServerPublicKey ])) expect(openGroups?.map { $0.isActive }).to(equal([false, false, false])) expect(openGroups?.map { $0.name }).to(equal(["", "TestRoomName", "TestRoomName2"])) @@ -360,9 +365,9 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { it("does not override existing rooms that were returned") { mockStorage.write { db in try OpenGroup( - server: OpenGroupAPI.defaultServer, + server: Network.SOGS.defaultServer, roomToken: "testRoom", - publicKey: OpenGroupAPI.defaultServerPublicKey, + publicKey: Network.SOGS.defaultServerPublicKey, isActive: false, name: "TestExisting", userCount: 0, @@ -375,15 +380,15 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { .thenReturn( MockNetwork.batchResponseData( with: [ - (OpenGroupAPI.Endpoint.capabilities, OpenGroupAPI.Capabilities.mockBatchSubResponse()), + (Network.SOGS.Endpoint.capabilities, Network.SOGS.CapabilitiesResponse.mockBatchSubResponse()), ( - OpenGroupAPI.Endpoint.rooms, + Network.SOGS.Endpoint.rooms, try! JSONEncoder().with(outputFormatting: .sortedKeys).encode( Network.BatchSubResponse( code: 200, headers: [:], body: [ - OpenGroupAPI.Room.mock.with( + Network.SOGS.Room.mock.with( token: "testRoom", name: "TestReplacementName" ) @@ -408,10 +413,10 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { let openGroups: [OpenGroup]? = mockStorage.read { db in try OpenGroup.fetchAll(db) } expect(openGroups?.count).to(equal(2)) // 1 for the entry used to fetch the default rooms expect(openGroups?.map { $0.server }) - .to(equal([OpenGroupAPI.defaultServer, OpenGroupAPI.defaultServer])) + .to(equal([Network.SOGS.defaultServer, Network.SOGS.defaultServer])) expect(openGroups?.map { $0.roomToken }.sorted()).to(equal(["", "testRoom"])) expect(openGroups?.map { $0.publicKey }) - .to(equal([OpenGroupAPI.defaultServerPublicKey, OpenGroupAPI.defaultServerPublicKey])) + .to(equal([Network.SOGS.defaultServerPublicKey, Network.SOGS.defaultServerPublicKey])) expect(openGroups?.map { $0.isActive }).to(equal([false, false])) expect(openGroups?.map { $0.name }.sorted()).to(equal(["", "TestExisting"])) } @@ -438,7 +443,8 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { target: .community( imageId: "12", roomToken: "testRoom2", - server: OpenGroupAPI.defaultServer + server: Network.SOGS.defaultServer, + skipAuthentication: true ), timestamp: 1234567890 ) @@ -453,9 +459,9 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { it("schedules a display picture download if the imageId has changed") { mockStorage.write { db in try OpenGroup( - server: OpenGroupAPI.defaultServer, + server: Network.SOGS.defaultServer, roomToken: "testRoom2", - publicKey: OpenGroupAPI.defaultServerPublicKey, + publicKey: Network.SOGS.defaultServerPublicKey, isActive: false, name: "TestExisting", imageId: "10", @@ -485,7 +491,8 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { target: .community( imageId: "12", roomToken: "testRoom2", - server: OpenGroupAPI.defaultServer + server: Network.SOGS.defaultServer, + skipAuthentication: true ), timestamp: 1234567890 ) @@ -504,17 +511,22 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { MockNetwork.batchResponseData( with: [ ( - OpenGroupAPI.Endpoint.capabilities, - OpenGroupAPI.Capabilities(capabilities: [.blind, .reactions]).batchSubResponse() + Network.SOGS.Endpoint.capabilities, + Network.SOGS.CapabilitiesResponse( + capabilities: [ + Capability.Variant.blind.rawValue, + Capability.Variant.reactions.rawValue + ] + ).batchSubResponse() ), ( - OpenGroupAPI.Endpoint.rooms, + Network.SOGS.Endpoint.rooms, [ - OpenGroupAPI.Room.mock.with( + Network.SOGS.Room.mock.with( token: "testRoom", name: "TestRoomName" ), - OpenGroupAPI.Room.mock.with( + Network.SOGS.Room.mock.with( token: "testRoom2", name: "TestRoomName2" ) @@ -541,9 +553,9 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { it("does not schedule a display picture download if the imageId matches and the image has already been downloaded") { mockStorage.write { db in try OpenGroup( - server: OpenGroupAPI.defaultServer, + server: Network.SOGS.defaultServer, roomToken: "testRoom2", - publicKey: OpenGroupAPI.defaultServerPublicKey, + publicKey: Network.SOGS.defaultServerPublicKey, isActive: false, name: "TestExisting", imageId: "12", @@ -582,14 +594,14 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { .toNot(call(matchingParameters: .all) { $0.setDefaultRoomInfo([ ( - room: OpenGroupAPI.Room.mock.with( + room: Network.SOGS.Room.mock.with( token: "testRoom", name: "TestRoomName" ), openGroup: OpenGroup( - server: OpenGroupAPI.defaultServer, + server: Network.SOGS.defaultServer, roomToken: "testRoom", - publicKey: OpenGroupAPI.defaultServerPublicKey, + publicKey: Network.SOGS.defaultServerPublicKey, isActive: false, name: "TestRoomName", userCount: 0, @@ -597,16 +609,16 @@ class RetrieveDefaultOpenGroupRoomsJobSpec: QuickSpec { ) ), ( - room: OpenGroupAPI.Room.mock.with( + room: Network.SOGS.Room.mock.with( token: "testRoom2", name: "TestRoomName2", infoUpdates: 12, imageId: "12" ), openGroup: OpenGroup( - server: OpenGroupAPI.defaultServer, + server: Network.SOGS.defaultServer, roomToken: "testRoom2", - publicKey: OpenGroupAPI.defaultServerPublicKey, + publicKey: Network.SOGS.defaultServerPublicKey, isActive: false, name: "TestRoomName2", imageId: "12", diff --git a/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift b/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift index f9de94bac5..209274b6fd 100644 --- a/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift +++ b/SessionMessagingKitTests/LibSession/LibSessionGroupInfoSpec.swift @@ -4,12 +4,12 @@ import Foundation import GRDB import SessionUtil import SessionUtilitiesKit -import SessionSnodeKit +import SessionNetworkingKit import Quick import Nimble -@testable import SessionSnodeKit +@testable import SessionNetworkingKit @testable import SessionMessagingKit class LibSessionGroupInfoSpec: QuickSpec { @@ -28,11 +28,7 @@ class LibSessionGroupInfoSpec: QuickSpec { ) @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNMessagingKit.self, - SNSnodeKit.self - ], + migrations: SNMessagingKit.migrations, using: dependencies, initialData: { db in try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) @@ -886,7 +882,7 @@ class LibSessionGroupInfoSpec: QuickSpec { ) } - let expectedRequest: Network.PreparedRequest<[String: Bool]> = try SnodeAPI.preparedDeleteMessages( + let expectedRequest: Network.PreparedRequest<[String: Bool]> = try Network.SnodeAPI.preparedDeleteMessages( serverHashes: ["1234"], requireSuccessfulDeletion: false, authMethod: Authentication.groupAdmin( diff --git a/SessionMessagingKitTests/LibSession/LibSessionGroupMembersSpec.swift b/SessionMessagingKitTests/LibSession/LibSessionGroupMembersSpec.swift index 5e2884cd3e..43f6c045de 100644 --- a/SessionMessagingKitTests/LibSession/LibSessionGroupMembersSpec.swift +++ b/SessionMessagingKitTests/LibSession/LibSessionGroupMembersSpec.swift @@ -8,7 +8,7 @@ import SessionUtilitiesKit import Quick import Nimble -@testable import SessionSnodeKit +@testable import SessionNetworkingKit @testable import SessionMessagingKit class LibSessionGroupMembersSpec: QuickSpec { @@ -27,10 +27,7 @@ class LibSessionGroupMembersSpec: QuickSpec { ) @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNMessagingKit.self - ], + migrations: SNMessagingKit.migrations, using: dependencies, initialData: { db in try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) diff --git a/SessionMessagingKitTests/LibSession/LibSessionSpec.swift b/SessionMessagingKitTests/LibSession/LibSessionSpec.swift index d135064fcf..cb8ff17dd8 100644 --- a/SessionMessagingKitTests/LibSession/LibSessionSpec.swift +++ b/SessionMessagingKitTests/LibSession/LibSessionSpec.swift @@ -8,7 +8,7 @@ import SessionUtilitiesKit import Quick import Nimble -@testable import SessionSnodeKit +@testable import SessionNetworkingKit @testable import SessionMessagingKit class LibSessionSpec: QuickSpec { @@ -27,10 +27,7 @@ class LibSessionSpec: QuickSpec { ) @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNMessagingKit.self - ], + migrations: SNMessagingKit.migrations, using: dependencies, initialData: { db in try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) diff --git a/SessionMessagingKitTests/LibSession/LibSessionUtilSpec.swift b/SessionMessagingKitTests/LibSession/LibSessionUtilSpec.swift index 56ab349c08..84c3252952 100644 --- a/SessionMessagingKitTests/LibSession/LibSessionUtilSpec.swift +++ b/SessionMessagingKitTests/LibSession/LibSessionUtilSpec.swift @@ -882,12 +882,20 @@ fileprivate extension LibSessionUtilSpec { expect(pushData5.pointee.seqno).to(equal(3)) expect(pushData6.pointee.seqno).to(equal(3)) - // They should have resolved the conflict to the same thing: - expect(String(cString: user_profile_get_name(conf)!)).to(equal("Nibbler")) - expect(String(cString: user_profile_get_name(conf2)!)).to(equal("Nibbler")) - // (Note that they could have also both resolved to "Raz" here, but the hash of the serialized - // message just happens to have a higher hash -- and thus gets priority -- for this particular - // test). + // They should have resolved the conflict to the same thing - since the configs set + // a timestamp to the current time when modifying the profile (and we don't have a + // mechanism via the C API to set it directly, we can do this directly in the C++ but + // not here) we don't actually know whether "Nibbler" or "Raz" will win here so instead + // the best we can do is insure they match each other, and that they match one of the options + let confNamePtr: UnsafePointer? = user_profile_get_name(conf) + let conf2NamePtr: UnsafePointer? = user_profile_get_name(conf2) + try require(confNamePtr).toNot(beNil()) + try require(conf2NamePtr).toNot(beNil()) + let confName: String = String(cString: confNamePtr!) + let conf2Name: String = String(cString: conf2NamePtr!) + expect(Set(["Nibbler", "Raz"])).to(contain(confName)) + expect(Set(["Nibbler", "Raz"])).to(contain(conf2Name)) + expect(confName).to(equal(conf2Name)) // Since only one of them set a profile pic there should be no conflict there: let pic3: user_profile_pic = user_profile_get_pic(conf) diff --git a/SessionMessagingKitTests/Open Groups/Crypto/CryptoOpenGroupAPISpec.swift b/SessionMessagingKitTests/Open Groups/Crypto/CryptoOpenGroupSpec.swift similarity index 61% rename from SessionMessagingKitTests/Open Groups/Crypto/CryptoOpenGroupAPISpec.swift rename to SessionMessagingKitTests/Open Groups/Crypto/CryptoOpenGroupSpec.swift index 35b3ba1679..7b04fa35a1 100644 --- a/SessionMessagingKitTests/Open Groups/Crypto/CryptoOpenGroupAPISpec.swift +++ b/SessionMessagingKitTests/Open Groups/Crypto/CryptoOpenGroupSpec.swift @@ -8,7 +8,7 @@ import Nimble @testable import SessionMessagingKit -class CryptoOpenGroupAPISpec: QuickSpec { +class CryptoOpenGroupSpec: QuickSpec { override class func spec() { // MARK: Configuration @@ -21,161 +21,8 @@ class CryptoOpenGroupAPISpec: QuickSpec { } ) - // MARK: - Crypto for OpenGroupAPI - describe("Crypto for OpenGroupAPI") { - // MARK: -- when generating a blinded15 key pair - context("when generating a blinded15 key pair") { - // MARK: ---- successfully generates - it("successfully generates") { - let result = crypto.generate( - .blinded15KeyPair( - serverPublicKey: TestConstants.serverPublicKey, - ed25519SecretKey: Data(hex: TestConstants.edSecretKey).bytes - ) - ) - - // Note: The first 64 characters of the secretKey are consistent but the chars after that always differ - expect(result?.publicKey.toHexString()).to(equal(TestConstants.blind15PublicKey)) - expect(result?.secretKey.toHexString()).to(equal(TestConstants.blind15SecretKey)) - } - - // MARK: ---- fails if the edKeyPair secret key length wrong - it("fails if the ed25519SecretKey length wrong") { - let result = crypto.generate( - .blinded15KeyPair( - serverPublicKey: TestConstants.serverPublicKey, - ed25519SecretKey: Array(Data(hex: String(TestConstants.edSecretKey.prefix(4)))) - ) - ) - - expect(result).to(beNil()) - } - } - - // MARK: -- when generating a blinded25 key pair - context("when generating a blinded25 key pair") { - // MARK: ---- successfully generates - it("successfully generates") { - let result = crypto.generate( - .blinded25KeyPair( - serverPublicKey: TestConstants.serverPublicKey, - ed25519SecretKey: Data(hex: TestConstants.edSecretKey).bytes - ) - ) - - // Note: The first 64 characters of the secretKey are consistent but the chars after that always differ - expect(result?.publicKey.toHexString()).to(equal(TestConstants.blind25PublicKey)) - expect(result?.secretKey.toHexString()).to(equal(TestConstants.blind25SecretKey)) - } - - // MARK: ---- fails if the edKeyPair secret key length wrong - it("fails if the ed25519SecretKey length wrong") { - let result = crypto.generate( - .blinded25KeyPair( - serverPublicKey: TestConstants.serverPublicKey, - ed25519SecretKey: Data(hex: String(TestConstants.edSecretKey.prefix(4))).bytes - ) - ) - - expect(result).to(beNil()) - } - } - - // MARK: -- when generating a signatureBlind15 - context("when generating a signatureBlind15") { - // MARK: ---- generates a correct signature - it("generates a correct signature") { - let result = crypto.generate( - .signatureBlind15( - message: "TestMessage".bytes, - serverPublicKey: TestConstants.serverPublicKey, - ed25519SecretKey: Array(Data(hex: TestConstants.edSecretKey)) - ) - ) - - expect(result?.toHexString()) - .to(equal( - "245003f1627ebdfc6099c32597d426ef84d1b301861a5ffbbac92dde6c608334" + - "ceb56a022a094a9a664fae034b50eed40bd1bfb262c7e542c979eec265ae3f07" - )) - } - } - - // MARK: -- when generating a signatureBlind25 - context("when generating a signatureBlind25") { - // MARK: ---- generates a correct signature - it("generates a correct signature") { - let result = crypto.generate( - .signatureBlind25( - message: "TestMessage".bytes, - serverPublicKey: TestConstants.serverPublicKey, - ed25519SecretKey: Data(hex: TestConstants.edSecretKey).bytes - ) - ) - - expect(result?.toHexString()) - .to(equal( - "9ff9b7fb7d435c7a2c0b0b2ae64963baaf394386b9f7c7f924eeac44ec0f74c7" + - "fe6304c73a9b3a65491f81e44b545e54631e83e9a412eaed5fd4db2e05ec830c" - )) - } - } - - // MARK: -- when checking if a session id matches a blinded id - context("when checking if a session id matches a blinded id") { - // MARK: ---- returns true when a blind15 id matches - it("returns true when a blind15 id matches") { - let result = crypto.verify( - .sessionId( - "05\(TestConstants.publicKey)", - matchesBlindedId: "15\(TestConstants.blind15PublicKey)", - serverPublicKey: TestConstants.serverPublicKey - ) - ) - - expect(result).to(beTrue()) - } - - // MARK: ---- returns true when a blind25 id matches - it("returns true when a blind25 id matches") { - let result = crypto.verify( - .sessionId( - "05\(TestConstants.publicKey)", - matchesBlindedId: "25\(TestConstants.blind25PublicKey)", - serverPublicKey: TestConstants.serverPublicKey - ) - ) - - expect(result).to(beTrue()) - } - - // MARK: ---- returns false if given an invalid session id - it("returns false if given an invalid session id") { - let result = crypto.verify( - .sessionId( - "AB\(TestConstants.publicKey)", - matchesBlindedId: "15\(TestConstants.blind15PublicKey)", - serverPublicKey: TestConstants.serverPublicKey - ) - ) - - expect(result).to(beFalse()) - } - - // MARK: ---- returns false if given an invalid blinded id - it("returns false if given an invalid blinded id") { - let result = crypto.verify( - .sessionId( - "05\(TestConstants.publicKey)", - matchesBlindedId: "AB\(TestConstants.blind15PublicKey)", - serverPublicKey: TestConstants.serverPublicKey - ) - ) - - expect(result).to(beFalse()) - } - } - + // MARK: - Crypto for Open Group + describe("Crypto for Open Group") { // MARK: -- when encrypting with the session blinding protocol context("when encrypting with the session blinding protocol") { // MARK: ---- can encrypt for a blind15 recipient correctly diff --git a/SessionMessagingKitTests/Open Groups/Models/CapabilitiesSpec.swift b/SessionMessagingKitTests/Open Groups/Models/CapabilitySpec.swift similarity index 73% rename from SessionMessagingKitTests/Open Groups/Models/CapabilitiesSpec.swift rename to SessionMessagingKitTests/Open Groups/Models/CapabilitySpec.swift index 0fc98cf1a8..a3a918bcff 100644 --- a/SessionMessagingKitTests/Open Groups/Models/CapabilitiesSpec.swift +++ b/SessionMessagingKitTests/Open Groups/Models/CapabilitySpec.swift @@ -7,34 +7,8 @@ import Nimble @testable import SessionMessagingKit -class CapabilitiesSpec: QuickSpec { +class CapabilitySpec: QuickSpec { override class func spec() { - // MARK: - Capabilities - describe("Capabilities") { - // MARK: -- when initializing - context("when initializing") { - // MARK: ---- assigns values correctly - it("assigns values correctly") { - let capabilities: OpenGroupAPI.Capabilities = OpenGroupAPI.Capabilities( - capabilities: [.sogs], - missing: [.sogs] - ) - - expect(capabilities.capabilities).to(equal([.sogs])) - expect(capabilities.missing).to(equal([.sogs])) - } - - it("defaults missing to nil") { - let capabilities: OpenGroupAPI.Capabilities = OpenGroupAPI.Capabilities( - capabilities: [.sogs] - ) - - expect(capabilities.capabilities).to(equal([.sogs])) - expect(capabilities.missing).to(beNil()) - } - } - } - // MARK: - a Capability describe("a Capability") { // MARK: -- when initializing diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift index 556a2adee0..9d2d2d4cb3 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift @@ -4,13 +4,13 @@ import UIKit import Combine import GRDB import SessionUtil -import SessionSnodeKit import SessionUtilitiesKit import Quick import Nimble @testable import SessionMessagingKit +@testable import SessionNetworkingKit class OpenGroupManagerSpec: QuickSpec { override class func spec() { @@ -61,12 +61,12 @@ class OpenGroupManagerSpec: QuickSpec { infoUpdates: 10, sequenceNumber: 5 ) - @TestState var testPollInfo: OpenGroupAPI.RoomPollInfo! = OpenGroupAPI.RoomPollInfo.mock.with( + @TestState var testPollInfo: Network.SOGS.RoomPollInfo! = Network.SOGS.RoomPollInfo.mock.with( token: "testRoom", activeUsers: 10, details: .mock ) - @TestState var testMessage: OpenGroupAPI.Message! = OpenGroupAPI.Message( + @TestState var testMessage: Network.SOGS.Message! = Network.SOGS.Message( id: 127, sender: "05\(TestConstants.publicKey)", posted: 123, @@ -92,14 +92,14 @@ class OpenGroupManagerSpec: QuickSpec { base64EncodedSignature: nil, reactions: nil ) - @TestState var testDirectMessage: OpenGroupAPI.DirectMessage! = { + @TestState var testDirectMessage: Network.SOGS.DirectMessage! = { let proto = SNProtoContent.builder() let protoDataBuilder = SNProtoDataMessage.builder() proto.setSigTimestamp(1234567890000) protoDataBuilder.setBody("TestMessage") proto.setDataMessage(try! protoDataBuilder.build()) - return OpenGroupAPI.DirectMessage( + return Network.SOGS.DirectMessage( id: 128, sender: "15\(TestConstants.blind15PublicKey)", recipient: "15\(TestConstants.blind15PublicKey)", @@ -110,10 +110,7 @@ class OpenGroupManagerSpec: QuickSpec { }() @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNMessagingKit.self - ], + migrations: SNMessagingKit.migrations, using: dependencies, initialData: { db in try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) @@ -199,6 +196,7 @@ class OpenGroupManagerSpec: QuickSpec { @TestState(defaults: .appGroup, in: dependencies) var mockAppGroupDefaults: MockUserDefaults! = MockUserDefaults( initialSetup: { defaults in defaults.when { $0.bool(forKey: .any) }.thenReturn(false) + defaults.when { $0.object(forKey: .any) }.thenReturn(nil) } ) @TestState(cache: .general, in: dependencies) var mockGeneralCache: MockGeneralCache! = MockGeneralCache( @@ -854,7 +852,7 @@ class OpenGroupManagerSpec: QuickSpec { context("when deleting") { beforeEach { mockStorage.write { db in - try Interaction.deleteAll(db) + try Interaction.deleteWhere(db, .deleteAll) try SessionThread.deleteAll(db) try testGroupThread.insert(db) @@ -972,7 +970,7 @@ class OpenGroupManagerSpec: QuickSpec { mockStorage.write { db in try OpenGroup.deleteAll(db) try OpenGroup( - server: OpenGroupAPI.defaultServer, + server: Network.SOGS.defaultServer, roomToken: "testRoom", publicKey: TestConstants.publicKey, isActive: true, @@ -986,7 +984,7 @@ class OpenGroupManagerSpec: QuickSpec { outboxLatestMessageId: 0 ).insert(db) try OpenGroup( - server: OpenGroupAPI.defaultServer, + server: Network.SOGS.defaultServer, roomToken: "testRoom1", publicKey: TestConstants.publicKey, isActive: true, @@ -1007,7 +1005,7 @@ class OpenGroupManagerSpec: QuickSpec { mockStorage.write { db in try openGroupManager.delete( db, - openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: OpenGroupAPI.defaultServer), + openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: Network.SOGS.defaultServer), skipLibSessionUpdate: true ) } @@ -1021,7 +1019,7 @@ class OpenGroupManagerSpec: QuickSpec { mockStorage.write { db in try openGroupManager.delete( db, - openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: OpenGroupAPI.defaultServer), + openGroupId: OpenGroup.idFor(roomToken: "testRoom", server: Network.SOGS.defaultServer), skipLibSessionUpdate: true ) } @@ -1030,7 +1028,7 @@ class OpenGroupManagerSpec: QuickSpec { mockStorage.read { db in try OpenGroup .select(.isActive) - .filter(id: OpenGroup.idFor(roomToken: "testRoom", server: OpenGroupAPI.defaultServer)) + .filter(id: OpenGroup.idFor(roomToken: "testRoom", server: Network.SOGS.defaultServer)) .asRequest(of: Bool.self) .fetchOne(db) } @@ -1046,7 +1044,10 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager .handleCapabilities( db, - capabilities: OpenGroupAPI.Capabilities(capabilities: [.sogs], missing: []), + capabilities: Network.SOGS.CapabilitiesResponse( + capabilities: ["sogs"], + missing: [] + ), on: "http://127.0.0.1" ) } @@ -1183,10 +1184,10 @@ class OpenGroupManagerSpec: QuickSpec { context("and updating the moderator list") { // MARK: ------ successfully updates it("successfully updates") { - testPollInfo = OpenGroupAPI.RoomPollInfo.mock.with( + testPollInfo = Network.SOGS.RoomPollInfo.mock.with( token: "testRoom", activeUsers: 10, - details: OpenGroupAPI.Room.mock.with( + details: Network.SOGS.Room.mock.with( moderators: ["TestMod"], hiddenModerators: [], admins: [], @@ -1230,10 +1231,10 @@ class OpenGroupManagerSpec: QuickSpec { // MARK: ------ updates for hidden moderators it("updates for hidden moderators") { - testPollInfo = OpenGroupAPI.RoomPollInfo.mock.with( + testPollInfo = Network.SOGS.RoomPollInfo.mock.with( token: "testRoom", activeUsers: 10, - details: OpenGroupAPI.Room.mock.with( + details: Network.SOGS.Room.mock.with( moderators: [], hiddenModerators: ["TestMod2"], admins: [], @@ -1277,7 +1278,7 @@ class OpenGroupManagerSpec: QuickSpec { // MARK: ------ does not insert mods if no moderators are provided it("does not insert mods if no moderators are provided") { - testPollInfo = OpenGroupAPI.RoomPollInfo.mock.with( + testPollInfo = Network.SOGS.RoomPollInfo.mock.with( token: "testRoom", activeUsers: 10 ) @@ -1302,10 +1303,10 @@ class OpenGroupManagerSpec: QuickSpec { context("and updating the admin list") { // MARK: ------ successfully updates it("successfully updates") { - testPollInfo = OpenGroupAPI.RoomPollInfo.mock.with( + testPollInfo = Network.SOGS.RoomPollInfo.mock.with( token: "testRoom", activeUsers: 10, - details: OpenGroupAPI.Room.mock.with( + details: Network.SOGS.Room.mock.with( moderators: [], hiddenModerators: [], admins: ["TestAdmin"], @@ -1349,10 +1350,10 @@ class OpenGroupManagerSpec: QuickSpec { // MARK: ------ updates for hidden admins it("updates for hidden admins") { - testPollInfo = OpenGroupAPI.RoomPollInfo.mock.with( + testPollInfo = Network.SOGS.RoomPollInfo.mock.with( token: "testRoom", activeUsers: 10, - details: OpenGroupAPI.Room.mock.with( + details: Network.SOGS.Room.mock.with( moderators: [], hiddenModerators: [], admins: [], @@ -1396,7 +1397,7 @@ class OpenGroupManagerSpec: QuickSpec { // MARK: ------ does not insert an admin if no admins are provided it("does not insert an admin if no admins are provided") { - testPollInfo = OpenGroupAPI.RoomPollInfo.mock.with( + testPollInfo = Network.SOGS.RoomPollInfo.mock.with( token: "testRoom", activeUsers: 10, details: nil @@ -1478,10 +1479,10 @@ class OpenGroupManagerSpec: QuickSpec { // MARK: ------ schedules a download for the room image it("schedules a download for the room image") { - testPollInfo = OpenGroupAPI.RoomPollInfo.mock.with( + testPollInfo = Network.SOGS.RoomPollInfo.mock.with( token: "testRoom", activeUsers: 10, - details: OpenGroupAPI.Room.mock.with( + details: Network.SOGS.Room.mock.with( token: "test", name: "test", imageId: "10" @@ -1546,7 +1547,7 @@ class OpenGroupManagerSpec: QuickSpec { ).insert(db) } - testPollInfo = OpenGroupAPI.RoomPollInfo.mock.with( + testPollInfo = Network.SOGS.RoomPollInfo.mock.with( token: "testRoom", activeUsers: 10, details: nil @@ -1599,10 +1600,10 @@ class OpenGroupManagerSpec: QuickSpec { ).insert(db) } - testPollInfo = OpenGroupAPI.RoomPollInfo.mock.with( + testPollInfo = Network.SOGS.RoomPollInfo.mock.with( token: "testRoom", activeUsers: 10, - details: OpenGroupAPI.Room.mock.with( + details: Network.SOGS.Room.mock.with( token: "test", name: "test", infoUpdates: 10, @@ -1703,7 +1704,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.handleMessages( db, messages: [ - OpenGroupAPI.Message( + Network.SOGS.Message( id: 1, sender: nil, posted: 123, @@ -1759,14 +1760,14 @@ class OpenGroupManagerSpec: QuickSpec { // MARK: ---- ignores a message with no sender it("ignores a message with no sender") { mockStorage.write { db in - try Interaction.deleteAll(db) + try Interaction.deleteWhere(db, .deleteAll) } mockStorage.write { db in OpenGroupManager.handleMessages( db, messages: [ - OpenGroupAPI.Message( + Network.SOGS.Message( id: 1, sender: nil, posted: 123, @@ -1793,14 +1794,14 @@ class OpenGroupManagerSpec: QuickSpec { // MARK: ---- ignores a message with invalid data it("ignores a message with invalid data") { mockStorage.write { db in - try Interaction.deleteAll(db) + try Interaction.deleteWhere(db, .deleteAll) } mockStorage.write { db in OpenGroupManager.handleMessages( db, messages: [ - OpenGroupAPI.Message( + Network.SOGS.Message( id: 1, sender: "05\(TestConstants.publicKey)", posted: 123, @@ -1845,7 +1846,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.handleMessages( db, messages: [ - OpenGroupAPI.Message( + Network.SOGS.Message( id: 2, sender: "05\(TestConstants.publicKey)", posted: 122, @@ -1886,7 +1887,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.handleMessages( db, messages: [ - OpenGroupAPI.Message( + Network.SOGS.Message( id: 127, sender: "05\(TestConstants.publicKey)", posted: 123, @@ -1916,7 +1917,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.handleMessages( db, messages: [ - OpenGroupAPI.Message( + Network.SOGS.Message( id: 127, sender: "05\(TestConstants.publicKey)", posted: 123, @@ -2033,7 +2034,7 @@ class OpenGroupManagerSpec: QuickSpec { // MARK: ---- ignores messages with non base64 encoded data it("ignores messages with non base64 encoded data") { - testDirectMessage = OpenGroupAPI.DirectMessage( + testDirectMessage = Network.SOGS.DirectMessage( id: testDirectMessage.id, sender: testDirectMessage.sender.replacingOccurrences(of: "8", with: "9"), recipient: testDirectMessage.recipient, @@ -2137,7 +2138,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.handleDirectMessages( db, messages: [ - OpenGroupAPI.DirectMessage( + Network.SOGS.DirectMessage( id: testDirectMessage.id, sender: testDirectMessage.sender.replacingOccurrences(of: "8", with: "9"), recipient: testDirectMessage.recipient, @@ -2293,7 +2294,7 @@ class OpenGroupManagerSpec: QuickSpec { OpenGroupManager.handleDirectMessages( db, messages: [ - OpenGroupAPI.DirectMessage( + Network.SOGS.DirectMessage( id: testDirectMessage.id, sender: testDirectMessage.sender.replacingOccurrences(of: "8", with: "9"), recipient: testDirectMessage.recipient, @@ -2529,9 +2530,9 @@ class OpenGroupManagerSpec: QuickSpec { .thenReturn(true) mockStorage.write { db in try OpenGroup( - server: OpenGroupAPI.defaultServer, + server: Network.SOGS.defaultServer, roomToken: "", - publicKey: OpenGroupAPI.defaultServerPublicKey, + publicKey: Network.SOGS.defaultServerPublicKey, isActive: false, name: "TestExisting", userCount: 0, @@ -2539,13 +2540,13 @@ class OpenGroupManagerSpec: QuickSpec { ) .insert(db) } - let expectedRequest: Network.PreparedRequest! = mockStorage.read { db in - try OpenGroupAPI.preparedCapabilitiesAndRooms( + let expectedRequest: Network.PreparedRequest! = mockStorage.read { db in + try Network.SOGS.preparedCapabilitiesAndRooms( authMethod: Authentication.community( info: LibSession.OpenGroupCapabilityInfo( roomToken: "", - server: OpenGroupAPI.defaultServer, - publicKey: OpenGroupAPI.defaultServerPublicKey, + server: Network.SOGS.defaultServer, + publicKey: Network.SOGS.defaultServerPublicKey, capabilities: [] ), forceBlinded: false @@ -2569,7 +2570,7 @@ class OpenGroupManagerSpec: QuickSpec { // MARK: ---- does not start a job to retrieve the default rooms if we already have rooms it("does not start a job to retrieve the default rooms if we already have rooms") { mockAppGroupDefaults.when { $0.bool(forKey: UserDefaults.BoolKey.isMainAppActive.rawValue) }.thenReturn(true) - cache.setDefaultRoomInfo([(room: OpenGroupAPI.Room.mock, openGroup: OpenGroup.mock)]) + cache.setDefaultRoomInfo([(room: Network.SOGS.Room.mock, openGroup: OpenGroup.mock)]) cache.defaultRoomsPublisher.sinkUntilComplete() expect(mockNetwork) @@ -2582,7 +2583,7 @@ class OpenGroupManagerSpec: QuickSpec { // MARK: - Convenience Extensions -extension OpenGroupAPI.Room { +extension Network.SOGS.Room { func with( token: String? = nil, name: String? = nil, @@ -2592,8 +2593,8 @@ extension OpenGroupAPI.Room { hiddenModerators: [String]? = nil, admins: [String]? = nil, hiddenAdmins: [String]? = nil - ) -> OpenGroupAPI.Room { - return OpenGroupAPI.Room( + ) -> Network.SOGS.Room { + return Network.SOGS.Room( token: (token ?? self.token), name: (name ?? self.name), roomDescription: self.roomDescription, @@ -2623,13 +2624,13 @@ extension OpenGroupAPI.Room { } } -extension OpenGroupAPI.RoomPollInfo { +extension Network.SOGS.RoomPollInfo { func with( token: String? = nil, activeUsers: Int64? = nil, - details: OpenGroupAPI.Room? = .mock - ) -> OpenGroupAPI.RoomPollInfo { - return OpenGroupAPI.RoomPollInfo( + details: Network.SOGS.Room? = .mock + ) -> Network.SOGS.RoomPollInfo { + return Network.SOGS.RoomPollInfo( token: (token ?? self.token), activeUsers: (activeUsers ?? self.activeUsers), admin: self.admin, @@ -2661,133 +2662,49 @@ extension OpenGroup: Mocked { infoUpdates: 0 ) } - -extension OpenGroupAPI.Capabilities: Mocked { - static var mock: OpenGroupAPI.Capabilities = OpenGroupAPI.Capabilities(capabilities: [], missing: nil) -} - -extension OpenGroupAPI.Room: Mocked { - static var mock: OpenGroupAPI.Room = OpenGroupAPI.Room( - token: "test", - name: "testRoom", - roomDescription: nil, - infoUpdates: 1, - messageSequence: 1, - created: 1, - activeUsers: 1, - activeUsersCutoff: 1, - imageId: nil, - pinnedMessages: nil, - admin: false, - globalAdmin: false, - admins: [], - hiddenAdmins: nil, - moderator: false, - globalModerator: false, - moderators: [], - hiddenModerators: nil, - read: true, - defaultRead: nil, - defaultAccessible: nil, - write: true, - defaultWrite: nil, - upload: true, - defaultUpload: nil - ) -} - -extension OpenGroupAPI.RoomPollInfo: Mocked { - static var mock: OpenGroupAPI.RoomPollInfo = OpenGroupAPI.RoomPollInfo( - token: "test", - activeUsers: 1, - admin: false, - globalAdmin: false, - moderator: false, - globalModerator: false, - read: true, - defaultRead: nil, - defaultAccessible: nil, - write: true, - defaultWrite: nil, - upload: true, - defaultUpload: false, - details: .mock - ) -} - -extension OpenGroupAPI.Message: Mocked { - static var mock: OpenGroupAPI.Message = OpenGroupAPI.Message( - id: 100, - sender: TestConstants.blind15PublicKey, - posted: 1, - edited: nil, - deleted: nil, - seqNo: 1, - whisper: false, - whisperMods: false, - whisperTo: nil, - base64EncodedData: nil, - base64EncodedSignature: nil, - reactions: nil - ) -} - -extension OpenGroupAPI.SendDirectMessageResponse: Mocked { - static var mock: OpenGroupAPI.SendDirectMessageResponse = OpenGroupAPI.SendDirectMessageResponse( - id: 1, - sender: TestConstants.blind15PublicKey, - recipient: "testRecipient", - posted: 1122, - expires: 2233 - ) -} - -extension OpenGroupAPI.DirectMessage: Mocked { - static var mock: OpenGroupAPI.DirectMessage = OpenGroupAPI.DirectMessage( - id: 101, - sender: TestConstants.blind15PublicKey, - recipient: "testRecipient", - posted: 1212, - expires: 2323, - base64EncodedMessage: "TestMessage".data(using: .utf8)!.base64EncodedString() - ) -} extension Network.BatchResponse { static let mockUnblindedPollResponse: AnyPublisher<(ResponseInfoType, Data?), Error> = MockNetwork.batchResponseData( with: [ - (OpenGroupAPI.Endpoint.capabilities, OpenGroupAPI.Capabilities.mockBatchSubResponse()), - (OpenGroupAPI.Endpoint.roomPollInfo("testRoom", 0), OpenGroupAPI.RoomPollInfo.mockBatchSubResponse()), - (OpenGroupAPI.Endpoint.roomMessagesRecent("testRoom"), [OpenGroupAPI.Message].mockBatchSubResponse()) + (Network.SOGS.Endpoint.capabilities, Network.SOGS.CapabilitiesResponse.mockBatchSubResponse()), + (Network.SOGS.Endpoint.roomPollInfo("testRoom", 0), Network.SOGS.RoomPollInfo.mockBatchSubResponse()), + (Network.SOGS.Endpoint.roomMessagesRecent("testRoom"), [Network.SOGS.Message].mockBatchSubResponse()) ] ) static let mockBlindedPollResponse: AnyPublisher<(ResponseInfoType, Data?), Error> = MockNetwork.batchResponseData( with: [ - (OpenGroupAPI.Endpoint.capabilities, OpenGroupAPI.Capabilities.mockBatchSubResponse()), - (OpenGroupAPI.Endpoint.roomPollInfo("testRoom", 0), OpenGroupAPI.RoomPollInfo.mockBatchSubResponse()), - (OpenGroupAPI.Endpoint.roomMessagesRecent("testRoom"), OpenGroupAPI.Message.mockBatchSubResponse()), - (OpenGroupAPI.Endpoint.inboxSince(id: 0), OpenGroupAPI.DirectMessage.mockBatchSubResponse()), - (OpenGroupAPI.Endpoint.outboxSince(id: 0), OpenGroupAPI.DirectMessage.self.mockBatchSubResponse()) + (Network.SOGS.Endpoint.capabilities, Network.SOGS.CapabilitiesResponse.mockBatchSubResponse()), + (Network.SOGS.Endpoint.roomPollInfo("testRoom", 0), Network.SOGS.RoomPollInfo.mockBatchSubResponse()), + (Network.SOGS.Endpoint.roomMessagesRecent("testRoom"), Network.SOGS.Message.mockBatchSubResponse()), + (Network.SOGS.Endpoint.inboxSince(id: 0), Network.SOGS.DirectMessage.mockBatchSubResponse()), + (Network.SOGS.Endpoint.outboxSince(id: 0), Network.SOGS.DirectMessage.self.mockBatchSubResponse()) ] ) static let mockCapabilitiesResponse: AnyPublisher<(ResponseInfoType, Data?), Error> = MockNetwork.batchResponseData( with: [ - (OpenGroupAPI.Endpoint.capabilities, OpenGroupAPI.Capabilities.mockBatchSubResponse()) + (Network.SOGS.Endpoint.capabilities, Network.SOGS.CapabilitiesResponse.mockBatchSubResponse()) ] ) static let mockRoomResponse: AnyPublisher<(ResponseInfoType, Data?), Error> = MockNetwork.batchResponseData( with: [ - (OpenGroupAPI.Endpoint.capabilities, OpenGroupAPI.Room.mockBatchSubResponse()) + (Network.SOGS.Endpoint.capabilities, Network.SOGS.Room.mockBatchSubResponse()) ] ) static let mockBanAndDeleteAllResponse: AnyPublisher<(ResponseInfoType, Data?), Error> = MockNetwork.batchResponseData( with: [ - (OpenGroupAPI.Endpoint.userBan(""), NoResponse.mockBatchSubResponse()), - (OpenGroupAPI.Endpoint.roomDeleteMessages("testRoon", sessionId: ""), NoResponse.mockBatchSubResponse()) + (Network.SOGS.Endpoint.userBan(""), NoResponse.mockBatchSubResponse()), + (Network.SOGS.Endpoint.roomDeleteMessages("testRoon", sessionId: ""), NoResponse.mockBatchSubResponse()) + ] + ) + + static let mockCapabilitiesAndRoomResponse: AnyPublisher<(ResponseInfoType, Data?), Error> = MockNetwork.batchResponseData( + with: [ + (Network.SOGS.Endpoint.capabilities, Network.SOGS.CapabilitiesResponse.mockBatchSubResponse()), + (Network.SOGS.Endpoint.room("testRoom"), Network.SOGS.Room.mockBatchSubResponse()) ] ) } diff --git a/SessionMessagingKitTests/Open Groups/Types/SOGSErrorSpec.swift b/SessionMessagingKitTests/Open Groups/Types/SOGSErrorSpec.swift deleted file mode 100644 index 7dba9ab779..0000000000 --- a/SessionMessagingKitTests/Open Groups/Types/SOGSErrorSpec.swift +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import Foundation - -import Quick -import Nimble - -@testable import SessionMessagingKit - -class SOGSErrorSpec: QuickSpec { - override class func spec() { - // MARK: - a SOGSError - describe("a SOGSError") { - // MARK: -- generates the error description correctly - it("generates the error description correctly") { - expect(OpenGroupAPIError.decryptionFailed.description).to(equal("Couldn't decrypt response.")) - expect(OpenGroupAPIError.signingFailed.description).to(equal("Couldn't sign message.")) - expect(OpenGroupAPIError.noPublicKey.description).to(equal("Couldn't find server public key.")) - expect(OpenGroupAPIError.invalidEmoji.description).to(equal("The emoji is invalid.")) - expect(OpenGroupAPIError.invalidPoll.description).to(equal("Poller in invalid state.")) - } - } - } -} diff --git a/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift b/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift index debc33045b..2926f10f11 100644 --- a/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/MessageReceiverGroupsSpec.swift @@ -9,7 +9,7 @@ import SessionUtil import SessionUtilitiesKit import SessionUIKit -@testable import SessionSnodeKit +@testable import SessionNetworkingKit @testable import SessionMessagingKit class MessageReceiverGroupsSpec: QuickSpec { @@ -29,11 +29,7 @@ class MessageReceiverGroupsSpec: QuickSpec { } @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNSnodeKit.self, - SNMessagingKit.self - ], + migrations: SNMessagingKit.migrations, using: dependencies, initialData: { db in try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) @@ -72,7 +68,7 @@ class MessageReceiverGroupsSpec: QuickSpec { initialSetup: { network in network .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } - .thenReturn(MockNetwork.response(with: FileUploadResponse(id: "1"))) + .thenReturn(MockNetwork.response(with: FileUploadResponse(id: "1", expires: nil))) network .when { $0.getSwarm(for: .any) } .thenReturn([ @@ -142,7 +138,7 @@ class MessageReceiverGroupsSpec: QuickSpec { .thenReturn(Data([1, 2, 3])) keychain .when { try $0.data(forKey: .pushNotificationEncryptionKey) } - .thenReturn(Data((0.. = mockStorage.write { db in + let expectedRequest: Network.PreparedRequest = mockStorage.write { db in _ = try SessionThread.upsert( db, id: groupId.hexString, @@ -860,10 +858,17 @@ class MessageReceiverGroupsSpec: QuickSpec { groupIdentityPrivateKey: groupSecretKey, invited: nil ).upsert(db) - let result = try PushNotificationAPI.preparedSubscribe( - db, + let result = try Network.PushNotification.preparedSubscribe( token: Data([5, 4, 3, 2, 1]), - sessionIds: [groupId], + swarms: [ + ( + groupId, + Authentication.groupAdmin( + groupSessionId: groupId, + ed25519SecretKey: Array(groupSecretKey) + ) + ) + ], using: dependencies ) @@ -914,7 +919,7 @@ class MessageReceiverGroupsSpec: QuickSpec { // MARK: -------- subscribes for push notifications it("subscribes for push notifications") { - let expectedRequest: Network.PreparedRequest = mockStorage.write { db in + let expectedRequest: Network.PreparedRequest = mockStorage.write { db in _ = try SessionThread.upsert( db, id: groupId.hexString, @@ -933,10 +938,17 @@ class MessageReceiverGroupsSpec: QuickSpec { authData: inviteMessage.memberAuthData, invited: nil ).upsert(db) - let result = try PushNotificationAPI.preparedSubscribe( - db, + let result = try Network.PushNotification.preparedSubscribe( token: Data(hex: Data([5, 4, 3, 2, 1]).toHexString()), - sessionIds: [groupId], + swarms: [ + ( + groupId, + Authentication.groupMember( + groupSessionId: groupId, + authData: inviteMessage.memberAuthData + ) + ) + ], using: dependencies ) @@ -2825,7 +2837,7 @@ class MessageReceiverGroupsSpec: QuickSpec { deleteContentMessage.sender = "051111111111111111111111111111111111111111111111111111111111111112" deleteContentMessage.sentTimestampMs = 1234567800000 - let preparedRequest: Network.PreparedRequest<[String: Bool]> = try! SnodeAPI + let preparedRequest: Network.PreparedRequest<[String: Bool]> = try! Network.SnodeAPI .preparedDeleteMessages( serverHashes: ["TestMessageHash3"], requireSuccessfulDeletion: false, @@ -3096,11 +3108,18 @@ class MessageReceiverGroupsSpec: QuickSpec { .when { $0.bool(forKey: UserDefaults.BoolKey.isUsingFullAPNs.rawValue) } .thenReturn(true) - let expectedRequest: Network.PreparedRequest = mockStorage.read { db in - try PushNotificationAPI.preparedUnsubscribe( - db, + let expectedRequest: Network.PreparedRequest = mockStorage.read { db in + try Network.PushNotification.preparedUnsubscribe( token: Data([5, 4, 3, 2, 1]), - sessionIds: [groupId], + swarms: [ + ( + groupId, + Authentication.groupMember( + groupSessionId: groupId, + authData: Data([1, 2, 3]) + ) + ) + ], using: dependencies ) }! diff --git a/SessionMessagingKitTests/Sending & Receiving/MessageSenderGroupsSpec.swift b/SessionMessagingKitTests/Sending & Receiving/MessageSenderGroupsSpec.swift index 1bd4a1b614..80139dd1c5 100644 --- a/SessionMessagingKitTests/Sending & Receiving/MessageSenderGroupsSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/MessageSenderGroupsSpec.swift @@ -10,7 +10,7 @@ import Quick import Nimble @testable import SessionMessagingKit -@testable import SessionSnodeKit +@testable import SessionNetworkingKit class MessageSenderGroupsSpec: QuickSpec { override class func spec() { @@ -29,10 +29,7 @@ class MessageSenderGroupsSpec: QuickSpec { } @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNMessagingKit.self - ], + migrations: SNMessagingKit.migrations, using: dependencies, initialData: { db in try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) @@ -162,7 +159,7 @@ class MessageSenderGroupsSpec: QuickSpec { .thenReturn(Data([1, 2, 3])) keychain .when { try $0.data(forKey: .pushNotificationEncryptionKey) } - .thenReturn(Data((0.. = try SnodeAPI.preparedSequence( + let preparedRequest: Network.PreparedRequest = try Network.SnodeAPI.preparedSequence( requests: [ - try SnodeAPI + try Network.SnodeAPI .preparedSendMessage( message: SnodeMessage( recipient: groupId.hexString, @@ -613,7 +610,7 @@ class MessageSenderGroupsSpec: QuickSpec { it("uploads the image") { mockNetwork .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } - .thenReturn(MockNetwork.response(with: FileUploadResponse(id: "1"))) + .thenReturn(MockNetwork.response(with: FileUploadResponse(id: "1", expires: nil))) MessageSender .createGroup( @@ -652,7 +649,7 @@ class MessageSenderGroupsSpec: QuickSpec { mockLibSessionCache.when { $0.isEmpty }.thenReturn(true) mockNetwork .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } - .thenReturn(MockNetwork.response(with: FileUploadResponse(id: "1"))) + .thenReturn(MockNetwork.response(with: FileUploadResponse(id: "1", expires: nil))) MessageSender .createGroup( @@ -734,7 +731,7 @@ class MessageSenderGroupsSpec: QuickSpec { // MARK: ------ and trying to subscribe for push notifications context("and trying to subscribe for push notifications") { - @TestState var expectedRequest: Network.PreparedRequest! + @TestState var expectedRequest: Network.PreparedRequest! beforeEach { // Need to set `isUsingFullAPNs` to true to generate the `expectedRequest` @@ -763,10 +760,17 @@ class MessageSenderGroupsSpec: QuickSpec { groupIdentityPrivateKey: groupSecretKey, invited: nil ).upsert(db) - let result = try PushNotificationAPI.preparedSubscribe( - db, + let result = try Network.PushNotification.preparedSubscribe( token: Data([5, 4, 3, 2, 1]), - sessionIds: [groupId], + swarms: [ + ( + groupId, + Authentication.groupAdmin( + groupSessionId: groupId, + ed25519SecretKey: Array(groupSecretKey) + ) + ) + ], using: dependencies ) @@ -1027,9 +1031,9 @@ class MessageSenderGroupsSpec: QuickSpec { "LPczVOFKOPs+rrB3aUpMsNUnJHOEhW9g6zi/UPjuCWTnnvpxlMTpHaTFlMTp+NjQ6dKi86jZJ" + "l3oiJEA5h5pBE5oOJHQNvtF8GOcsYwrIFTZKnI7AGkBSu1TxP0xLWwTUzjOGMgmKvlIgkQ6e9" + "r3JBmU=" - let expectedRequest: Network.PreparedRequest = try SnodeAPI.preparedSequence( + let expectedRequest: Network.PreparedRequest = try Network.SnodeAPI.preparedSequence( requests: [] - .appending(try SnodeAPI.preparedUnrevokeSubaccounts( + .appending(try Network.SnodeAPI.preparedUnrevokeSubaccounts( subaccountsToUnrevoke: [Array("TestSubAccountToken".data(using: .utf8)!)], authMethod: Authentication.groupAdmin( groupSessionId: groupId, @@ -1037,7 +1041,7 @@ class MessageSenderGroupsSpec: QuickSpec { ), using: dependencies )) - .appending(try SnodeAPI.preparedSendMessage( + .appending(try Network.SnodeAPI.preparedSendMessage( message: SnodeMessage( recipient: groupId.hexString, data: Data(base64Encoded: requestDataString)!, @@ -1051,7 +1055,7 @@ class MessageSenderGroupsSpec: QuickSpec { ), using: dependencies )) - .appending(try SnodeAPI.preparedDeleteMessages( + .appending(try Network.SnodeAPI.preparedDeleteMessages( serverHashes: ["testHash"], requireSuccessfulDeletion: false, authMethod: Authentication.groupAdmin( @@ -1242,9 +1246,9 @@ class MessageSenderGroupsSpec: QuickSpec { // MARK: ---- includes the unrevoke subaccounts as part of the config sync sequence it("includes the unrevoke subaccounts as part of the config sync sequence") { - let expectedRequest: Network.PreparedRequest = try SnodeAPI.preparedSequence( + let expectedRequest: Network.PreparedRequest = try Network.SnodeAPI.preparedSequence( requests: [] - .appending(try SnodeAPI + .appending(try Network.SnodeAPI .preparedUnrevokeSubaccounts( subaccountsToUnrevoke: [Array("TestSubAccountToken".data(using: .utf8)!)], authMethod: Authentication.groupAdmin( @@ -1254,7 +1258,7 @@ class MessageSenderGroupsSpec: QuickSpec { using: dependencies ) ) - .appending(try SnodeAPI.preparedDeleteMessages( + .appending(try Network.SnodeAPI.preparedDeleteMessages( serverHashes: ["testHash"], requireSuccessfulDeletion: false, authMethod: Authentication.groupAdmin( @@ -1475,25 +1479,25 @@ extension Network.BatchResponse { fileprivate static let mockConfigSyncResponse: AnyPublisher<(ResponseInfoType, Data?), Error> = MockNetwork.batchResponseData( with: [ - (SnodeAPI.Endpoint.sendMessage, SendMessagesResponse.mockBatchSubResponse()), - (SnodeAPI.Endpoint.sendMessage, SendMessagesResponse.mockBatchSubResponse()), - (SnodeAPI.Endpoint.sendMessage, SendMessagesResponse.mockBatchSubResponse()), - (SnodeAPI.Endpoint.deleteMessages, DeleteMessagesResponse.mockBatchSubResponse()) + (Network.SnodeAPI.Endpoint.sendMessage, SendMessagesResponse.mockBatchSubResponse()), + (Network.SnodeAPI.Endpoint.sendMessage, SendMessagesResponse.mockBatchSubResponse()), + (Network.SnodeAPI.Endpoint.sendMessage, SendMessagesResponse.mockBatchSubResponse()), + (Network.SnodeAPI.Endpoint.deleteMessages, DeleteMessagesResponse.mockBatchSubResponse()) ] ) fileprivate static let mockAddMemberConfigSyncResponse: AnyPublisher<(ResponseInfoType, Data?), Error> = MockNetwork.batchResponseData( with: [ - (SnodeAPI.Endpoint.unrevokeSubaccount, UnrevokeSubaccountResponse.mockBatchSubResponse()), - (SnodeAPI.Endpoint.deleteMessages, DeleteMessagesResponse.mockBatchSubResponse()) + (Network.SnodeAPI.Endpoint.unrevokeSubaccount, UnrevokeSubaccountResponse.mockBatchSubResponse()), + (Network.SnodeAPI.Endpoint.deleteMessages, DeleteMessagesResponse.mockBatchSubResponse()) ] ) fileprivate static let mockAddMemberHistoricConfigSyncResponse: AnyPublisher<(ResponseInfoType, Data?), Error> = MockNetwork.batchResponseData( with: [ - (SnodeAPI.Endpoint.unrevokeSubaccount, UnrevokeSubaccountResponse.mockBatchSubResponse()), - (SnodeAPI.Endpoint.sendMessage, SendMessagesResponse.mockBatchSubResponse()), - (SnodeAPI.Endpoint.deleteMessages, DeleteMessagesResponse.mockBatchSubResponse()) + (Network.SnodeAPI.Endpoint.unrevokeSubaccount, UnrevokeSubaccountResponse.mockBatchSubResponse()), + (Network.SnodeAPI.Endpoint.sendMessage, SendMessagesResponse.mockBatchSubResponse()), + (Network.SnodeAPI.Endpoint.deleteMessages, DeleteMessagesResponse.mockBatchSubResponse()) ] ) } diff --git a/SessionMessagingKitTests/Sending & Receiving/MessageSenderSpec.swift b/SessionMessagingKitTests/Sending & Receiving/MessageSenderSpec.swift index 5e67562442..ee73e9fbc0 100644 --- a/SessionMessagingKitTests/Sending & Receiving/MessageSenderSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/MessageSenderSpec.swift @@ -2,7 +2,7 @@ import Foundation import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit import Quick @@ -17,10 +17,7 @@ class MessageSenderSpec: QuickSpec { @TestState var dependencies: TestDependencies! = TestDependencies() @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNMessagingKit.self - ], + migrations: SNMessagingKit.migrations, using: dependencies, initialData: { db in try Identity(variant: .ed25519PublicKey, data: Data(hex: TestConstants.edPublicKey)).insert(db) diff --git a/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerSpec.swift b/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerSpec.swift index 742f51d783..0ee95089ba 100644 --- a/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/Pollers/CommunityPollerSpec.swift @@ -3,7 +3,7 @@ import UIKit import Combine import GRDB -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit import Quick @@ -21,10 +21,7 @@ class CommunityPollerSpec: AsyncSpec { } @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNMessagingKit.self - ], + migrations: SNMessagingKit.migrations, using: dependencies, initialData: { db in try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) @@ -63,7 +60,7 @@ class CommunityPollerSpec: AsyncSpec { network .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } .thenReturn( - MockNetwork.response(with: FileUploadResponse(id: "1")) + MockNetwork.response(with: FileUploadResponse(id: "1", expires: nil)) .delay(for: .seconds(10), scheduler: DispatchQueue.main) .eraseToAnyPublisher() ) diff --git a/SessionMessagingKitTests/Utilities/ExtensionHelperSpec.swift b/SessionMessagingKitTests/Utilities/ExtensionHelperSpec.swift index 16d88729ea..13dd801e31 100644 --- a/SessionMessagingKitTests/Utilities/ExtensionHelperSpec.swift +++ b/SessionMessagingKitTests/Utilities/ExtensionHelperSpec.swift @@ -3,7 +3,7 @@ import Foundation import GRDB import SessionUtil -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit import Quick @@ -24,10 +24,7 @@ class ExtensionHelperSpec: AsyncSpec { @TestState(singleton: .extensionHelper, in: dependencies) var extensionHelper: ExtensionHelper! = ExtensionHelper(using: dependencies) @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNMessagingKit.self - ], + migrations: SNMessagingKit.migrations, using: dependencies ) @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = MockCrypto( @@ -738,7 +735,7 @@ class ExtensionHelperSpec: AsyncSpec { categories: [ Log.Category.create( "ExtensionHelper", - customPrefix: "", + group: nil, customSuffix: "", defaultLevel: .info ) @@ -2332,7 +2329,7 @@ class ExtensionHelperSpec: AsyncSpec { categories: [ Log.Category.create( "ExtensionHelper", - customPrefix: "", + group: nil, customSuffix: "", defaultLevel: .info ) @@ -2369,7 +2366,7 @@ class ExtensionHelperSpec: AsyncSpec { categories: [ Log.Category.create( "ExtensionHelper", - customPrefix: "", + group: nil, customSuffix: "", defaultLevel: .info ) @@ -2385,7 +2382,7 @@ class ExtensionHelperSpec: AsyncSpec { categories: [ Log.Category.create( "ExtensionHelper", - customPrefix: "", + group: nil, customSuffix: "", defaultLevel: .info ) @@ -2422,7 +2419,7 @@ class ExtensionHelperSpec: AsyncSpec { categories: [ Log.Category.create( "ExtensionHelper", - customPrefix: "", + group: nil, customSuffix: "", defaultLevel: .info ) @@ -2438,7 +2435,7 @@ class ExtensionHelperSpec: AsyncSpec { categories: [ Log.Category.create( "ExtensionHelper", - customPrefix: "", + group: nil, customSuffix: "", defaultLevel: .info ) @@ -2480,7 +2477,7 @@ class ExtensionHelperSpec: AsyncSpec { categories: [ Log.Category.create( "ExtensionHelper", - customPrefix: "", + group: nil, customSuffix: "", defaultLevel: .info ) diff --git a/SessionMessagingKitTests/_TestUtilities/CustomArgSummaryDescribable+SMK.swift b/SessionMessagingKitTests/_TestUtilities/CustomArgSummaryDescribable+SessionMessagingKit.swift similarity index 100% rename from SessionMessagingKitTests/_TestUtilities/CustomArgSummaryDescribable+SMK.swift rename to SessionMessagingKitTests/_TestUtilities/CustomArgSummaryDescribable+SessionMessagingKit.swift diff --git a/SessionMessagingKitTests/_TestUtilities/MockExtensionHelper.swift b/SessionMessagingKitTests/_TestUtilities/MockExtensionHelper.swift index e68a787acd..06a4562e50 100644 --- a/SessionMessagingKitTests/_TestUtilities/MockExtensionHelper.swift +++ b/SessionMessagingKitTests/_TestUtilities/MockExtensionHelper.swift @@ -1,7 +1,7 @@ // Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. import Foundation -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit @testable import SessionMessagingKit diff --git a/SessionMessagingKitTests/_TestUtilities/MockImageDataManager.swift b/SessionMessagingKitTests/_TestUtilities/MockImageDataManager.swift index d409bede10..78693da700 100644 --- a/SessionMessagingKitTests/_TestUtilities/MockImageDataManager.swift +++ b/SessionMessagingKitTests/_TestUtilities/MockImageDataManager.swift @@ -12,9 +12,10 @@ class MockImageDataManager: Mock, ImageDataManagerType { return mock(args: [source]) } + @MainActor func load( _ source: ImageDataManager.DataSource, - onComplete: @escaping (ImageDataManager.ProcessedImageData?) -> Void + onComplete: @MainActor @escaping (ImageDataManager.ProcessedImageData?) -> Void ) { mockNoReturn(args: [source], untrackedArgs: [onComplete]) } diff --git a/SessionMessagingKitTests/_TestUtilities/MockLibSessionCache.swift b/SessionMessagingKitTests/_TestUtilities/MockLibSessionCache.swift index e27b816544..24f5fa056a 100644 --- a/SessionMessagingKitTests/_TestUtilities/MockLibSessionCache.swift +++ b/SessionMessagingKitTests/_TestUtilities/MockLibSessionCache.swift @@ -184,8 +184,8 @@ class MockLibSessionCache: Mock, LibSessionCacheType { mockNoReturn(generics: [T.self], args: [key, value]) } - func updateProfile(displayName: String, displayPictureUrl: String?, displayPictureEncryptionKey: Data?) throws { - try mockThrowingNoReturn(args: [displayName, displayPictureUrl, displayPictureEncryptionKey]) + func updateProfile(displayName: String, displayPictureUrl: String?, displayPictureEncryptionKey: Data?, isReuploadProfilePicture: Bool) throws { + try mockThrowingNoReturn(args: [displayName, displayPictureUrl, displayPictureEncryptionKey, isReuploadProfilePicture]) } func canPerformChange( @@ -355,6 +355,7 @@ extension Mock where T == LibSessionCacheType { .when { try $0.pendingPushes(swarmPublicKey: .any) } .thenReturn(LibSession.PendingPushes()) self.when { $0.configNeedsDump(.any) }.thenReturn(false) + self.when { $0.activeHashes(for: .any) }.thenReturn([]) self .when { try $0.createDump(config: .any, for: .any, sessionId: .any, timestampMs: .any) } .thenReturn(nil) diff --git a/SessionMessagingKitTests/_TestUtilities/MockOGMCache.swift b/SessionMessagingKitTests/_TestUtilities/MockOGMCache.swift index a7db4da5de..191d60c4d0 100644 --- a/SessionMessagingKitTests/_TestUtilities/MockOGMCache.swift +++ b/SessionMessagingKitTests/_TestUtilities/MockOGMCache.swift @@ -11,7 +11,7 @@ class MockOGMCache: Mock, OGMCacheType { mock() } - var pendingChanges: [OpenGroupAPI.PendingChange] { + var pendingChanges: [OpenGroupManager.PendingChange] { get { return mock() } set { mockNoReturn(args: [newValue]) } } diff --git a/SessionMessagingKitTests/_TestUtilities/MockPoller.swift b/SessionMessagingKitTests/_TestUtilities/MockPoller.swift index b3f044e3bd..cfc5968bd2 100644 --- a/SessionMessagingKitTests/_TestUtilities/MockPoller.swift +++ b/SessionMessagingKitTests/_TestUtilities/MockPoller.swift @@ -2,7 +2,7 @@ import Foundation import Combine -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit @testable import SessionMessagingKit @@ -43,7 +43,7 @@ class MockPoller: Mock, PollerType { pollerQueue: DispatchQueue, pollerDestination: PollerDestination, pollerDrainBehaviour: ThreadSafeObject, - namespaces: [SnodeAPI.Namespace], + namespaces: [Network.SnodeAPI.Namespace], failureCount: Int, shouldStoreMessages: Bool, logStartAndStopCalls: Bool, diff --git a/SessionMessagingKitTests/_TestUtilities/MockSwarmPoller.swift b/SessionMessagingKitTests/_TestUtilities/MockSwarmPoller.swift index 43ab428f96..05af305032 100644 --- a/SessionMessagingKitTests/_TestUtilities/MockSwarmPoller.swift +++ b/SessionMessagingKitTests/_TestUtilities/MockSwarmPoller.swift @@ -2,7 +2,7 @@ import Foundation import Combine -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit @testable import SessionMessagingKit @@ -39,7 +39,7 @@ class MockSwarmPoller: Mock, SwarmPollerType & Pol pollerQueue: DispatchQueue, pollerDestination: PollerDestination, pollerDrainBehaviour: ThreadSafeObject, - namespaces: [SnodeAPI.Namespace], + namespaces: [Network.SnodeAPI.Namespace], failureCount: Int, shouldStoreMessages: Bool, logStartAndStopCalls: Bool, diff --git a/SessionNetworkingKit/Crypto/Crypto+SessionNetworkingKit.swift b/SessionNetworkingKit/Crypto/Crypto+SessionNetworkingKit.swift new file mode 100644 index 0000000000..dad09fce75 --- /dev/null +++ b/SessionNetworkingKit/Crypto/Crypto+SessionNetworkingKit.swift @@ -0,0 +1,61 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +import Foundation +import SessionUtil +import SessionUtilitiesKit + +// MARK: - ONS Response + +internal extension Crypto.Generator { + static func sessionId( + name: String, + response: Network.SnodeAPI.ONSResolveResponse + ) -> Crypto.Generator { + return Crypto.Generator( + id: "sessionId_for_ONS_response", + args: [name, response] + ) { + guard var cName: [CChar] = name.lowercased().cString(using: .utf8) else { + throw SnodeAPIError.onsDecryptionFailed + } + + // Name must be in lowercase + var cCiphertext: [UInt8] = Array(Data(hex: response.result.encryptedValue)) + var cSessionId: [CChar] = [CChar](repeating: 0, count: 67) + + // Need to switch on `result.nonce` and explciitly pass `nil` because passing an optional + // to a C function doesn't seem to work correctly + switch response.result.nonce { + case .none: + guard + session_decrypt_ons_response( + &cName, + &cCiphertext, + cCiphertext.count, + nil, + &cSessionId + ) + else { throw SnodeAPIError.onsDecryptionFailed } + + case .some(let nonce): + var cNonce: [UInt8] = Array(Data(hex: nonce)) + + guard + cNonce.count == 24, + session_decrypt_ons_response( + &cName, + &cCiphertext, + cCiphertext.count, + &cNonce, + &cSessionId + ) + else { throw SnodeAPIError.onsDecryptionFailed } + } + + return String(cString: cSessionId) + } + } +} + diff --git a/SessionSnodeKit/Crypto/Crypto+SessionSnodeKit.swift b/SessionNetworkingKit/FileServer/Crypto/Crypto+FileServer.swift similarity index 58% rename from SessionSnodeKit/Crypto/Crypto+SessionSnodeKit.swift rename to SessionNetworkingKit/FileServer/Crypto/Crypto+FileServer.swift index 94fed80795..2dced50d25 100644 --- a/SessionSnodeKit/Crypto/Crypto+SessionSnodeKit.swift +++ b/SessionNetworkingKit/FileServer/Crypto/Crypto+FileServer.swift @@ -1,4 +1,4 @@ -// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. // // stringlint:disable @@ -6,59 +6,6 @@ import Foundation import SessionUtil import SessionUtilitiesKit -// MARK: - ONS Response - -internal extension Crypto.Generator { - static func sessionId( - name: String, - response: SnodeAPI.ONSResolveResponse - ) -> Crypto.Generator { - return Crypto.Generator( - id: "sessionId_for_ONS_response", - args: [name, response] - ) { - guard var cName: [CChar] = name.lowercased().cString(using: .utf8) else { - throw SnodeAPIError.onsDecryptionFailed - } - - // Name must be in lowercase - var cCiphertext: [UInt8] = Array(Data(hex: response.result.encryptedValue)) - var cSessionId: [CChar] = [CChar](repeating: 0, count: 67) - - // Need to switch on `result.nonce` and explciitly pass `nil` because passing an optional - // to a C function doesn't seem to work correctly - switch response.result.nonce { - case .none: - guard - session_decrypt_ons_response( - &cName, - &cCiphertext, - cCiphertext.count, - nil, - &cSessionId - ) - else { throw SnodeAPIError.onsDecryptionFailed } - - case .some(let nonce): - var cNonce: [UInt8] = Array(Data(hex: nonce)) - - guard - cNonce.count == 24, - session_decrypt_ons_response( - &cName, - &cCiphertext, - cCiphertext.count, - &cNonce, - &cSessionId - ) - else { throw SnodeAPIError.onsDecryptionFailed } - } - - return String(cString: cSessionId) - } - } -} - // MARK: - Version Blinded ID public extension Crypto.Generator { diff --git a/SessionNetworkingKit/FileServer/FileServer.swift b/SessionNetworkingKit/FileServer/FileServer.swift new file mode 100644 index 0000000000..90fdec9e08 --- /dev/null +++ b/SessionNetworkingKit/FileServer/FileServer.swift @@ -0,0 +1,91 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +import Foundation +import SessionUtilitiesKit + +public extension Network { + enum FileServer { + internal static let fileServer = "http://filev2.getsession.org" + internal static let fileServerPublicKey = "da21e1d886c6fbaea313f75298bd64aab03a97ce985b46bb2dad9f2089c8ee59" + internal static let legacyFileServer = "http://88.99.175.227" + internal static let legacyFileServerPublicKey = "7cb31905b55cd5580c686911debf672577b3fb0bff81df4ce2d5c4cb3a7aaa69" + + static func fileServerPubkey(url: String? = nil) -> String { + switch url?.contains(legacyFileServer) { + case true: return legacyFileServerPublicKey + default: return fileServerPublicKey + } + } + + static func isFileServerUrl(url: URL) -> Bool { + return ( + url.absoluteString.starts(with: fileServer) || + url.absoluteString.starts(with: legacyFileServer) + ) + } + + public static func downloadUrlString(for url: String, fileId: String) -> String { + switch url.contains(legacyFileServer) { + case true: return "\(fileServer)/\(Endpoint.fileIndividual(fileId).path)" + default: return downloadUrlString(for: fileId) + } + } + + public static func downloadUrlString(for fileId: String) -> String { + return "\(fileServer)/\(Endpoint.fileIndividual(fileId).path)" + } + + public static func fileId(for downloadUrl: String?) -> String? { + return downloadUrl + .map { urlString -> String? in + urlString + .split(separator: "/") // stringlint:ignore + .last + .map { String($0) } + } + } + } + + static func preparedUpload( + data: Data, + requestAndPathBuildTimeout: TimeInterval? = nil, + using dependencies: Dependencies + ) throws -> PreparedRequest { + return try PreparedRequest( + request: Request( + endpoint: FileServer.Endpoint.file, + destination: .serverUpload( + server: FileServer.fileServer, + x25519PublicKey: FileServer.fileServerPublicKey, + fileName: nil + ), + body: data + ), + responseType: FileUploadResponse.self, + requestTimeout: Network.fileUploadTimeout, + requestAndPathBuildTimeout: requestAndPathBuildTimeout, + using: dependencies + ) + } + + static func preparedDownload( + url: URL, + using dependencies: Dependencies + ) throws -> PreparedRequest { + return try PreparedRequest( + request: Request( + endpoint: FileServer.Endpoint.directUrl(url), + destination: .serverDownload( + url: url, + x25519PublicKey: FileServer.fileServerPublicKey, + fileName: nil + ) + ), + responseType: Data.self, + requestTimeout: Network.fileUploadTimeout, + using: dependencies + ) + } +} diff --git a/SessionNetworkingKit/FileServer/FileServerAPI.swift b/SessionNetworkingKit/FileServer/FileServerAPI.swift new file mode 100644 index 0000000000..9e8e46a5b3 --- /dev/null +++ b/SessionNetworkingKit/FileServer/FileServerAPI.swift @@ -0,0 +1,57 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import SessionUtilitiesKit + +private typealias FileServer = Network.FileServer +private typealias Endpoint = Network.FileServer.Endpoint + +public extension Network.FileServer { + static func preparedUpload( + data: Data, + requestAndPathBuildTimeout: TimeInterval? = nil, + using dependencies: Dependencies + ) throws -> Network.PreparedRequest { + var headers: [HTTPHeader: String] = [:] + + if dependencies[feature: .shortenFileTTL] { + headers = [.fileCustomTTL : "60"] + } + + return try Network.PreparedRequest( + request: Request( + endpoint: .file, + destination: .serverUpload( + server: FileServer.fileServer, + headers: headers, + x25519PublicKey: FileServer.fileServerPublicKey, + fileName: nil + ), + body: data + ), + responseType: FileUploadResponse.self, + requestTimeout: Network.fileUploadTimeout, + requestAndPathBuildTimeout: requestAndPathBuildTimeout, + using: dependencies + ) + } + + static func preparedDownload( + url: URL, + using dependencies: Dependencies + ) throws -> Network.PreparedRequest { + return try Network.PreparedRequest( + request: Request( + endpoint: .directUrl(url), + destination: .serverDownload( + url: url, + x25519PublicKey: FileServer.fileServerPublicKey, + fileName: nil + ) + ), + responseType: Data.self, + requestTimeout: Network.fileUploadTimeout, + using: dependencies + ) + } +} diff --git a/SessionNetworkingKit/FileServer/FileServerEndpoint.swift b/SessionNetworkingKit/FileServer/FileServerEndpoint.swift new file mode 100644 index 0000000000..5f23b85624 --- /dev/null +++ b/SessionNetworkingKit/FileServer/FileServerEndpoint.swift @@ -0,0 +1,23 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public extension Network.FileServer { + enum Endpoint: EndpointType { + case file + case fileIndividual(String) + case directUrl(URL) + case sessionVersion + + public static var name: String { "FileServer.Endpoint" } + + public var path: String { + switch self { + case .file: return "file" + case .fileIndividual(let fileId): return "file/\(fileId)" + case .directUrl(let url): return url.path.removingPrefix("/") + case .sessionVersion: return "session_version" + } + } + } +} diff --git a/SessionNetworkingKit/FileServer/Models/AppVersionResponse.swift b/SessionNetworkingKit/FileServer/Models/AppVersionResponse.swift new file mode 100644 index 0000000000..12c1d7fcbb --- /dev/null +++ b/SessionNetworkingKit/FileServer/Models/AppVersionResponse.swift @@ -0,0 +1,99 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public extension Network.FileServer { + class AppVersionResponse: AppVersionInfo { + enum CodingKeys: String, CodingKey { + case prerelease + } + + public let prerelease: AppVersionInfo? + + public init( + version: String, + updated: TimeInterval?, + name: String?, + notes: String?, + assets: [Asset]?, + prerelease: AppVersionInfo? + ) { + self.prerelease = prerelease + + super.init( + version: version, + updated: updated, + name: name, + notes: notes, + assets: assets + ) + } + + required init(from decoder: Decoder) throws { + let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) + + self.prerelease = try? container.decode(AppVersionInfo?.self, forKey: .prerelease) + + try super.init(from: decoder) + } + + public override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + + var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(prerelease, forKey: .prerelease) + } + } + + // MARK: - AppVersionInfo + + class AppVersionInfo: Codable { + enum CodingKeys: String, CodingKey { + case version = "result" + case updated + case name + case notes + case assets + } + + public struct Asset: Codable { + enum CodingKeys: String, CodingKey { + case name + case url + } + + public let name: String + public let url: String + } + + public let version: String + public let updated: TimeInterval? + public let name: String? + public let notes: String? + public let assets: [Asset]? + + public init( + version: String, + updated: TimeInterval?, + name: String?, + notes: String?, + assets: [Asset]? + ) { + self.version = version + self.updated = updated + self.name = name + self.notes = notes + self.assets = assets + } + + public func encode(to encoder: Encoder) throws { + var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(version, forKey: .version) + try container.encodeIfPresent(updated, forKey: .updated) + try container.encodeIfPresent(name, forKey: .name) + try container.encodeIfPresent(notes, forKey: .notes) + try container.encodeIfPresent(assets, forKey: .assets) + } + } +} diff --git a/SessionNetworkingKit/FileServer/Types/HTTPHeader+FileServer.swift b/SessionNetworkingKit/FileServer/Types/HTTPHeader+FileServer.swift new file mode 100644 index 0000000000..5edb20c045 --- /dev/null +++ b/SessionNetworkingKit/FileServer/Types/HTTPHeader+FileServer.swift @@ -0,0 +1,9 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +import Foundation + +extension HTTPHeader { + static let fileCustomTTL: HTTPHeader = "X-FS-TTL" +} diff --git a/SessionSnodeKit/LibSession/LibSession+Networking.swift b/SessionNetworkingKit/LibSession/LibSession+Networking.swift similarity index 98% rename from SessionSnodeKit/LibSession/LibSession+Networking.swift rename to SessionNetworkingKit/LibSession/LibSession+Networking.swift index 326b5e729f..53fabef8b7 100644 --- a/SessionSnodeKit/LibSession/LibSession+Networking.swift +++ b/SessionNetworkingKit/LibSession/LibSession+Networking.swift @@ -158,7 +158,7 @@ class LibSessionNetwork: NetworkType { return getSwarm(for: swarmPublicKey) .tryFlatMapWithRandomSnode(retry: retryCount, using: dependencies) { [weak self, dependencies] snode in - try SnodeAPI + try Network.SnodeAPI .preparedGetNetworkTime(from: snode, using: dependencies) .send(using: dependencies) .tryFlatMap { _, timestampMs in @@ -175,7 +175,7 @@ class LibSessionNetwork: NetworkType { ) .map { info, response -> (ResponseInfoType, Data?) in ( - SnodeAPI.LatestTimestampResponseInfo( + Network.SnodeAPI.LatestTimestampResponseInfo( code: info.code, headers: info.headers, timestampMs: timestampMs @@ -188,7 +188,7 @@ class LibSessionNetwork: NetworkType { } } - func checkClientVersion(ed25519SecretKey: [UInt8]) -> AnyPublisher<(ResponseInfoType, AppVersionResponse), Error> { + func checkClientVersion(ed25519SecretKey: [UInt8]) -> AnyPublisher<(ResponseInfoType, Network.FileServer.AppVersionResponse), Error> { typealias Output = (success: Bool, timeout: Bool, statusCode: Int, headers: [String: String], data: Data?) return dependencies @@ -213,14 +213,14 @@ class LibSessionNetwork: NetworkType { ctx ) } - .tryMap { [dependencies] success, timeout, statusCode, headers, maybeData -> (any ResponseInfoType, AppVersionResponse) in + .tryMap { [dependencies] success, timeout, statusCode, headers, maybeData -> (any ResponseInfoType, Network.FileServer.AppVersionResponse) in try LibSessionNetwork.throwErrorIfNeeded(success, timeout, statusCode, headers, maybeData, using: dependencies) guard let data: Data = maybeData else { throw NetworkError.parsingFailed } return ( Network.ResponseInfo(code: statusCode), - try AppVersionResponse.decoded(from: data, using: dependencies) + try Network.FileServer.AppVersionResponse.decoded(from: data, using: dependencies) ) } .eraseToAnyPublisher() @@ -704,23 +704,27 @@ public extension LibSession { // MARK: - Functions public func suspendNetworkAccess() { - Log.info(.network, "Network access suspended.") isSuspended = true + Log.info(.network, "Network access suspended.") switch network { case .none: break case .some(let network): network_suspend(network) } + + dependencies.notifyAsync(key: .networkLifecycle(.suspended)) } public func resumeNetworkAccess() { isSuspended = false - Log.info(.network, "Network access resumed.") switch network { case .none: break case .some(let network): network_resume(network) } + + Log.info(.network, "Network access resumed.") + dependencies.notifyAsync(key: .networkLifecycle(.resumed)) } public func getOrCreateNetwork() -> AnyPublisher?, Error> { @@ -917,7 +921,7 @@ public extension LibSession { func snodeCacheSize() -> Int } - class NoopNetworkCache: NetworkCacheType { + class NoopNetworkCache: NetworkCacheType, NoopDependency { public var isSuspended: Bool { return false } public var networkStatus: AnyPublisher { Just(NetworkStatus.unknown).eraseToAnyPublisher() diff --git a/SessionSnodeKit/Meta/Info.plist b/SessionNetworkingKit/Meta/Info.plist similarity index 100% rename from SessionSnodeKit/Meta/Info.plist rename to SessionNetworkingKit/Meta/Info.plist diff --git a/SessionNetworkingKit/Meta/SessionNetworkingKit.h b/SessionNetworkingKit/Meta/SessionNetworkingKit.h new file mode 100644 index 0000000000..dd9ec08864 --- /dev/null +++ b/SessionNetworkingKit/Meta/SessionNetworkingKit.h @@ -0,0 +1,4 @@ +#import + +FOUNDATION_EXPORT double SessionNetworkingKitVersionNumber; +FOUNDATION_EXPORT const unsigned char SessionNetworkingKitVersionString[]; diff --git a/SessionSnodeKit/Models/FileUploadResponse.swift b/SessionNetworkingKit/Models/FileUploadResponse.swift similarity index 64% rename from SessionSnodeKit/Models/FileUploadResponse.swift rename to SessionNetworkingKit/Models/FileUploadResponse.swift index 41ba747b0f..9d28838d92 100644 --- a/SessionSnodeKit/Models/FileUploadResponse.swift +++ b/SessionNetworkingKit/Models/FileUploadResponse.swift @@ -1,10 +1,14 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. +import Foundation + public struct FileUploadResponse: Codable { public let id: String + public let expires: TimeInterval? - public init(id: String) { + public init(id: String, expires: TimeInterval?) { self.id = id + self.expires = expires } } @@ -18,12 +22,16 @@ extension FileUploadResponse { // that and convert the value to a string so we can be consistent (SOGS is able to handle // an array of Strings for the `files` param when posting a message just fine) if let intValue: Int64 = try? container.decode(Int64.self, forKey: .id) { - self = FileUploadResponse(id: "\(intValue)") + self = FileUploadResponse( + id: "\(intValue)", + expires: try? container.decode(TimeInterval?.self, forKey: .expires) + ) return } self = FileUploadResponse( - id: try container.decode(String.self, forKey: .id) + id: try container.decode(String.self, forKey: .id), + expires: try? container.decode(TimeInterval?.self, forKey: .expires) ) } } diff --git a/SessionSnodeKit/Networking/SnodeAPI.swift b/SessionNetworkingKit/Networking/SnodeAPI.swift similarity index 100% rename from SessionSnodeKit/Networking/SnodeAPI.swift rename to SessionNetworkingKit/Networking/SnodeAPI.swift diff --git a/SessionNetworkingKit/PushNotification/Crypto/Crypto+PushNotification.swift b/SessionNetworkingKit/PushNotification/Crypto/Crypto+PushNotification.swift new file mode 100644 index 0000000000..d0b3158e38 --- /dev/null +++ b/SessionNetworkingKit/PushNotification/Crypto/Crypto+PushNotification.swift @@ -0,0 +1,41 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +import Foundation +import SessionUtil +import SessionUtilitiesKit + +public extension Crypto.Generator { + static func plaintextWithPushNotificationPayload( + payload: Data, + encKey: Data + ) -> Crypto.Generator { + return Crypto.Generator( + id: "plaintextWithPushNotificationPayload", + args: [payload, encKey] + ) { + var cPayload: [UInt8] = Array(payload) + var cEncKey: [UInt8] = Array(encKey) + var maybePlaintext: UnsafeMutablePointer? = nil + var plaintextLen: Int = 0 + + guard + cEncKey.count == 32, + session_decrypt_push_notification( + &cPayload, + cPayload.count, + &cEncKey, + &maybePlaintext, + &plaintextLen + ), + plaintextLen > 0, + let plaintext: Data = maybePlaintext.map({ Data(bytes: $0, count: plaintextLen) }) + else { throw CryptoError.decryptionFailed } + + free(UnsafeMutableRawPointer(mutating: maybePlaintext)) + + return plaintext + } + } +} diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/AuthenticatedRequest.swift b/SessionNetworkingKit/PushNotification/Models/AuthenticatedRequest.swift similarity index 97% rename from SessionMessagingKit/Sending & Receiving/Notifications/Models/AuthenticatedRequest.swift rename to SessionNetworkingKit/PushNotification/Models/AuthenticatedRequest.swift index 9e7d30447b..6fc0c0ff54 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Models/AuthenticatedRequest.swift +++ b/SessionNetworkingKit/PushNotification/Models/AuthenticatedRequest.swift @@ -3,8 +3,8 @@ import Foundation import SessionUtilitiesKit -extension PushNotificationAPI { - public class AuthenticatedRequest: Encodable { +extension Network.PushNotification { + class AuthenticatedRequest: Encodable { private enum CodingKeys: String, CodingKey { case pubkey case subaccount diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/NotificationMetadata.swift b/SessionNetworkingKit/PushNotification/Models/NotificationMetadata.swift similarity index 81% rename from SessionMessagingKit/Sending & Receiving/Notifications/Models/NotificationMetadata.swift rename to SessionNetworkingKit/PushNotification/Models/NotificationMetadata.swift index fefdbad9de..817e19185d 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Models/NotificationMetadata.swift +++ b/SessionNetworkingKit/PushNotification/Models/NotificationMetadata.swift @@ -1,10 +1,9 @@ // Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. import Foundation -import SessionSnodeKit -extension PushNotificationAPI { - public struct NotificationMetadata: Codable, Equatable { +public extension Network.PushNotification { + struct NotificationMetadata: Codable, Equatable { private enum CodingKeys: String, CodingKey { case accountId = "@" case hash = "#" @@ -22,7 +21,7 @@ extension PushNotificationAPI { public let hash: String /// The swarm namespace in which this message arrived. - public let namespace: SnodeAPI.Namespace + public let namespace: Network.SnodeAPI.Namespace /// The swarm timestamp when the message was created (unix epoch milliseconds) public let createdTimestampMs: Int64 @@ -43,18 +42,19 @@ extension PushNotificationAPI { // MARK: - Decodable -extension PushNotificationAPI.NotificationMetadata { +extension Network.PushNotification.NotificationMetadata { public init(from decoder: Decoder) throws { let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) /// There was a bug at one point where the metadata would include a `null` value for the namespace because we were storing /// messages in a namespace that the storage server didn't have an explicit `namespace_id` for, as a result we need to assume /// that the `namespace` value may not be present in the payload - let namespace: SnodeAPI.Namespace = try container.decodeIfPresent(Int.self, forKey: .namespace) - .map { SnodeAPI.Namespace(rawValue: $0) } + let namespace: Network.SnodeAPI.Namespace = try container + .decodeIfPresent(Int.self, forKey: .namespace) + .map { Network.SnodeAPI.Namespace(rawValue: $0) } .defaulting(to: .unknown) - self = PushNotificationAPI.NotificationMetadata( + self = Network.PushNotification.NotificationMetadata( accountId: try container.decode(String.self, forKey: .accountId), hash: try container.decode(String.self, forKey: .hash), namespace: namespace, @@ -68,9 +68,9 @@ extension PushNotificationAPI.NotificationMetadata { // MARK: - Convenience -public extension PushNotificationAPI.NotificationMetadata { - static var invalid: PushNotificationAPI.NotificationMetadata { - PushNotificationAPI.NotificationMetadata( +public extension Network.PushNotification.NotificationMetadata { + static var invalid: Network.PushNotification.NotificationMetadata { + Network.PushNotification.NotificationMetadata( accountId: "", hash: "", namespace: .unknown, diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/SubscribeRequest.swift b/SessionNetworkingKit/PushNotification/Models/SubscribeRequest.swift similarity index 93% rename from SessionMessagingKit/Sending & Receiving/Notifications/Models/SubscribeRequest.swift rename to SessionNetworkingKit/PushNotification/Models/SubscribeRequest.swift index 5138d5d8f5..400c8884f8 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Models/SubscribeRequest.swift +++ b/SessionNetworkingKit/PushNotification/Models/SubscribeRequest.swift @@ -3,10 +3,9 @@ // stringlint:disable import Foundation -import SessionSnodeKit import SessionUtilitiesKit -extension PushNotificationAPI { +public extension Network.PushNotification { struct SubscribeRequest: Encodable { class Subscription: AuthenticatedRequest { private enum CodingKeys: String, CodingKey { @@ -18,7 +17,7 @@ extension PushNotificationAPI { } /// List of integer namespace (-32768 through 32767). These must be sorted in ascending order. - private let namespaces: [SnodeAPI.Namespace] + private let namespaces: [Network.SnodeAPI.Namespace] /// If provided and true then notifications will include the body of the message (as long as it isn't too large); if false then the body will /// not be included in notifications. @@ -68,7 +67,7 @@ extension PushNotificationAPI { // MARK: - Initialization init( - namespaces: [SnodeAPI.Namespace], + namespaces: [Network.SnodeAPI.Namespace], includeMessageData: Bool, serviceInfo: ServiceInfo, notificationsEncryptionKey: Data, @@ -109,21 +108,18 @@ extension PushNotificationAPI { private let subscriptions: [Subscription] - public init( - subscriptions: [Subscription] - ) { + init(subscriptions: [Subscription]) { self.subscriptions = subscriptions } // MARK: - Coding public func encode(to encoder: Encoder) throws { - guard subscriptions.count > 1 else { - try subscriptions[0].encode(to: encoder) - return + switch subscriptions.count { + case 0: return + case 1: try subscriptions[0].encode(to: encoder) + default: try subscriptions.encode(to: encoder) } - - try subscriptions.encode(to: encoder) } } } diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/SubscribeResponse.swift b/SessionNetworkingKit/PushNotification/Models/SubscribeResponse.swift similarity index 80% rename from SessionMessagingKit/Sending & Receiving/Notifications/Models/SubscribeResponse.swift rename to SessionNetworkingKit/PushNotification/Models/SubscribeResponse.swift index bff4193f7d..5e5f3f193f 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Models/SubscribeResponse.swift +++ b/SessionNetworkingKit/PushNotification/Models/SubscribeResponse.swift @@ -2,17 +2,17 @@ import Foundation -public extension PushNotificationAPI { +public extension Network.PushNotification { struct SubscribeResponse: Codable { - struct SubResponse: Codable { + public struct SubResponse: Codable { /// Flag indicating the success of the registration - let success: Bool? + public let success: Bool? /// Value is `true` upon an initial registration - let added: Bool? + public let added: Bool? /// Value is `true` upon a renewal/update registration - let updated: Bool? + public let updated: Bool? /// This will be one of the errors found here: /// https://github.com/jagerman/session-push-notification-server/blob/spns-v2/spns/hive/subscription.hpp#L21 @@ -24,13 +24,17 @@ public extension PushNotificationAPI { /// SERVICE_TIMEOUT = 3 // The backend service did not response /// ERROR = 4 // There was some other error processing the subscription (details in the string) /// INTERNAL_ERROR = 5 // An internal program error occured processing the request - let error: Int? + public let error: Int? /// Includes additional information about the error - let message: String? + public let message: String? } - let subResponses: [SubResponse] + public let subResponses: [SubResponse] + + public init(subResponses: [SubResponse]) { + self.subResponses = subResponses + } public init(from decoder: Decoder) throws { guard diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/UnsubscribeRequest.swift b/SessionNetworkingKit/PushNotification/Models/UnsubscribeRequest.swift similarity index 92% rename from SessionMessagingKit/Sending & Receiving/Notifications/Models/UnsubscribeRequest.swift rename to SessionNetworkingKit/PushNotification/Models/UnsubscribeRequest.swift index 1d29c882d8..0c98e9d3fe 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Models/UnsubscribeRequest.swift +++ b/SessionNetworkingKit/PushNotification/Models/UnsubscribeRequest.swift @@ -3,10 +3,9 @@ // stringlint:disable import Foundation -import SessionSnodeKit import SessionUtilitiesKit -extension PushNotificationAPI { +extension Network.PushNotification { struct UnsubscribeRequest: Encodable { class Subscription: AuthenticatedRequest { private enum CodingKeys: String, CodingKey { @@ -75,12 +74,11 @@ extension PushNotificationAPI { // MARK: - Coding public func encode(to encoder: Encoder) throws { - guard subscriptions.count > 1 else { - try subscriptions[0].encode(to: encoder) - return + switch subscriptions.count { + case 0: return + case 1: try subscriptions[0].encode(to: encoder) + default: try subscriptions.encode(to: encoder) } - - try subscriptions.encode(to: encoder) } } } diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/UnsubscribeResponse.swift b/SessionNetworkingKit/PushNotification/Models/UnsubscribeResponse.swift similarity index 80% rename from SessionMessagingKit/Sending & Receiving/Notifications/Models/UnsubscribeResponse.swift rename to SessionNetworkingKit/PushNotification/Models/UnsubscribeResponse.swift index c89aa19f3a..3093a9371f 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Models/UnsubscribeResponse.swift +++ b/SessionNetworkingKit/PushNotification/Models/UnsubscribeResponse.swift @@ -2,17 +2,17 @@ import Foundation -public extension PushNotificationAPI { +public extension Network.PushNotification { struct UnsubscribeResponse: Codable { - struct SubResponse: Codable { + public struct SubResponse: Codable { /// Flag indicating the success of the registration - let success: Bool? + public let success: Bool? /// Value is `true` upon an initial registration - let added: Bool? + public let added: Bool? /// Value is `true` upon a renewal/update registration - let updated: Bool? + public let updated: Bool? /// This will be one of the errors found here: /// https://github.com/jagerman/session-push-notification-server/blob/spns-v2/spns/hive/subscription.hpp#L21 @@ -24,13 +24,17 @@ public extension PushNotificationAPI { /// SERVICE_TIMEOUT = 3 // The backend service did not response /// ERROR = 4 // There was some other error processing the subscription (details in the string) /// INTERNAL_ERROR = 5 // An internal program error occured processing the request - let error: Int? + public let error: Int? /// Includes additional information about the error - let message: String? + public let message: String? } - let subResponses: [SubResponse] + public let subResponses: [SubResponse] + + public init(subResponses: [SubResponse]) { + self.subResponses = subResponses + } public init(from decoder: Decoder) throws { guard diff --git a/SessionNetworkingKit/PushNotification/PushNotification.swift b/SessionNetworkingKit/PushNotification/PushNotification.swift new file mode 100644 index 0000000000..b080f7154e --- /dev/null +++ b/SessionNetworkingKit/PushNotification/PushNotification.swift @@ -0,0 +1,29 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +import Foundation +import SessionUtilitiesKit + +// MARK: - KeychainStorage + +public extension KeychainStorage.DataKey { static let pushNotificationEncryptionKey: Self = "PNEncryptionKeyKey" } + +// MARK: - Log.Category + +public extension Log.Category { + static let pushNotificationAPI: Log.Category = .create("PushNotificationAPI", defaultLevel: .info) +} + +// MARK: - Network.PushNotification + +public extension Network { + enum PushNotification { + internal static let encryptionKeyLength: Int = 32 + internal static let maxRetryCount: Int = 4 + public static let tokenExpirationInterval: TimeInterval = (12 * 60 * 60) + + internal static let server: String = "https://push.getsession.org" + internal static let serverPublicKey = "d7557fe563e2610de876c0ac7341b62f3c82d5eea4b62c702392ea4368f51b3b" + } +} diff --git a/SessionNetworkingKit/PushNotification/PushNotificationAPI.swift b/SessionNetworkingKit/PushNotification/PushNotificationAPI.swift new file mode 100644 index 0000000000..d852beeaed --- /dev/null +++ b/SessionNetworkingKit/PushNotification/PushNotificationAPI.swift @@ -0,0 +1,209 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +import Foundation +import Combine +import UserNotifications +import SessionUtilitiesKit + +public extension Network.PushNotification { + static func preparedSubscribe( + token: Data, + swarms: [(sessionId: SessionId, authMethod: AuthenticationMethod)], + using dependencies: Dependencies + ) throws -> Network.PreparedRequest { + guard dependencies[defaults: .standard, key: .isUsingFullAPNs] else { + throw NetworkError.invalidPreparedRequest + } + guard !swarms.isEmpty else { + return try Network.PreparedRequest.cached( + SubscribeResponse(subResponses: []), + endpoint: Endpoint.subscribe, + using: dependencies + ) + } + + guard let notificationsEncryptionKey: Data = try? dependencies[singleton: .keychain].getOrGenerateEncryptionKey( + forKey: .pushNotificationEncryptionKey, + length: encryptionKeyLength, + cat: .pushNotificationAPI, + legacyKey: "PNEncryptionKeyKey", + legacyService: "PNKeyChainService" + ) else { + Log.error(.pushNotificationAPI, "Unable to retrieve PN encryption key.") + throw KeychainStorageError.keySpecInvalid + } + + return try Network.PreparedRequest( + request: Request( + method: .post, + endpoint: Endpoint.subscribe, + body: SubscribeRequest( + subscriptions: swarms.map { sessionId, authMethod -> SubscribeRequest.Subscription in + SubscribeRequest.Subscription( + namespaces: { + switch sessionId.prefix { + case .group: return [ + .groupMessages, + .configGroupKeys, + .configGroupInfo, + .configGroupMembers, + .revokedRetrievableGroupMessages + ] + default: return [ + .default, + .configUserProfile, + .configContacts, + .configConvoInfoVolatile, + .configUserGroups + ] + } + }(), + /// Note: Unfortunately we always need the message content because without the content + /// control messages can't be distinguished from visible messages which results in the + /// 'generic' notification being shown when receiving things like typing indicator updates + includeMessageData: true, + serviceInfo: ServiceInfo( + token: token.toHexString() + ), + notificationsEncryptionKey: notificationsEncryptionKey, + authMethod: authMethod, + timestamp: (dependencies[cache: .snodeAPI].currentOffsetTimestampMs() / 1000) // Seconds + ) + } + ) + ), + responseType: SubscribeResponse.self, + retryCount: Network.PushNotification.maxRetryCount, + using: dependencies + ) + .handleEvents( + receiveOutput: { _, response in + zip(response.subResponses, swarms).forEach { subResponse, swarm in + guard subResponse.success != true else { return } + + Log.error(.pushNotificationAPI, "Couldn't subscribe for push notifications for: \(swarm.sessionId) due to error (\(subResponse.error ?? -1)): \(subResponse.message ?? "nil").") + } + }, + receiveCompletion: { result in + switch result { + case .finished: break + case .failure(let error): Log.error(.pushNotificationAPI, "Couldn't subscribe for push notifications due to error: \(error).") + } + } + ) + } + + static func preparedUnsubscribe( + token: Data, + swarms: [(sessionId: SessionId, authMethod: AuthenticationMethod)], + using dependencies: Dependencies + ) throws -> Network.PreparedRequest { + guard !swarms.isEmpty else { + return try Network.PreparedRequest.cached( + UnsubscribeResponse(subResponses: []), + endpoint: Endpoint.subscribe, + using: dependencies + ) + } + + return try Network.PreparedRequest( + request: Request( + method: .post, + endpoint: Endpoint.unsubscribe, + body: UnsubscribeRequest( + subscriptions: swarms.map { sessionId, authMethod -> UnsubscribeRequest.Subscription in + UnsubscribeRequest.Subscription( + serviceInfo: ServiceInfo( + token: token.toHexString() + ), + authMethod: authMethod, + timestamp: (dependencies[cache: .snodeAPI].currentOffsetTimestampMs() / 1000) // Seconds + ) + } + ) + ), + responseType: UnsubscribeResponse.self, + retryCount: Network.PushNotification.maxRetryCount, + using: dependencies + ) + .handleEvents( + receiveOutput: { _, response in + zip(response.subResponses, swarms).forEach { subResponse, swarm in + guard subResponse.success != true else { return } + + Log.error(.pushNotificationAPI, "Couldn't unsubscribe for push notifications for: \(swarm.sessionId) due to error (\(subResponse.error ?? -1)): \(subResponse.message ?? "nil").") + } + }, + receiveCompletion: { result in + switch result { + case .finished: break + case .failure(let error): Log.error(.pushNotificationAPI, "Couldn't unsubscribe for push notifications due to error: \(error).") + } + } + ) + } + + // MARK: - Notification Handling + + static func processNotification( + notificationContent: UNNotificationContent, + using dependencies: Dependencies + ) -> (data: Data?, metadata: NotificationMetadata, result: ProcessResult) { + // Make sure the notification is from the updated push server + guard notificationContent.userInfo["spns"] != nil else { + return (nil, .invalid, .legacyFailure) + } + + guard let base64EncodedEncString: String = notificationContent.userInfo["enc_payload"] as? String else { + return (nil, .invalid, .failureNoContent) + } + + // Decrypt and decode the payload + let notification: BencodeResponse + + do { + guard let encryptedData: Data = Data(base64Encoded: base64EncodedEncString) else { + throw CryptoError.invalidBase64EncodedData + } + + let notificationsEncryptionKey: Data = try dependencies[singleton: .keychain].getOrGenerateEncryptionKey( + forKey: .pushNotificationEncryptionKey, + length: encryptionKeyLength, + cat: .pushNotificationAPI, + legacyKey: "PNEncryptionKeyKey", + legacyService: "PNKeyChainService" + ) + let decryptedData: Data = try dependencies[singleton: .crypto].tryGenerate( + .plaintextWithPushNotificationPayload( + payload: encryptedData, + encKey: notificationsEncryptionKey + ) + ) + notification = try BencodeDecoder(using: dependencies) + .decode(BencodeResponse.self, from: decryptedData) + } + catch { + Log.error(.pushNotificationAPI, "Failed to decrypt or decode notification due to error: \(error)") + return (nil, .invalid, .failure) + } + + // If the metadata says that the message was too large then we should show the generic + // notification (this is a valid case) + guard !notification.info.dataTooLong else { return (nil, notification.info, .successTooLong) } + + // Check that the body we were given is valid and not empty + guard + let notificationData: Data = notification.data, + notification.info.dataLength == notificationData.count, + !notificationData.isEmpty + else { + Log.error(.pushNotificationAPI, "Get notification data failed") + return (nil, notification.info, .failureNoContent) + } + + // Success, we have the notification content + return (notificationData, notification.info, .success) + } +} diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Types/PushNotificationAPIEndpoint.swift b/SessionNetworkingKit/PushNotification/PushNotificationEndpoint.swift similarity index 75% rename from SessionMessagingKit/Sending & Receiving/Notifications/Types/PushNotificationAPIEndpoint.swift rename to SessionNetworkingKit/PushNotification/PushNotificationEndpoint.swift index 36ed02e3e2..5fdb987b04 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Types/PushNotificationAPIEndpoint.swift +++ b/SessionNetworkingKit/PushNotification/PushNotificationEndpoint.swift @@ -3,15 +3,15 @@ // stringlint:disable import Foundation -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit -public extension PushNotificationAPI { +public extension Network.PushNotification { enum Endpoint: EndpointType { case subscribe case unsubscribe - public static var name: String { "PushNotificationAPI.Endpoint" } + public static var name: String { "PushNotification.Endpoint" } public var path: String { switch self { diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Types/ProcessResult.swift b/SessionNetworkingKit/PushNotification/Types/ProcessResult.swift similarity index 84% rename from SessionMessagingKit/Sending & Receiving/Notifications/Types/ProcessResult.swift rename to SessionNetworkingKit/PushNotification/Types/ProcessResult.swift index 07496de265..a33e84f6c0 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Types/ProcessResult.swift +++ b/SessionNetworkingKit/PushNotification/Types/ProcessResult.swift @@ -2,7 +2,7 @@ import Foundation -public extension PushNotificationAPI { +public extension Network.PushNotification { enum ProcessResult { case success case successTooLong diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Types/Request+PushNotificationAPI.swift b/SessionNetworkingKit/PushNotification/Types/Request+PushNotificationAPI.swift similarity index 69% rename from SessionMessagingKit/Sending & Receiving/Notifications/Types/Request+PushNotificationAPI.swift rename to SessionNetworkingKit/PushNotification/Types/Request+PushNotificationAPI.swift index 78000ad2ce..480c90bf65 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Types/Request+PushNotificationAPI.swift +++ b/SessionNetworkingKit/PushNotification/Types/Request+PushNotificationAPI.swift @@ -1,12 +1,9 @@ // Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. import Foundation -import SessionSnodeKit import SessionUtilitiesKit -// MARK: Request - PushNotificationAPI - -public extension Request where Endpoint == PushNotificationAPI.Endpoint { +public extension Request where Endpoint == Network.PushNotification.Endpoint { init( method: HTTPMethod, endpoint: Endpoint, @@ -18,10 +15,10 @@ public extension Request where Endpoint == PushNotificationAPI.Endpoint { endpoint: endpoint, destination: try .server( method: method, - server: PushNotificationAPI.server, + server: Network.PushNotification.server, queryParameters: queryParameters, headers: headers, - x25519PublicKey: PushNotificationAPI.serverPublicKey + x25519PublicKey: Network.PushNotification.serverPublicKey ), body: body ) diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Types/Service.swift b/SessionNetworkingKit/PushNotification/Types/Service.swift similarity index 84% rename from SessionMessagingKit/Sending & Receiving/Notifications/Types/Service.swift rename to SessionNetworkingKit/PushNotification/Types/Service.swift index c930c0a176..fa33fe3cde 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Types/Service.swift +++ b/SessionNetworkingKit/PushNotification/Types/Service.swift @@ -8,15 +8,15 @@ import SessionUtilitiesKit // MARK: - FeatureStorage public extension FeatureStorage { - static let pushNotificationService: FeatureConfig = Dependencies.create( + static let pushNotificationService: FeatureConfig = Dependencies.create( identifier: "pushNotificationService", defaultOption: .apns ) } -// MARK: - PushNotificationAPI.Service +// MARK: - Network.PushNotification.Service -public extension PushNotificationAPI { +public extension Network.PushNotification { enum Service: String, Codable, CaseIterable, FeatureOption { case apns case sandbox = "apns-sandbox" // Use for push notifications in Testnet diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Types/ServiceInfo.swift b/SessionNetworkingKit/PushNotification/Types/ServiceInfo.swift similarity index 91% rename from SessionMessagingKit/Sending & Receiving/Notifications/Types/ServiceInfo.swift rename to SessionNetworkingKit/PushNotification/Types/ServiceInfo.swift index 8b1ccfeaed..b534816f35 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Types/ServiceInfo.swift +++ b/SessionNetworkingKit/PushNotification/Types/ServiceInfo.swift @@ -2,7 +2,7 @@ import Foundation -extension PushNotificationAPI { +extension Network.PushNotification { struct ServiceInfo: Codable { private enum CodingKeys: String, CodingKey { case token diff --git a/SessionMessagingKit/Open Groups/Crypto/Crypto+OpenGroupAPI.swift b/SessionNetworkingKit/SOGS/Crypto/Crypto+SOGS.swift similarity index 60% rename from SessionMessagingKit/Open Groups/Crypto/Crypto+OpenGroupAPI.swift rename to SessionNetworkingKit/SOGS/Crypto/Crypto+SOGS.swift index 516fb0a6ac..9b48b1c7f4 100644 --- a/SessionMessagingKit/Open Groups/Crypto/Crypto+OpenGroupAPI.swift +++ b/SessionNetworkingKit/SOGS/Crypto/Crypto+SOGS.swift @@ -3,7 +3,6 @@ // stringlint:disable import Foundation -import CryptoKit import SessionUtil import SessionUtilitiesKit @@ -152,90 +151,3 @@ public extension Crypto.Verification { } } } - -// MARK: - Messages - -public extension Crypto.Generator { - static func ciphertextWithSessionBlindingProtocol( - plaintext: Data, - recipientBlindedId: String, - serverPublicKey: String - ) -> Crypto.Generator { - return Crypto.Generator( - id: "ciphertextWithSessionBlindingProtocol", - args: [plaintext, serverPublicKey] - ) { dependencies in - var cPlaintext: [UInt8] = Array(plaintext) - var cEd25519SecretKey: [UInt8] = dependencies[cache: .general].ed25519SecretKey - var cRecipientBlindedId: [UInt8] = Array(Data(hex: recipientBlindedId)) - var cServerPublicKey: [UInt8] = Array(Data(hex: serverPublicKey)) - var maybeCiphertext: UnsafeMutablePointer? = nil - var ciphertextLen: Int = 0 - - guard !cEd25519SecretKey.isEmpty else { throw MessageSenderError.noUserED25519KeyPair } - guard - cEd25519SecretKey.count == 64, - cServerPublicKey.count == 32, - session_encrypt_for_blinded_recipient( - &cPlaintext, - cPlaintext.count, - &cEd25519SecretKey, - &cServerPublicKey, - &cRecipientBlindedId, - &maybeCiphertext, - &ciphertextLen - ), - ciphertextLen > 0, - let ciphertext: Data = maybeCiphertext.map({ Data(bytes: $0, count: ciphertextLen) }) - else { throw MessageSenderError.encryptionFailed } - - free(UnsafeMutableRawPointer(mutating: maybeCiphertext)) - - return ciphertext - } - } - - static func plaintextWithSessionBlindingProtocol( - ciphertext: Data, - senderId: String, - recipientId: String, - serverPublicKey: String - ) -> Crypto.Generator<(plaintext: Data, senderSessionIdHex: String)> { - return Crypto.Generator( - id: "plaintextWithSessionBlindingProtocol", - args: [ciphertext, senderId, recipientId] - ) { dependencies in - var cCiphertext: [UInt8] = Array(ciphertext) - var cEd25519SecretKey: [UInt8] = dependencies[cache: .general].ed25519SecretKey - var cSenderId: [UInt8] = Array(Data(hex: senderId)) - var cRecipientId: [UInt8] = Array(Data(hex: recipientId)) - var cServerPublicKey: [UInt8] = Array(Data(hex: serverPublicKey)) - var cSenderSessionId: [CChar] = [CChar](repeating: 0, count: 67) - var maybePlaintext: UnsafeMutablePointer? = nil - var plaintextLen: Int = 0 - - guard !cEd25519SecretKey.isEmpty else { throw MessageSenderError.noUserED25519KeyPair } - guard - cEd25519SecretKey.count == 64, - cServerPublicKey.count == 32, - session_decrypt_for_blinded_recipient( - &cCiphertext, - cCiphertext.count, - &cEd25519SecretKey, - &cServerPublicKey, - &cSenderId, - &cRecipientId, - &cSenderSessionId, - &maybePlaintext, - &plaintextLen - ), - plaintextLen > 0, - let plaintext: Data = maybePlaintext.map({ Data(bytes: $0, count: plaintextLen) }) - else { throw MessageReceiverError.decryptionFailed } - - free(UnsafeMutableRawPointer(mutating: maybePlaintext)) - - return (plaintext, String(cString: cSenderSessionId)) - } - } -} diff --git a/SessionNetworkingKit/SOGS/Models/CapabilitiesResponse.swift b/SessionNetworkingKit/SOGS/Models/CapabilitiesResponse.swift new file mode 100644 index 0000000000..1e5ddf28c1 --- /dev/null +++ b/SessionNetworkingKit/SOGS/Models/CapabilitiesResponse.swift @@ -0,0 +1,74 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension Network.SOGS { + public struct CapabilitiesResponse: Codable, Equatable { + public let capabilities: [String] + public let missing: [String]? + + // MARK: - Initialization + + public init(capabilities: [String], missing: [String]? = nil) { + self.capabilities = capabilities + self.missing = missing + } + } +} + +//public struct Capability: Codable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible { +// public static var databaseTableName: String { "capability" } +// +// public typealias Columns = CodingKeys +// public enum CodingKeys: String, CodingKey, ColumnExpression { +// case openGroupServer +// case variant +// case isMissing +// } +// +// public enum Variant: Equatable, Hashable, CaseIterable, Codable, DatabaseValueConvertible { +// public static var allCases: [Variant] { +// [.sogs, .blind, .reactions] +// } +// +// case sogs +// case blind +// case reactions +// +// /// Fallback case if the capability isn't supported by this version of the app +// case unsupported(String) +// +// // MARK: - Convenience +// +// public var rawValue: String { +// switch self { +// case .unsupported(let originalValue): return originalValue +// default: return "\(self)" +// } +// } +// +// // MARK: - Initialization +// +// public init(from valueString: String) { +// let maybeValue: Variant? = Variant.allCases.first { $0.rawValue == valueString } +// +// self = (maybeValue ?? .unsupported(valueString)) +// } +// } +// +// public let openGroupServer: String +// public let variant: Variant +// public let isMissing: Bool +// +// // MARK: - Initialization +// +// public init( +// openGroupServer: String, +// variant: Variant, +// isMissing: Bool +// ) { +// self.openGroupServer = openGroupServer +// self.variant = variant +// self.isMissing = isMissing +// } +//} diff --git a/SessionMessagingKit/Open Groups/Models/DeleteInboxResponse.swift b/SessionNetworkingKit/SOGS/Models/DeleteInboxResponse.swift similarity index 86% rename from SessionMessagingKit/Open Groups/Models/DeleteInboxResponse.swift rename to SessionNetworkingKit/SOGS/Models/DeleteInboxResponse.swift index fa67ea9351..c5b82413bf 100644 --- a/SessionMessagingKit/Open Groups/Models/DeleteInboxResponse.swift +++ b/SessionNetworkingKit/SOGS/Models/DeleteInboxResponse.swift @@ -2,7 +2,7 @@ import Foundation -extension OpenGroupAPI { +extension Network.SOGS { public struct DeleteInboxResponse: Codable { let deleted: UInt64 } diff --git a/SessionMessagingKit/Open Groups/Models/DirectMessage.swift b/SessionNetworkingKit/SOGS/Models/DirectMessage.swift similarity index 97% rename from SessionMessagingKit/Open Groups/Models/DirectMessage.swift rename to SessionNetworkingKit/SOGS/Models/DirectMessage.swift index f2e7421eff..507eb5c576 100644 --- a/SessionMessagingKit/Open Groups/Models/DirectMessage.swift +++ b/SessionNetworkingKit/SOGS/Models/DirectMessage.swift @@ -2,7 +2,7 @@ import Foundation -extension OpenGroupAPI { +extension Network.SOGS { public struct DirectMessage: Codable { enum CodingKeys: String, CodingKey { case id diff --git a/SessionMessagingKit/Open Groups/Models/PinnedMessage.swift b/SessionNetworkingKit/SOGS/Models/PinnedMessage.swift similarity index 96% rename from SessionMessagingKit/Open Groups/Models/PinnedMessage.swift rename to SessionNetworkingKit/SOGS/Models/PinnedMessage.swift index e8f1f7a8e2..332a8bb34a 100644 --- a/SessionMessagingKit/Open Groups/Models/PinnedMessage.swift +++ b/SessionNetworkingKit/SOGS/Models/PinnedMessage.swift @@ -2,7 +2,7 @@ import Foundation -extension OpenGroupAPI { +extension Network.SOGS { public struct PinnedMessage: Codable, Equatable { enum CodingKeys: String, CodingKey { case id diff --git a/SessionMessagingKit/Open Groups/Models/ReactionResponse.swift b/SessionNetworkingKit/SOGS/Models/ReactionResponse.swift similarity index 98% rename from SessionMessagingKit/Open Groups/Models/ReactionResponse.swift rename to SessionNetworkingKit/SOGS/Models/ReactionResponse.swift index cfded186d2..6e4992d688 100644 --- a/SessionMessagingKit/Open Groups/Models/ReactionResponse.swift +++ b/SessionNetworkingKit/SOGS/Models/ReactionResponse.swift @@ -2,7 +2,7 @@ import Foundation -extension OpenGroupAPI { +extension Network.SOGS { public struct ReactionAddResponse: Codable, Equatable { enum CodingKeys: String, CodingKey { case added diff --git a/SessionMessagingKit/Open Groups/Models/Room.swift b/SessionNetworkingKit/SOGS/Models/Room.swift similarity index 98% rename from SessionMessagingKit/Open Groups/Models/Room.swift rename to SessionNetworkingKit/SOGS/Models/Room.swift index f1a2f32d6f..6f188c5ce7 100644 --- a/SessionMessagingKit/Open Groups/Models/Room.swift +++ b/SessionNetworkingKit/SOGS/Models/Room.swift @@ -2,7 +2,7 @@ import Foundation -extension OpenGroupAPI { +extension Network.SOGS { public struct Room: Codable, Equatable { enum CodingKeys: String, CodingKey { case token @@ -146,7 +146,7 @@ extension OpenGroupAPI { // MARK: - Decoding -extension OpenGroupAPI.Room { +extension Network.SOGS.Room { public init(from decoder: Decoder) throws { let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) @@ -156,7 +156,7 @@ extension OpenGroupAPI.Room { (try? container.decode(String.self, forKey: .imageId)) ) - self = OpenGroupAPI.Room( + self = Network.SOGS.Room( token: try container.decode(String.self, forKey: .token), name: try container.decode(String.self, forKey: .name), roomDescription: try? container.decode(String.self, forKey: .roomDescription), @@ -167,7 +167,7 @@ extension OpenGroupAPI.Room { activeUsers: try container.decode(Int64.self, forKey: .activeUsers), activeUsersCutoff: try container.decode(Int64.self, forKey: .activeUsersCutoff), imageId: maybeImageId, - pinnedMessages: try? container.decode([OpenGroupAPI.PinnedMessage].self, forKey: .pinnedMessages), + pinnedMessages: try? container.decode([Network.SOGS.PinnedMessage].self, forKey: .pinnedMessages), admin: ((try? container.decode(Bool.self, forKey: .admin)) ?? false), globalAdmin: ((try? container.decode(Bool.self, forKey: .globalAdmin)) ?? false), diff --git a/SessionMessagingKit/Open Groups/Models/RoomPollInfo.swift b/SessionNetworkingKit/SOGS/Models/RoomPollInfo.swift similarity index 95% rename from SessionMessagingKit/Open Groups/Models/RoomPollInfo.swift rename to SessionNetworkingKit/SOGS/Models/RoomPollInfo.swift index de43a68a62..a2b87f424d 100644 --- a/SessionMessagingKit/Open Groups/Models/RoomPollInfo.swift +++ b/SessionNetworkingKit/SOGS/Models/RoomPollInfo.swift @@ -2,7 +2,7 @@ import Foundation -extension OpenGroupAPI { +extension Network.SOGS { /// This only contains ephemeral data public struct RoomPollInfo: Codable { enum CodingKeys: String, CodingKey { @@ -92,8 +92,8 @@ extension OpenGroupAPI { // MARK: - Convenience -extension OpenGroupAPI.RoomPollInfo { - init(room: OpenGroupAPI.Room) { +public extension Network.SOGS.RoomPollInfo { + init(room: Network.SOGS.Room) { self.init( token: room.token, activeUsers: room.activeUsers, @@ -115,11 +115,11 @@ extension OpenGroupAPI.RoomPollInfo { // MARK: - Decoding -extension OpenGroupAPI.RoomPollInfo { +extension Network.SOGS.RoomPollInfo { public init(from decoder: Decoder) throws { let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) - self = OpenGroupAPI.RoomPollInfo( + self = Network.SOGS.RoomPollInfo( token: try container.decode(String.self, forKey: .token), activeUsers: try container.decode(Int64.self, forKey: .activeUsers), @@ -137,7 +137,7 @@ extension OpenGroupAPI.RoomPollInfo { upload: try container.decode(Bool.self, forKey: .upload), defaultUpload: try? container.decode(Bool.self, forKey: .defaultUpload), - details: try? container.decode(OpenGroupAPI.Room.self, forKey: .details) + details: try? container.decode(Network.SOGS.Room.self, forKey: .details) ) } } diff --git a/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift b/SessionNetworkingKit/SOGS/Models/SOGSMessage.swift similarity index 94% rename from SessionMessagingKit/Open Groups/Models/SOGSMessage.swift rename to SessionNetworkingKit/SOGS/Models/SOGSMessage.swift index 219021f442..5b902a0941 100644 --- a/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift +++ b/SessionNetworkingKit/SOGS/Models/SOGSMessage.swift @@ -1,10 +1,9 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation -import SessionSnodeKit import SessionUtilitiesKit -extension OpenGroupAPI { +extension Network.SOGS { public struct Message: Codable, Equatable { enum CodingKeys: String, CodingKey { case id @@ -25,7 +24,7 @@ extension OpenGroupAPI { public let id: Int64 public let sender: String? - public let posted: TimeInterval + public let posted: TimeInterval? public let edited: TimeInterval? public let deleted: Bool? public let seqNo: Int64 @@ -56,7 +55,7 @@ extension OpenGroupAPI { // MARK: - Decoder -extension OpenGroupAPI.Message { +extension Network.SOGS.Message { public init(from decoder: Decoder) throws { let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) @@ -102,10 +101,10 @@ extension OpenGroupAPI.Message { } } - self = OpenGroupAPI.Message( + self = Network.SOGS.Message( id: try container.decode(Int64.self, forKey: .id), sender: try container.decodeIfPresent(String.self, forKey: .sender), - posted: try container.decode(TimeInterval.self, forKey: .posted), + posted: try container.decodeIfPresent(TimeInterval.self, forKey: .posted), edited: try container.decodeIfPresent(TimeInterval.self, forKey: .edited), deleted: try container.decodeIfPresent(Bool.self, forKey: .deleted), seqNo: try container.decode(Int64.self, forKey: .seqNo), @@ -119,11 +118,11 @@ extension OpenGroupAPI.Message { } } -extension OpenGroupAPI.Message.Reaction { +extension Network.SOGS.Message.Reaction { public init(from decoder: Decoder) throws { let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) - self = OpenGroupAPI.Message.Reaction( + self = Network.SOGS.Message.Reaction( count: try container.decode(Int64.self, forKey: .count), reactors: try container.decodeIfPresent([String].self, forKey: .reactors), you: ((try container.decodeIfPresent(Bool.self, forKey: .you)) ?? false), diff --git a/SessionMessagingKit/Open Groups/Models/SendDirectMessageRequest.swift b/SessionNetworkingKit/SOGS/Models/SendDirectMessageRequest.swift similarity index 95% rename from SessionMessagingKit/Open Groups/Models/SendDirectMessageRequest.swift rename to SessionNetworkingKit/SOGS/Models/SendDirectMessageRequest.swift index 19df350f9e..4a72a11423 100644 --- a/SessionMessagingKit/Open Groups/Models/SendDirectMessageRequest.swift +++ b/SessionNetworkingKit/SOGS/Models/SendDirectMessageRequest.swift @@ -2,7 +2,7 @@ import Foundation -extension OpenGroupAPI { +extension Network.SOGS { public struct SendDirectMessageRequest: Codable { let message: Data diff --git a/SessionMessagingKit/Open Groups/Models/SendDirectMessageResponse.swift b/SessionNetworkingKit/SOGS/Models/SendDirectMessageResponse.swift similarity index 90% rename from SessionMessagingKit/Open Groups/Models/SendDirectMessageResponse.swift rename to SessionNetworkingKit/SOGS/Models/SendDirectMessageResponse.swift index a8e998f8ac..a076f4edd4 100644 --- a/SessionMessagingKit/Open Groups/Models/SendDirectMessageResponse.swift +++ b/SessionNetworkingKit/SOGS/Models/SendDirectMessageResponse.swift @@ -2,8 +2,8 @@ import Foundation -extension OpenGroupAPI { - public struct SendDirectMessageResponse: Codable, Equatable { +public extension Network.SOGS { + struct SendDirectMessageResponse: Codable, Equatable { enum CodingKeys: String, CodingKey { case id case sender diff --git a/SessionMessagingKit/Open Groups/Models/SendMessageRequest.swift b/SessionNetworkingKit/SOGS/Models/SendSOGSMessageRequest.swift similarity index 97% rename from SessionMessagingKit/Open Groups/Models/SendMessageRequest.swift rename to SessionNetworkingKit/SOGS/Models/SendSOGSMessageRequest.swift index 98007184aa..b6520ad7cd 100644 --- a/SessionMessagingKit/Open Groups/Models/SendMessageRequest.swift +++ b/SessionNetworkingKit/SOGS/Models/SendSOGSMessageRequest.swift @@ -2,8 +2,8 @@ import Foundation -extension OpenGroupAPI { - public struct SendMessageRequest: Codable { +extension Network.SOGS { + public struct SendSOGSMessageRequest: Codable { enum CodingKeys: String, CodingKey { case data case signature diff --git a/SessionMessagingKit/Open Groups/Models/UpdateMessageRequest.swift b/SessionNetworkingKit/SOGS/Models/UpdateMessageRequest.swift similarity index 98% rename from SessionMessagingKit/Open Groups/Models/UpdateMessageRequest.swift rename to SessionNetworkingKit/SOGS/Models/UpdateMessageRequest.swift index f18f72a633..640a5d92d8 100644 --- a/SessionMessagingKit/Open Groups/Models/UpdateMessageRequest.swift +++ b/SessionNetworkingKit/SOGS/Models/UpdateMessageRequest.swift @@ -2,7 +2,7 @@ import Foundation -extension OpenGroupAPI { +extension Network.SOGS { public struct UpdateMessageRequest: Codable { /// The serialized message body (encoded in base64 when encoding) let data: Data diff --git a/SessionMessagingKit/Open Groups/Models/UserBanRequest.swift b/SessionNetworkingKit/SOGS/Models/UserBanRequest.swift similarity index 98% rename from SessionMessagingKit/Open Groups/Models/UserBanRequest.swift rename to SessionNetworkingKit/SOGS/Models/UserBanRequest.swift index caff1a17de..249bf8db0c 100644 --- a/SessionMessagingKit/Open Groups/Models/UserBanRequest.swift +++ b/SessionNetworkingKit/SOGS/Models/UserBanRequest.swift @@ -2,7 +2,7 @@ import Foundation -extension OpenGroupAPI { +extension Network.SOGS { struct UserBanRequest: Codable { /// List of one or more room tokens from which the user should be banned (the invoking user must be a `moderator` /// of all of the given rooms diff --git a/SessionMessagingKit/Open Groups/Models/UserModeratorRequest.swift b/SessionNetworkingKit/SOGS/Models/UserModeratorRequest.swift similarity index 99% rename from SessionMessagingKit/Open Groups/Models/UserModeratorRequest.swift rename to SessionNetworkingKit/SOGS/Models/UserModeratorRequest.swift index ece21d2baa..8151ede9ee 100644 --- a/SessionMessagingKit/Open Groups/Models/UserModeratorRequest.swift +++ b/SessionNetworkingKit/SOGS/Models/UserModeratorRequest.swift @@ -2,7 +2,7 @@ import Foundation -extension OpenGroupAPI { +extension Network.SOGS { struct UserModeratorRequest: Codable { /// List of room tokens to which the moderator status should be applied. The invoking user must be an admin of all of the given rooms. /// diff --git a/SessionMessagingKit/Open Groups/Models/UserUnbanRequest.swift b/SessionNetworkingKit/SOGS/Models/UserUnbanRequest.swift similarity index 96% rename from SessionMessagingKit/Open Groups/Models/UserUnbanRequest.swift rename to SessionNetworkingKit/SOGS/Models/UserUnbanRequest.swift index b0e8a2ab99..d1524d3e4a 100644 --- a/SessionMessagingKit/Open Groups/Models/UserUnbanRequest.swift +++ b/SessionNetworkingKit/SOGS/Models/UserUnbanRequest.swift @@ -2,7 +2,7 @@ import Foundation -extension OpenGroupAPI { +extension Network.SOGS { struct UserUnbanRequest: Codable { /// List of one or more room tokens from which the user should be banned (the invoking user must be a `moderator` /// of all of the given rooms diff --git a/SessionNetworkingKit/SOGS/SOGS.swift b/SessionNetworkingKit/SOGS/SOGS.swift new file mode 100644 index 0000000000..0c5e5ce75e --- /dev/null +++ b/SessionNetworkingKit/SOGS/SOGS.swift @@ -0,0 +1,15 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public extension Network { + enum SOGS { + public static let legacyDefaultServerIP = "116.203.70.33" + public static let defaultServer = "https://open.getsession.org" + public static let defaultServerPublicKey = "a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238" + public static let validTimestampVarianceThreshold: TimeInterval = (6 * 60 * 60) + internal static let maxInactivityPeriodForPolling: TimeInterval = (14 * 24 * 60 * 60) + + public static let workQueue = DispatchQueue(label: "SOGS.workQueue", qos: .userInitiated) // It's important that this is a serial queue + } +} diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift b/SessionNetworkingKit/SOGS/SOGSAPI.swift similarity index 88% rename from SessionMessagingKit/Open Groups/OpenGroupAPI.swift rename to SessionNetworkingKit/SOGS/SOGSAPI.swift index 78b97e3f08..981058ae32 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift +++ b/SessionNetworkingKit/SOGS/SOGSAPI.swift @@ -3,25 +3,15 @@ // stringlint:disable import Foundation -import SessionSnodeKit import SessionUtilitiesKit -public enum OpenGroupAPI { - public struct RoomInfo: Codable { +public extension Network.SOGS { + struct PollRoomInfo: Codable { let roomToken: String let infoUpdates: Int64 let sequenceNumber: Int64 } - // MARK: - Settings - - public static let legacyDefaultServerIP = "116.203.70.33" - public static let defaultServer = "https://open.getsession.org" - public static let defaultServerPublicKey = "a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238" - public static let validTimestampVarianceThreshold: TimeInterval = (6 * 60 * 60) - - public static let workQueue = DispatchQueue(label: "OpenGroupAPI.workQueue", qos: .userInitiated) // It's important that this is a serial queue - // MARK: - Batching & Polling /// This is a convenience method which calls `/batch` with a pre-defined set of requests used to update an Open @@ -32,10 +22,11 @@ public enum OpenGroupAPI { /// - Messages (includes additions and deletions) /// - Inbox for the server /// - Outbox for the server - public static func preparedPoll( - roomInfo: [RoomInfo], + static func preparedPoll( + roomInfo: [PollRoomInfo], lastInboxMessageId: Int64, lastOutboxMessageId: Int64, + checkForCommunityMessageRequests: Bool, hasPerformedInitialPoll: Bool, timeSinceLastPoll: TimeInterval, authMethod: AuthenticationMethod, @@ -60,7 +51,7 @@ public enum OpenGroupAPI { // 'maxInactivityPeriod' then just retrieve recent messages instead // of trying to get all messages since the last one retrieved !hasPerformedInitialPoll && - timeSinceLastPoll > CommunityPoller.maxInactivityPeriod + timeSinceLastPoll > maxInactivityPeriodForPolling ) ) @@ -93,7 +84,7 @@ public enum OpenGroupAPI { !supportsBlinding ? [] : [ // Inbox (only check the inbox if the user want's community message requests) - (!dependencies.mutate(cache: .libSession) { $0.get(.checkForCommunityMessageRequests) } ? nil : + (!checkForCommunityMessageRequests ? nil : (lastInboxMessageId == 0 ? try preparedInbox(authMethod: authMethod, using: dependencies) : try preparedInboxSince( @@ -117,13 +108,13 @@ public enum OpenGroupAPI { ) ) - return try OpenGroupAPI + return try Network.SOGS .preparedBatch( requests: preparedRequests, authMethod: authMethod, using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } /// Submits multiple requests wrapped up in a single request, runs them all, then returns the result of each one @@ -133,7 +124,7 @@ public enum OpenGroupAPI { /// /// For contained subrequests that specify a body (i.e. POST or PUT requests) exactly one of `json`, `b64`, or `bytes` must be provided /// with the request body. - public static func preparedBatch( + static func preparedBatch( requests: [any ErasedPreparedRequest], authMethod: AuthenticationMethod, using dependencies: Dependencies @@ -149,7 +140,7 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } /// This is like `/batch`, except that it guarantees to perform requests sequentially in the order provided and will stop processing requests @@ -165,9 +156,10 @@ public enum OpenGroupAPI { private static func preparedSequence( requests: [any ErasedPreparedRequest], authMethod: AuthenticationMethod, + skipAuthentication: Bool = false, using dependencies: Dependencies ) throws -> Network.PreparedRequest> { - return try Network.PreparedRequest( + let preparedRequest = try Network.PreparedRequest( request: Request( method: .post, endpoint: Endpoint.sequence, @@ -178,7 +170,11 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + + return (skipAuthentication ? + preparedRequest : + try preparedRequest.signed(with: Network.SOGS.signRequest, using: dependencies) + ) } // MARK: - Capabilities @@ -190,20 +186,25 @@ public enum OpenGroupAPI { /// /// Eg. `GET /capabilities` could return `{"capabilities": ["sogs", "batch"]}` `GET /capabilities?required=magic,batch` /// could return: `{"capabilities": ["sogs", "batch"], "missing": ["magic"]}` - public static func preparedCapabilities( + static func preparedCapabilities( authMethod: AuthenticationMethod, + skipAuthentication: Bool = false, using dependencies: Dependencies - ) throws -> Network.PreparedRequest { - return try Network.PreparedRequest( + ) throws -> Network.PreparedRequest { + let preparedRequest = try Network.PreparedRequest( request: Request( endpoint: .capabilities, authMethod: authMethod ), - responseType: Capabilities.self, + responseType: CapabilitiesResponse.self, additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + + return (skipAuthentication ? + preparedRequest : + try preparedRequest.signed(with: Network.SOGS.signRequest, using: dependencies) + ) } // MARK: - Room @@ -211,11 +212,12 @@ public enum OpenGroupAPI { /// Returns a list of available rooms on the server /// /// Rooms to which the user does not have access (e.g. because they are banned, or the room has restricted access permissions) are not included - public static func preparedRooms( + static func preparedRooms( authMethod: AuthenticationMethod, + skipAuthentication: Bool = false, using dependencies: Dependencies ) throws -> Network.PreparedRequest<[Room]> { - return try Network.PreparedRequest( + let preparedRequest = try Network.PreparedRequest( request: Request( endpoint: .rooms, authMethod: authMethod @@ -224,11 +226,15 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + + return (skipAuthentication ? + preparedRequest : + try preparedRequest.signed(with: Network.SOGS.signRequest, using: dependencies) + ) } /// Returns the details of a single room - public static func preparedRoom( + static func preparedRoom( roomToken: String, authMethod: AuthenticationMethod, using dependencies: Dependencies @@ -242,14 +248,14 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } /// Polls a room for metadata updates /// /// The endpoint polls room metadata for this room, always including the instantaneous room details (such as the user's permission and current /// number of active users), and including the full room metadata if the room's info_updated counter has changed from the provided value - public static func preparedRoomPollInfo( + static func preparedRoomPollInfo( lastUpdated: Int64, roomToken: String, authMethod: AuthenticationMethod, @@ -264,22 +270,22 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } - public typealias CapabilitiesAndRoomResponse = ( - capabilities: (info: ResponseInfoType, data: Capabilities), + typealias CapabilitiesAndRoomResponse = ( + capabilities: (info: ResponseInfoType, data: Network.SOGS.CapabilitiesResponse), room: (info: ResponseInfoType, data: Room) ) /// This is a convenience method which constructs a `/sequence` of the `capabilities` and `room` requests, refer to those /// methods for the documented behaviour of each method - public static func preparedCapabilitiesAndRoom( + static func preparedCapabilitiesAndRoom( roomToken: String, authMethod: AuthenticationMethod, using dependencies: Dependencies ) throws -> Network.PreparedRequest { - return try OpenGroupAPI + return try Network.SOGS .preparedSequence( requests: [ // Get the latest capabilities for the server (in case it's a new server or the @@ -290,9 +296,9 @@ public enum OpenGroupAPI { authMethod: authMethod, using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) .tryMap { (info: ResponseInfoType, response: Network.BatchResponseMap) -> CapabilitiesAndRoomResponse in - let maybeCapabilities: Network.BatchSubResponse? = (response[.capabilities] as? Network.BatchSubResponse) + let maybeCapabilities: Network.BatchSubResponse? = (response[.capabilities] as? Network.BatchSubResponse) let maybeRoomResponse: Any? = response.data .first(where: { key, _ in switch key { @@ -305,7 +311,7 @@ public enum OpenGroupAPI { guard let capabilitiesInfo: ResponseInfoType = maybeCapabilities, - let capabilities: Capabilities = maybeCapabilities?.body, + let capabilities: Network.SOGS.CapabilitiesResponse = maybeCapabilities?.body, let roomInfo: ResponseInfoType = maybeRoom, let room: Room = maybeRoom?.body else { throw NetworkError.parsingFailed } @@ -317,31 +323,38 @@ public enum OpenGroupAPI { } } - public typealias CapabilitiesAndRoomsResponse = ( - capabilities: (info: ResponseInfoType, data: Capabilities), + typealias CapabilitiesAndRoomsResponse = ( + capabilities: (info: ResponseInfoType, data: Network.SOGS.CapabilitiesResponse), rooms: (info: ResponseInfoType, data: [Room]) ) /// This is a convenience method which constructs a `/sequence` of the `capabilities` and `rooms` requests, refer to those /// methods for the documented behaviour of each method - public static func preparedCapabilitiesAndRooms( + static func preparedCapabilitiesAndRooms( authMethod: AuthenticationMethod, + skipAuthentication: Bool = false, using dependencies: Dependencies ) throws -> Network.PreparedRequest { - return try OpenGroupAPI + let preparedRequest = try Network.SOGS .preparedSequence( requests: [ // Get the latest capabilities for the server (in case it's a new server or the // cached ones are stale) - preparedCapabilities(authMethod: authMethod, using: dependencies), - preparedRooms(authMethod: authMethod, using: dependencies) + preparedCapabilities(authMethod: authMethod, skipAuthentication: skipAuthentication, using: dependencies), + preparedRooms(authMethod: authMethod, skipAuthentication: skipAuthentication, using: dependencies) ], authMethod: authMethod, + skipAuthentication: skipAuthentication, using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + let finalRequest = (skipAuthentication ? + preparedRequest : + try preparedRequest.signed(with: Network.SOGS.signRequest, using: dependencies) + ) + + return finalRequest .tryMap { (info: ResponseInfoType, response: Network.BatchResponseMap) -> CapabilitiesAndRoomsResponse in - let maybeCapabilities: Network.BatchSubResponse? = (response[.capabilities] as? Network.BatchSubResponse) + let maybeCapabilities: Network.BatchSubResponse? = (response[.capabilities] as? Network.BatchSubResponse) let maybeRooms: Network.BatchSubResponse<[Room]>? = response.data .first(where: { key, _ in switch key { @@ -353,7 +366,7 @@ public enum OpenGroupAPI { guard let capabilitiesInfo: ResponseInfoType = maybeCapabilities, - let capabilities: Capabilities = maybeCapabilities?.body, + let capabilities: Network.SOGS.CapabilitiesResponse = maybeCapabilities?.body, let roomsInfo: ResponseInfoType = maybeRooms, let roomsResponse: Network.BatchSubResponse<[Room]> = maybeRooms, !roomsResponse.failedToParseBody @@ -370,7 +383,7 @@ public enum OpenGroupAPI { // MARK: - Messages /// Posts a new message to a room - public static func preparedSend( + static func preparedSend( plaintext: Data, roomToken: String, whisperTo: String?, @@ -390,7 +403,7 @@ public enum OpenGroupAPI { request: Request( method: .post, endpoint: Endpoint.roomMessage(roomToken), - body: SendMessageRequest( + body: SendSOGSMessageRequest( data: plaintext, signature: Data(signResult.signature), whisperTo: whisperTo, @@ -403,11 +416,11 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } /// Returns a single message by ID - public static func preparedMessage( + static func preparedMessage( id: Int64, roomToken: String, authMethod: AuthenticationMethod, @@ -422,13 +435,13 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } /// Edits a message, replacing its existing content with new content and a new signature /// /// **Note:** This edit may only be initiated by the creator of the post, and the poster must currently have write permissions in the room - public static func preparedMessageUpdate( + static func preparedMessageUpdate( id: Int64, plaintext: Data, fileIds: [Int64]?, @@ -458,11 +471,11 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } /// Remove a message by its message id - public static func preparedMessageDelete( + static func preparedMessageDelete( id: Int64, roomToken: String, authMethod: AuthenticationMethod, @@ -478,7 +491,7 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } /// Retrieves recent messages posted to this room @@ -486,7 +499,7 @@ public enum OpenGroupAPI { /// Returns the most recent limit messages (100 if no limit is given). This only returns extant messages, and always returns the latest /// versions: that is, deleted message indicators and pre-editing versions of messages are not returned. Messages are returned in order /// from most recent to least recent - public static func preparedRecentMessages( + static func preparedRecentMessages( roomToken: String, authMethod: AuthenticationMethod, using dependencies: Dependencies @@ -496,7 +509,8 @@ public enum OpenGroupAPI { endpoint: .roomMessagesRecent(roomToken), queryParameters: [ .updateTypes: UpdateTypes.reaction.rawValue, - .reactors: "5" + .reactors: "5", + .limit: "\(dependencies[feature: .communityPollLimit])" ], authMethod: authMethod ), @@ -504,7 +518,7 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } /// Retrieves messages from the room preceding a given id. @@ -513,7 +527,7 @@ public enum OpenGroupAPI { /// through batches of ever-older messages. As with .../recent, messages are returned in order from most recent to least recent. /// /// As with .../recent, this endpoint does not include deleted messages and always returns the current version, for edited messages. - public static func preparedMessagesBefore( + static func preparedMessagesBefore( messageId: Int64, roomToken: String, authMethod: AuthenticationMethod, @@ -524,7 +538,8 @@ public enum OpenGroupAPI { endpoint: .roomMessagesBefore(roomToken, id: messageId), queryParameters: [ .updateTypes: UpdateTypes.reaction.rawValue, - .reactors: "5" + .reactors: "5", + .limit: "\(dependencies[feature: .communityPollLimit])" ], authMethod: authMethod ), @@ -532,7 +547,7 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } /// Retrieves message updates from a room. This is the main message polling endpoint in SOGS. @@ -541,7 +556,7 @@ public enum OpenGroupAPI { /// sequence counter. Returns limit messages at a time (100 if no limit is given). Returned messages include any new messages, updates /// to existing messages (i.e. edits), and message deletions made to the room since the given update id. Messages are returned in "update" /// order, that is, in the order in which the change was applied to the room, from oldest the newest. - public static func preparedMessagesSince( + static func preparedMessagesSince( seqNo: Int64, roomToken: String, authMethod: AuthenticationMethod, @@ -552,7 +567,8 @@ public enum OpenGroupAPI { endpoint: .roomMessagesSince(roomToken, seqNo: seqNo), queryParameters: [ .updateTypes: UpdateTypes.reaction.rawValue, - .reactors: "5" + .reactors: "5", + .limit: "\(dependencies[feature: .communityPollLimit])" ], authMethod: authMethod ), @@ -560,7 +576,7 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } /// Deletes all messages from a given sessionId within the provided rooms (or globally) on a server @@ -576,7 +592,7 @@ public enum OpenGroupAPI { /// - server: The server to delete messages from /// /// - dependencies: Injected dependencies (used for unit testing) - public static func preparedMessagesDeleteAll( + static func preparedMessagesDeleteAll( sessionId: String, roomToken: String, authMethod: AuthenticationMethod, @@ -592,13 +608,13 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } // MARK: - Reactions /// Returns the list of all reactors who have added a particular reaction to a particular message. - public static func preparedReactors( + static func preparedReactors( emoji: String, id: Int64, roomToken: String, @@ -608,7 +624,7 @@ public enum OpenGroupAPI { /// URL(String:) won't convert raw emojis, so need to do a little encoding here. /// The raw emoji will come back when calling url.path guard let encodedEmoji: String = emoji.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else { - throw OpenGroupAPIError.invalidEmoji + throw SOGSError.invalidEmoji } return try Network.PreparedRequest( @@ -621,14 +637,14 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } /// Adds a reaction to the given message in this room. The user must have read access in the room. /// /// Reactions are short strings of 1-12 unicode codepoints, typically emoji (or character sequences to produce an emoji variant, /// such as 👨🏿‍🦰, which is composed of 4 unicode "characters" but usually renders as a single emoji "Man: Dark Skin Tone, Red Hair"). - public static func preparedReactionAdd( + static func preparedReactionAdd( emoji: String, id: Int64, roomToken: String, @@ -638,7 +654,7 @@ public enum OpenGroupAPI { /// URL(String:) won't convert raw emojis, so need to do a little encoding here. /// The raw emoji will come back when calling url.path guard let encodedEmoji: String = emoji.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else { - throw OpenGroupAPIError.invalidEmoji + throw SOGSError.invalidEmoji } return try Network.PreparedRequest( @@ -651,12 +667,12 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } /// Removes a reaction from a post this room. The user must have read access in the room. This only removes the user's own reaction /// but does not affect the reactions of other users. - public static func preparedReactionDelete( + static func preparedReactionDelete( emoji: String, id: Int64, roomToken: String, @@ -666,7 +682,7 @@ public enum OpenGroupAPI { /// URL(String:) won't convert raw emojis, so need to do a little encoding here. /// The raw emoji will come back when calling url.path guard let encodedEmoji: String = emoji.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else { - throw OpenGroupAPIError.invalidEmoji + throw SOGSError.invalidEmoji } return try Network.PreparedRequest( @@ -679,13 +695,13 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } /// Removes all reactions of all users from a post in this room. The calling must have moderator permissions in the room. This endpoint /// can either remove a single reaction (e.g. remove all 🍆 reactions) by specifying it after the message id (following a /), or remove all /// reactions from the post by not including the / suffix of the URL. - public static func preparedReactionDeleteAll( + static func preparedReactionDeleteAll( emoji: String, id: Int64, roomToken: String, @@ -695,7 +711,7 @@ public enum OpenGroupAPI { /// URL(String:) won't convert raw emojis, so need to do a little encoding here. /// The raw emoji will come back when calling url.path guard let encodedEmoji: String = emoji.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else { - throw OpenGroupAPIError.invalidEmoji + throw SOGSError.invalidEmoji } return try Network.PreparedRequest( @@ -708,7 +724,7 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } // MARK: - Pinning @@ -723,7 +739,7 @@ public enum OpenGroupAPI { /// Pinned messages that are already pinned will be re-pinned (that is, their pin timestamp and pinning admin user will be updated) - because pinned /// messages are returned in pinning-order this allows admins to order multiple pinned messages in a room by re-pinning (via this endpoint) in the /// order in which pinned messages should be displayed - public static func preparedPinMessage( + static func preparedPinMessage( id: Int64, roomToken: String, authMethod: AuthenticationMethod, @@ -739,13 +755,13 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } /// Remove a message from this room's pinned message list /// /// The user must have `admin` (not just `moderator`) permissions in the room - public static func preparedUnpinMessage( + static func preparedUnpinMessage( id: Int64, roomToken: String, authMethod: AuthenticationMethod, @@ -761,13 +777,13 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } /// Removes _all_ pinned messages from this room /// /// The user must have `admin` (not just `moderator`) permissions in the room - public static func preparedUnpinAll( + static func preparedUnpinAll( roomToken: String, authMethod: AuthenticationMethod, using dependencies: Dependencies @@ -782,12 +798,12 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } // MARK: - Files - public static func preparedUpload( + static func preparedUpload( data: Data, roomToken: String, fileName: String? = nil, @@ -813,10 +829,10 @@ public enum OpenGroupAPI { requestTimeout: Network.fileUploadTimeout, using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } - public static func downloadUrlString( + static func downloadUrlString( for fileId: String, server: String, roomToken: String @@ -824,24 +840,27 @@ public enum OpenGroupAPI { return "\(server)/\(Endpoint.roomFileIndividual(roomToken, fileId).path)" } - public static func preparedDownload( + static func preparedDownload( url: URL, roomToken: String, authMethod: AuthenticationMethod, using dependencies: Dependencies ) throws -> Network.PreparedRequest { - guard let fileId: String = Attachment.fileId(for: url.absoluteString) else { throw NetworkError.invalidURL } + guard let fileId: String = Network.FileServer.fileId(for: url.absoluteString) else { + throw NetworkError.invalidURL + } return try preparedDownload(fileId: fileId, roomToken: roomToken, authMethod: authMethod, using: dependencies) } - public static func preparedDownload( + static func preparedDownload( fileId: String, roomToken: String, authMethod: AuthenticationMethod, + skipAuthentication: Bool = false, using dependencies: Dependencies ) throws -> Network.PreparedRequest { - return try Network.PreparedRequest( + let preparedRequest = try Network.PreparedRequest( request: Request( endpoint: .roomFileIndividual(roomToken, fileId), authMethod: authMethod @@ -851,7 +870,11 @@ public enum OpenGroupAPI { requestTimeout: Network.fileDownloadTimeout, using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + + return (skipAuthentication ? + preparedRequest : + try preparedRequest.signed(with: Network.SOGS.signRequest, using: dependencies) + ) } // MARK: - Inbox/Outbox (Message Requests) @@ -859,7 +882,7 @@ public enum OpenGroupAPI { /// Retrieves all of the user's current DMs (up to limit) /// /// **Note:** `inbox` will return a `304` with an empty response if no messages (hence the optional return type) - public static func preparedInbox( + static func preparedInbox( authMethod: AuthenticationMethod, using dependencies: Dependencies ) throws -> Network.PreparedRequest<[DirectMessage]?> { @@ -872,13 +895,13 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } /// Polls for any DMs received since the given id, this method will return a `304` with an empty response if there are no messages /// /// **Note:** `inboxSince` will return a `304` with an empty response if no messages (hence the optional return type) - public static func preparedInboxSince( + static func preparedInboxSince( id: Int64, authMethod: AuthenticationMethod, using dependencies: Dependencies @@ -892,11 +915,11 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } /// Remove all message requests from inbox, this methrod will return the number of messages deleted - public static func preparedClearInbox( + static func preparedClearInbox( requestTimeout: TimeInterval = Network.defaultTimeout, requestAndPathBuildTimeout: TimeInterval? = nil, authMethod: AuthenticationMethod, @@ -914,13 +937,13 @@ public enum OpenGroupAPI { requestAndPathBuildTimeout: requestAndPathBuildTimeout, using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } /// Delivers a direct message to a user via their blinded Session ID /// /// The body of this request is a JSON object containing a message key with a value of the encrypted-then-base64-encoded message to deliver - public static func preparedSend( + static func preparedSend( ciphertext: Data, toInboxFor blindedSessionId: String, authMethod: AuthenticationMethod, @@ -939,13 +962,13 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } /// Retrieves all of the user's sent DMs (up to limit) /// /// **Note:** `outbox` will return a `304` with an empty response if no messages (hence the optional return type) - public static func preparedOutbox( + static func preparedOutbox( authMethod: AuthenticationMethod, using dependencies: Dependencies ) throws -> Network.PreparedRequest<[DirectMessage]?> { @@ -958,13 +981,13 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } /// Polls for any DMs sent since the given id, this method will return a `304` with an empty response if there are no messages /// /// **Note:** `outboxSince` will return a `304` with an empty response if no messages (hence the optional return type) - public static func preparedOutboxSince( + static func preparedOutboxSince( id: Int64, authMethod: AuthenticationMethod, using dependencies: Dependencies @@ -978,7 +1001,7 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } // MARK: - Users @@ -1014,7 +1037,7 @@ public enum OpenGroupAPI { /// - server: The server to delete messages from /// /// - dependencies: Injected dependencies (used for unit testing) - public static func preparedUserBan( + static func preparedUserBan( sessionId: String, for timeout: TimeInterval? = nil, from roomTokens: [String]? = nil, @@ -1036,7 +1059,7 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } /// Removes a user ban from specific rooms, or from the server globally @@ -1063,7 +1086,7 @@ public enum OpenGroupAPI { /// - server: The server to delete messages from /// /// - dependencies: Injected dependencies (used for unit testing) - public static func preparedUserUnban( + static func preparedUserUnban( sessionId: String, from roomTokens: [String]?, authMethod: AuthenticationMethod, @@ -1083,7 +1106,7 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } /// Appoints or removes a moderator or admin @@ -1137,7 +1160,7 @@ public enum OpenGroupAPI { /// - server: The server to perform the permission changes on /// /// - dependencies: Injected dependencies (used for unit testing) - public static func preparedUserModeratorUpdate( + static func preparedUserModeratorUpdate( sessionId: String, moderator: Bool? = nil, admin: Bool? = nil, @@ -1167,18 +1190,18 @@ public enum OpenGroupAPI { additionalSignatureData: AdditionalSigningData(authMethod), using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } /// This is a convenience method which constructs a `/sequence` of the `userBan` and `userDeleteMessages` requests, refer to those /// methods for the documented behaviour of each method - public static func preparedUserBanAndDeleteAllMessages( + static func preparedUserBanAndDeleteAllMessages( sessionId: String, roomToken: String, authMethod: AuthenticationMethod, using dependencies: Dependencies ) throws -> Network.PreparedRequest> { - return try OpenGroupAPI + return try Network.SOGS .preparedSequence( requests: [ preparedUserBan( @@ -1197,7 +1220,7 @@ public enum OpenGroupAPI { authMethod: authMethod, using: dependencies ) - .signed(with: OpenGroupAPI.signRequest, using: dependencies) + .signed(with: Network.SOGS.signRequest, using: dependencies) } // MARK: - Authentication @@ -1219,7 +1242,7 @@ public enum OpenGroupAPI { !publicKey.isEmpty, let nonce: [UInt8] = dependencies[singleton: .crypto].generate(.randomBytes(16)), let timestampBytes: [UInt8] = "\(timestamp)".data(using: .ascii).map({ Array($0) }) - else { throw OpenGroupAPIError.signingFailed } + else { throw SOGSError.signingFailed } /// Get a hash of any body content let bodyHash: [UInt8]? = { @@ -1270,7 +1293,7 @@ public enum OpenGroupAPI { !dependencies[cache: .general].ed25519SecretKey.isEmpty, !dependencies[cache: .general].ed25519Seed.isEmpty, case .community(_, let publicKey, let hasCapabilities, let supportsBlinding, let forceBlinded) = authMethod.info - else { throw OpenGroupAPIError.signingFailed } + else { throw SOGSError.signingFailed } // If we have no capabilities or if the server supports blinded keys then sign using the blinded key if forceBlinded || !hasCapabilities || supportsBlinding { @@ -1288,7 +1311,7 @@ public enum OpenGroupAPI { ed25519SecretKey: dependencies[cache: .general].ed25519SecretKey ) ) - else { throw OpenGroupAPIError.signingFailed } + else { throw SOGSError.signingFailed } return ( publicKey: SessionId(.blinded15, publicKey: blinded15KeyPair.publicKey).hexString, @@ -1310,7 +1333,7 @@ public enum OpenGroupAPI { .ed25519KeyPair(seed: dependencies[cache: .general].ed25519Seed) ), case .standard(let signatureResult) = signature - else { throw OpenGroupAPIError.signingFailed } + else { throw SOGSError.signingFailed } return ( publicKey: SessionId(.unblinded, publicKey: ed25519KeyPair.publicKey).hexString, @@ -1332,7 +1355,7 @@ public enum OpenGroupAPI { let signatureResult: [UInt8] = dependencies[singleton: .crypto].generate( .signatureXed25519(data: messageBytes, curve25519PrivateKey: x25519SecretKey) ) - else { throw OpenGroupAPIError.signingFailed } + else { throw SOGSError.signingFailed } return ( publicKey: SessionId(.standard, publicKey: x25519PublicKey).hexString, @@ -1347,7 +1370,7 @@ public enum OpenGroupAPI { using dependencies: Dependencies ) throws -> Network.Destination { guard let signingData: AdditionalSigningData = preparedRequest.additionalSignatureData as? AdditionalSigningData else { - throw OpenGroupAPIError.signingFailed + throw SOGSError.signingFailed } return try preparedRequest.destination @@ -1355,7 +1378,7 @@ public enum OpenGroupAPI { } } -private extension OpenGroupAPI { +private extension Network.SOGS { struct AdditionalSigningData { let authMethod: AuthenticationMethod @@ -1366,7 +1389,7 @@ private extension OpenGroupAPI { } private extension Network.Destination { - func signed(data: OpenGroupAPI.AdditionalSigningData, body: Data?, using dependencies: Dependencies) throws -> Network.Destination { + func signed(data: Network.SOGS.AdditionalSigningData, body: Data?, using dependencies: Dependencies) throws -> Network.Destination { switch self { case .snode, .randomSnode, .randomSnodeLatestNetworkTimeTarget: throw NetworkError.unauthorised case .cached: return self @@ -1381,8 +1404,8 @@ private extension Network.Destination { } private extension Network.Destination.ServerInfo { - func signed(_ data: OpenGroupAPI.AdditionalSigningData, _ body: Data?, using dependencies: Dependencies) throws -> Network.Destination.ServerInfo { - return updated(with: try OpenGroupAPI.signatureHeaders( + func signed(_ data: Network.SOGS.AdditionalSigningData, _ body: Data?, using dependencies: Dependencies) throws -> Network.Destination.ServerInfo { + return updated(with: try Network.SOGS.signatureHeaders( url: url, method: method, body: body, diff --git a/SessionMessagingKit/Open Groups/Types/SOGSEndpoint.swift b/SessionNetworkingKit/SOGS/SOGSEndpoint.swift similarity index 97% rename from SessionMessagingKit/Open Groups/Types/SOGSEndpoint.swift rename to SessionNetworkingKit/SOGS/SOGSEndpoint.swift index 9da8faf919..218bfadbe3 100644 --- a/SessionMessagingKit/Open Groups/Types/SOGSEndpoint.swift +++ b/SessionNetworkingKit/SOGS/SOGSEndpoint.swift @@ -3,10 +3,9 @@ // stringlint:disable import Foundation -import SessionSnodeKit -extension OpenGroupAPI { - public enum Endpoint: EndpointType { +public extension Network.SOGS { + enum Endpoint: EndpointType { // Utility case onion @@ -61,7 +60,7 @@ extension OpenGroupAPI { case userUnban(String) case userModerator(String) - public static var name: String { "OpenGroupAPI.Endpoint" } + public static var name: String { "SOGS.Endpoint" } public static var batchRequestVariant: Network.BatchRequest.Child.Variant = .sogs public static var excludedSubRequestHeaders: [HTTPHeader] = [ .sogsPubKey, .sogsTimestamp, .sogsNonce, .sogsSignature diff --git a/SessionMessagingKit/Open Groups/Types/OpenGroupAPIError.swift b/SessionNetworkingKit/SOGS/SOGSError.swift similarity index 92% rename from SessionMessagingKit/Open Groups/Types/OpenGroupAPIError.swift rename to SessionNetworkingKit/SOGS/SOGSError.swift index d5ab81cbe1..bf91640de0 100644 --- a/SessionMessagingKit/Open Groups/Types/OpenGroupAPIError.swift +++ b/SessionNetworkingKit/SOGS/SOGSError.swift @@ -4,7 +4,7 @@ import Foundation -public enum OpenGroupAPIError: Error, CustomStringConvertible { +public enum SOGSError: Error, CustomStringConvertible { case decryptionFailed case signingFailed case noPublicKey diff --git a/SessionMessagingKit/Open Groups/Types/HTTPHeader+OpenGroup.swift b/SessionNetworkingKit/SOGS/Types/HTTPHeader+SOGS.swift similarity index 94% rename from SessionMessagingKit/Open Groups/Types/HTTPHeader+OpenGroup.swift rename to SessionNetworkingKit/SOGS/Types/HTTPHeader+SOGS.swift index 0b3dbc54ab..0c100cfbd1 100644 --- a/SessionMessagingKit/Open Groups/Types/HTTPHeader+OpenGroup.swift +++ b/SessionNetworkingKit/SOGS/Types/HTTPHeader+SOGS.swift @@ -3,7 +3,6 @@ // stringlint:disable import Foundation -import SessionSnodeKit public extension HTTPHeader { static let sogsPubKey: HTTPHeader = "X-SOGS-Pubkey" diff --git a/SessionMessagingKit/Open Groups/Types/HTTPQueryParam+OpenGroup.swift b/SessionNetworkingKit/SOGS/Types/HTTPQueryParam+SOGS.swift similarity index 96% rename from SessionMessagingKit/Open Groups/Types/HTTPQueryParam+OpenGroup.swift rename to SessionNetworkingKit/SOGS/Types/HTTPQueryParam+SOGS.swift index a9af9824ad..dd6c86e3c5 100644 --- a/SessionMessagingKit/Open Groups/Types/HTTPQueryParam+OpenGroup.swift +++ b/SessionNetworkingKit/SOGS/Types/HTTPQueryParam+SOGS.swift @@ -3,7 +3,6 @@ // stringlint:disable import Foundation -import SessionSnodeKit public extension HTTPQueryParam { static let publicKey: HTTPQueryParam = "public_key" diff --git a/SessionMessagingKit/Open Groups/Types/Personalization.swift b/SessionNetworkingKit/SOGS/Types/Personalization.swift similarity index 92% rename from SessionMessagingKit/Open Groups/Types/Personalization.swift rename to SessionNetworkingKit/SOGS/Types/Personalization.swift index b0b827b20d..5b24626fe9 100644 --- a/SessionMessagingKit/Open Groups/Types/Personalization.swift +++ b/SessionNetworkingKit/SOGS/Types/Personalization.swift @@ -2,7 +2,7 @@ import Foundation -extension OpenGroupAPI { +extension Network.SOGS { public enum Personalization: String { case sharedKeys = "sogs.shared_keys" case authHeader = "sogs.auth_header" diff --git a/SessionMessagingKit/Open Groups/Types/Request+OpenGroupAPI.swift b/SessionNetworkingKit/SOGS/Types/Request+SOGS.swift similarity index 86% rename from SessionMessagingKit/Open Groups/Types/Request+OpenGroupAPI.swift rename to SessionNetworkingKit/SOGS/Types/Request+SOGS.swift index 6242a05447..5373174584 100644 --- a/SessionMessagingKit/Open Groups/Types/Request+OpenGroupAPI.swift +++ b/SessionNetworkingKit/SOGS/Types/Request+SOGS.swift @@ -1,13 +1,9 @@ // Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. import Foundation -import GRDB -import SessionSnodeKit import SessionUtilitiesKit -// MARK: Request - OpenGroupAPI - -public extension Request where Endpoint == OpenGroupAPI.Endpoint { +public extension Request where Endpoint == Network.SOGS.Endpoint { init( method: HTTPMethod = .get, endpoint: Endpoint, diff --git a/SessionMessagingKit/Open Groups/Types/UpdateTypes.swift b/SessionNetworkingKit/SOGS/Types/UpdateTypes.swift similarity index 85% rename from SessionMessagingKit/Open Groups/Types/UpdateTypes.swift rename to SessionNetworkingKit/SOGS/Types/UpdateTypes.swift index 93c63893fa..61baf5ee47 100644 --- a/SessionMessagingKit/Open Groups/Types/UpdateTypes.swift +++ b/SessionNetworkingKit/SOGS/Types/UpdateTypes.swift @@ -2,7 +2,7 @@ import Foundation -extension OpenGroupAPI { +extension Network.SOGS { enum UpdateTypes: String { case reaction = "r" } diff --git a/SessionNetworkingKit/SessionNetwork/Models/Info.swift b/SessionNetworkingKit/SessionNetwork/Models/Info.swift new file mode 100644 index 0000000000..6a9b8055f9 --- /dev/null +++ b/SessionNetworkingKit/SessionNetwork/Models/Info.swift @@ -0,0 +1,21 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public extension Network.SessionNetwork { + struct Info: Codable, Equatable { + enum CodingKeys: String, CodingKey { + case timestamp = "t" + case statusCode = "status_code" + case price + case token + case network + } + + public let timestamp: Int64? // Request timestamp. (seconds) + public let statusCode: Int? // Status code of the request. + public let price: Price? + public let token: Token? + public let network: NetworkInfo? + } +} diff --git a/SessionNetworkingKit/SessionNetwork/Models/NetworkInfo.swift b/SessionNetworkingKit/SessionNetwork/Models/NetworkInfo.swift new file mode 100644 index 0000000000..28c49ff9d6 --- /dev/null +++ b/SessionNetworkingKit/SessionNetwork/Models/NetworkInfo.swift @@ -0,0 +1,17 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public extension Network.SessionNetwork { + struct NetworkInfo: Codable, Equatable { + enum CodingKeys: String, CodingKey { + case networkSize = "network_size" // The number of nodes in the Session Network (integer) + case networkStakedTokens = "network_staked_tokens" // + case networkStakedUSD = "network_staked_usd" // + } + + public let networkSize: Int? + public let networkStakedTokens: Double? + public let networkStakedUSD: Double? + } +} diff --git a/SessionNetworkingKit/SessionNetwork/Models/Price.swift b/SessionNetworkingKit/SessionNetwork/Models/Price.swift new file mode 100644 index 0000000000..28eaa5d991 --- /dev/null +++ b/SessionNetworkingKit/SessionNetwork/Models/Price.swift @@ -0,0 +1,19 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public extension Network.SessionNetwork { + struct Price: Codable, Equatable { + enum CodingKeys: String, CodingKey { + case tokenUsd = "usd" + case marketCapUsd = "usd_market_cap" + case priceTimestamp = "t_price" + case staleTimestamp = "t_stale" + } + + public let tokenUsd: Double? // Current token price (USD) + public let marketCapUsd: Double? // Current market cap value in (USD) + public let priceTimestamp: Int64? // The timestamp the price data is accurate at. (seconds) + public let staleTimestamp: Int64? // Stale timestamp for the price data. (seconds) + } +} diff --git a/SessionNetworkingKit/SessionNetwork/Models/Token.swift b/SessionNetworkingKit/SessionNetwork/Models/Token.swift new file mode 100644 index 0000000000..741da31784 --- /dev/null +++ b/SessionNetworkingKit/SessionNetwork/Models/Token.swift @@ -0,0 +1,17 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public extension Network.SessionNetwork { + struct Token: Codable, Equatable { + enum CodingKeys: String, CodingKey { + case stakingRequirement = "staking_requirement" + case stakingRewardPool = "staking_reward_pool" + case contractAddress = "contract_address" + } + + public let stakingRequirement: Double? // The number of tokens required to stake a node. This is the effective "token amount" per node (SESH) + public let stakingRewardPool: Double? // The number of tokens in the staking reward pool (SESH) + public let contractAddress: String? // Token contract address (42 char Hexadecimal - Including 0x prefix) + } +} diff --git a/SessionNetworkingKit/SessionNetwork/SessionNetwork.swift b/SessionNetworkingKit/SessionNetwork/SessionNetwork.swift new file mode 100644 index 0000000000..c4c0e933c7 --- /dev/null +++ b/SessionNetworkingKit/SessionNetwork/SessionNetwork.swift @@ -0,0 +1,18 @@ +// Copyright © 2024 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +import Foundation + +public extension Network { + enum SessionNetwork { + public static let workQueue: DispatchQueue = DispatchQueue( + label: "SessionNetworkAPI.workQueue", + qos: .userInitiated + ) + public static let client: HTTPClient = HTTPClient() + + static let networkAPIServer = "http://networkv1.getsession.org" + static let networkAPIServerPublicKey = "cbf461a4431dc9174dceef4421680d743a2a0e1a3131fc794240bcb0bc3dd449" + } +} diff --git a/SessionSnodeKit/SessionNetworkAPI/SessionNetworkAPI.swift b/SessionNetworkingKit/SessionNetwork/SessionNetworkAPI.swift similarity index 86% rename from SessionSnodeKit/SessionNetworkAPI/SessionNetworkAPI.swift rename to SessionNetworkingKit/SessionNetwork/SessionNetworkAPI.swift index b2be2d86ba..1d11aa7888 100644 --- a/SessionSnodeKit/SessionNetworkAPI/SessionNetworkAPI.swift +++ b/SessionNetworkingKit/SessionNetwork/SessionNetworkAPI.swift @@ -6,34 +6,31 @@ import Foundation import Combine import SessionUtilitiesKit -public enum SessionNetworkAPI { - public static let workQueue = DispatchQueue(label: "SessionNetworkAPI.workQueue", qos: .userInitiated) - public static let client = HTTPClient() - +public extension Network.SessionNetwork { // MARK: - Info /// General token info. This endpoint combines the `/price` and `/token` endpoint information. /// /// `GET/info` - public static func prepareInfo( + static func prepareInfo( using dependencies: Dependencies ) throws -> Network.PreparedRequest { return try Network.PreparedRequest( - request: Request( - endpoint: Network.NetworkAPI.Endpoint.info, + request: Request( + endpoint: Network.SessionNetwork.Endpoint.info, destination: .server( method: .get, - server: Network.NetworkAPI.networkAPIServer, + server: Network.SessionNetwork.networkAPIServer, queryParameters: [:], - x25519PublicKey: Network.NetworkAPI.networkAPIServerPublicKey + x25519PublicKey: Network.SessionNetwork.networkAPIServerPublicKey ) ), responseType: Info.self, requestAndPathBuildTimeout: Network.defaultTimeout, using: dependencies ) - .signed(with: SessionNetworkAPI.signRequest, using: dependencies) + .signed(with: Network.SessionNetwork.signRequest, using: dependencies) } // MARK: - Authentication diff --git a/SessionNetworkingKit/SessionNetwork/SessionNetworkEndpoint.swift b/SessionNetworkingKit/SessionNetwork/SessionNetworkEndpoint.swift new file mode 100644 index 0000000000..a96d9fd2d4 --- /dev/null +++ b/SessionNetworkingKit/SessionNetwork/SessionNetworkEndpoint.swift @@ -0,0 +1,21 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public extension Network.SessionNetwork { + public enum Endpoint: EndpointType { + case info + case price + case token + + public static var name: String { "NetworkAPI.Endpoint" } + + public var path: String { + switch self { + case .info: return "info" + case .price: return "price" + case .token: return "token" + } + } + } +} diff --git a/SessionNetworkingKit/SessionNetwork/Types/HTTPClient.swift b/SessionNetworkingKit/SessionNetwork/Types/HTTPClient.swift new file mode 100644 index 0000000000..eee5080e16 --- /dev/null +++ b/SessionNetworkingKit/SessionNetwork/Types/HTTPClient.swift @@ -0,0 +1,104 @@ +// Copyright © 2024 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +import Foundation +import Combine +import GRDB +import SessionUtilitiesKit + +// MARK: - Log.Category + +public extension Log.Category { + static let sessionNetwork: Log.Category = .create("SessionNetwork", defaultLevel: .info) +} + +public extension Network.SessionNetwork { + final class HTTPClient { + private var cancellable: AnyCancellable? + private var dependencies: Dependencies? + + public func initialize(using dependencies: Dependencies) { + self.dependencies = dependencies + cancellable = getInfo(using: dependencies) + .subscribe(on: Network.SessionNetwork.workQueue, using: dependencies) + .receive(on: Network.SessionNetwork.workQueue) + .sink(receiveCompletion: { _ in }, receiveValue: { _ in }) + } + + public func getInfo(using dependencies: Dependencies) -> AnyPublisher { + cancellable?.cancel() + + let staleTimestampMs: Int64 = dependencies[singleton: .storage].read { db in db[.staleTimestampMs] }.defaulting(to: 0) + guard staleTimestampMs < dependencies[cache: .snodeAPI].currentOffsetTimestampMs() else { + return Just(()) + .delay(for: .milliseconds(500), scheduler: Network.SessionNetwork.workQueue) + .setFailureType(to: Error.self) + .flatMapStorageWritePublisher(using: dependencies) { [dependencies] db, info -> Bool in + db[.lastUpdatedTimestampMs] = dependencies[cache: .snodeAPI].currentOffsetTimestampMs() + return true + } + .eraseToAnyPublisher() + } + + return Result { + try Network.SessionNetwork + .prepareInfo(using: dependencies) + } + .publisher + .flatMap { [dependencies] in $0.send(using: dependencies) } + .map { _, info in info } + .flatMapStorageWritePublisher(using: dependencies) { [dependencies] db, info -> Bool in + // Token info + db[.lastUpdatedTimestampMs] = dependencies[cache: .snodeAPI].currentOffsetTimestampMs() + db[.tokenUsd] = info.price?.tokenUsd + db[.marketCapUsd] = info.price?.marketCapUsd + if let priceTimestamp = info.price?.priceTimestamp { + db[.priceTimestampMs] = priceTimestamp * 1000 + } else { + db[.priceTimestampMs] = nil + } + if let staleTimestamp = info.price?.staleTimestamp { + db[.staleTimestampMs] = staleTimestamp * 1000 + } else { + db[.staleTimestampMs] = nil + } + db[.stakingRequirement] = info.token?.stakingRequirement + db[.stakingRewardPool] = info.token?.stakingRewardPool + db[.contractAddress] = info.token?.contractAddress + // Network info + db[.networkSize] = info.network?.networkSize + db[.networkStakedTokens] = info.network?.networkStakedTokens + db[.networkStakedUSD] = info.network?.networkStakedUSD + + return true + } + .catch { error -> AnyPublisher in + Log.error(.sessionNetwork, "Failed to fetch token info due to error: \(error).") + return self.cleanUpSessionNetworkPageData(using: dependencies) + .map { _ in false } + .eraseToAnyPublisher() + + } + .eraseToAnyPublisher() + } + + private func cleanUpSessionNetworkPageData(using dependencies: Dependencies) -> AnyPublisher { + dependencies[singleton: .storage].writePublisher { db in + // Token info + db[.lastUpdatedTimestampMs] = nil + db[.tokenUsd] = nil + db[.marketCapUsd] = nil + db[.priceTimestampMs] = nil + db[.staleTimestampMs] = nil + db[.stakingRequirement] = nil + db[.stakingRewardPool] = nil + db[.contractAddress] = nil + // Network info + db[.networkSize] = nil + db[.networkStakedTokens] = nil + db[.networkStakedUSD] = nil + } + } + } +} diff --git a/SessionSnodeKit/SessionNetworkAPI/HTTPHeader+SessionNetwork.swift b/SessionNetworkingKit/SessionNetwork/Types/HTTPHeader+SessionNetwork.swift similarity index 100% rename from SessionSnodeKit/SessionNetworkAPI/HTTPHeader+SessionNetwork.swift rename to SessionNetworkingKit/SessionNetwork/Types/HTTPHeader+SessionNetwork.swift diff --git a/SessionSnodeKit/SessionNetworkAPI/SessionNetworkAPI+Database.swift b/SessionNetworkingKit/SessionNetwork/Types/KeyValueStore+SessionNetwork.swift similarity index 100% rename from SessionSnodeKit/SessionNetworkAPI/SessionNetworkAPI+Database.swift rename to SessionNetworkingKit/SessionNetwork/Types/KeyValueStore+SessionNetwork.swift diff --git a/SessionSnodeKit/Database/Models/SnodeReceivedMessageInfo.swift b/SessionNetworkingKit/StorageServer/Database/SnodeReceivedMessageInfo.swift similarity index 98% rename from SessionSnodeKit/Database/Models/SnodeReceivedMessageInfo.swift rename to SessionNetworkingKit/StorageServer/Database/SnodeReceivedMessageInfo.swift index a54cbc083f..eb8857f776 100644 --- a/SessionSnodeKit/Database/Models/SnodeReceivedMessageInfo.swift +++ b/SessionNetworkingKit/StorageServer/Database/SnodeReceivedMessageInfo.swift @@ -55,7 +55,7 @@ public extension SnodeReceivedMessageInfo { init( snode: LibSession.Snode, swarmPublicKey: String, - namespace: SnodeAPI.Namespace, + namespace: Network.SnodeAPI.Namespace, hash: String, expirationDateMs: Int64? ) { @@ -75,7 +75,7 @@ public extension SnodeReceivedMessageInfo { static func fetchLastNotExpired( _ db: ObservingDatabase, for snode: LibSession.Snode, - namespace: SnodeAPI.Namespace, + namespace: Network.SnodeAPI.Namespace, swarmPublicKey: String, using dependencies: Dependencies ) throws -> SnodeReceivedMessageInfo? { diff --git a/SessionSnodeKit/Models/DeleteAllBeforeRequest.swift b/SessionNetworkingKit/StorageServer/Models/DeleteAllBeforeRequest.swift similarity index 89% rename from SessionSnodeKit/Models/DeleteAllBeforeRequest.swift rename to SessionNetworkingKit/StorageServer/Models/DeleteAllBeforeRequest.swift index 6e6e1e64af..bd720328f5 100644 --- a/SessionSnodeKit/Models/DeleteAllBeforeRequest.swift +++ b/SessionNetworkingKit/StorageServer/Models/DeleteAllBeforeRequest.swift @@ -5,22 +5,22 @@ import Foundation import SessionUtilitiesKit -extension SnodeAPI { - public final class DeleteAllBeforeRequest: SnodeAuthenticatedRequestBody, UpdatableTimestamp { +extension Network.SnodeAPI { + final class DeleteAllBeforeRequest: SnodeAuthenticatedRequestBody, UpdatableTimestamp { enum CodingKeys: String, CodingKey { case beforeMs = "before" case namespace } let beforeMs: UInt64 - let namespace: SnodeAPI.Namespace? + let namespace: Network.SnodeAPI.Namespace? override var verificationBytes: [UInt8] { /// Ed25519 signature of `("delete_before" || namespace || before)`, signed by /// `pubkey`. Must be base64 encoded (json) or bytes (OMQ). `namespace` is the stringified /// version of the given non-default namespace parameter (i.e. "-42" or "all"), or the empty /// string for the default namespace (whether explicitly given or not). - SnodeAPI.Endpoint.deleteAllBefore.path.bytes + Network.SnodeAPI.Endpoint.deleteAllBefore.path.bytes .appending( contentsOf: (namespace == nil ? "all" : @@ -34,7 +34,7 @@ extension SnodeAPI { public init( beforeMs: UInt64, - namespace: SnodeAPI.Namespace?, + namespace: Network.SnodeAPI.Namespace?, authMethod: AuthenticationMethod, timestampMs: UInt64 ) { diff --git a/SessionSnodeKit/Models/DeleteAllBeforeResponse.swift b/SessionNetworkingKit/StorageServer/Models/DeleteAllBeforeResponse.swift similarity index 100% rename from SessionSnodeKit/Models/DeleteAllBeforeResponse.swift rename to SessionNetworkingKit/StorageServer/Models/DeleteAllBeforeResponse.swift diff --git a/SessionSnodeKit/Models/DeleteAllMessagesRequest.swift b/SessionNetworkingKit/StorageServer/Models/DeleteAllMessagesRequest.swift similarity index 90% rename from SessionSnodeKit/Models/DeleteAllMessagesRequest.swift rename to SessionNetworkingKit/StorageServer/Models/DeleteAllMessagesRequest.swift index 57f0c28f2b..3dc0d5bdbe 100644 --- a/SessionSnodeKit/Models/DeleteAllMessagesRequest.swift +++ b/SessionNetworkingKit/StorageServer/Models/DeleteAllMessagesRequest.swift @@ -3,8 +3,8 @@ import Foundation import SessionUtilitiesKit -extension SnodeAPI { - public final class DeleteAllMessagesRequest: SnodeAuthenticatedRequestBody, UpdatableTimestamp { +extension Network.SnodeAPI { + final class DeleteAllMessagesRequest: SnodeAuthenticatedRequestBody, UpdatableTimestamp { enum CodingKeys: String, CodingKey { case namespace } @@ -14,7 +14,7 @@ extension SnodeAPI { /// /// **Note:** If omitted when sending the request, messages are deleted from the default namespace /// only (namespace 0) - let namespace: SnodeAPI.Namespace + let namespace: Network.SnodeAPI.Namespace override var verificationBytes: [UInt8] { /// Ed25519 signature of `( "delete_all" || namespace || timestamp )`, where @@ -22,7 +22,7 @@ extension SnodeAPI { /// not), and otherwise the stringified version of the namespace parameter (i.e. "99" or "-42" or "all"). /// The signature must be signed by the ed25519 pubkey in `pubkey` (omitting the leading prefix). /// Must be base64 encoded for json requests; binary for OMQ requests. - SnodeAPI.Endpoint.deleteAll.path.bytes + Network.SnodeAPI.Endpoint.deleteAll.path.bytes .appending(contentsOf: namespace.verificationString.bytes) .appending(contentsOf: timestampMs.map { "\($0)" }?.data(using: .ascii)?.bytes) } @@ -30,7 +30,7 @@ extension SnodeAPI { // MARK: - Init public init( - namespace: SnodeAPI.Namespace, + namespace: Network.SnodeAPI.Namespace, authMethod: AuthenticationMethod, timestampMs: UInt64 ) { diff --git a/SessionSnodeKit/Models/DeleteAllMessagesResponse.swift b/SessionNetworkingKit/StorageServer/Models/DeleteAllMessagesResponse.swift similarity index 100% rename from SessionSnodeKit/Models/DeleteAllMessagesResponse.swift rename to SessionNetworkingKit/StorageServer/Models/DeleteAllMessagesResponse.swift diff --git a/SessionSnodeKit/Models/DeleteMessagesRequest.swift b/SessionNetworkingKit/StorageServer/Models/DeleteMessagesRequest.swift similarity index 91% rename from SessionSnodeKit/Models/DeleteMessagesRequest.swift rename to SessionNetworkingKit/StorageServer/Models/DeleteMessagesRequest.swift index 4adc127240..c1736499ac 100644 --- a/SessionSnodeKit/Models/DeleteMessagesRequest.swift +++ b/SessionNetworkingKit/StorageServer/Models/DeleteMessagesRequest.swift @@ -3,8 +3,8 @@ import Foundation import SessionUtilitiesKit -extension SnodeAPI { - public class DeleteMessagesRequest: SnodeAuthenticatedRequestBody { +extension Network.SnodeAPI { + class DeleteMessagesRequest: SnodeAuthenticatedRequestBody { enum CodingKeys: String, CodingKey { case messageHashes = "messages" case requireSuccessfulDeletion = "required" @@ -17,7 +17,7 @@ extension SnodeAPI { /// Ed25519 signature of `("delete" || messages...)`; this signs the value constructed /// by concatenating "delete" and all `messages` values, using `pubkey` to sign. Must be base64 /// encoded for json requests; binary for OMQ requests. - SnodeAPI.Endpoint.deleteMessages.path.bytes + Network.SnodeAPI.Endpoint.deleteMessages.path.bytes .appending(contentsOf: messageHashes.joined().bytes) } diff --git a/SessionSnodeKit/Models/DeleteMessagesResponse.swift b/SessionNetworkingKit/StorageServer/Models/DeleteMessagesResponse.swift similarity index 100% rename from SessionSnodeKit/Models/DeleteMessagesResponse.swift rename to SessionNetworkingKit/StorageServer/Models/DeleteMessagesResponse.swift diff --git a/SessionSnodeKit/Models/GetExpiriesRequest.swift b/SessionNetworkingKit/StorageServer/Models/GetExpiriesRequest.swift similarity index 91% rename from SessionSnodeKit/Models/GetExpiriesRequest.swift rename to SessionNetworkingKit/StorageServer/Models/GetExpiriesRequest.swift index 091729fb3b..d1f85ebf2b 100644 --- a/SessionSnodeKit/Models/GetExpiriesRequest.swift +++ b/SessionNetworkingKit/StorageServer/Models/GetExpiriesRequest.swift @@ -3,8 +3,8 @@ import Foundation import SessionUtilitiesKit -extension SnodeAPI { - public class GetExpiriesRequest: SnodeAuthenticatedRequestBody { +extension Network.SnodeAPI { + class GetExpiriesRequest: SnodeAuthenticatedRequestBody { enum CodingKeys: String, CodingKey { case messageHashes = "messages" } @@ -16,7 +16,7 @@ extension SnodeAPI { override var verificationBytes: [UInt8] { /// Ed25519 signature of `("get_expiries" || timestamp || messages[0] || ... || messages[N])` /// where `timestamp` is expressed as a string (base10). The signature must be base64 encoded (json) or bytes (bt). - SnodeAPI.Endpoint.getExpiries.path.bytes + Network.SnodeAPI.Endpoint.getExpiries.path.bytes .appending(contentsOf: timestampMs.map { "\($0)" }?.data(using: .ascii)?.bytes) .appending(contentsOf: messageHashes.joined().bytes) } diff --git a/SessionSnodeKit/Models/GetExpiriesResponse.swift b/SessionNetworkingKit/StorageServer/Models/GetExpiriesResponse.swift similarity index 100% rename from SessionSnodeKit/Models/GetExpiriesResponse.swift rename to SessionNetworkingKit/StorageServer/Models/GetExpiriesResponse.swift diff --git a/SessionSnodeKit/Models/GetMessagesRequest.swift b/SessionNetworkingKit/StorageServer/Models/GetMessagesRequest.swift similarity index 89% rename from SessionSnodeKit/Models/GetMessagesRequest.swift rename to SessionNetworkingKit/StorageServer/Models/GetMessagesRequest.swift index 3b7e7ca05b..c0c3f0ef7b 100644 --- a/SessionSnodeKit/Models/GetMessagesRequest.swift +++ b/SessionNetworkingKit/StorageServer/Models/GetMessagesRequest.swift @@ -3,8 +3,8 @@ import Foundation import SessionUtilitiesKit -extension SnodeAPI { - public class GetMessagesRequest: SnodeAuthenticatedRequestBody { +extension Network.SnodeAPI { + class GetMessagesRequest: SnodeAuthenticatedRequestBody { enum CodingKeys: String, CodingKey { case lastHash = "last_hash" case namespace @@ -13,7 +13,7 @@ extension SnodeAPI { } let lastHash: String - let namespace: SnodeAPI.Namespace? + let namespace: Network.SnodeAPI.Namespace? let maxCount: Int64? let maxSize: Int64? @@ -22,7 +22,7 @@ extension SnodeAPI { /// namespace), or `("retrieve" || timestamp)` when fetching from the default namespace. Both /// namespace and timestamp are the base10 expressions of the relevant values. Must be base64 /// encoded for json requests; binary for OMQ requests. - SnodeAPI.Endpoint.getMessages.path.bytes + Network.SnodeAPI.Endpoint.getMessages.path.bytes .appending(contentsOf: namespace?.verificationString.bytes) .appending(contentsOf: timestampMs.map { "\($0)" }?.data(using: .ascii)?.bytes) } @@ -31,7 +31,7 @@ extension SnodeAPI { public init( lastHash: String, - namespace: SnodeAPI.Namespace?, + namespace: Network.SnodeAPI.Namespace?, authMethod: AuthenticationMethod, timestampMs: UInt64, maxCount: Int64? = nil, diff --git a/SessionSnodeKit/Models/GetMessagesResponse.swift b/SessionNetworkingKit/StorageServer/Models/GetMessagesResponse.swift similarity index 100% rename from SessionSnodeKit/Models/GetMessagesResponse.swift rename to SessionNetworkingKit/StorageServer/Models/GetMessagesResponse.swift diff --git a/SessionSnodeKit/Models/GetNetworkTimestampResponse.swift b/SessionNetworkingKit/StorageServer/Models/GetNetworkTimestampResponse.swift similarity index 75% rename from SessionSnodeKit/Models/GetNetworkTimestampResponse.swift rename to SessionNetworkingKit/StorageServer/Models/GetNetworkTimestampResponse.swift index 71428bab9d..d29488ffc2 100644 --- a/SessionSnodeKit/Models/GetNetworkTimestampResponse.swift +++ b/SessionNetworkingKit/StorageServer/Models/GetNetworkTimestampResponse.swift @@ -2,8 +2,8 @@ import Foundation -extension SnodeAPI { - public struct GetNetworkTimestampResponse: Decodable { +public extension Network.SnodeAPI { + struct GetNetworkTimestampResponse: Decodable { enum CodingKeys: String, CodingKey { case timestamp case version diff --git a/SessionSnodeKit/Models/LegacyGetMessagesRequest.swift b/SessionNetworkingKit/StorageServer/Models/LegacyGetMessagesRequest.swift similarity index 89% rename from SessionSnodeKit/Models/LegacyGetMessagesRequest.swift rename to SessionNetworkingKit/StorageServer/Models/LegacyGetMessagesRequest.swift index 70dc7aa3a8..ab008a94bb 100644 --- a/SessionSnodeKit/Models/LegacyGetMessagesRequest.swift +++ b/SessionNetworkingKit/StorageServer/Models/LegacyGetMessagesRequest.swift @@ -2,9 +2,9 @@ import Foundation -extension SnodeAPI { +extension Network.SnodeAPI { /// This is the legacy unauthenticated message retrieval request - public struct LegacyGetMessagesRequest: Encodable { + struct LegacyGetMessagesRequest: Encodable { enum CodingKeys: String, CodingKey { case pubkey case lastHash = "last_hash" @@ -15,7 +15,7 @@ extension SnodeAPI { let pubkey: String let lastHash: String - let namespace: SnodeAPI.Namespace? + let namespace: Network.SnodeAPI.Namespace? let maxCount: Int64? let maxSize: Int64? diff --git a/SessionSnodeKit/Models/LegacySendMessageRequest.swift b/SessionNetworkingKit/StorageServer/Models/LegacySendMessageRequest.swift similarity index 82% rename from SessionSnodeKit/Models/LegacySendMessageRequest.swift rename to SessionNetworkingKit/StorageServer/Models/LegacySendMessageRequest.swift index 08cfe72ef6..a9c1000119 100644 --- a/SessionSnodeKit/Models/LegacySendMessageRequest.swift +++ b/SessionNetworkingKit/StorageServer/Models/LegacySendMessageRequest.swift @@ -2,15 +2,15 @@ import Foundation -extension SnodeAPI { +extension Network.SnodeAPI { /// This is the legacy unauthenticated message store request - public struct LegacySendMessagesRequest: Encodable { + struct LegacySendMessagesRequest: Encodable { enum CodingKeys: String, CodingKey { case namespace } let message: SnodeMessage - let namespace: SnodeAPI.Namespace + let namespace: Network.SnodeAPI.Namespace // MARK: - Coding diff --git a/SessionSnodeKit/Models/ONSResolveRequest.swift b/SessionNetworkingKit/StorageServer/Models/ONSResolveRequest.swift similarity index 80% rename from SessionSnodeKit/Models/ONSResolveRequest.swift rename to SessionNetworkingKit/StorageServer/Models/ONSResolveRequest.swift index eaef290853..2e0534cf18 100644 --- a/SessionSnodeKit/Models/ONSResolveRequest.swift +++ b/SessionNetworkingKit/StorageServer/Models/ONSResolveRequest.swift @@ -2,8 +2,8 @@ import Foundation -extension SnodeAPI { - public struct ONSResolveRequest: Encodable { +extension Network.SnodeAPI { + struct ONSResolveRequest: Encodable { enum CodingKeys: String, CodingKey { case type case base64EncodedNameHash = "name_hash" diff --git a/SessionSnodeKit/Models/ONSResolveResponse.swift b/SessionNetworkingKit/StorageServer/Models/ONSResolveResponse.swift similarity index 93% rename from SessionSnodeKit/Models/ONSResolveResponse.swift rename to SessionNetworkingKit/StorageServer/Models/ONSResolveResponse.swift index 8ca850a123..527efed87a 100644 --- a/SessionSnodeKit/Models/ONSResolveResponse.swift +++ b/SessionNetworkingKit/StorageServer/Models/ONSResolveResponse.swift @@ -3,8 +3,8 @@ import Foundation import SessionUtilitiesKit -extension SnodeAPI { - public class ONSResolveResponse: SnodeResponse { +public extension Network.SnodeAPI { + class ONSResolveResponse: SnodeResponse { internal struct Result: Codable { enum CodingKeys: String, CodingKey { case nonce diff --git a/SessionNetworkingKit/StorageServer/Models/OxenDaemonRPCRequest.swift b/SessionNetworkingKit/StorageServer/Models/OxenDaemonRPCRequest.swift new file mode 100644 index 0000000000..93ff3933a2 --- /dev/null +++ b/SessionNetworkingKit/StorageServer/Models/OxenDaemonRPCRequest.swift @@ -0,0 +1,23 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension Network.SnodeAPI { + struct OxenDaemonRPCRequest: Encodable { + private enum CodingKeys: String, CodingKey { + case endpoint + case body = "params" + } + + private let endpoint: String + private let body: T + + public init( + endpoint: Network.SnodeAPI.Endpoint, + body: T + ) { + self.endpoint = endpoint.path + self.body = body + } + } +} diff --git a/SessionSnodeKit/Models/RevokeSubaccountRequest.swift b/SessionNetworkingKit/StorageServer/Models/RevokeSubaccountRequest.swift similarity index 91% rename from SessionSnodeKit/Models/RevokeSubaccountRequest.swift rename to SessionNetworkingKit/StorageServer/Models/RevokeSubaccountRequest.swift index a7f1690e1a..7495a14258 100644 --- a/SessionSnodeKit/Models/RevokeSubaccountRequest.swift +++ b/SessionNetworkingKit/StorageServer/Models/RevokeSubaccountRequest.swift @@ -3,8 +3,8 @@ import Foundation import SessionUtilitiesKit -extension SnodeAPI { - public class RevokeSubaccountRequest: SnodeAuthenticatedRequestBody { +extension Network.SnodeAPI { + class RevokeSubaccountRequest: SnodeAuthenticatedRequestBody { enum CodingKeys: String, CodingKey { case subaccountsToRevoke = "revoke" } @@ -14,7 +14,7 @@ extension SnodeAPI { override var verificationBytes: [UInt8] { /// Ed25519 signature of `("revoke_subaccount" || timestamp || SUBACCOUNT_TAG_BYTES...)`; this /// signs the subaccount token, using `pubkey` to sign. Must be base64 encoded for json requests; binary for OMQ requests. - SnodeAPI.Endpoint.revokeSubaccount.path.bytes + Network.SnodeAPI.Endpoint.revokeSubaccount.path.bytes .appending(contentsOf: timestampMs.map { "\($0)" }?.data(using: .ascii)?.bytes) .appending(contentsOf: Array(subaccountsToRevoke.joined())) } diff --git a/SessionSnodeKit/Models/RevokeSubaccountResponse.swift b/SessionNetworkingKit/StorageServer/Models/RevokeSubaccountResponse.swift similarity index 100% rename from SessionSnodeKit/Models/RevokeSubaccountResponse.swift rename to SessionNetworkingKit/StorageServer/Models/RevokeSubaccountResponse.swift diff --git a/SessionSnodeKit/Models/SendMessageRequest.swift b/SessionNetworkingKit/StorageServer/Models/SendMessageRequest.swift similarity index 89% rename from SessionSnodeKit/Models/SendMessageRequest.swift rename to SessionNetworkingKit/StorageServer/Models/SendMessageRequest.swift index b97ae8d672..69063606ac 100644 --- a/SessionSnodeKit/Models/SendMessageRequest.swift +++ b/SessionNetworkingKit/StorageServer/Models/SendMessageRequest.swift @@ -3,14 +3,14 @@ import Foundation import SessionUtilitiesKit -extension SnodeAPI { - public class SendMessageRequest: SnodeAuthenticatedRequestBody { +extension Network.SnodeAPI { + class SendMessageRequest: SnodeAuthenticatedRequestBody { enum CodingKeys: String, CodingKey { case namespace } let message: SnodeMessage - let namespace: SnodeAPI.Namespace + let namespace: Network.SnodeAPI.Namespace override var verificationBytes: [UInt8] { /// Ed25519 signature of `("store" || namespace || timestamp)`, where namespace and @@ -18,7 +18,7 @@ extension SnodeAPI { /// base64 encoded for json requests; binary for OMQ requests. For non-05 type pubkeys (i.e. non /// session ids) the signature will be verified using `pubkey`. For 05 pubkeys, see the following /// option. - SnodeAPI.Endpoint.sendMessage.path.bytes + Network.SnodeAPI.Endpoint.sendMessage.path.bytes .appending(contentsOf: namespace.verificationString.bytes) .appending(contentsOf: timestampMs.map { "\($0)" }?.data(using: .ascii)?.bytes) } @@ -27,7 +27,7 @@ extension SnodeAPI { public init( message: SnodeMessage, - namespace: SnodeAPI.Namespace, + namespace: Network.SnodeAPI.Namespace, authMethod: AuthenticationMethod, timestampMs: UInt64 ) { diff --git a/SessionSnodeKit/Models/SendMessageResponse.swift b/SessionNetworkingKit/StorageServer/Models/SendMessageResponse.swift similarity index 100% rename from SessionSnodeKit/Models/SendMessageResponse.swift rename to SessionNetworkingKit/StorageServer/Models/SendMessageResponse.swift diff --git a/SessionSnodeKit/Models/SnodeAuthenticatedRequestBody.swift b/SessionNetworkingKit/StorageServer/Models/SnodeAuthenticatedRequestBody.swift similarity index 97% rename from SessionSnodeKit/Models/SnodeAuthenticatedRequestBody.swift rename to SessionNetworkingKit/StorageServer/Models/SnodeAuthenticatedRequestBody.swift index 4bcaa17c42..307e612059 100644 --- a/SessionSnodeKit/Models/SnodeAuthenticatedRequestBody.swift +++ b/SessionNetworkingKit/StorageServer/Models/SnodeAuthenticatedRequestBody.swift @@ -3,7 +3,7 @@ import Foundation import SessionUtilitiesKit -public class SnodeAuthenticatedRequestBody: Encodable { +class SnodeAuthenticatedRequestBody: Encodable { private enum CodingKeys: String, CodingKey { case pubkey case subaccount diff --git a/SessionSnodeKit/Models/SnodeBatchRequest.swift b/SessionNetworkingKit/StorageServer/Models/SnodeBatchRequest.swift similarity index 96% rename from SessionSnodeKit/Models/SnodeBatchRequest.swift rename to SessionNetworkingKit/StorageServer/Models/SnodeBatchRequest.swift index e6a05c0c19..083dec184b 100644 --- a/SessionSnodeKit/Models/SnodeBatchRequest.swift +++ b/SessionNetworkingKit/StorageServer/Models/SnodeBatchRequest.swift @@ -3,7 +3,7 @@ import Foundation import SessionUtilitiesKit -internal extension SnodeAPI { +extension Network.SnodeAPI { struct BatchRequest: Encodable { let requests: [Child] @@ -38,7 +38,7 @@ internal extension SnodeAPI { case params } - let endpoint: SnodeAPI.Endpoint + let endpoint: Network.SnodeAPI.Endpoint /// The `jsonBodyEncoder` is used to avoid having to make `BatchSubRequest` a generic type (haven't found /// a good way to keep `BatchSubRequest` encodable using protocols unfortunately so need this work around) diff --git a/SessionSnodeKit/Models/SnodeRecursiveResponse.swift b/SessionNetworkingKit/StorageServer/Models/SnodeRecursiveResponse.swift similarity index 100% rename from SessionSnodeKit/Models/SnodeRecursiveResponse.swift rename to SessionNetworkingKit/StorageServer/Models/SnodeRecursiveResponse.swift diff --git a/SessionSnodeKit/Models/SnodeRequest.swift b/SessionNetworkingKit/StorageServer/Models/SnodeRequest.swift similarity index 92% rename from SessionSnodeKit/Models/SnodeRequest.swift rename to SessionNetworkingKit/StorageServer/Models/SnodeRequest.swift index ab23b427b3..c88d159c8f 100644 --- a/SessionSnodeKit/Models/SnodeRequest.swift +++ b/SessionNetworkingKit/StorageServer/Models/SnodeRequest.swift @@ -9,13 +9,13 @@ public struct SnodeRequest: Encodable { case body = "params" } - internal let endpoint: SnodeAPI.Endpoint + internal let endpoint: Network.SnodeAPI.Endpoint internal let body: T // MARK: - Initialization public init( - endpoint: SnodeAPI.Endpoint, + endpoint: Network.SnodeAPI.Endpoint, body: T ) { self.endpoint = endpoint diff --git a/SessionSnodeKit/Models/SnodeResponse.swift b/SessionNetworkingKit/StorageServer/Models/SnodeResponse.swift similarity index 100% rename from SessionSnodeKit/Models/SnodeResponse.swift rename to SessionNetworkingKit/StorageServer/Models/SnodeResponse.swift diff --git a/SessionSnodeKit/Models/SnodeSwarmItem.swift b/SessionNetworkingKit/StorageServer/Models/SnodeSwarmItem.swift similarity index 100% rename from SessionSnodeKit/Models/SnodeSwarmItem.swift rename to SessionNetworkingKit/StorageServer/Models/SnodeSwarmItem.swift diff --git a/SessionSnodeKit/Models/UnrevokeSubaccountRequest.swift b/SessionNetworkingKit/StorageServer/Models/UnrevokeSubaccountRequest.swift similarity index 91% rename from SessionSnodeKit/Models/UnrevokeSubaccountRequest.swift rename to SessionNetworkingKit/StorageServer/Models/UnrevokeSubaccountRequest.swift index 63fb0f6b40..3f8cb6f30d 100644 --- a/SessionSnodeKit/Models/UnrevokeSubaccountRequest.swift +++ b/SessionNetworkingKit/StorageServer/Models/UnrevokeSubaccountRequest.swift @@ -3,8 +3,8 @@ import Foundation import SessionUtilitiesKit -extension SnodeAPI { - public class UnrevokeSubaccountRequest: SnodeAuthenticatedRequestBody { +extension Network.SnodeAPI { + class UnrevokeSubaccountRequest: SnodeAuthenticatedRequestBody { enum CodingKeys: String, CodingKey { case subaccountsToUnrevoke = "unrevoke" } @@ -14,7 +14,7 @@ extension SnodeAPI { override var verificationBytes: [UInt8] { /// Ed25519 signature of `("unrevoke_subaccount" || timestamp || subaccount)`; this signs /// the subaccount token, using `pubkey` to sign. Must be base64 encoded for json requests; binary for OMQ requests. - SnodeAPI.Endpoint.unrevokeSubaccount.path.bytes + Network.SnodeAPI.Endpoint.unrevokeSubaccount.path.bytes .appending(contentsOf: timestampMs.map { "\($0)" }?.data(using: .ascii)?.bytes) .appending(contentsOf: Array(subaccountsToUnrevoke.joined())) } diff --git a/SessionSnodeKit/Models/UnrevokeSubaccountResponse.swift b/SessionNetworkingKit/StorageServer/Models/UnrevokeSubaccountResponse.swift similarity index 100% rename from SessionSnodeKit/Models/UnrevokeSubaccountResponse.swift rename to SessionNetworkingKit/StorageServer/Models/UnrevokeSubaccountResponse.swift diff --git a/SessionSnodeKit/Models/UpdateExpiryAllRequest.swift b/SessionNetworkingKit/StorageServer/Models/UpdateExpiryAllRequest.swift similarity index 90% rename from SessionSnodeKit/Models/UpdateExpiryAllRequest.swift rename to SessionNetworkingKit/StorageServer/Models/UpdateExpiryAllRequest.swift index 184e767764..dbc4fbf3ff 100644 --- a/SessionSnodeKit/Models/UpdateExpiryAllRequest.swift +++ b/SessionNetworkingKit/StorageServer/Models/UpdateExpiryAllRequest.swift @@ -5,8 +5,8 @@ import Foundation import SessionUtilitiesKit -extension SnodeAPI { - public class UpdateExpiryAllRequest: SnodeAuthenticatedRequestBody { +extension Network.SnodeAPI { + class UpdateExpiryAllRequest: SnodeAuthenticatedRequestBody { enum CodingKeys: String, CodingKey { case expiryMs = "expiry" case namespace @@ -19,14 +19,14 @@ extension SnodeAPI { /// /// **Note:** If omitted when sending the request, message expiries are updated from the default namespace /// only (namespace 0) - let namespace: SnodeAPI.Namespace? + let namespace: Network.SnodeAPI.Namespace? override var verificationBytes: [UInt8] { /// Ed25519 signature of `("expire_all" || namespace || expiry)`, signed by `pubkey`. Must be /// base64 encoded (json) or bytes (OMQ). namespace should be the stringified namespace for /// non-default namespace expiries (i.e. "42", "-99", "all"), or an empty string for the default /// namespace (whether or not explicitly provided). - SnodeAPI.Endpoint.expireAll.path.bytes + Network.SnodeAPI.Endpoint.expireAll.path.bytes .appending( contentsOf: (namespace == nil ? "all" : @@ -40,7 +40,7 @@ extension SnodeAPI { public init( expiryMs: UInt64, - namespace: SnodeAPI.Namespace?, + namespace: Network.SnodeAPI.Namespace?, authMethod: AuthenticationMethod ) { self.expiryMs = expiryMs diff --git a/SessionSnodeKit/Models/UpdateExpiryAllResponse.swift b/SessionNetworkingKit/StorageServer/Models/UpdateExpiryAllResponse.swift similarity index 100% rename from SessionSnodeKit/Models/UpdateExpiryAllResponse.swift rename to SessionNetworkingKit/StorageServer/Models/UpdateExpiryAllResponse.swift diff --git a/SessionSnodeKit/Models/UpdateExpiryRequest.swift b/SessionNetworkingKit/StorageServer/Models/UpdateExpiryRequest.swift similarity index 95% rename from SessionSnodeKit/Models/UpdateExpiryRequest.swift rename to SessionNetworkingKit/StorageServer/Models/UpdateExpiryRequest.swift index ba3546abac..6e237557d0 100644 --- a/SessionSnodeKit/Models/UpdateExpiryRequest.swift +++ b/SessionNetworkingKit/StorageServer/Models/UpdateExpiryRequest.swift @@ -5,8 +5,8 @@ import Foundation import SessionUtilitiesKit -extension SnodeAPI { - public class UpdateExpiryRequest: SnodeAuthenticatedRequestBody { +extension Network.SnodeAPI { + class UpdateExpiryRequest: SnodeAuthenticatedRequestBody { enum CodingKeys: String, CodingKey { case messageHashes = "messages" case expiryMs = "expiry" @@ -39,7 +39,7 @@ extension SnodeAPI { /// ` || messages[N])` where `expiry` is the expiry timestamp expressed as a string. /// `ShortenOrExtend` is string signature must be base64 "shorten" if the shorten option is given (and true), /// "extend" if `extend` is true, and empty otherwise. The signature must be base64 encoded (json) or bytes (bt). - SnodeAPI.Endpoint.expire.path.bytes + Network.SnodeAPI.Endpoint.expire.path.bytes .appending(contentsOf: (shorten == true ? "shorten".bytes : [])) .appending(contentsOf: (extend == true ? "extend".bytes : [])) .appending(contentsOf: "\(expiryMs)".data(using: .ascii)?.bytes) diff --git a/SessionSnodeKit/Models/UpdateExpiryResponse.swift b/SessionNetworkingKit/StorageServer/Models/UpdateExpiryResponse.swift similarity index 100% rename from SessionSnodeKit/Models/UpdateExpiryResponse.swift rename to SessionNetworkingKit/StorageServer/Models/UpdateExpiryResponse.swift diff --git a/SessionNetworkingKit/StorageServer/SnodeAPI.swift b/SessionNetworkingKit/StorageServer/SnodeAPI.swift new file mode 100644 index 0000000000..68e7357e4a --- /dev/null +++ b/SessionNetworkingKit/StorageServer/SnodeAPI.swift @@ -0,0 +1,859 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +import Foundation +import Combine +import GRDB +import Punycode +import SessionUtilitiesKit + +public extension Network { + enum SnodeAPI { + // MARK: - Settings + + public static let maxRetryCount: Int = 8 + + // MARK: - Batching & Polling + + public typealias PollResponse = [SnodeAPI.Namespace: (info: ResponseInfoType, data: PreparedGetMessagesResponse?)] + + public static func preparedPoll( + _ db: ObservingDatabase, + namespaces: [SnodeAPI.Namespace], + refreshingConfigHashes: [String] = [], + from snode: LibSession.Snode, + authMethod: AuthenticationMethod, + using dependencies: Dependencies + ) throws -> Network.PreparedRequest { + // Determine the maxSize each namespace in the request should take up + var requests: [any ErasedPreparedRequest] = [] + let namespaceMaxSizeMap: [SnodeAPI.Namespace: Int64] = SnodeAPI.Namespace.maxSizeMap(for: namespaces) + let fallbackSize: Int64 = (namespaceMaxSizeMap.values.min() ?? 1) + + // If we have any config hashes to refresh TTLs then add those requests first + if !refreshingConfigHashes.isEmpty { + let updatedExpiryMS: Int64 = ( + dependencies[cache: .snodeAPI].currentOffsetTimestampMs() + + (30 * 24 * 60 * 60 * 1000) // 30 days + ) + requests.append( + try SnodeAPI.preparedUpdateExpiry( + serverHashes: refreshingConfigHashes, + updatedExpiryMs: updatedExpiryMS, + extendOnly: true, + ignoreValidationFailure: true, + explicitTargetNode: snode, + authMethod: authMethod, + using: dependencies + ) + ) + } + + // Add the various 'getMessages' requests + requests.append( + contentsOf: try namespaces.map { namespace -> any ErasedPreparedRequest in + try SnodeAPI.preparedGetMessages( + db, + namespace: namespace, + snode: snode, + maxSize: namespaceMaxSizeMap[namespace] + .defaulting(to: fallbackSize), + authMethod: authMethod, + using: dependencies + ) + } + ) + + return try preparedBatch( + requests: requests, + requireAllBatchResponses: true, + snode: snode, + swarmPublicKey: try authMethod.swarmPublicKey, + using: dependencies + ) + .map { (_: ResponseInfoType, batchResponse: Network.BatchResponse) -> [SnodeAPI.Namespace: (info: ResponseInfoType, data: PreparedGetMessagesResponse?)] in + let messageResponses: [Network.BatchSubResponse] = batchResponse + .compactMap { $0 as? Network.BatchSubResponse } + + return zip(namespaces, messageResponses) + .reduce(into: [:]) { result, next in + guard let messageResponse: PreparedGetMessagesResponse = next.1.body else { return } + + result[next.0] = (next.1, messageResponse) + } + } + } + + public static func preparedBatch( + requests: [any ErasedPreparedRequest], + requireAllBatchResponses: Bool, + snode: LibSession.Snode? = nil, + swarmPublicKey: String, + requestTimeout: TimeInterval = Network.defaultTimeout, + requestAndPathBuildTimeout: TimeInterval? = nil, + using dependencies: Dependencies + ) throws -> Network.PreparedRequest { + return try SnodeAPI + .prepareRequest( + request: { + switch snode { + case .none: + return try Request( + endpoint: .batch, + swarmPublicKey: swarmPublicKey, + body: Network.BatchRequest(requestsKey: .requests, requests: requests) + ) + + case .some(let snode): + return try Request( + endpoint: .batch, + snode: snode, + swarmPublicKey: swarmPublicKey, + body: Network.BatchRequest(requestsKey: .requests, requests: requests) + ) + } + }(), + responseType: Network.BatchResponse.self, + requireAllBatchResponses: requireAllBatchResponses, + requestTimeout: requestTimeout, + requestAndPathBuildTimeout: requestAndPathBuildTimeout, + using: dependencies + ) + } + + public static func preparedSequence( + requests: [any ErasedPreparedRequest], + requireAllBatchResponses: Bool, + swarmPublicKey: String, + snodeRetrievalRetryCount: Int, + requestTimeout: TimeInterval = Network.defaultTimeout, + requestAndPathBuildTimeout: TimeInterval? = nil, + using dependencies: Dependencies + ) throws -> Network.PreparedRequest { + return try SnodeAPI + .prepareRequest( + request: Request( + endpoint: .sequence, + swarmPublicKey: swarmPublicKey, + body: Network.BatchRequest(requestsKey: .requests, requests: requests), + snodeRetrievalRetryCount: snodeRetrievalRetryCount + ), + responseType: Network.BatchResponse.self, + requireAllBatchResponses: requireAllBatchResponses, + requestTimeout: requestTimeout, + requestAndPathBuildTimeout: requestAndPathBuildTimeout, + using: dependencies + ) + } + + // MARK: - Retrieve + + public typealias PreparedGetMessagesResponse = (messages: [SnodeReceivedMessage], lastHash: String?) + + public static func preparedGetMessages( + _ db: ObservingDatabase, + namespace: SnodeAPI.Namespace, + snode: LibSession.Snode, + maxSize: Int64? = nil, + authMethod: AuthenticationMethod, + using dependencies: Dependencies + ) throws -> Network.PreparedRequest { + let maybeLastHash: String? = try SnodeReceivedMessageInfo + .fetchLastNotExpired( + db, + for: snode, + namespace: namespace, + swarmPublicKey: try authMethod.swarmPublicKey, + using: dependencies + )? + .hash + let preparedRequest: Network.PreparedRequest = try { + // Check if this namespace requires authentication + guard namespace.requiresReadAuthentication else { + return try SnodeAPI.prepareRequest( + request: Request( + endpoint: .getMessages, + swarmPublicKey: try authMethod.swarmPublicKey, + body: LegacyGetMessagesRequest( + pubkey: try authMethod.swarmPublicKey, + lastHash: (maybeLastHash ?? ""), + namespace: namespace, + maxCount: nil, + maxSize: maxSize + ) + ), + responseType: GetMessagesResponse.self, + using: dependencies + ) + } + + return try SnodeAPI.prepareRequest( + request: Request( + endpoint: .getMessages, + swarmPublicKey: try authMethod.swarmPublicKey, + body: GetMessagesRequest( + lastHash: (maybeLastHash ?? ""), + namespace: namespace, + authMethod: authMethod, + timestampMs: dependencies[cache: .snodeAPI].currentOffsetTimestampMs(), + maxSize: maxSize + ) + ), + responseType: GetMessagesResponse.self, + using: dependencies + ) + }() + + return preparedRequest + .tryMap { _, response -> (messages: [SnodeReceivedMessage], lastHash: String?) in + return ( + try response.messages.compactMap { rawMessage -> SnodeReceivedMessage? in + SnodeReceivedMessage( + snode: snode, + publicKey: try authMethod.swarmPublicKey, + namespace: namespace, + rawMessage: rawMessage + ) + }, + maybeLastHash + ) + } + } + + public static func getSessionID( + for onsName: String, + using dependencies: Dependencies + ) -> AnyPublisher { + let validationCount = 3 + + // The name must be lowercased + let onsName = onsName.lowercased().idnaEncoded ?? onsName.lowercased() + + // Hash the ONS name using BLAKE2b + guard + let nameHash = dependencies[singleton: .crypto].generate( + .hash(message: Array(onsName.utf8)) + ) + else { + return Fail(error: SnodeAPIError.onsHashingFailed) + .eraseToAnyPublisher() + } + + // Ask 3 different snodes for the Session ID associated with the given name hash + let base64EncodedNameHash = nameHash.toBase64() + + return dependencies[singleton: .network] + .getRandomNodes(count: validationCount) + .tryFlatMap { nodes in + Publishers.MergeMany( + try nodes.map { snode in + try SnodeAPI + .prepareRequest( + request: Request( + endpoint: .oxenDaemonRPCCall, + snode: snode, + body: OxenDaemonRPCRequest( + endpoint: .daemonOnsResolve, + body: ONSResolveRequest( + type: 0, // type 0 means Session + base64EncodedNameHash: base64EncodedNameHash + ) + ) + ), + responseType: ONSResolveResponse.self, + using: dependencies + ) + .tryMap { _, response -> String in + try dependencies[singleton: .crypto].tryGenerate( + .sessionId(name: onsName, response: response) + ) + } + .send(using: dependencies) + .map { _, sessionId in sessionId } + .eraseToAnyPublisher() + } + ) + } + .collect() + .tryMap { results -> String in + guard results.count == validationCount, Set(results).count == 1 else { + throw SnodeAPIError.onsValidationFailed + } + + return results[0] + } + .eraseToAnyPublisher() + } + + public static func preparedGetExpiries( + of serverHashes: [String], + authMethod: AuthenticationMethod, + using dependencies: Dependencies + ) throws -> Network.PreparedRequest { + return try SnodeAPI + .prepareRequest( + request: Request( + endpoint: .getExpiries, + swarmPublicKey: try authMethod.swarmPublicKey, + body: GetExpiriesRequest( + messageHashes: serverHashes, + authMethod: authMethod, + timestampMs: dependencies[cache: .snodeAPI].currentOffsetTimestampMs() + ) + ), + responseType: GetExpiriesResponse.self, + using: dependencies + ) + } + + // MARK: - Store + + public static func preparedSendMessage( + message: SnodeMessage, + in namespace: Namespace, + authMethod: AuthenticationMethod, + using dependencies: Dependencies + ) throws -> Network.PreparedRequest { + let request: Network.PreparedRequest = try { + // Check if this namespace requires authentication + guard namespace.requiresWriteAuthentication else { + return try SnodeAPI.prepareRequest( + request: Request( + endpoint: .sendMessage, + swarmPublicKey: try authMethod.swarmPublicKey, + body: LegacySendMessagesRequest( + message: message, + namespace: namespace + ), + snodeRetrievalRetryCount: 0 // The SendMessageJob already has a retry mechanism + ), + responseType: SendMessagesResponse.self, + requestAndPathBuildTimeout: Network.defaultTimeout, + using: dependencies + ) + } + + return try SnodeAPI.prepareRequest( + request: Request( + endpoint: .sendMessage, + swarmPublicKey: try authMethod.swarmPublicKey, + body: SendMessageRequest( + message: message, + namespace: namespace, + authMethod: authMethod, + timestampMs: dependencies[cache: .snodeAPI].currentOffsetTimestampMs() + ), + snodeRetrievalRetryCount: 0 // The SendMessageJob already has a retry mechanism + ), + responseType: SendMessagesResponse.self, + requestAndPathBuildTimeout: Network.defaultTimeout, + using: dependencies + ) + }() + + return request + .tryMap { _, response -> SendMessagesResponse in + try response.validateResultMap( + swarmPublicKey: try authMethod.swarmPublicKey, + using: dependencies + ) + + return response + } + } + + // MARK: - Edit + + public static func preparedUpdateExpiry( + serverHashes: [String], + updatedExpiryMs: Int64, + shortenOnly: Bool? = nil, + extendOnly: Bool? = nil, + ignoreValidationFailure: Bool = false, + explicitTargetNode: LibSession.Snode? = nil, + authMethod: AuthenticationMethod, + using dependencies: Dependencies + ) throws -> Network.PreparedRequest<[String: UpdateExpiryResponseResult]> { + // ShortenOnly and extendOnly cannot be true at the same time + guard shortenOnly == nil || extendOnly == nil else { throw NetworkError.invalidPreparedRequest } + + return try SnodeAPI + .prepareRequest( + request: Request( + endpoint: .expire, + swarmPublicKey: try authMethod.swarmPublicKey, + body: UpdateExpiryRequest( + messageHashes: serverHashes, + expiryMs: UInt64(updatedExpiryMs), + shorten: shortenOnly, + extend: extendOnly, + authMethod: authMethod + ) + ), + responseType: UpdateExpiryResponse.self, + using: dependencies + ) + .tryMap { _, response -> [String: UpdateExpiryResponseResult] in + do { + return try response.validResultMap( + swarmPublicKey: try authMethod.swarmPublicKey, + validationData: serverHashes, + using: dependencies + ) + } + catch { + guard ignoreValidationFailure else { throw error } + + return [:] + } + } + .handleEvents( + receiveOutput: { _, result in + /// Since we have updated the TTL we need to make sure we also update the local + /// `SnodeReceivedMessageInfo.expirationDateMs` values so they match the updated swarm, if + /// we had a specific `snode` we we're sending the request to then we should use those values, otherwise + /// we can just grab the first value from the response and use that + let maybeTargetResult: UpdateExpiryResponseResult? = { + guard let snode: LibSession.Snode = explicitTargetNode else { + return result.first?.value + } + + return result[snode.ed25519PubkeyHex] + }() + guard + let targetResult: UpdateExpiryResponseResult = maybeTargetResult, + let groupedExpiryResult: [UInt64: [String]] = targetResult.changed + .updated(with: targetResult.unchanged) + .groupedByValue() + .nullIfEmpty + else { return } + + dependencies[singleton: .storage].writeAsync { db in + try groupedExpiryResult.forEach { updatedExpiry, hashes in + try SnodeReceivedMessageInfo + .filter(hashes.contains(SnodeReceivedMessageInfo.Columns.hash)) + .updateAll( + db, + SnodeReceivedMessageInfo.Columns.expirationDateMs + .set(to: updatedExpiry) + ) + } + } + } + ) + } + + public static func preparedRevokeSubaccounts( + subaccountsToRevoke: [[UInt8]], + authMethod: AuthenticationMethod, + using dependencies: Dependencies + ) throws -> Network.PreparedRequest { + let timestampMs: UInt64 = dependencies[cache: .snodeAPI].currentOffsetTimestampMs() + + return try SnodeAPI + .prepareRequest( + request: Request( + endpoint: .revokeSubaccount, + swarmPublicKey: try authMethod.swarmPublicKey, + body: RevokeSubaccountRequest( + subaccountsToRevoke: subaccountsToRevoke, + authMethod: authMethod, + timestampMs: timestampMs + ) + ), + responseType: RevokeSubaccountResponse.self, + using: dependencies + ) + .tryMap { _, response -> Void in + try response.validateResultMap( + swarmPublicKey: try authMethod.swarmPublicKey, + validationData: (subaccountsToRevoke, timestampMs), + using: dependencies + ) + + return () + } + } + + public static func preparedUnrevokeSubaccounts( + subaccountsToUnrevoke: [[UInt8]], + authMethod: AuthenticationMethod, + using dependencies: Dependencies + ) throws -> Network.PreparedRequest { + let timestampMs: UInt64 = dependencies[cache: .snodeAPI].currentOffsetTimestampMs() + + return try SnodeAPI + .prepareRequest( + request: Request( + endpoint: .unrevokeSubaccount, + swarmPublicKey: try authMethod.swarmPublicKey, + body: UnrevokeSubaccountRequest( + subaccountsToUnrevoke: subaccountsToUnrevoke, + authMethod: authMethod, + timestampMs: timestampMs + ) + ), + responseType: UnrevokeSubaccountResponse.self, + using: dependencies + ) + .tryMap { _, response -> Void in + try response.validateResultMap( + swarmPublicKey: try authMethod.swarmPublicKey, + validationData: (subaccountsToUnrevoke, timestampMs), + using: dependencies + ) + + return () + } + } + + // MARK: - Delete + + public static func preparedDeleteMessages( + serverHashes: [String], + requireSuccessfulDeletion: Bool, + authMethod: AuthenticationMethod, + using dependencies: Dependencies + ) throws -> Network.PreparedRequest<[String: Bool]> { + return try SnodeAPI + .prepareRequest( + request: Request( + endpoint: .deleteMessages, + swarmPublicKey: try authMethod.swarmPublicKey, + body: DeleteMessagesRequest( + messageHashes: serverHashes, + requireSuccessfulDeletion: requireSuccessfulDeletion, + authMethod: authMethod + ) + ), + responseType: DeleteMessagesResponse.self, + using: dependencies + ) + .tryMap { _, response -> [String: Bool] in + let validResultMap: [String: Bool] = try response.validResultMap( + swarmPublicKey: try authMethod.swarmPublicKey, + validationData: serverHashes, + using: dependencies + ) + + // If `validResultMap` didn't throw then at least one service node + // deleted successfully so we should mark the hash as invalid so we + // don't try to fetch updates using that hash going forward (if we + // do we would end up re-fetching all old messages) + dependencies[singleton: .storage].writeAsync { db in + try? SnodeReceivedMessageInfo.handlePotentialDeletedOrInvalidHash( + db, + potentiallyInvalidHashes: serverHashes + ) + } + + return validResultMap + } + } + + + /// Clears all the user's data from their swarm. Returns a dictionary of snode public key to deletion confirmation. + public static func preparedDeleteAllMessages( + namespace: SnodeAPI.Namespace, + requestTimeout: TimeInterval = Network.defaultTimeout, + requestAndPathBuildTimeout: TimeInterval? = nil, + authMethod: AuthenticationMethod, + using dependencies: Dependencies + ) throws -> Network.PreparedRequest<[String: Bool]> { + return try SnodeAPI + .prepareRequest( + request: Request( + endpoint: .deleteAll, + swarmPublicKey: try authMethod.swarmPublicKey, + requiresLatestNetworkTime: true, + body: DeleteAllMessagesRequest( + namespace: namespace, + authMethod: authMethod, + timestampMs: dependencies[cache: .snodeAPI].currentOffsetTimestampMs() + ), + snodeRetrievalRetryCount: 0 + ), + responseType: DeleteAllMessagesResponse.self, + requestTimeout: requestTimeout, + requestAndPathBuildTimeout: requestAndPathBuildTimeout, + using: dependencies + ) + .tryMap { info, response -> [String: Bool] in + guard let targetInfo: LatestTimestampResponseInfo = info as? LatestTimestampResponseInfo else { + throw NetworkError.invalidResponse + } + + return try response.validResultMap( + swarmPublicKey: try authMethod.swarmPublicKey, + validationData: targetInfo.timestampMs, + using: dependencies + ) + } + } + + /// Clears all the user's data from their swarm. Returns a dictionary of snode public key to deletion confirmation. + public static func preparedDeleteAllMessages( + beforeMs: UInt64, + namespace: SnodeAPI.Namespace, + authMethod: AuthenticationMethod, + using dependencies: Dependencies + ) throws -> Network.PreparedRequest<[String: Bool]> { + return try SnodeAPI + .prepareRequest( + request: Request( + endpoint: .deleteAllBefore, + swarmPublicKey: try authMethod.swarmPublicKey, + requiresLatestNetworkTime: true, + body: DeleteAllBeforeRequest( + beforeMs: beforeMs, + namespace: namespace, + authMethod: authMethod, + timestampMs: dependencies[cache: .snodeAPI].currentOffsetTimestampMs() + ) + ), + responseType: DeleteAllMessagesResponse.self, + retryCount: maxRetryCount, + using: dependencies + ) + .tryMap { _, response -> [String: Bool] in + try response.validResultMap( + swarmPublicKey: try authMethod.swarmPublicKey, + validationData: beforeMs, + using: dependencies + ) + } + } + + // MARK: - Internal API + + public static func preparedGetNetworkTime( + from snode: LibSession.Snode, + using dependencies: Dependencies + ) throws -> Network.PreparedRequest { + return try SnodeAPI + .prepareRequest( + request: Request, Endpoint>( + endpoint: .getInfo, + snode: snode, + body: [:] + ), + responseType: GetNetworkTimestampResponse.self, + using: dependencies + ) + .map { _, response in + // Assume we've fetched the networkTime in order to send a message to the specified snode, in + // which case we want to update the 'clockOffsetMs' value for subsequent requests + let offset = (Int64(response.timestamp) - Int64(floor(dependencies.dateNow.timeIntervalSince1970 * 1000))) + dependencies.mutate(cache: .snodeAPI) { $0.setClockOffsetMs(offset) } + + return response.timestamp + } + } + + // MARK: - Convenience + + private static func prepareRequest( + request: Request, + responseType: R.Type, + requireAllBatchResponses: Bool = true, + retryCount: Int = 0, + requestTimeout: TimeInterval = Network.defaultTimeout, + requestAndPathBuildTimeout: TimeInterval? = nil, + using dependencies: Dependencies + ) throws -> Network.PreparedRequest { + return try Network.PreparedRequest( + request: request, + responseType: responseType, + requireAllBatchResponses: requireAllBatchResponses, + retryCount: retryCount, + requestTimeout: requestTimeout, + requestAndPathBuildTimeout: requestAndPathBuildTimeout, + using: dependencies + ) + .handleEvents( + receiveOutput: { _, response in + switch response { + case let snodeResponse as SnodeResponse: + // Update the network offset based on the response so subsequent requests have + // the correct network offset time + let offset = (Int64(snodeResponse.timeOffset) - Int64(floor(dependencies.dateNow.timeIntervalSince1970 * 1000))) + dependencies.mutate(cache: .snodeAPI) { + $0.setClockOffsetMs(offset) + + // Extract and store hard fork information if returned + guard snodeResponse.hardForkVersion.count > 1 else { return } + + if snodeResponse.hardForkVersion[1] > $0.softfork { + $0.softfork = snodeResponse.hardForkVersion[1] + dependencies[defaults: .standard, key: .softfork] = $0.softfork + } + + if snodeResponse.hardForkVersion[0] > $0.hardfork { + $0.hardfork = snodeResponse.hardForkVersion[0] + dependencies[defaults: .standard, key: .hardfork] = $0.hardfork + $0.softfork = snodeResponse.hardForkVersion[1] + dependencies[defaults: .standard, key: .softfork] = $0.softfork + } + } + + default: break + } + } + ) + } + } +} + +// MARK: - Publisher Convenience + +public extension Publisher where Output == Set { + func tryMapWithRandomSnode( + using dependencies: Dependencies, + _ transform: @escaping (LibSession.Snode) throws -> T + ) -> AnyPublisher { + return self + .tryMap { swarm -> T in + var remainingSnodes: Set = swarm + let snode: LibSession.Snode = try dependencies.popRandomElement(&remainingSnodes) ?? { + throw SnodeAPIError.insufficientSnodes + }() + + return try transform(snode) + } + .eraseToAnyPublisher() + } + + func tryFlatMapWithRandomSnode( + maxPublishers: Subscribers.Demand = .unlimited, + retry retries: Int = 0, + drainBehaviour: ThreadSafeObject = .alwaysRandom, + using dependencies: Dependencies, + _ transform: @escaping (LibSession.Snode) throws -> P + ) -> AnyPublisher where T == P.Output, P: Publisher, P.Failure == Error { + return self + .mapError { $0 } + .flatMap(maxPublishers: maxPublishers) { swarm -> AnyPublisher in + // If we don't want to reuse a specific snode multiple times then just grab a + // random one from the swarm every time + var remainingSnodes: Set = drainBehaviour.performUpdateAndMap { behaviour in + switch behaviour { + case .alwaysRandom: return (behaviour, swarm) + case .limitedReuse(_, let targetSnode, _, let usedSnodes, let swarmHash): + // If we've used all of the snodes or the swarm has changed then reset the used list + guard swarmHash == swarm.hashValue && (targetSnode != nil || usedSnodes != swarm) else { + return (behaviour.reset(), swarm) + } + + return (behaviour, swarm.subtracting(usedSnodes)) + } + } + var lastError: Error? + + return Just(()) + .setFailureType(to: Error.self) + .tryFlatMap(maxPublishers: maxPublishers) { _ -> AnyPublisher in + let snode: LibSession.Snode = try drainBehaviour.performUpdateAndMap { behaviour in + switch behaviour { + case .limitedReuse(_, .some(let targetSnode), _, _, _): + return (behaviour.use(snode: targetSnode, from: swarm), targetSnode) + default: break + } + + // Select the next snode + let result: LibSession.Snode = try dependencies.popRandomElement(&remainingSnodes) ?? { + throw SnodeAPIError.ranOutOfRandomSnodes(lastError) + }() + + return (behaviour.use(snode: result, from: swarm), result) + } + + return try transform(snode) + .eraseToAnyPublisher() + } + .mapError { error in + // Prevent nesting the 'ranOutOfRandomSnodes' errors + switch error { + case SnodeAPIError.ranOutOfRandomSnodes: break + default: lastError = error + } + + return error + } + .retry(retries) + .eraseToAnyPublisher() + } + .eraseToAnyPublisher() + } +} + +// MARK: - SnodeAPI Cache + +public extension Network.SnodeAPI { + class Cache: SnodeAPICacheType { + private let dependencies: Dependencies + public var hardfork: Int + public var softfork: Int + public var clockOffsetMs: Int64 = 0 + + init(using dependencies: Dependencies) { + self.dependencies = dependencies + self.hardfork = dependencies[defaults: .standard, key: .hardfork] + self.softfork = dependencies[defaults: .standard, key: .softfork] + } + + public func currentOffsetTimestampMs() -> T { + let timestampNowMs: Int64 = (Int64(floor(dependencies.dateNow.timeIntervalSince1970 * 1000)) + clockOffsetMs) + + guard let convertedTimestampNowMs: T = T(exactly: timestampNowMs) else { + Log.critical("[SnodeAPI.Cache] Failed to convert the timestamp to the desired type: \(type(of: T.self)).") + return 0 + } + + return convertedTimestampNowMs + } + + public func setClockOffsetMs(_ clockOffsetMs: Int64) { + self.clockOffsetMs = clockOffsetMs + } + } +} + +public extension Cache { + static let snodeAPI: CacheConfig = Dependencies.create( + identifier: "snodeAPI", + createInstance: { dependencies in Network.SnodeAPI.Cache(using: dependencies) }, + mutableInstance: { $0 }, + immutableInstance: { $0 } + ) +} + +// MARK: - SnodeAPICacheType + +/// This is a read-only version of the Cache designed to avoid unintentionally mutating the instance in a non-thread-safe way +public protocol SnodeAPIImmutableCacheType: ImmutableCacheType { + /// The last seen storage server hard fork version. + var hardfork: Int { get } + + /// The last seen storage server soft fork version. + var softfork: Int { get } + + /// The offset between the user's clock and the Service Node's clock. Used in cases where the + /// user's clock is incorrect. + var clockOffsetMs: Int64 { get } + + /// Tthe current user clock timestamp in milliseconds offset by the difference between the user's clock and the clock of the most + /// recent Service Node's that was communicated with. + func currentOffsetTimestampMs() -> T +} + +public protocol SnodeAPICacheType: SnodeAPIImmutableCacheType, MutableCacheType { + /// The last seen storage server hard fork version. + var hardfork: Int { get set } + + /// The last seen storage server soft fork version. + var softfork: Int { get set } + + /// A function to update the offset between the user's clock and the Service Node's clock. + func setClockOffsetMs(_ clockOffsetMs: Int64) +} diff --git a/SessionSnodeKit/SnodeAPI/SnodeAPIEndpoint.swift b/SessionNetworkingKit/StorageServer/SnodeAPIEndpoint.swift similarity index 95% rename from SessionSnodeKit/SnodeAPI/SnodeAPIEndpoint.swift rename to SessionNetworkingKit/StorageServer/SnodeAPIEndpoint.swift index 45cec1c60e..6bd1f1b1a9 100644 --- a/SessionSnodeKit/SnodeAPI/SnodeAPIEndpoint.swift +++ b/SessionNetworkingKit/StorageServer/SnodeAPIEndpoint.swift @@ -3,9 +3,8 @@ // stringlint:disable import Foundation -import SessionUtilitiesKit -public extension SnodeAPI { +public extension Network.SnodeAPI { enum Endpoint: EndpointType { case sendMessage case getMessages @@ -35,7 +34,7 @@ public extension SnodeAPI { case daemonOnsResolve case daemonGetServiceNodes - public static var name: String { "SnodeAPI.Endpoint" } + public static var name: String { "StorageServer.Endpoint" } public static var batchRequestVariant: Network.BatchRequest.Child.Variant = .storageServer public var path: String { diff --git a/SessionSnodeKit/SnodeAPI/SnodeAPIError.swift b/SessionNetworkingKit/StorageServer/SnodeAPIError.swift similarity index 99% rename from SessionSnodeKit/SnodeAPI/SnodeAPIError.swift rename to SessionNetworkingKit/StorageServer/SnodeAPIError.swift index 0de7eb2ba8..c09b5ed963 100644 --- a/SessionSnodeKit/SnodeAPI/SnodeAPIError.swift +++ b/SessionNetworkingKit/StorageServer/SnodeAPIError.swift @@ -3,7 +3,6 @@ // stringlint:disable import Foundation -import SessionUtilitiesKit public enum SnodeAPIError: Error, CustomStringConvertible { case clockOutOfSync diff --git a/SessionSnodeKit/SnodeAPI/SnodeAPINamespace.swift b/SessionNetworkingKit/StorageServer/SnodeAPINamespace.swift similarity index 99% rename from SessionSnodeKit/SnodeAPI/SnodeAPINamespace.swift rename to SessionNetworkingKit/StorageServer/SnodeAPINamespace.swift index c6b23c1e75..ea3399d543 100644 --- a/SessionSnodeKit/SnodeAPI/SnodeAPINamespace.swift +++ b/SessionNetworkingKit/StorageServer/SnodeAPINamespace.swift @@ -6,7 +6,7 @@ import Foundation import SessionUtil import SessionUtilitiesKit -public extension SnodeAPI { +public extension Network.SnodeAPI { enum Namespace: Int, Codable, Hashable, CustomStringConvertible { /// Messages sent to one-to-one conversations are stored in this namespace case `default` = 0 diff --git a/SessionSnodeKit/SnodeAPI/Request+SnodeAPI.swift b/SessionNetworkingKit/StorageServer/Types/Request+SnodeAPI.swift similarity index 86% rename from SessionSnodeKit/SnodeAPI/Request+SnodeAPI.swift rename to SessionNetworkingKit/StorageServer/Types/Request+SnodeAPI.swift index 48914fdd90..4f2c50e84f 100644 --- a/SessionSnodeKit/SnodeAPI/Request+SnodeAPI.swift +++ b/SessionNetworkingKit/StorageServer/Types/Request+SnodeAPI.swift @@ -5,11 +5,9 @@ import Foundation import SessionUtilitiesKit -// MARK: Request - SnodeAPI - -public extension Request where Endpoint == SnodeAPI.Endpoint { +public extension Request where Endpoint == Network.SnodeAPI.Endpoint { init( - endpoint: SnodeAPI.Endpoint, + endpoint: Endpoint, snode: LibSession.Snode, swarmPublicKey: String? = nil, body: B @@ -28,10 +26,10 @@ public extension Request where Endpoint == SnodeAPI.Endpoint { } init( - endpoint: SnodeAPI.Endpoint, + endpoint: Endpoint, swarmPublicKey: String, body: B, - snodeRetrievalRetryCount: Int = SnodeAPI.maxRetryCount + snodeRetrievalRetryCount: Int = Network.SnodeAPI.maxRetryCount ) throws where T == SnodeRequest { self = try Request( endpoint: endpoint, @@ -51,7 +49,7 @@ public extension Request where Endpoint == SnodeAPI.Endpoint { swarmPublicKey: String, requiresLatestNetworkTime: Bool, body: B, - snodeRetrievalRetryCount: Int = SnodeAPI.maxRetryCount + snodeRetrievalRetryCount: Int = Network.SnodeAPI.maxRetryCount ) throws where T == SnodeRequest, B: Encodable & UpdatableTimestamp { self = try Request( endpoint: endpoint, diff --git a/SessionSnodeKit/SnodeAPI/ResponseInfo+SnodeAPI.swift b/SessionNetworkingKit/StorageServer/Types/ResponseInfo+SnodeAPI.swift similarity index 93% rename from SessionSnodeKit/SnodeAPI/ResponseInfo+SnodeAPI.swift rename to SessionNetworkingKit/StorageServer/Types/ResponseInfo+SnodeAPI.swift index 91d81a1aba..1c821429aa 100644 --- a/SessionSnodeKit/SnodeAPI/ResponseInfo+SnodeAPI.swift +++ b/SessionNetworkingKit/StorageServer/Types/ResponseInfo+SnodeAPI.swift @@ -3,7 +3,7 @@ import Foundation import SessionUtilitiesKit -public extension SnodeAPI { +public extension Network.SnodeAPI { struct LatestTimestampResponseInfo: ResponseInfoType { public let code: Int public let headers: [String: String] diff --git a/SessionSnodeKit/Models/SnodeMessage.swift b/SessionNetworkingKit/StorageServer/Types/SnodeMessage.swift similarity index 100% rename from SessionSnodeKit/Models/SnodeMessage.swift rename to SessionNetworkingKit/StorageServer/Types/SnodeMessage.swift diff --git a/SessionSnodeKit/Models/SnodeReceivedMessage.swift b/SessionNetworkingKit/StorageServer/Types/SnodeReceivedMessage.swift similarity index 95% rename from SessionSnodeKit/Models/SnodeReceivedMessage.swift rename to SessionNetworkingKit/StorageServer/Types/SnodeReceivedMessage.swift index 99604b49e8..ad2384fbbb 100644 --- a/SessionSnodeKit/Models/SnodeReceivedMessage.swift +++ b/SessionNetworkingKit/StorageServer/Types/SnodeReceivedMessage.swift @@ -15,7 +15,7 @@ public struct SnodeReceivedMessage: Codable, CustomDebugStringConvertible { public let snode: LibSession.Snode? public let swarmPublicKey: String - public let namespace: SnodeAPI.Namespace + public let namespace: Network.SnodeAPI.Namespace public let hash: String public let timestampMs: Int64 public let expirationTimestampMs: Int64 @@ -36,7 +36,7 @@ public struct SnodeReceivedMessage: Codable, CustomDebugStringConvertible { public init?( snode: LibSession.Snode?, publicKey: String, - namespace: SnodeAPI.Namespace, + namespace: Network.SnodeAPI.Namespace, rawMessage: GetMessagesResponse.RawMessage ) { guard let data: Data = Data(base64Encoded: rawMessage.base64EncodedDataString) else { diff --git a/SessionSnodeKit/Types/BatchRequest.swift b/SessionNetworkingKit/Types/BatchRequest.swift similarity index 100% rename from SessionSnodeKit/Types/BatchRequest.swift rename to SessionNetworkingKit/Types/BatchRequest.swift diff --git a/SessionSnodeKit/Types/BatchResponse.swift b/SessionNetworkingKit/Types/BatchResponse.swift similarity index 100% rename from SessionSnodeKit/Types/BatchResponse.swift rename to SessionNetworkingKit/Types/BatchResponse.swift diff --git a/SessionSnodeKit/Types/BencodeResponse.swift b/SessionNetworkingKit/Types/BencodeResponse.swift similarity index 100% rename from SessionSnodeKit/Types/BencodeResponse.swift rename to SessionNetworkingKit/Types/BencodeResponse.swift diff --git a/SessionSnodeKit/Types/ContentProxy.swift b/SessionNetworkingKit/Types/ContentProxy.swift similarity index 100% rename from SessionSnodeKit/Types/ContentProxy.swift rename to SessionNetworkingKit/Types/ContentProxy.swift diff --git a/SessionSnodeKit/Types/Destination.swift b/SessionNetworkingKit/Types/Destination.swift similarity index 100% rename from SessionSnodeKit/Types/Destination.swift rename to SessionNetworkingKit/Types/Destination.swift diff --git a/SessionSnodeKit/Types/HTTPHeader.swift b/SessionNetworkingKit/Types/HTTPHeader.swift similarity index 100% rename from SessionSnodeKit/Types/HTTPHeader.swift rename to SessionNetworkingKit/Types/HTTPHeader.swift diff --git a/SessionSnodeKit/Types/HTTPMethod.swift b/SessionNetworkingKit/Types/HTTPMethod.swift similarity index 100% rename from SessionSnodeKit/Types/HTTPMethod.swift rename to SessionNetworkingKit/Types/HTTPMethod.swift diff --git a/SessionSnodeKit/Types/HTTPQueryParam.swift b/SessionNetworkingKit/Types/HTTPQueryParam.swift similarity index 100% rename from SessionSnodeKit/Types/HTTPQueryParam.swift rename to SessionNetworkingKit/Types/HTTPQueryParam.swift diff --git a/SessionSnodeKit/Types/IPv4.swift b/SessionNetworkingKit/Types/IPv4.swift similarity index 100% rename from SessionSnodeKit/Types/IPv4.swift rename to SessionNetworkingKit/Types/IPv4.swift diff --git a/SessionSnodeKit/Types/JSON.swift b/SessionNetworkingKit/Types/JSON.swift similarity index 100% rename from SessionSnodeKit/Types/JSON.swift rename to SessionNetworkingKit/Types/JSON.swift diff --git a/SessionNetworkingKit/Types/Network.swift b/SessionNetworkingKit/Types/Network.swift new file mode 100644 index 0000000000..a11afeb54c --- /dev/null +++ b/SessionNetworkingKit/Types/Network.swift @@ -0,0 +1,53 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +import Foundation +import Combine +import SessionUtilitiesKit + +// MARK: - Singleton + +public extension Singleton { + static let network: SingletonConfig = Dependencies.create( + identifier: "network", + createInstance: { dependencies in LibSessionNetwork(using: dependencies) } + ) +} + +// MARK: - Network Constants + +public class Network { + public static let defaultTimeout: TimeInterval = 10 + public static let fileUploadTimeout: TimeInterval = 60 + public static let fileDownloadTimeout: TimeInterval = 30 + + /// **Note:** The max file size is 10,000,000 bytes (rather than 10MiB which would be `(10 * 1024 * 1024)`), 10,000,000 + /// exactly will be fine but a single byte more will result in an error + public static let maxFileSize: UInt = 10_000_000 +} + +// MARK: - NetworkStatus + +public enum NetworkStatus { + case unknown + case connecting + case connected + case disconnected +} + +// MARK: - NetworkType + +public protocol NetworkType { + func getSwarm(for swarmPublicKey: String) -> AnyPublisher, Error> + func getRandomNodes(count: Int) -> AnyPublisher, Error> + + func send( + _ body: Data?, + to destination: Network.Destination, + requestTimeout: TimeInterval, + requestAndPathBuildTimeout: TimeInterval? + ) -> AnyPublisher<(ResponseInfoType, Data?), Error> + + func checkClientVersion(ed25519SecretKey: [UInt8]) -> AnyPublisher<(ResponseInfoType, Network.FileServer.AppVersionResponse), Error> +} diff --git a/SessionSnodeKit/Types/NetworkError.swift b/SessionNetworkingKit/Types/NetworkError.swift similarity index 100% rename from SessionSnodeKit/Types/NetworkError.swift rename to SessionNetworkingKit/Types/NetworkError.swift diff --git a/SessionSnodeKit/Types/PreparedRequest+Sending.swift b/SessionNetworkingKit/Types/PreparedRequest+Sending.swift similarity index 100% rename from SessionSnodeKit/Types/PreparedRequest+Sending.swift rename to SessionNetworkingKit/Types/PreparedRequest+Sending.swift diff --git a/SessionSnodeKit/Types/PreparedRequest.swift b/SessionNetworkingKit/Types/PreparedRequest.swift similarity index 100% rename from SessionSnodeKit/Types/PreparedRequest.swift rename to SessionNetworkingKit/Types/PreparedRequest.swift diff --git a/SessionSnodeKit/Types/ProxiedContentDownloader.swift b/SessionNetworkingKit/Types/ProxiedContentDownloader.swift similarity index 100% rename from SessionSnodeKit/Types/ProxiedContentDownloader.swift rename to SessionNetworkingKit/Types/ProxiedContentDownloader.swift diff --git a/SessionSnodeKit/Types/Request.swift b/SessionNetworkingKit/Types/Request.swift similarity index 100% rename from SessionSnodeKit/Types/Request.swift rename to SessionNetworkingKit/Types/Request.swift diff --git a/SessionNetworkingKit/Types/RequestCategory.swift b/SessionNetworkingKit/Types/RequestCategory.swift new file mode 100644 index 0000000000..e69de29bb2 diff --git a/SessionSnodeKit/Types/ResponseInfo.swift b/SessionNetworkingKit/Types/ResponseInfo.swift similarity index 100% rename from SessionSnodeKit/Types/ResponseInfo.swift rename to SessionNetworkingKit/Types/ResponseInfo.swift diff --git a/SessionNetworkingKit/Types/SessionNetworkAPI/HTTPHeader+SessionNetwork.swift b/SessionNetworkingKit/Types/SessionNetworkAPI/HTTPHeader+SessionNetwork.swift new file mode 100644 index 0000000000..464a3b1b9e --- /dev/null +++ b/SessionNetworkingKit/Types/SessionNetworkAPI/HTTPHeader+SessionNetwork.swift @@ -0,0 +1,9 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +public extension HTTPHeader { + static let tokenServerPubKey: HTTPHeader = "X-FS-Pubkey" + static let tokenServerTimestamp: HTTPHeader = "X-FS-Timestamp" + static let tokenServerSignature: HTTPHeader = "X-FS-Signature" +} diff --git a/SessionNetworkingKit/Types/SessionNetworkAPI/SessionNetworkAPI+Database.swift b/SessionNetworkingKit/Types/SessionNetworkAPI/SessionNetworkAPI+Database.swift new file mode 100644 index 0000000000..9316b1a440 --- /dev/null +++ b/SessionNetworkingKit/Types/SessionNetworkAPI/SessionNetworkAPI+Database.swift @@ -0,0 +1,32 @@ +// Copyright © 2024 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +import Foundation +import AudioToolbox +import GRDB +import DifferenceKit +import SessionUtilitiesKit + +public extension KeyValueStore.StringKey { + static let contractAddress: KeyValueStore.StringKey = "contractAddress" +} + +public extension KeyValueStore.DoubleKey { + static let tokenUsd: KeyValueStore.DoubleKey = "tokenUsd" + static let marketCapUsd: KeyValueStore.DoubleKey = "marketCapUsd" + static let stakingRequirement: KeyValueStore.DoubleKey = "stakingRequirement" + static let stakingRewardPool: KeyValueStore.DoubleKey = "stakingRewardPool" + static let networkStakedTokens: KeyValueStore.DoubleKey = "networkStakedTokens" + static let networkStakedUSD: KeyValueStore.DoubleKey = "networkStakedUSD" +} + +public extension KeyValueStore.IntKey { + static let networkSize: KeyValueStore.IntKey = "networkSize" +} + +public extension KeyValueStore.Int64Key { + static let lastUpdatedTimestampMs: KeyValueStore.Int64Key = "lastUpdatedTimestampMs" + static let staleTimestampMs: KeyValueStore.Int64Key = "staleTimestampMs" + static let priceTimestampMs: KeyValueStore.Int64Key = "priceTimestampMs" +} diff --git a/SessionSnodeKit/SessionNetworkAPI/SessionNetworkAPI+Models.swift b/SessionNetworkingKit/Types/SessionNetworkAPI/SessionNetworkAPI+Models.swift similarity index 100% rename from SessionSnodeKit/SessionNetworkAPI/SessionNetworkAPI+Models.swift rename to SessionNetworkingKit/Types/SessionNetworkAPI/SessionNetworkAPI+Models.swift diff --git a/SessionSnodeKit/SessionNetworkAPI/SessionNetworkAPI+Network.swift b/SessionNetworkingKit/Types/SessionNetworkAPI/SessionNetworkAPI+Network.swift similarity index 92% rename from SessionSnodeKit/SessionNetworkAPI/SessionNetworkAPI+Network.swift rename to SessionNetworkingKit/Types/SessionNetworkAPI/SessionNetworkAPI+Network.swift index fb26688ac1..947e55d15d 100644 --- a/SessionSnodeKit/SessionNetworkAPI/SessionNetworkAPI+Network.swift +++ b/SessionNetworkingKit/Types/SessionNetworkAPI/SessionNetworkAPI+Network.swift @@ -41,12 +41,15 @@ extension SessionNetworkAPI { .eraseToAnyPublisher() } - return Result { - try SessionNetworkAPI - .prepareInfo(using: dependencies) + return dependencies[singleton: .storage] + .readPublisher { db -> Network.PreparedRequest in + try SessionNetworkAPI + .prepareInfo( + db, + using: dependencies + ) } - .publisher - .flatMap { [dependencies] in $0.send(using: dependencies) } + .flatMap { $0.send(using: dependencies) } .map { _, info in info } .flatMapStorageWritePublisher(using: dependencies) { [dependencies] db, info -> Bool in // Token info diff --git a/SessionNetworkingKit/Types/SessionNetworkAPI/SessionNetworkAPI.swift b/SessionNetworkingKit/Types/SessionNetworkAPI/SessionNetworkAPI.swift new file mode 100644 index 0000000000..1d1891a51d --- /dev/null +++ b/SessionNetworkingKit/Types/SessionNetworkAPI/SessionNetworkAPI.swift @@ -0,0 +1,131 @@ +// Copyright © 2024 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +import Foundation +import Combine +import GRDB +import SessionUtilitiesKit + +public enum SessionNetworkAPI { + public static let workQueue = DispatchQueue(label: "SessionNetworkAPI.workQueue", qos: .userInitiated) + public static let client = HTTPClient() + + // MARK: - Info + + /// General token info. This endpoint combines the `/price` and `/token` endpoint information. + /// + /// `GET/info` + + public static func prepareInfo( + _ db: Database, + using dependencies: Dependencies + ) throws -> Network.PreparedRequest { + return try Network.PreparedRequest( + request: Request( + endpoint: Network.NetworkAPI.Endpoint.info, + destination: .server( + method: .get, + server: Network.NetworkAPI.networkAPIServer, + queryParameters: [:], + x25519PublicKey: Network.NetworkAPI.networkAPIServerPublicKey + ) + ), + responseType: Info.self, + requestAndPathBuildTimeout: Network.defaultTimeout, + using: dependencies + ) + .signed(db, with: SessionNetworkAPI.signRequest, using: dependencies) + } + + // MARK: - Authentication + + fileprivate static func signatureHeaders( + _ db: Database, + url: URL, + method: HTTPMethod, + body: Data?, + using dependencies: Dependencies + ) throws -> [HTTPHeader: String] { + let timestamp: UInt64 = UInt64(floor(dependencies.dateNow.timeIntervalSince1970)) + let path: String = url.path + .appending(url.query.map { value in "?\(value)" }) + + let signResult: (publicKey: String, signature: [UInt8]) = try sign( + db, + timestamp: timestamp, + method: method.rawValue, + path: path, + body: body, + using: dependencies + ) + + return [ + HTTPHeader.tokenServerPubKey: signResult.publicKey, + HTTPHeader.tokenServerTimestamp: "\(timestamp)", + HTTPHeader.tokenServerSignature: signResult.signature.toBase64() + ] + } + + private static func sign( + _ db: Database, + timestamp: UInt64, + method: String, + path: String, + body: Data?, + using dependencies: Dependencies + ) throws -> (publicKey: String, signature: [UInt8]) { + let bodyString: String? = { + guard let bodyData: Data = body else { return nil } + return String(data: bodyData, encoding: .utf8) + }() + + guard + let userEdKeyPair: KeyPair = Identity.fetchUserEd25519KeyPair(db), + let blinded07KeyPair: KeyPair = dependencies[singleton: .crypto].generate( + .versionBlinded07KeyPair(ed25519SecretKey: userEdKeyPair.secretKey) + ), + let signatureResult: [UInt8] = dependencies[singleton: .crypto].generate( + .signatureVersionBlind07( + timestamp: timestamp, + method: method, + path: path, + body: bodyString, + ed25519SecretKey: userEdKeyPair.secretKey + ) + ) + else { throw NetworkError.signingFailed } + + return ( + publicKey: SessionId(.versionBlinded07, publicKey: blinded07KeyPair.publicKey).hexString, + signature: signatureResult + ) + } + + private static func signRequest( + _ db: Database, + preparedRequest: Network.PreparedRequest, + using dependencies: Dependencies + ) throws -> Network.Destination { + guard let url: URL = preparedRequest.destination.url else { + throw NetworkError.signingFailed + } + + guard case let .server(info) = preparedRequest.destination else { + throw NetworkError.signingFailed + } + + return .server( + info: info.updated( + with: try signatureHeaders( + db, + url: url, + method: preparedRequest.method, + body: preparedRequest.body, + using: dependencies + ) + ) + ) + } +} + diff --git a/SessionSnodeKit/Types/SwarmDrainBehaviour.swift b/SessionNetworkingKit/Types/SwarmDrainBehaviour.swift similarity index 100% rename from SessionSnodeKit/Types/SwarmDrainBehaviour.swift rename to SessionNetworkingKit/Types/SwarmDrainBehaviour.swift diff --git a/SessionSnodeKit/Types/UpdatableTimestamp.swift b/SessionNetworkingKit/Types/UpdatableTimestamp.swift similarity index 100% rename from SessionSnodeKit/Types/UpdatableTimestamp.swift rename to SessionNetworkingKit/Types/UpdatableTimestamp.swift diff --git a/SessionSnodeKit/Types/ValidatableResponse.swift b/SessionNetworkingKit/Types/ValidatableResponse.swift similarity index 100% rename from SessionSnodeKit/Types/ValidatableResponse.swift rename to SessionNetworkingKit/Types/ValidatableResponse.swift diff --git a/SessionSnodeKit/Utilities/Data+Utilities.swift b/SessionNetworkingKit/Utilities/Data+Utilities.swift similarity index 100% rename from SessionSnodeKit/Utilities/Data+Utilities.swift rename to SessionNetworkingKit/Utilities/Data+Utilities.swift diff --git a/SessionNetworkingKit/Utilities/ObservableKey+SessionNetworkingKit.swift b/SessionNetworkingKit/Utilities/ObservableKey+SessionNetworkingKit.swift new file mode 100644 index 0000000000..372aaf717d --- /dev/null +++ b/SessionNetworkingKit/Utilities/ObservableKey+SessionNetworkingKit.swift @@ -0,0 +1,23 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +import Foundation +import SessionUtilitiesKit + +public extension ObservableKey { + static func networkLifecycle(_ event: NetworkLifecycle) -> ObservableKey { + ObservableKey("networkLifecycle-\(event)", .networkLifecycle) + } +} + +public extension GenericObservableKey { + static let networkLifecycle: GenericObservableKey = "networkLifecycle" +} + +// MARK: - NetworkLifecycle + +public enum NetworkLifecycle: String, Sendable { + case suspended + case resumed +} diff --git a/SessionSnodeKit/Utilities/Publisher+Utilities.swift b/SessionNetworkingKit/Utilities/Publisher+Utilities.swift similarity index 100% rename from SessionSnodeKit/Utilities/Publisher+Utilities.swift rename to SessionNetworkingKit/Utilities/Publisher+Utilities.swift diff --git a/SessionSnodeKit/Utilities/RetryWithDependencies.swift b/SessionNetworkingKit/Utilities/RetryWithDependencies.swift similarity index 100% rename from SessionSnodeKit/Utilities/RetryWithDependencies.swift rename to SessionNetworkingKit/Utilities/RetryWithDependencies.swift diff --git a/SessionSnodeKit/Utilities/String+Trimming.swift b/SessionNetworkingKit/Utilities/String+Trimming.swift similarity index 100% rename from SessionSnodeKit/Utilities/String+Trimming.swift rename to SessionNetworkingKit/Utilities/String+Trimming.swift diff --git a/SessionSnodeKit/Utilities/URLResponse+Utilities.swift b/SessionNetworkingKit/Utilities/URLResponse+Utilities.swift similarity index 100% rename from SessionSnodeKit/Utilities/URLResponse+Utilities.swift rename to SessionNetworkingKit/Utilities/URLResponse+Utilities.swift diff --git a/SessionSnodeKitTests/Models/FileUploadResponseSpec.swift b/SessionNetworkingKitTests/Models/FileUploadResponseSpec.swift similarity index 96% rename from SessionSnodeKitTests/Models/FileUploadResponseSpec.swift rename to SessionNetworkingKitTests/Models/FileUploadResponseSpec.swift index 1454fbe383..4339984b89 100644 --- a/SessionSnodeKitTests/Models/FileUploadResponseSpec.swift +++ b/SessionNetworkingKitTests/Models/FileUploadResponseSpec.swift @@ -5,7 +5,7 @@ import Foundation import Quick import Nimble -@testable import SessionSnodeKit +@testable import SessionNetworkingKit class FileUploadResponseSpec: QuickSpec { override class func spec() { diff --git a/SessionSnodeKitTests/Models/SnodeRequestSpec.swift b/SessionNetworkingKitTests/Models/SnodeRequestSpec.swift similarity index 98% rename from SessionSnodeKitTests/Models/SnodeRequestSpec.swift rename to SessionNetworkingKitTests/Models/SnodeRequestSpec.swift index 3d6836709b..0405d71e7c 100644 --- a/SessionSnodeKitTests/Models/SnodeRequestSpec.swift +++ b/SessionNetworkingKitTests/Models/SnodeRequestSpec.swift @@ -7,7 +7,7 @@ import SessionUtilitiesKit import Quick import Nimble -@testable import SessionSnodeKit +@testable import SessionNetworkingKit class SnodeRequestSpec: QuickSpec { override class func spec() { diff --git a/SessionNetworkingKitTests/SOGS/Crypto/Authentication+SOGS.swift b/SessionNetworkingKitTests/SOGS/Crypto/Authentication+SOGS.swift new file mode 100644 index 0000000000..92862325b1 --- /dev/null +++ b/SessionNetworkingKitTests/SOGS/Crypto/Authentication+SOGS.swift @@ -0,0 +1,50 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import SessionUtilitiesKit + +// MARK: - Authentication Types + +public extension Authentication { + /// Used when interacting with a community + struct community: AuthenticationMethod { + public let roomToken: String + public let server: String + public let publicKey: String + public let hasCapabilities: Bool + public let supportsBlinding: Bool + public let forceBlinded: Bool + + public var info: Info { + .community( + server: server, + publicKey: publicKey, + hasCapabilities: hasCapabilities, + supportsBlinding: supportsBlinding, + forceBlinded: forceBlinded + ) + } + + public init( + roomToken: String, + server: String, + publicKey: String, + hasCapabilities: Bool, + supportsBlinding: Bool, + forceBlinded: Bool = false + ) { + self.roomToken = roomToken + self.server = server + self.publicKey = publicKey + self.hasCapabilities = hasCapabilities + self.supportsBlinding = supportsBlinding + self.forceBlinded = forceBlinded + } + + // MARK: - SignatureGenerator + + public func generateSignature(with verificationBytes: [UInt8], using dependencies: Dependencies) throws -> Authentication.Signature { + throw CryptoError.signatureGenerationFailed + } + } +} diff --git a/SessionNetworkingKitTests/SOGS/Crypto/CryptoSOGSAPISpec.swift b/SessionNetworkingKitTests/SOGS/Crypto/CryptoSOGSAPISpec.swift new file mode 100644 index 0000000000..d141b5549f --- /dev/null +++ b/SessionNetworkingKitTests/SOGS/Crypto/CryptoSOGSAPISpec.swift @@ -0,0 +1,180 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import SessionUtilitiesKit + +import Quick +import Nimble + +@testable import SessionNetworkingKit + +class CryptoSOGSAPISpec: QuickSpec { + override class func spec() { + // MARK: Configuration + + @TestState var dependencies: TestDependencies! = TestDependencies() + @TestState(singleton: .crypto, in: dependencies) var crypto: Crypto! = Crypto(using: dependencies) + @TestState(cache: .general, in: dependencies) var mockGeneralCache: MockGeneralCache! = MockGeneralCache( + initialSetup: { cache in + cache.when { $0.sessionId }.thenReturn(SessionId(.standard, hex: TestConstants.publicKey)) + cache.when { $0.ed25519SecretKey }.thenReturn(Array(Data(hex: TestConstants.edSecretKey))) + } + ) + + // MARK: - Crypto for SOGSAPI + describe("Crypto for SOGSAPI") { + // MARK: -- when generating a blinded15 key pair + context("when generating a blinded15 key pair") { + // MARK: ---- successfully generates + it("successfully generates") { + let result = crypto.generate( + .blinded15KeyPair( + serverPublicKey: TestConstants.serverPublicKey, + ed25519SecretKey: Data(hex: TestConstants.edSecretKey).bytes + ) + ) + + // Note: The first 64 characters of the secretKey are consistent but the chars after that always differ + expect(result?.publicKey.toHexString()).to(equal(TestConstants.blind15PublicKey)) + expect(result?.secretKey.toHexString()).to(equal(TestConstants.blind15SecretKey)) + } + + // MARK: ---- fails if the edKeyPair secret key length wrong + it("fails if the ed25519SecretKey length wrong") { + let result = crypto.generate( + .blinded15KeyPair( + serverPublicKey: TestConstants.serverPublicKey, + ed25519SecretKey: Array(Data(hex: String(TestConstants.edSecretKey.prefix(4)))) + ) + ) + + expect(result).to(beNil()) + } + } + + // MARK: -- when generating a blinded25 key pair + context("when generating a blinded25 key pair") { + // MARK: ---- successfully generates + it("successfully generates") { + let result = crypto.generate( + .blinded25KeyPair( + serverPublicKey: TestConstants.serverPublicKey, + ed25519SecretKey: Data(hex: TestConstants.edSecretKey).bytes + ) + ) + + // Note: The first 64 characters of the secretKey are consistent but the chars after that always differ + expect(result?.publicKey.toHexString()).to(equal(TestConstants.blind25PublicKey)) + expect(result?.secretKey.toHexString()).to(equal(TestConstants.blind25SecretKey)) + } + + // MARK: ---- fails if the edKeyPair secret key length wrong + it("fails if the ed25519SecretKey length wrong") { + let result = crypto.generate( + .blinded25KeyPair( + serverPublicKey: TestConstants.serverPublicKey, + ed25519SecretKey: Data(hex: String(TestConstants.edSecretKey.prefix(4))).bytes + ) + ) + + expect(result).to(beNil()) + } + } + + // MARK: -- when generating a signatureBlind15 + context("when generating a signatureBlind15") { + // MARK: ---- generates a correct signature + it("generates a correct signature") { + let result = crypto.generate( + .signatureBlind15( + message: "TestMessage".bytes, + serverPublicKey: TestConstants.serverPublicKey, + ed25519SecretKey: Array(Data(hex: TestConstants.edSecretKey)) + ) + ) + + expect(result?.toHexString()) + .to(equal( + "245003f1627ebdfc6099c32597d426ef84d1b301861a5ffbbac92dde6c608334" + + "ceb56a022a094a9a664fae034b50eed40bd1bfb262c7e542c979eec265ae3f07" + )) + } + } + + // MARK: -- when generating a signatureBlind25 + context("when generating a signatureBlind25") { + // MARK: ---- generates a correct signature + it("generates a correct signature") { + let result = crypto.generate( + .signatureBlind25( + message: "TestMessage".bytes, + serverPublicKey: TestConstants.serverPublicKey, + ed25519SecretKey: Data(hex: TestConstants.edSecretKey).bytes + ) + ) + + expect(result?.toHexString()) + .to(equal( + "9ff9b7fb7d435c7a2c0b0b2ae64963baaf394386b9f7c7f924eeac44ec0f74c7" + + "fe6304c73a9b3a65491f81e44b545e54631e83e9a412eaed5fd4db2e05ec830c" + )) + } + } + + // MARK: -- when checking if a session id matches a blinded id + context("when checking if a session id matches a blinded id") { + // MARK: ---- returns true when a blind15 id matches + it("returns true when a blind15 id matches") { + let result = crypto.verify( + .sessionId( + "05\(TestConstants.publicKey)", + matchesBlindedId: "15\(TestConstants.blind15PublicKey)", + serverPublicKey: TestConstants.serverPublicKey + ) + ) + + expect(result).to(beTrue()) + } + + // MARK: ---- returns true when a blind25 id matches + it("returns true when a blind25 id matches") { + let result = crypto.verify( + .sessionId( + "05\(TestConstants.publicKey)", + matchesBlindedId: "25\(TestConstants.blind25PublicKey)", + serverPublicKey: TestConstants.serverPublicKey + ) + ) + + expect(result).to(beTrue()) + } + + // MARK: ---- returns false if given an invalid session id + it("returns false if given an invalid session id") { + let result = crypto.verify( + .sessionId( + "AB\(TestConstants.publicKey)", + matchesBlindedId: "15\(TestConstants.blind15PublicKey)", + serverPublicKey: TestConstants.serverPublicKey + ) + ) + + expect(result).to(beFalse()) + } + + // MARK: ---- returns false if given an invalid blinded id + it("returns false if given an invalid blinded id") { + let result = crypto.verify( + .sessionId( + "05\(TestConstants.publicKey)", + matchesBlindedId: "AB\(TestConstants.blind15PublicKey)", + serverPublicKey: TestConstants.serverPublicKey + ) + ) + + expect(result).to(beFalse()) + } + } + } + } +} diff --git a/SessionNetworkingKitTests/SOGS/Models/CapabilitiesResponse.swift b/SessionNetworkingKitTests/SOGS/Models/CapabilitiesResponse.swift new file mode 100644 index 0000000000..4b6ee85475 --- /dev/null +++ b/SessionNetworkingKitTests/SOGS/Models/CapabilitiesResponse.swift @@ -0,0 +1,38 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +import Quick +import Nimble + +@testable import SessionNetworkingKit + +class CapabilitiesResponseSpec: QuickSpec { + override class func spec() { + // MARK: - CapabilitiesResponse + describe("CapabilitiesResponse") { + // MARK: -- when initializing + context("when initializing") { + // MARK: ---- assigns values correctly + it("assigns values correctly") { + let capabilities: Network.SOGS.CapabilitiesResponse = Network.SOGS.CapabilitiesResponse( + capabilities: ["sogs"], + missing: ["test"] + ) + + expect(capabilities.capabilities).to(equal(["sogs"])) + expect(capabilities.missing).to(equal(["test"])) + } + + it("defaults missing to nil") { + let capabilities: Network.SOGS.CapabilitiesResponse = Network.SOGS.CapabilitiesResponse( + capabilities: ["sogs"] + ) + + expect(capabilities.capabilities).to(equal(["sogs"])) + expect(capabilities.missing).to(beNil()) + } + } + } + } +} diff --git a/SessionMessagingKitTests/Open Groups/Models/RoomPollInfoSpec.swift b/SessionNetworkingKitTests/SOGS/Models/RoomPollInfoSpec.swift similarity index 92% rename from SessionMessagingKitTests/Open Groups/Models/RoomPollInfoSpec.swift rename to SessionNetworkingKitTests/SOGS/Models/RoomPollInfoSpec.swift index 386f2f02bc..f088a86205 100644 --- a/SessionMessagingKitTests/Open Groups/Models/RoomPollInfoSpec.swift +++ b/SessionNetworkingKitTests/SOGS/Models/RoomPollInfoSpec.swift @@ -5,7 +5,7 @@ import Foundation import Quick import Nimble -@testable import SessionMessagingKit +@testable import SessionNetworkingKit class RoomPollInfoSpec: QuickSpec { override class func spec() { @@ -15,7 +15,7 @@ class RoomPollInfoSpec: QuickSpec { context("when initializing with a room") { // MARK: ---- copies all the relevant values across it("copies all the relevant values across") { - let room: OpenGroupAPI.Room = OpenGroupAPI.Room( + let room: Network.SOGS.Room = Network.SOGS.Room( token: "testToken", name: "testName", roomDescription: nil, @@ -42,7 +42,7 @@ class RoomPollInfoSpec: QuickSpec { upload: true, defaultUpload: true ) - let roomPollInfo: OpenGroupAPI.RoomPollInfo = OpenGroupAPI.RoomPollInfo(room: room) + let roomPollInfo: Network.SOGS.RoomPollInfo = Network.SOGS.RoomPollInfo(room: room) expect(roomPollInfo.token).to(equal(room.token)) expect(roomPollInfo.activeUsers).to(equal(room.activeUsers)) @@ -82,7 +82,7 @@ class RoomPollInfoSpec: QuickSpec { } """ let roomData: Data = roomPollInfoJson.data(using: .utf8)! - let result: OpenGroupAPI.RoomPollInfo = try! JSONDecoder().decode(OpenGroupAPI.RoomPollInfo.self, from: roomData) + let result: Network.SOGS.RoomPollInfo = try! JSONDecoder().decode(Network.SOGS.RoomPollInfo.self, from: roomData) expect(result.admin).to(beFalse()) expect(result.globalAdmin).to(beFalse()) @@ -115,7 +115,7 @@ class RoomPollInfoSpec: QuickSpec { } """ let roomData: Data = roomPollInfoJson.data(using: .utf8)! - let result: OpenGroupAPI.RoomPollInfo = try! JSONDecoder().decode(OpenGroupAPI.RoomPollInfo.self, from: roomData) + let result: Network.SOGS.RoomPollInfo = try! JSONDecoder().decode(Network.SOGS.RoomPollInfo.self, from: roomData) expect(result.admin).to(beTrue()) expect(result.globalAdmin).to(beTrue()) diff --git a/SessionMessagingKitTests/Open Groups/Models/RoomSpec.swift b/SessionNetworkingKitTests/SOGS/Models/RoomSpec.swift similarity index 93% rename from SessionMessagingKitTests/Open Groups/Models/RoomSpec.swift rename to SessionNetworkingKitTests/SOGS/Models/RoomSpec.swift index 2fd43d679b..2238a9e021 100644 --- a/SessionMessagingKitTests/Open Groups/Models/RoomSpec.swift +++ b/SessionNetworkingKitTests/SOGS/Models/RoomSpec.swift @@ -5,7 +5,7 @@ import Foundation import Quick import Nimble -@testable import SessionMessagingKit +@testable import SessionNetworkingKit class RoomSpec: QuickSpec { override class func spec() { @@ -45,7 +45,7 @@ class RoomSpec: QuickSpec { } """ let roomData: Data = roomJson.data(using: .utf8)! - let result: OpenGroupAPI.Room = try! JSONDecoder().decode(OpenGroupAPI.Room.self, from: roomData) + let result: Network.SOGS.Room = try! JSONDecoder().decode(Network.SOGS.Room.self, from: roomData) expect(result.admin).to(beFalse()) expect(result.globalAdmin).to(beFalse()) @@ -89,7 +89,7 @@ class RoomSpec: QuickSpec { } """ let roomData: Data = roomJson.data(using: .utf8)! - let result: OpenGroupAPI.Room = try! JSONDecoder().decode(OpenGroupAPI.Room.self, from: roomData) + let result: Network.SOGS.Room = try! JSONDecoder().decode(Network.SOGS.Room.self, from: roomData) expect(result.admin).to(beTrue()) expect(result.globalAdmin).to(beTrue()) diff --git a/SessionMessagingKitTests/Open Groups/Models/SOGSMessageSpec.swift b/SessionNetworkingKitTests/SOGS/Models/SOGSMessageSpec.swift similarity index 92% rename from SessionMessagingKitTests/Open Groups/Models/SOGSMessageSpec.swift rename to SessionNetworkingKitTests/SOGS/Models/SOGSMessageSpec.swift index db9aa0df35..315f6e0e33 100644 --- a/SessionMessagingKitTests/Open Groups/Models/SOGSMessageSpec.swift +++ b/SessionNetworkingKitTests/SOGS/Models/SOGSMessageSpec.swift @@ -1,13 +1,12 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation -import SessionSnodeKit import SessionUtilitiesKit import Quick import Nimble -@testable import SessionMessagingKit +@testable import SessionNetworkingKit class SOGSMessageSpec: QuickSpec { override class func spec() { @@ -45,7 +44,7 @@ class SOGSMessageSpec: QuickSpec { } """ messageData = messageJson.data(using: .utf8)! - let result: OpenGroupAPI.Message? = try? decoder.decode(OpenGroupAPI.Message.self, from: messageData) + let result: Network.SOGS.Message? = try? decoder.decode(Network.SOGS.Message.self, from: messageData) expect(result).toNot(beNil()) expect(result?.whisper).to(beFalse()) @@ -66,7 +65,7 @@ class SOGSMessageSpec: QuickSpec { } """ messageData = messageJson.data(using: .utf8)! - let result: OpenGroupAPI.Message? = try? decoder.decode(OpenGroupAPI.Message.self, from: messageData) + let result: Network.SOGS.Message? = try? decoder.decode(Network.SOGS.Message.self, from: messageData) expect(result).toNot(beNil()) expect(result?.sender).to(beNil()) @@ -94,7 +93,7 @@ class SOGSMessageSpec: QuickSpec { messageData = messageJson.data(using: .utf8)! expect { - try decoder.decode(OpenGroupAPI.Message.self, from: messageData) + try decoder.decode(Network.SOGS.Message.self, from: messageData) } .to(throwError(NetworkError.parsingFailed)) } @@ -117,7 +116,7 @@ class SOGSMessageSpec: QuickSpec { messageData = messageJson.data(using: .utf8)! expect { - try decoder.decode(OpenGroupAPI.Message.self, from: messageData) + try decoder.decode(Network.SOGS.Message.self, from: messageData) } .to(throwError(NetworkError.parsingFailed)) } @@ -140,7 +139,7 @@ class SOGSMessageSpec: QuickSpec { messageData = messageJson.data(using: .utf8)! expect { - try decoder.decode(OpenGroupAPI.Message.self, from: messageData) + try decoder.decode(Network.SOGS.Message.self, from: messageData) } .to(throwError(NetworkError.parsingFailed)) } @@ -150,7 +149,7 @@ class SOGSMessageSpec: QuickSpec { decoder = JSONDecoder() expect { - try decoder.decode(OpenGroupAPI.Message.self, from: messageData) + try decoder.decode(Network.SOGS.Message.self, from: messageData) } .to(throwError(DependenciesError.missingDependencies)) } @@ -173,7 +172,7 @@ class SOGSMessageSpec: QuickSpec { messageData = messageJson.data(using: .utf8)! expect { - try decoder.decode(OpenGroupAPI.Message.self, from: messageData) + try decoder.decode(Network.SOGS.Message.self, from: messageData) } .to(throwError(NetworkError.parsingFailed)) } @@ -204,7 +203,7 @@ class SOGSMessageSpec: QuickSpec { .thenReturn(true) expect { - try decoder.decode(OpenGroupAPI.Message.self, from: messageData) + try decoder.decode(Network.SOGS.Message.self, from: messageData) } .toNot(beNil()) } @@ -215,7 +214,7 @@ class SOGSMessageSpec: QuickSpec { .when { $0.verify(.signature(message: .any, publicKey: .any, signature: .any)) } .thenReturn(true) - _ = try? decoder.decode(OpenGroupAPI.Message.self, from: messageData) + _ = try? decoder.decode(Network.SOGS.Message.self, from: messageData) expect(mockCrypto) .to(call(matchingParameters: .all) { @@ -236,7 +235,7 @@ class SOGSMessageSpec: QuickSpec { .thenReturn(false) expect { - try decoder.decode(OpenGroupAPI.Message.self, from: messageData) + try decoder.decode(Network.SOGS.Message.self, from: messageData) } .to(throwError(NetworkError.parsingFailed)) } @@ -251,7 +250,7 @@ class SOGSMessageSpec: QuickSpec { .thenReturn(true) expect { - try decoder.decode(OpenGroupAPI.Message.self, from: messageData) + try decoder.decode(Network.SOGS.Message.self, from: messageData) } .toNot(beNil()) } @@ -262,7 +261,7 @@ class SOGSMessageSpec: QuickSpec { .when { $0.verify(.signatureXed25519(.any, curve25519PublicKey: .any, data: .any)) } .thenReturn(true) - _ = try? decoder.decode(OpenGroupAPI.Message.self, from: messageData) + _ = try? decoder.decode(Network.SOGS.Message.self, from: messageData) expect(mockCrypto) .to(call(matchingParameters: .all) { @@ -283,7 +282,7 @@ class SOGSMessageSpec: QuickSpec { .thenReturn(false) expect { - try decoder.decode(OpenGroupAPI.Message.self, from: messageData) + try decoder.decode(Network.SOGS.Message.self, from: messageData) } .to(throwError(NetworkError.parsingFailed)) } diff --git a/SessionMessagingKitTests/Open Groups/Models/SendDirectMessageRequestSpec.swift b/SessionNetworkingKitTests/SOGS/Models/SendDirectMessageRequestSpec.swift similarity index 86% rename from SessionMessagingKitTests/Open Groups/Models/SendDirectMessageRequestSpec.swift rename to SessionNetworkingKitTests/SOGS/Models/SendDirectMessageRequestSpec.swift index 27e96dd206..c7c462f307 100644 --- a/SessionMessagingKitTests/Open Groups/Models/SendDirectMessageRequestSpec.swift +++ b/SessionNetworkingKitTests/SOGS/Models/SendDirectMessageRequestSpec.swift @@ -5,7 +5,7 @@ import Foundation import Quick import Nimble -@testable import SessionMessagingKit +@testable import SessionNetworkingKit class SendDirectMessageRequestSpec: QuickSpec { override class func spec() { @@ -15,7 +15,7 @@ class SendDirectMessageRequestSpec: QuickSpec { context("when encoding") { // MARK: ---- encodes the data as a base64 string it("encodes the data as a base64 string") { - let request: OpenGroupAPI.SendDirectMessageRequest = OpenGroupAPI.SendDirectMessageRequest( + let request: Network.SOGS.SendDirectMessageRequest = Network.SOGS.SendDirectMessageRequest( message: "TestData".data(using: .utf8)! ) let requestData: Data = try! JSONEncoder().encode(request) diff --git a/SessionMessagingKitTests/Open Groups/Models/SendMessageRequestSpec.swift b/SessionNetworkingKitTests/SOGS/Models/SendSOGSMessageRequestSpec.swift similarity index 82% rename from SessionMessagingKitTests/Open Groups/Models/SendMessageRequestSpec.swift rename to SessionNetworkingKitTests/SOGS/Models/SendSOGSMessageRequestSpec.swift index ec0c15d380..9c00a94671 100644 --- a/SessionMessagingKitTests/Open Groups/Models/SendMessageRequestSpec.swift +++ b/SessionNetworkingKitTests/SOGS/Models/SendSOGSMessageRequestSpec.swift @@ -5,17 +5,17 @@ import Foundation import Quick import Nimble -@testable import SessionMessagingKit +@testable import SessionNetworkingKit -class SendMessageRequestSpec: QuickSpec { +class SendSOGSMessageRequestSpec: QuickSpec { override class func spec() { - // MARK: - a SendMessageRequest - describe("a SendMessageRequest") { + // MARK: - a SendSOGSMessageRequest + describe("a SendSOGSMessageRequest") { // MARK: -- when initializing context("when initializing") { // MARK: ---- defaults the optional values to nil it("defaults the optional values to nil") { - let request: OpenGroupAPI.SendMessageRequest = OpenGroupAPI.SendMessageRequest( + let request: Network.SOGS.SendSOGSMessageRequest = Network.SOGS.SendSOGSMessageRequest( data: "TestData".data(using: .utf8)!, signature: "TestSignature".data(using: .utf8)! ) @@ -30,7 +30,7 @@ class SendMessageRequestSpec: QuickSpec { context("when encoding") { // MARK: ---- encodes the data as a base64 string it("encodes the data as a base64 string") { - let request: OpenGroupAPI.SendMessageRequest = OpenGroupAPI.SendMessageRequest( + let request: Network.SOGS.SendSOGSMessageRequest = Network.SOGS.SendSOGSMessageRequest( data: "TestData".data(using: .utf8)!, signature: "TestSignature".data(using: .utf8)!, whisperTo: nil, @@ -46,7 +46,7 @@ class SendMessageRequestSpec: QuickSpec { // MARK: ---- encodes the signature as a base64 string it("encodes the signature as a base64 string") { - let request: OpenGroupAPI.SendMessageRequest = OpenGroupAPI.SendMessageRequest( + let request: Network.SOGS.SendSOGSMessageRequest = Network.SOGS.SendSOGSMessageRequest( data: "TestData".data(using: .utf8)!, signature: "TestSignature".data(using: .utf8)!, whisperTo: nil, diff --git a/SessionMessagingKitTests/Open Groups/Models/UpdateMessageRequestSpec.swift b/SessionNetworkingKitTests/SOGS/Models/UpdateMessageRequestSpec.swift similarity index 87% rename from SessionMessagingKitTests/Open Groups/Models/UpdateMessageRequestSpec.swift rename to SessionNetworkingKitTests/SOGS/Models/UpdateMessageRequestSpec.swift index 106bd04c52..847cdf7ac2 100644 --- a/SessionMessagingKitTests/Open Groups/Models/UpdateMessageRequestSpec.swift +++ b/SessionNetworkingKitTests/SOGS/Models/UpdateMessageRequestSpec.swift @@ -5,7 +5,7 @@ import Foundation import Quick import Nimble -@testable import SessionMessagingKit +@testable import SessionNetworkingKit class UpdateMessageRequestSpec: QuickSpec { override class func spec() { @@ -15,7 +15,7 @@ class UpdateMessageRequestSpec: QuickSpec { context("when encoding") { // MARK: ---- encodes the data as a base64 string it("encodes the data as a base64 string") { - let request: OpenGroupAPI.UpdateMessageRequest = OpenGroupAPI.UpdateMessageRequest( + let request: Network.SOGS.UpdateMessageRequest = Network.SOGS.UpdateMessageRequest( data: "TestData".data(using: .utf8)!, signature: "TestSignature".data(using: .utf8)!, fileIds: nil @@ -29,7 +29,7 @@ class UpdateMessageRequestSpec: QuickSpec { // MARK: ---- encodes the signature as a base64 string it("encodes the signature as a base64 string") { - let request: OpenGroupAPI.UpdateMessageRequest = OpenGroupAPI.UpdateMessageRequest( + let request: Network.SOGS.UpdateMessageRequest = Network.SOGS.UpdateMessageRequest( data: "TestData".data(using: .utf8)!, signature: "TestSignature".data(using: .utf8)!, fileIds: nil diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift b/SessionNetworkingKitTests/SOGS/SOGSAPISpec.swift similarity index 73% rename from SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift rename to SessionNetworkingKitTests/SOGS/SOGSAPISpec.swift index fa13eeea02..342be00b57 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift +++ b/SessionNetworkingKitTests/SOGS/SOGSAPISpec.swift @@ -3,15 +3,14 @@ import Foundation import Combine import GRDB -import SessionSnodeKit import SessionUtilitiesKit import Quick import Nimble -@testable import SessionMessagingKit +@testable import SessionNetworkingKit -class OpenGroupAPISpec: QuickSpec { +class SOGSAPISpec: QuickSpec { override class func spec() { // MARK: Configuration @@ -73,24 +72,21 @@ class OpenGroupAPISpec: QuickSpec { .thenReturn(Array(Array(Data(hex: TestConstants.edSecretKey)).prefix(upTo: 32))) } ) - @TestState(cache: .libSession, in: dependencies) var mockLibSessionCache: MockLibSessionCache! = MockLibSessionCache( - initialSetup: { $0.defaultInitialSetup() } - ) @TestState var disposables: [AnyCancellable]! = [] @TestState var error: Error? - // MARK: - an OpenGroupAPI - describe("an OpenGroupAPI") { + // MARK: - a SOGSAPI + describe("a SOGSAPI") { // MARK: -- when preparing a poll request context("when preparing a poll request") { - @TestState var preparedRequest: Network.PreparedRequest>? + @TestState var preparedRequest: Network.PreparedRequest>? // MARK: ---- generates the correct request it("generates the correct request") { expect { - preparedRequest = try OpenGroupAPI.preparedPoll( + preparedRequest = try Network.SOGS.preparedPoll( roomInfo: [ - OpenGroupAPI.RoomInfo( + Network.SOGS.PollRoomInfo( roomToken: "testRoom", infoUpdates: 0, sequenceNumber: 0 @@ -98,15 +94,15 @@ class OpenGroupAPISpec: QuickSpec { ], lastInboxMessageId: 0, lastOutboxMessageId: 0, + checkForCommunityMessageRequests: false, hasPerformedInitialPoll: false, timeSinceLastPoll: 0, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -116,20 +112,20 @@ class OpenGroupAPISpec: QuickSpec { expect(preparedRequest?.path).to(equal("/batch")) expect(preparedRequest?.method.rawValue).to(equal("POST")) expect(preparedRequest?.batchEndpoints.count).to(equal(3)) - expect(preparedRequest?.batchEndpoints[test: 0].asType(OpenGroupAPI.Endpoint.self)) + expect(preparedRequest?.batchEndpoints[test: 0].asType(Network.SOGS.Endpoint.self)) .to(equal(.capabilities)) - expect(preparedRequest?.batchEndpoints[test: 1].asType(OpenGroupAPI.Endpoint.self)) + expect(preparedRequest?.batchEndpoints[test: 1].asType(Network.SOGS.Endpoint.self)) .to(equal(.roomPollInfo("testRoom", 0))) - expect(preparedRequest?.batchEndpoints[test: 2].asType(OpenGroupAPI.Endpoint.self)) + expect(preparedRequest?.batchEndpoints[test: 2].asType(Network.SOGS.Endpoint.self)) .to(equal(.roomMessagesRecent("testRoom"))) } // MARK: ---- retrieves recent messages if there was no last message it("retrieves recent messages if there was no last message") { expect { - preparedRequest = try OpenGroupAPI.preparedPoll( + preparedRequest = try Network.SOGS.preparedPoll( roomInfo: [ - OpenGroupAPI.RoomInfo( + Network.SOGS.PollRoomInfo( roomToken: "testRoom", infoUpdates: 0, sequenceNumber: 0 @@ -137,31 +133,31 @@ class OpenGroupAPISpec: QuickSpec { ], lastInboxMessageId: 0, lastOutboxMessageId: 0, + checkForCommunityMessageRequests: false, hasPerformedInitialPoll: false, timeSinceLastPoll: 0, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies ) }.toNot(throwError()) - expect(preparedRequest?.batchEndpoints[test: 2].asType(OpenGroupAPI.Endpoint.self)) + expect(preparedRequest?.batchEndpoints[test: 2].asType(Network.SOGS.Endpoint.self)) .to(equal(.roomMessagesRecent("testRoom"))) } // MARK: ---- retrieves recent messages if there was a last message and it has not performed the initial poll and the last message was too long ago it("retrieves recent messages if there was a last message and it has not performed the initial poll and the last message was too long ago") { expect { - preparedRequest = try OpenGroupAPI.preparedPoll( + preparedRequest = try Network.SOGS.preparedPoll( roomInfo: [ - OpenGroupAPI.RoomInfo( + Network.SOGS.PollRoomInfo( roomToken: "testRoom", infoUpdates: 0, sequenceNumber: 121 @@ -169,31 +165,31 @@ class OpenGroupAPISpec: QuickSpec { ], lastInboxMessageId: 0, lastOutboxMessageId: 0, + checkForCommunityMessageRequests: false, hasPerformedInitialPoll: false, - timeSinceLastPoll: (CommunityPoller.maxInactivityPeriod + 1), + timeSinceLastPoll: (Network.SOGS.maxInactivityPeriodForPolling + 1), authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies ) }.toNot(throwError()) - expect(preparedRequest?.batchEndpoints[test: 2].asType(OpenGroupAPI.Endpoint.self)) + expect(preparedRequest?.batchEndpoints[test: 2].asType(Network.SOGS.Endpoint.self)) .to(equal(.roomMessagesRecent("testRoom"))) } // MARK: ---- retrieves recent messages if there was a last message and it has performed an initial poll but it was not too long ago it("retrieves recent messages if there was a last message and it has performed an initial poll but it was not too long ago") { expect { - preparedRequest = try OpenGroupAPI.preparedPoll( + preparedRequest = try Network.SOGS.preparedPoll( roomInfo: [ - OpenGroupAPI.RoomInfo( + Network.SOGS.PollRoomInfo( roomToken: "testRoom", infoUpdates: 0, sequenceNumber: 122 @@ -201,31 +197,31 @@ class OpenGroupAPISpec: QuickSpec { ], lastInboxMessageId: 0, lastOutboxMessageId: 0, + checkForCommunityMessageRequests: false, hasPerformedInitialPoll: false, timeSinceLastPoll: 0, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies ) }.toNot(throwError()) - expect(preparedRequest?.batchEndpoints[test: 2].asType(OpenGroupAPI.Endpoint.self)) + expect(preparedRequest?.batchEndpoints[test: 2].asType(Network.SOGS.Endpoint.self)) .to(equal(.roomMessagesSince("testRoom", seqNo: 122))) } // MARK: ---- retrieves recent messages if there was a last message and there has already been a poll this session it("retrieves recent messages if there was a last message and there has already been a poll this session") { expect { - preparedRequest = try OpenGroupAPI.preparedPoll( + preparedRequest = try Network.SOGS.preparedPoll( roomInfo: [ - OpenGroupAPI.RoomInfo( + Network.SOGS.PollRoomInfo( roomToken: "testRoom", infoUpdates: 0, sequenceNumber: 123 @@ -233,22 +229,22 @@ class OpenGroupAPISpec: QuickSpec { ], lastInboxMessageId: 0, lastOutboxMessageId: 0, + checkForCommunityMessageRequests: false, hasPerformedInitialPoll: true, timeSinceLastPoll: 0, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies ) }.toNot(throwError()) - expect(preparedRequest?.batchEndpoints[test: 2].asType(OpenGroupAPI.Endpoint.self)) + expect(preparedRequest?.batchEndpoints[test: 2].asType(Network.SOGS.Endpoint.self)) .to(equal(.roomMessagesSince("testRoom", seqNo: 123))) } @@ -257,9 +253,9 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ------ does not call the inbox and outbox endpoints it("does not call the inbox and outbox endpoints") { expect { - preparedRequest = try OpenGroupAPI.preparedPoll( + preparedRequest = try Network.SOGS.preparedPoll( roomInfo: [ - OpenGroupAPI.RoomInfo( + Network.SOGS.PollRoomInfo( roomToken: "testRoom", infoUpdates: 0, sequenceNumber: 0 @@ -267,38 +263,34 @@ class OpenGroupAPISpec: QuickSpec { ], lastInboxMessageId: 0, lastOutboxMessageId: 0, + checkForCommunityMessageRequests: false, hasPerformedInitialPoll: true, timeSinceLastPoll: 0, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: false, forceBlinded: false ), using: dependencies ) }.toNot(throwError()) - expect(preparedRequest?.batchEndpoints as? [OpenGroupAPI.Endpoint]).toNot(contain(.inbox)) - expect(preparedRequest?.batchEndpoints as? [OpenGroupAPI.Endpoint]).toNot(contain(.outbox)) + expect(preparedRequest?.batchEndpoints as? [Network.SOGS.Endpoint]).toNot(contain(.inbox)) + expect(preparedRequest?.batchEndpoints as? [Network.SOGS.Endpoint]).toNot(contain(.outbox)) } } // MARK: ---- when blinded and checking for message requests context("when blinded and checking for message requests") { - beforeEach { - mockLibSessionCache.when { $0.get(.checkForCommunityMessageRequests) }.thenReturn(true) - } - // MARK: ------ includes the inbox and outbox endpoints it("includes the inbox and outbox endpoints") { expect { - preparedRequest = try OpenGroupAPI.preparedPoll( + preparedRequest = try Network.SOGS.preparedPoll( roomInfo: [ - OpenGroupAPI.RoomInfo( + Network.SOGS.PollRoomInfo( roomToken: "testRoom", infoUpdates: 0, sequenceNumber: 0 @@ -306,31 +298,31 @@ class OpenGroupAPISpec: QuickSpec { ], lastInboxMessageId: 0, lastOutboxMessageId: 0, + checkForCommunityMessageRequests: true, hasPerformedInitialPoll: true, timeSinceLastPoll: 0, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs, .blind] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: true, forceBlinded: false ), using: dependencies ) }.toNot(throwError()) - expect(preparedRequest?.batchEndpoints as? [OpenGroupAPI.Endpoint]).to(contain(.inbox)) - expect(preparedRequest?.batchEndpoints as? [OpenGroupAPI.Endpoint]).to(contain(.outbox)) + expect(preparedRequest?.batchEndpoints as? [Network.SOGS.Endpoint]).to(contain(.inbox)) + expect(preparedRequest?.batchEndpoints as? [Network.SOGS.Endpoint]).to(contain(.outbox)) } // MARK: ------ retrieves recent inbox messages if there was no last message it("retrieves recent inbox messages if there was no last message") { expect { - preparedRequest = try OpenGroupAPI.preparedPoll( + preparedRequest = try Network.SOGS.preparedPoll( roomInfo: [ - OpenGroupAPI.RoomInfo( + Network.SOGS.PollRoomInfo( roomToken: "testRoom", infoUpdates: 0, sequenceNumber: 0 @@ -338,30 +330,30 @@ class OpenGroupAPISpec: QuickSpec { ], lastInboxMessageId: 0, lastOutboxMessageId: 0, + checkForCommunityMessageRequests: true, hasPerformedInitialPoll: true, timeSinceLastPoll: 0, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs, .blind] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: true, forceBlinded: false ), using: dependencies ) }.toNot(throwError()) - expect(preparedRequest?.batchEndpoints as? [OpenGroupAPI.Endpoint]).to(contain(.inbox)) + expect(preparedRequest?.batchEndpoints as? [Network.SOGS.Endpoint]).to(contain(.inbox)) } // MARK: ------ retrieves inbox messages since the last message if there was one it("retrieves inbox messages since the last message if there was one") { expect { - preparedRequest = try OpenGroupAPI.preparedPoll( + preparedRequest = try Network.SOGS.preparedPoll( roomInfo: [ - OpenGroupAPI.RoomInfo( + Network.SOGS.PollRoomInfo( roomToken: "testRoom", infoUpdates: 0, sequenceNumber: 0 @@ -369,30 +361,30 @@ class OpenGroupAPISpec: QuickSpec { ], lastInboxMessageId: 124, lastOutboxMessageId: 0, + checkForCommunityMessageRequests: true, hasPerformedInitialPoll: true, timeSinceLastPoll: 0, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs, .blind] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: true, forceBlinded: false ), using: dependencies ) }.toNot(throwError()) - expect(preparedRequest?.batchEndpoints as? [OpenGroupAPI.Endpoint]).to(contain(.inboxSince(id: 124))) + expect(preparedRequest?.batchEndpoints as? [Network.SOGS.Endpoint]).to(contain(.inboxSince(id: 124))) } // MARK: ------ retrieves recent outbox messages if there was no last message it("retrieves recent outbox messages if there was no last message") { expect { - preparedRequest = try OpenGroupAPI.preparedPoll( + preparedRequest = try Network.SOGS.preparedPoll( roomInfo: [ - OpenGroupAPI.RoomInfo( + Network.SOGS.PollRoomInfo( roomToken: "testRoom", infoUpdates: 0, sequenceNumber: 0 @@ -400,30 +392,30 @@ class OpenGroupAPISpec: QuickSpec { ], lastInboxMessageId: 0, lastOutboxMessageId: 0, + checkForCommunityMessageRequests: true, hasPerformedInitialPoll: true, timeSinceLastPoll: 0, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs, .blind] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: true, forceBlinded: false ), using: dependencies ) }.toNot(throwError()) - expect(preparedRequest?.batchEndpoints as? [OpenGroupAPI.Endpoint]).to(contain(.outbox)) + expect(preparedRequest?.batchEndpoints as? [Network.SOGS.Endpoint]).to(contain(.outbox)) } // MARK: ------ retrieves outbox messages since the last message if there was one it("retrieves outbox messages since the last message if there was one") { expect { - preparedRequest = try OpenGroupAPI.preparedPoll( + preparedRequest = try Network.SOGS.preparedPoll( roomInfo: [ - OpenGroupAPI.RoomInfo( + Network.SOGS.PollRoomInfo( roomToken: "testRoom", infoUpdates: 0, sequenceNumber: 0 @@ -431,37 +423,33 @@ class OpenGroupAPISpec: QuickSpec { ], lastInboxMessageId: 0, lastOutboxMessageId: 125, + checkForCommunityMessageRequests: true, hasPerformedInitialPoll: true, timeSinceLastPoll: 0, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs, .blind] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: true, forceBlinded: false ), using: dependencies ) }.toNot(throwError()) - expect(preparedRequest?.batchEndpoints as? [OpenGroupAPI.Endpoint]).to(contain(.outboxSince(id: 125))) + expect(preparedRequest?.batchEndpoints as? [Network.SOGS.Endpoint]).to(contain(.outboxSince(id: 125))) } } // MARK: ---- when blinded and not checking for message requests context("when blinded and not checking for message requests") { - beforeEach { - mockLibSessionCache.when { $0.get(.checkForCommunityMessageRequests) }.thenReturn(false) - } - // MARK: ------ includes the inbox and outbox endpoints it("does not include the inbox endpoint") { expect { - preparedRequest = try OpenGroupAPI.preparedPoll( + preparedRequest = try Network.SOGS.preparedPoll( roomInfo: [ - OpenGroupAPI.RoomInfo( + Network.SOGS.PollRoomInfo( roomToken: "testRoom", infoUpdates: 0, sequenceNumber: 0 @@ -469,30 +457,30 @@ class OpenGroupAPISpec: QuickSpec { ], lastInboxMessageId: 0, lastOutboxMessageId: 0, + checkForCommunityMessageRequests: false, hasPerformedInitialPoll: true, timeSinceLastPoll: 0, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs, .blind] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: true, forceBlinded: false ), using: dependencies ) }.toNot(throwError()) - expect(preparedRequest?.batchEndpoints as? [OpenGroupAPI.Endpoint]).toNot(contain(.inbox)) + expect(preparedRequest?.batchEndpoints as? [Network.SOGS.Endpoint]).toNot(contain(.inbox)) } // MARK: ------ does not retrieve recent inbox messages if there was no last message it("does not retrieve recent inbox messages if there was no last message") { expect { - preparedRequest = try OpenGroupAPI.preparedPoll( + preparedRequest = try Network.SOGS.preparedPoll( roomInfo: [ - OpenGroupAPI.RoomInfo( + Network.SOGS.PollRoomInfo( roomToken: "testRoom", infoUpdates: 0, sequenceNumber: 0 @@ -500,30 +488,30 @@ class OpenGroupAPISpec: QuickSpec { ], lastInboxMessageId: 0, lastOutboxMessageId: 0, + checkForCommunityMessageRequests: false, hasPerformedInitialPoll: true, timeSinceLastPoll: 0, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs, .blind] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: true, forceBlinded: false ), using: dependencies ) }.toNot(throwError()) - expect(preparedRequest?.batchEndpoints as? [OpenGroupAPI.Endpoint]).toNot(contain(.inbox)) + expect(preparedRequest?.batchEndpoints as? [Network.SOGS.Endpoint]).toNot(contain(.inbox)) } // MARK: ------ does not retrieve inbox messages since the last message if there was one it("does not retrieve inbox messages since the last message if there was one") { expect { - preparedRequest = try OpenGroupAPI.preparedPoll( + preparedRequest = try Network.SOGS.preparedPoll( roomInfo: [ - OpenGroupAPI.RoomInfo( + Network.SOGS.PollRoomInfo( roomToken: "testRoom", infoUpdates: 0, sequenceNumber: 0 @@ -531,22 +519,22 @@ class OpenGroupAPISpec: QuickSpec { ], lastInboxMessageId: 124, lastOutboxMessageId: 0, + checkForCommunityMessageRequests: false, hasPerformedInitialPoll: true, timeSinceLastPoll: 0, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs, .blind] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: true, forceBlinded: false ), using: dependencies ) }.toNot(throwError()) - expect(preparedRequest?.batchEndpoints as? [OpenGroupAPI.Endpoint]).toNot(contain(.inboxSince(id: 124))) + expect(preparedRequest?.batchEndpoints as? [Network.SOGS.Endpoint]).toNot(contain(.inboxSince(id: 124))) } } } @@ -555,16 +543,15 @@ class OpenGroupAPISpec: QuickSpec { context("when preparing a capabilities request") { // MARK: ---- generates the request correctly it("generates the request and handles the response correctly") { - var preparedRequest: Network.PreparedRequest? + var preparedRequest: Network.PreparedRequest? expect { - preparedRequest = try OpenGroupAPI.preparedCapabilities( + preparedRequest = try Network.SOGS.preparedCapabilities( authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -580,16 +567,15 @@ class OpenGroupAPISpec: QuickSpec { context("when preparing a rooms request") { // MARK: ---- generates the request correctly it("generates the request correctly") { - var preparedRequest: Network.PreparedRequest<[OpenGroupAPI.Room]>? + var preparedRequest: Network.PreparedRequest<[Network.SOGS.Room]>? expect { - preparedRequest = try OpenGroupAPI.preparedRooms( + preparedRequest = try Network.SOGS.preparedRooms( authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -603,20 +589,19 @@ class OpenGroupAPISpec: QuickSpec { // MARK: -- when preparing a capabilitiesAndRoom request context("when preparing a capabilitiesAndRoom request") { - @TestState var preparedRequest: Network.PreparedRequest? + @TestState var preparedRequest: Network.PreparedRequest? // MARK: ---- generates the request correctly it("generates the request correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedCapabilitiesAndRoom( + preparedRequest = try Network.SOGS.preparedCapabilitiesAndRoom( roomToken: "testRoom", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -624,9 +609,9 @@ class OpenGroupAPISpec: QuickSpec { }.toNot(throwError()) expect(preparedRequest?.batchEndpoints.count).to(equal(2)) - expect(preparedRequest?.batchEndpoints[test: 0].asType(OpenGroupAPI.Endpoint.self)) + expect(preparedRequest?.batchEndpoints[test: 0].asType(Network.SOGS.Endpoint.self)) .to(equal(.capabilities)) - expect(preparedRequest?.batchEndpoints[test: 1].asType(OpenGroupAPI.Endpoint.self)) + expect(preparedRequest?.batchEndpoints[test: 1].asType(Network.SOGS.Endpoint.self)) .to(equal(.room("testRoom"))) expect(preparedRequest?.path).to(equal("/sequence")) @@ -639,18 +624,17 @@ class OpenGroupAPISpec: QuickSpec { .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } .thenReturn(Network.BatchResponse.mockCapabilitiesAndRoomResponse) - var response: (info: ResponseInfoType, data: OpenGroupAPI.CapabilitiesAndRoomResponse)? + var response: (info: ResponseInfoType, data: Network.SOGS.CapabilitiesAndRoomResponse)? expect { - preparedRequest = try OpenGroupAPI.preparedCapabilitiesAndRoom( + preparedRequest = try Network.SOGS.preparedCapabilitiesAndRoom( roomToken: "testRoom", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -675,18 +659,17 @@ class OpenGroupAPISpec: QuickSpec { .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } .thenReturn(Network.BatchResponse.mockCapabilitiesAndBanResponse) - var response: (info: ResponseInfoType, data: OpenGroupAPI.CapabilitiesAndRoomResponse)? + var response: (info: ResponseInfoType, data: Network.SOGS.CapabilitiesAndRoomResponse)? expect { - preparedRequest = try OpenGroupAPI.preparedCapabilitiesAndRoom( + preparedRequest = try Network.SOGS.preparedCapabilitiesAndRoom( roomToken: "testRoom", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -709,18 +692,17 @@ class OpenGroupAPISpec: QuickSpec { .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } .thenReturn(Network.BatchResponse.mockBanAndRoomResponse) - var response: (info: ResponseInfoType, data: OpenGroupAPI.CapabilitiesAndRoomResponse)? + var response: (info: ResponseInfoType, data: Network.SOGS.CapabilitiesAndRoomResponse)? expect { - preparedRequest = try OpenGroupAPI.preparedCapabilitiesAndRoom( + preparedRequest = try Network.SOGS.preparedCapabilitiesAndRoom( roomToken: "testRoom", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -740,22 +722,21 @@ class OpenGroupAPISpec: QuickSpec { } } - describe("an OpenGroupAPI") { + describe("an Network.SOGS") { // MARK: -- when preparing a capabilitiesAndRooms request context("when preparing a capabilitiesAndRooms request") { - @TestState var preparedRequest: Network.PreparedRequest? + @TestState var preparedRequest: Network.PreparedRequest? // MARK: ---- generates the request correctly it("generates the request correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedCapabilitiesAndRooms( + preparedRequest = try Network.SOGS.preparedCapabilitiesAndRooms( authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -763,13 +744,50 @@ class OpenGroupAPISpec: QuickSpec { }.toNot(throwError()) expect(preparedRequest?.batchEndpoints.count).to(equal(2)) - expect(preparedRequest?.batchEndpoints[test: 0].asType(OpenGroupAPI.Endpoint.self)) + expect(preparedRequest?.batchEndpoints[test: 0].asType(Network.SOGS.Endpoint.self)) .to(equal(.capabilities)) - expect(preparedRequest?.batchEndpoints[test: 1].asType(OpenGroupAPI.Endpoint.self)) + expect(preparedRequest?.batchEndpoints[test: 1].asType(Network.SOGS.Endpoint.self)) .to(equal(.rooms)) expect(preparedRequest?.path).to(equal("/sequence")) expect(preparedRequest?.method.rawValue).to(equal("POST")) + + expect(preparedRequest?.headers).toNot(beEmpty()) + expect(preparedRequest?.headers).to(equal([ + HTTPHeader.sogsNonce: "pK6YRtQApl4NhECGizF0Cg==", + HTTPHeader.sogsTimestamp: "1234567890", + HTTPHeader.sogsSignature: "VGVzdFNvZ3NTaWduYXR1cmU=", + HTTPHeader.sogsPubKey: "1588672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b" + ])) + } + + // MARK: ---- generates the request correctly and skips adding request headers + it("generates the request correctly and skips adding request headers") { + expect { + preparedRequest = try Network.SOGS.preparedCapabilitiesAndRooms( + authMethod: Authentication.community( + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, + forceBlinded: false + ), + skipAuthentication: true, + using: dependencies + ) + }.toNot(throwError()) + + expect(preparedRequest?.batchEndpoints.count).to(equal(2)) + expect(preparedRequest?.batchEndpoints[test: 0].asType(Network.SOGS.Endpoint.self)) + .to(equal(.capabilities)) + expect(preparedRequest?.batchEndpoints[test: 1].asType(Network.SOGS.Endpoint.self)) + .to(equal(.rooms)) + + expect(preparedRequest?.path).to(equal("/sequence")) + expect(preparedRequest?.method.rawValue).to(equal("POST")) + + expect(preparedRequest?.headers).to(beEmpty()) } // MARK: ---- processes a valid response correctly @@ -778,17 +796,16 @@ class OpenGroupAPISpec: QuickSpec { .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } .thenReturn(Network.BatchResponse.mockCapabilitiesAndRoomsResponse) - var response: (info: ResponseInfoType, data: OpenGroupAPI.CapabilitiesAndRoomsResponse)? + var response: (info: ResponseInfoType, data: Network.SOGS.CapabilitiesAndRoomsResponse)? expect { - preparedRequest = try OpenGroupAPI.preparedCapabilitiesAndRooms( + preparedRequest = try Network.SOGS.preparedCapabilitiesAndRooms( authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -813,25 +830,24 @@ class OpenGroupAPISpec: QuickSpec { .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } .thenReturn( MockNetwork.batchResponseData(with: [ - (OpenGroupAPI.Endpoint.capabilities, OpenGroupAPI.Capabilities.mockBatchSubResponse()), + (Network.SOGS.Endpoint.capabilities, Network.SOGS.CapabilitiesResponse.mockBatchSubResponse()), ( - OpenGroupAPI.Endpoint.userBan(""), - OpenGroupAPI.DirectMessage.mockBatchSubResponse() + Network.SOGS.Endpoint.userBan(""), + Network.SOGS.DirectMessage.mockBatchSubResponse() ) ]) ) - var response: (info: ResponseInfoType, data: OpenGroupAPI.CapabilitiesAndRoomsResponse)? + var response: (info: ResponseInfoType, data: Network.SOGS.CapabilitiesAndRoomsResponse)? expect { - preparedRequest = try OpenGroupAPI.preparedCapabilitiesAndRooms( + preparedRequest = try Network.SOGS.preparedCapabilitiesAndRooms( authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -854,17 +870,16 @@ class OpenGroupAPISpec: QuickSpec { .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } .thenReturn(Network.BatchResponse.mockBanAndRoomsResponse) - var response: (info: ResponseInfoType, data: OpenGroupAPI.CapabilitiesAndRoomsResponse)? + var response: (info: ResponseInfoType, data: Network.SOGS.CapabilitiesAndRoomsResponse)? expect { - preparedRequest = try OpenGroupAPI.preparedCapabilitiesAndRooms( + preparedRequest = try Network.SOGS.preparedCapabilitiesAndRooms( authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -885,24 +900,23 @@ class OpenGroupAPISpec: QuickSpec { // MARK: -- when preparing a send message request context("when preparing a send message request") { - @TestState var preparedRequest: Network.PreparedRequest? + @TestState var preparedRequest: Network.PreparedRequest? // MARK: ---- generates the request correctly it("generates the request correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedSend( + preparedRequest = try Network.SOGS.preparedSend( plaintext: "test".data(using: .utf8)!, roomToken: "testRoom", whisperTo: nil, whisperMods: false, fileIds: nil, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -918,27 +932,26 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ------ signs the message correctly it("signs the message correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedSend( + preparedRequest = try Network.SOGS.preparedSend( plaintext: "test".data(using: .utf8)!, roomToken: "testRoom", whisperTo: nil, whisperMods: false, fileIds: nil, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: false, forceBlinded: false ), using: dependencies ) }.toNot(throwError()) - let requestBody: OpenGroupAPI.SendMessageRequest? = try? preparedRequest?.body? - .decoded(as: OpenGroupAPI.SendMessageRequest.self, using: dependencies) + let requestBody: Network.SOGS.SendSOGSMessageRequest? = try? preparedRequest?.body? + .decoded(as: Network.SOGS.SendSOGSMessageRequest.self, using: dependencies) expect(requestBody?.data).to(equal("test".data(using: .utf8))) expect(requestBody?.signature).to(equal("TestStandardSignature".data(using: .utf8))) } @@ -948,24 +961,23 @@ class OpenGroupAPISpec: QuickSpec { mockGeneralCache.when { $0.ed25519SecretKey }.thenReturn([]) expect { - preparedRequest = try OpenGroupAPI.preparedSend( + preparedRequest = try Network.SOGS.preparedSend( plaintext: "test".data(using: .utf8)!, roomToken: "testRoom", whisperTo: nil, whisperMods: false, fileIds: nil, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: false, forceBlinded: false ), using: dependencies ) - }.to(throwError(OpenGroupAPIError.signingFailed)) + }.to(throwError(SOGSError.signingFailed)) expect(preparedRequest).to(beNil()) } @@ -975,24 +987,23 @@ class OpenGroupAPISpec: QuickSpec { mockGeneralCache.when { $0.ed25519Seed }.thenReturn([]) expect { - preparedRequest = try OpenGroupAPI.preparedSend( + preparedRequest = try Network.SOGS.preparedSend( plaintext: "test".data(using: .utf8)!, roomToken: "testRoom", whisperTo: nil, whisperMods: false, fileIds: nil, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: false, forceBlinded: false ), using: dependencies ) - }.to(throwError(OpenGroupAPIError.signingFailed)) + }.to(throwError(SOGSError.signingFailed)) expect(preparedRequest).to(beNil()) } @@ -1004,24 +1015,23 @@ class OpenGroupAPISpec: QuickSpec { .thenReturn(nil) expect { - preparedRequest = try OpenGroupAPI.preparedSend( + preparedRequest = try Network.SOGS.preparedSend( plaintext: "test".data(using: .utf8)!, roomToken: "testRoom", whisperTo: nil, whisperMods: false, fileIds: nil, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: false, forceBlinded: false ), using: dependencies ) - }.to(throwError(OpenGroupAPIError.signingFailed)) + }.to(throwError(SOGSError.signingFailed)) expect(preparedRequest).to(beNil()) } @@ -1032,27 +1042,26 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ------ signs the message correctly it("signs the message correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedSend( + preparedRequest = try Network.SOGS.preparedSend( plaintext: "test".data(using: .utf8)!, roomToken: "testRoom", whisperTo: nil, whisperMods: false, fileIds: nil, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs, .blind] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: true, forceBlinded: false ), using: dependencies ) }.toNot(throwError()) - let requestBody: OpenGroupAPI.SendMessageRequest? = try? preparedRequest?.body? - .decoded(as: OpenGroupAPI.SendMessageRequest.self, using: dependencies) + let requestBody: Network.SOGS.SendSOGSMessageRequest? = try? preparedRequest?.body? + .decoded(as: Network.SOGS.SendSOGSMessageRequest.self, using: dependencies) expect(requestBody?.data).to(equal("test".data(using: .utf8))) expect(requestBody?.signature).to(equal("TestSogsSignature".data(using: .utf8))) } @@ -1062,24 +1071,23 @@ class OpenGroupAPISpec: QuickSpec { mockGeneralCache.when { $0.ed25519SecretKey }.thenReturn([]) expect { - preparedRequest = try OpenGroupAPI.preparedSend( + preparedRequest = try Network.SOGS.preparedSend( plaintext: "test".data(using: .utf8)!, roomToken: "testRoom", whisperTo: nil, whisperMods: false, fileIds: nil, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs, .blind] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: true, forceBlinded: false ), using: dependencies ) - }.to(throwError(OpenGroupAPIError.signingFailed)) + }.to(throwError(SOGSError.signingFailed)) expect(preparedRequest).to(beNil()) } @@ -1089,24 +1097,23 @@ class OpenGroupAPISpec: QuickSpec { mockGeneralCache.when { $0.ed25519Seed }.thenReturn([]) expect { - preparedRequest = try OpenGroupAPI.preparedSend( + preparedRequest = try Network.SOGS.preparedSend( plaintext: "test".data(using: .utf8)!, roomToken: "testRoom", whisperTo: nil, whisperMods: false, fileIds: nil, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs, .blind] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: true, forceBlinded: false ), using: dependencies ) - }.to(throwError(OpenGroupAPIError.signingFailed)) + }.to(throwError(SOGSError.signingFailed)) expect(preparedRequest).to(beNil()) } @@ -1118,24 +1125,23 @@ class OpenGroupAPISpec: QuickSpec { .thenReturn(nil) expect { - preparedRequest = try OpenGroupAPI.preparedSend( + preparedRequest = try Network.SOGS.preparedSend( plaintext: "test".data(using: .utf8)!, roomToken: "testRoom", whisperTo: nil, whisperMods: false, fileIds: nil, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs, .blind] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: true, forceBlinded: false ), using: dependencies ) - }.to(throwError(OpenGroupAPIError.signingFailed)) + }.to(throwError(SOGSError.signingFailed)) expect(preparedRequest).to(beNil()) } @@ -1144,21 +1150,20 @@ class OpenGroupAPISpec: QuickSpec { // MARK: -- when preparing an individual message request context("when preparing an individual message request") { - var preparedRequest: Network.PreparedRequest? + var preparedRequest: Network.PreparedRequest? // MARK: ---- generates the request correctly it("generates the request correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedMessage( + preparedRequest = try Network.SOGS.preparedMessage( id: 123, roomToken: "testRoom", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -1177,18 +1182,17 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ---- generates the request correctly it("generates the request correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedMessageUpdate( + preparedRequest = try Network.SOGS.preparedMessageUpdate( id: 123, plaintext: "test".data(using: .utf8)!, fileIds: nil, roomToken: "testRoom", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -1204,26 +1208,25 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ------ signs the message correctly it("signs the message correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedMessageUpdate( + preparedRequest = try Network.SOGS.preparedMessageUpdate( id: 123, plaintext: "test".data(using: .utf8)!, fileIds: nil, roomToken: "testRoom", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: false, forceBlinded: false ), using: dependencies ) }.toNot(throwError()) - let requestBody: OpenGroupAPI.UpdateMessageRequest? = try? preparedRequest?.body? - .decoded(as: OpenGroupAPI.UpdateMessageRequest.self, using: dependencies) + let requestBody: Network.SOGS.UpdateMessageRequest? = try? preparedRequest?.body? + .decoded(as: Network.SOGS.UpdateMessageRequest.self, using: dependencies) expect(requestBody?.data).to(equal("test".data(using: .utf8))) expect(requestBody?.signature).to(equal("TestStandardSignature".data(using: .utf8))) } @@ -1233,23 +1236,22 @@ class OpenGroupAPISpec: QuickSpec { mockGeneralCache.when { $0.ed25519SecretKey }.thenReturn([]) expect { - preparedRequest = try OpenGroupAPI.preparedMessageUpdate( + preparedRequest = try Network.SOGS.preparedMessageUpdate( id: 123, plaintext: "test".data(using: .utf8)!, fileIds: nil, roomToken: "testRoom", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: false, forceBlinded: false ), using: dependencies ) - }.to(throwError(OpenGroupAPIError.signingFailed)) + }.to(throwError(SOGSError.signingFailed)) expect(preparedRequest).to(beNil()) } @@ -1259,23 +1261,22 @@ class OpenGroupAPISpec: QuickSpec { mockGeneralCache.when { $0.ed25519Seed }.thenReturn([]) expect { - preparedRequest = try OpenGroupAPI.preparedMessageUpdate( + preparedRequest = try Network.SOGS.preparedMessageUpdate( id: 123, plaintext: "test".data(using: .utf8)!, fileIds: nil, roomToken: "testRoom", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: false, forceBlinded: false ), using: dependencies ) - }.to(throwError(OpenGroupAPIError.signingFailed)) + }.to(throwError(SOGSError.signingFailed)) expect(preparedRequest).to(beNil()) } @@ -1287,23 +1288,22 @@ class OpenGroupAPISpec: QuickSpec { .thenReturn(nil) expect { - preparedRequest = try OpenGroupAPI.preparedMessageUpdate( + preparedRequest = try Network.SOGS.preparedMessageUpdate( id: 123, plaintext: "test".data(using: .utf8)!, fileIds: nil, roomToken: "testRoom", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: false, forceBlinded: false ), using: dependencies ) - }.to(throwError(OpenGroupAPIError.signingFailed)) + }.to(throwError(SOGSError.signingFailed)) expect(preparedRequest).to(beNil()) } @@ -1314,26 +1314,25 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ------ signs the message correctly it("signs the message correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedMessageUpdate( + preparedRequest = try Network.SOGS.preparedMessageUpdate( id: 123, plaintext: "test".data(using: .utf8)!, fileIds: nil, roomToken: "testRoom", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs, .blind] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: true, forceBlinded: false ), using: dependencies ) }.toNot(throwError()) - let requestBody: OpenGroupAPI.UpdateMessageRequest? = try? preparedRequest?.body? - .decoded(as: OpenGroupAPI.UpdateMessageRequest.self, using: dependencies) + let requestBody: Network.SOGS.UpdateMessageRequest? = try? preparedRequest?.body? + .decoded(as: Network.SOGS.UpdateMessageRequest.self, using: dependencies) expect(requestBody?.data).to(equal("test".data(using: .utf8))) expect(requestBody?.signature).to(equal("TestSogsSignature".data(using: .utf8))) } @@ -1343,23 +1342,22 @@ class OpenGroupAPISpec: QuickSpec { mockGeneralCache.when { $0.ed25519SecretKey }.thenReturn([]) expect { - preparedRequest = try OpenGroupAPI.preparedMessageUpdate( + preparedRequest = try Network.SOGS.preparedMessageUpdate( id: 123, plaintext: "test".data(using: .utf8)!, fileIds: nil, roomToken: "testRoom", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs, .blind] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: true, forceBlinded: false ), using: dependencies ) - }.to(throwError(OpenGroupAPIError.signingFailed)) + }.to(throwError(SOGSError.signingFailed)) expect(preparedRequest).to(beNil()) } @@ -1369,23 +1367,22 @@ class OpenGroupAPISpec: QuickSpec { mockGeneralCache.when { $0.ed25519Seed }.thenReturn([]) expect { - preparedRequest = try OpenGroupAPI.preparedMessageUpdate( + preparedRequest = try Network.SOGS.preparedMessageUpdate( id: 123, plaintext: "test".data(using: .utf8)!, fileIds: nil, roomToken: "testRoom", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs, .blind] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: true, forceBlinded: false ), using: dependencies ) - }.to(throwError(OpenGroupAPIError.signingFailed)) + }.to(throwError(SOGSError.signingFailed)) expect(preparedRequest).to(beNil()) } @@ -1397,23 +1394,22 @@ class OpenGroupAPISpec: QuickSpec { .thenReturn(nil) expect { - preparedRequest = try OpenGroupAPI.preparedMessageUpdate( + preparedRequest = try Network.SOGS.preparedMessageUpdate( id: 123, plaintext: "test".data(using: .utf8)!, fileIds: nil, roomToken: "testRoom", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs, .blind] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: true, forceBlinded: false ), using: dependencies ) - }.to(throwError(OpenGroupAPIError.signingFailed)) + }.to(throwError(SOGSError.signingFailed)) expect(preparedRequest).to(beNil()) } @@ -1427,16 +1423,15 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ---- generates the request correctly it("generates the request correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedMessageDelete( + preparedRequest = try Network.SOGS.preparedMessageDelete( id: 123, roomToken: "testRoom", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -1455,16 +1450,15 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ---- generates the request correctly it("generates the request correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedMessagesDeleteAll( + preparedRequest = try Network.SOGS.preparedMessagesDeleteAll( sessionId: "testUserId", roomToken: "testRoom", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -1483,16 +1477,15 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ---- generates the request correctly it("generates the request correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedPinMessage( + preparedRequest = try Network.SOGS.preparedPinMessage( id: 123, roomToken: "testRoom", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -1511,16 +1504,15 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ---- generates the request correctly it("generates the request correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedUnpinMessage( + preparedRequest = try Network.SOGS.preparedUnpinMessage( id: 123, roomToken: "testRoom", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -1539,15 +1531,14 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ---- generates the request correctly it("generates the request correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedUnpinAll( + preparedRequest = try Network.SOGS.preparedUnpinAll( roomToken: "testRoom", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -1566,16 +1557,15 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ---- generates the request correctly it("generates the request correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedUpload( + preparedRequest = try Network.SOGS.preparedUpload( data: Data([1, 2, 3]), roomToken: "testRoom", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -1593,23 +1583,22 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ---- generates the download url string correctly it("generates the download url string correctly") { - expect(OpenGroupAPI.downloadUrlString(for: "1", server: "testserver", roomToken: "roomToken")) + expect(Network.SOGS.downloadUrlString(for: "1", server: "testserver", roomToken: "roomToken")) .to(equal("testserver/room/roomToken/file/1")) } // MARK: ---- generates the download destination correctly when given an id it("generates the download destination correctly when given an id") { expect { - preparedRequest = try OpenGroupAPI.preparedDownload( + preparedRequest = try Network.SOGS.preparedDownload( fileId: "1", roomToken: "roomToken", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -1626,19 +1615,42 @@ class OpenGroupAPISpec: QuickSpec { ])) } + // MARK: ---- generates the download destination correctly when given an id and skips adding request headers + it("generates the download destination correctly when given an id and skips adding request headers") { + expect { + preparedRequest = try Network.SOGS.preparedDownload( + fileId: "1", + roomToken: "roomToken", + authMethod: Authentication.community( + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, + forceBlinded: false + ), + skipAuthentication: true, + using: dependencies + ) + }.toNot(throwError()) + + expect(preparedRequest?.path).to(equal("/room/roomToken/file/1")) + expect(preparedRequest?.method.rawValue).to(equal("GET")) + expect(preparedRequest?.headers).to(beEmpty()) + } + // MARK: ---- generates the download request correctly when given a URL it("generates the download request correctly when given a URL") { expect { - preparedRequest = try OpenGroupAPI.preparedDownload( + preparedRequest = try Network.SOGS.preparedDownload( url: URL(string: "http://oxen.io/room/roomToken/file/1")!, roomToken: "roomToken", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -1658,19 +1670,18 @@ class OpenGroupAPISpec: QuickSpec { // MARK: -- when preparing an inbox request context("when preparing an inbox request") { - @TestState var preparedRequest: Network.PreparedRequest<[OpenGroupAPI.DirectMessage]?>? + @TestState var preparedRequest: Network.PreparedRequest<[Network.SOGS.DirectMessage]?>? // MARK: ---- generates the request correctly it("generates the request correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedInbox( + preparedRequest = try Network.SOGS.preparedInbox( authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -1684,20 +1695,19 @@ class OpenGroupAPISpec: QuickSpec { // MARK: -- when preparing an inbox since request context("when preparing an inbox since request") { - @TestState var preparedRequest: Network.PreparedRequest<[OpenGroupAPI.DirectMessage]?>? + @TestState var preparedRequest: Network.PreparedRequest<[Network.SOGS.DirectMessage]?>? // MARK: ---- generates the request correctly it("generates the request correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedInboxSince( + preparedRequest = try Network.SOGS.preparedInboxSince( id: 1, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -1711,19 +1721,18 @@ class OpenGroupAPISpec: QuickSpec { // MARK: -- when preparing a clear inbox request context("when preparing an inbox since request") { - @TestState var preparedRequest: Network.PreparedRequest? + @TestState var preparedRequest: Network.PreparedRequest? // MARK: ---- generates the request correctly it("generates the request correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedClearInbox( + preparedRequest = try Network.SOGS.preparedClearInbox( authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -1737,21 +1746,20 @@ class OpenGroupAPISpec: QuickSpec { // MARK: -- when preparing a send direct message request context("when preparing a send direct message request") { - @TestState var preparedRequest: Network.PreparedRequest? + @TestState var preparedRequest: Network.PreparedRequest? // MARK: ---- generates the request correctly it("generates the request correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedSend( + preparedRequest = try Network.SOGS.preparedSend( ciphertext: "test".data(using: .utf8)!, toInboxFor: "testUserId", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -1764,7 +1772,7 @@ class OpenGroupAPISpec: QuickSpec { } } - describe("an OpenGroupAPI") { + describe("an Network.SOGS") { // MARK: -- when preparing a ban user request context("when preparing a ban user request") { @TestState var preparedRequest: Network.PreparedRequest? @@ -1772,17 +1780,16 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ---- generates the request correctly it("generates the request correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedUserBan( + preparedRequest = try Network.SOGS.preparedUserBan( sessionId: "testUserId", for: nil, from: nil, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -1796,25 +1803,24 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ---- does a global ban if no room tokens are provided it("does a global ban if no room tokens are provided") { expect { - preparedRequest = try OpenGroupAPI.preparedUserBan( + preparedRequest = try Network.SOGS.preparedUserBan( sessionId: "testUserId", for: nil, from: nil, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies ) }.toNot(throwError()) - let requestBody: OpenGroupAPI.UserBanRequest? = try? preparedRequest?.body? - .decoded(as: OpenGroupAPI.UserBanRequest.self, using: dependencies) + let requestBody: Network.SOGS.UserBanRequest? = try? preparedRequest?.body? + .decoded(as: Network.SOGS.UserBanRequest.self, using: dependencies) expect(requestBody?.global).to(beTrue()) expect(requestBody?.rooms).to(beNil()) } @@ -1822,25 +1828,24 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ---- does room specific bans if room tokens are provided it("does room specific bans if room tokens are provided") { expect { - preparedRequest = try OpenGroupAPI.preparedUserBan( + preparedRequest = try Network.SOGS.preparedUserBan( sessionId: "testUserId", for: nil, from: ["testRoom"], authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies ) }.toNot(throwError()) - let requestBody: OpenGroupAPI.UserBanRequest? = try? preparedRequest?.body? - .decoded(as: OpenGroupAPI.UserBanRequest.self, using: dependencies) + let requestBody: Network.SOGS.UserBanRequest? = try? preparedRequest?.body? + .decoded(as: Network.SOGS.UserBanRequest.self, using: dependencies) expect(requestBody?.global).to(beNil()) expect(requestBody?.rooms).to(equal(["testRoom"])) } @@ -1853,16 +1858,15 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ---- generates the request correctly it("generates the request correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedUserUnban( + preparedRequest = try Network.SOGS.preparedUserUnban( sessionId: "testUserId", from: nil, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -1876,24 +1880,23 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ---- does a global unban if no room tokens are provided it("does a global unban if no room tokens are provided") { expect { - preparedRequest = try OpenGroupAPI.preparedUserUnban( + preparedRequest = try Network.SOGS.preparedUserUnban( sessionId: "testUserId", from: nil, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies ) }.toNot(throwError()) - let requestBody: OpenGroupAPI.UserUnbanRequest? = try? preparedRequest?.body? - .decoded(as: OpenGroupAPI.UserUnbanRequest.self, using: dependencies) + let requestBody: Network.SOGS.UserUnbanRequest? = try? preparedRequest?.body? + .decoded(as: Network.SOGS.UserUnbanRequest.self, using: dependencies) expect(requestBody?.global).to(beTrue()) expect(requestBody?.rooms).to(beNil()) } @@ -1901,24 +1904,23 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ---- does room specific unbans if room tokens are provided it("does room specific unbans if room tokens are provided") { expect { - preparedRequest = try OpenGroupAPI.preparedUserUnban( + preparedRequest = try Network.SOGS.preparedUserUnban( sessionId: "testUserId", from: ["testRoom"], authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies ) }.toNot(throwError()) - let requestBody: OpenGroupAPI.UserUnbanRequest? = try? preparedRequest?.body? - .decoded(as: OpenGroupAPI.UserUnbanRequest.self, using: dependencies) + let requestBody: Network.SOGS.UserUnbanRequest? = try? preparedRequest?.body? + .decoded(as: Network.SOGS.UserUnbanRequest.self, using: dependencies) expect(requestBody?.global).to(beNil()) expect(requestBody?.rooms).to(equal(["testRoom"])) } @@ -1931,19 +1933,18 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ---- generates the request correctly it("generates the request correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedUserModeratorUpdate( + preparedRequest = try Network.SOGS.preparedUserModeratorUpdate( sessionId: "testUserId", moderator: true, admin: nil, visible: true, for: nil, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -1957,27 +1958,26 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ---- does a global update if no room tokens are provided it("does a global update if no room tokens are provided") { expect { - preparedRequest = try OpenGroupAPI.preparedUserModeratorUpdate( + preparedRequest = try Network.SOGS.preparedUserModeratorUpdate( sessionId: "testUserId", moderator: true, admin: nil, visible: true, for: nil, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies ) }.toNot(throwError()) - let requestBody: OpenGroupAPI.UserModeratorRequest? = try? preparedRequest?.body? - .decoded(as: OpenGroupAPI.UserModeratorRequest.self, using: dependencies) + let requestBody: Network.SOGS.UserModeratorRequest? = try? preparedRequest?.body? + .decoded(as: Network.SOGS.UserModeratorRequest.self, using: dependencies) expect(requestBody?.global).to(beTrue()) expect(requestBody?.rooms).to(beNil()) } @@ -1985,27 +1985,26 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ---- does room specific updates if room tokens are provided it("does room specific updates if room tokens are provided") { expect { - preparedRequest = try OpenGroupAPI.preparedUserModeratorUpdate( + preparedRequest = try Network.SOGS.preparedUserModeratorUpdate( sessionId: "testUserId", moderator: true, admin: nil, visible: true, for: ["testRoom"], authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies ) }.toNot(throwError()) - let requestBody: OpenGroupAPI.UserModeratorRequest? = try? preparedRequest?.body? - .decoded(as: OpenGroupAPI.UserModeratorRequest.self, using: dependencies) + let requestBody: Network.SOGS.UserModeratorRequest? = try? preparedRequest?.body? + .decoded(as: Network.SOGS.UserModeratorRequest.self, using: dependencies) expect(requestBody?.global).to(beNil()) expect(requestBody?.rooms).to(equal(["testRoom"])) } @@ -2013,19 +2012,18 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ---- fails if neither moderator or admin are set it("fails if neither moderator or admin are set") { expect { - preparedRequest = try OpenGroupAPI.preparedUserModeratorUpdate( + preparedRequest = try Network.SOGS.preparedUserModeratorUpdate( sessionId: "testUserId", moderator: nil, admin: nil, visible: true, for: nil, authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -2038,21 +2036,20 @@ class OpenGroupAPISpec: QuickSpec { // MARK: -- when preparing a ban and delete all request context("when preparing a ban and delete all request") { - @TestState var preparedRequest: Network.PreparedRequest>? + @TestState var preparedRequest: Network.PreparedRequest>? // MARK: ---- generates the request correctly it("generates the request correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedUserBanAndDeleteAllMessages( + preparedRequest = try Network.SOGS.preparedUserBanAndDeleteAllMessages( sessionId: "testUserId", roomToken: "testRoom", authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -2062,37 +2059,36 @@ class OpenGroupAPISpec: QuickSpec { expect(preparedRequest?.path).to(equal("/sequence")) expect(preparedRequest?.method.rawValue).to(equal("POST")) expect(preparedRequest?.batchEndpoints.count).to(equal(2)) - expect(preparedRequest?.batchEndpoints[test: 0].asType(OpenGroupAPI.Endpoint.self)) + expect(preparedRequest?.batchEndpoints[test: 0].asType(Network.SOGS.Endpoint.self)) .to(equal(.userBan("testUserId"))) - expect(preparedRequest?.batchEndpoints[test: 1].asType(OpenGroupAPI.Endpoint.self)) + expect(preparedRequest?.batchEndpoints[test: 1].asType(Network.SOGS.Endpoint.self)) .to(equal(.roomDeleteMessages("testRoom", sessionId: "testUserId"))) } } } - describe("an OpenGroupAPI") { + describe("an Network.SOGS") { // MARK: -- when signing context("when signing") { - @TestState var preparedRequest: Network.PreparedRequest<[OpenGroupAPI.Room]>? + @TestState var preparedRequest: Network.PreparedRequest<[Network.SOGS.Room]>? // MARK: ---- fails when there is no ed25519SecretKey it("fails when there is no ed25519SecretKey") { mockGeneralCache.when { $0.ed25519SecretKey }.thenReturn([]) expect { - preparedRequest = try OpenGroupAPI.preparedRooms( + preparedRequest = try Network.SOGS.preparedRooms( authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), using: dependencies ) - }.to(throwError(OpenGroupAPIError.signingFailed)) + }.to(throwError(SOGSError.signingFailed)) expect(preparedRequest).to(beNil()) } @@ -2102,14 +2098,13 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ------ signs correctly it("signs correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedRooms( + preparedRequest = try Network.SOGS.preparedRooms( authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: false, forceBlinded: false ), using: dependencies @@ -2138,19 +2133,18 @@ class OpenGroupAPISpec: QuickSpec { .thenThrow(CryptoError.failedToGenerateOutput) expect { - preparedRequest = try OpenGroupAPI.preparedRooms( + preparedRequest = try Network.SOGS.preparedRooms( authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: false, forceBlinded: false ), using: dependencies ) - }.to(throwError(OpenGroupAPIError.signingFailed)) + }.to(throwError(SOGSError.signingFailed)) expect(preparedRequest).to(beNil()) } @@ -2161,14 +2155,13 @@ class OpenGroupAPISpec: QuickSpec { // MARK: ------ signs correctly it("signs correctly") { expect { - preparedRequest = try OpenGroupAPI.preparedRooms( + preparedRequest = try Network.SOGS.preparedRooms( authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs, .blind] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: true, forceBlinded: false ), using: dependencies @@ -2197,19 +2190,18 @@ class OpenGroupAPISpec: QuickSpec { .thenReturn(nil) expect { - preparedRequest = try OpenGroupAPI.preparedRooms( + preparedRequest = try Network.SOGS.preparedRooms( authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs, .blind] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: true, forceBlinded: false ), using: dependencies ) - }.to(throwError(OpenGroupAPIError.signingFailed)) + }.to(throwError(SOGSError.signingFailed)) expect(preparedRequest).to(beNil()) } @@ -2221,19 +2213,18 @@ class OpenGroupAPISpec: QuickSpec { .thenReturn(nil) expect { - preparedRequest = try OpenGroupAPI.preparedRooms( + preparedRequest = try Network.SOGS.preparedRooms( authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [.sogs, .blind] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: true, + supportsBlinding: true, forceBlinded: false ), using: dependencies ) - }.to(throwError(OpenGroupAPIError.signingFailed)) + }.to(throwError(SOGSError.signingFailed)) expect(preparedRequest).to(beNil()) } @@ -2242,29 +2233,59 @@ class OpenGroupAPISpec: QuickSpec { // MARK: -- when sending context("when sending") { - @TestState var preparedRequest: Network.PreparedRequest<[OpenGroupAPI.Room]>? + @TestState var preparedRequest: Network.PreparedRequest<[Network.SOGS.Room]>? beforeEach { mockNetwork .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } - .thenReturn(MockNetwork.response(type: [OpenGroupAPI.Room].self)) + .thenReturn(MockNetwork.response(type: [Network.SOGS.Room].self)) } // MARK: ---- triggers sending correctly it("triggers sending correctly") { - var response: (info: ResponseInfoType, data: [OpenGroupAPI.Room])? + var response: (info: ResponseInfoType, data: [Network.SOGS.Room])? expect { - preparedRequest = try OpenGroupAPI.preparedRooms( + preparedRequest = try Network.SOGS.preparedRooms( authMethod: Authentication.community( - info: LibSession.OpenGroupCapabilityInfo( - roomToken: "", - server: "testserver", - publicKey: TestConstants.publicKey, - capabilities: [] - ), + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, + forceBlinded: false + ), + using: dependencies + ) + }.toNot(throwError()) + + preparedRequest? + .send(using: dependencies) + .handleEvents(receiveOutput: { result in response = result }) + .mapError { error.setting(to: $0) } + .sinkAndStore(in: &disposables) + + expect(preparedRequest?.headers).toNot(beEmpty()) + + expect(response).toNot(beNil()) + expect(error).to(beNil()) + } + + // MARK: ---- triggers sending correctly without headers + it("triggers sending correctly without headers") { + var response: (info: ResponseInfoType, data: [Network.SOGS.Room])? + + expect { + preparedRequest = try Network.SOGS.preparedRooms( + authMethod: Authentication.community( + roomToken: "", + server: "testserver", + publicKey: TestConstants.publicKey, + hasCapabilities: false, + supportsBlinding: false, forceBlinded: false ), + skipAuthentication: true, using: dependencies ) }.toNot(throwError()) @@ -2274,6 +2295,8 @@ class OpenGroupAPISpec: QuickSpec { .handleEvents(receiveOutput: { result in response = result }) .mapError { error.setting(to: $0) } .sinkAndStore(in: &disposables) + + expect(preparedRequest?.headers).to(beEmpty()) expect(response).toNot(beNil()) expect(error).to(beNil()) @@ -2290,15 +2313,15 @@ extension Network.BatchResponse { static let mockCapabilitiesAndRoomResponse: AnyPublisher<(ResponseInfoType, Data?), Error> = MockNetwork.batchResponseData( with: [ - (OpenGroupAPI.Endpoint.capabilities, OpenGroupAPI.Capabilities.mockBatchSubResponse()), - (OpenGroupAPI.Endpoint.room("testRoom"), OpenGroupAPI.Room.mockBatchSubResponse()) + (Network.SOGS.Endpoint.capabilities, Network.SOGS.CapabilitiesResponse.mockBatchSubResponse()), + (Network.SOGS.Endpoint.room("testRoom"), Network.SOGS.Room.mockBatchSubResponse()) ] ) static let mockCapabilitiesAndRoomsResponse: AnyPublisher<(ResponseInfoType, Data?), Error> = MockNetwork.batchResponseData( with: [ - (OpenGroupAPI.Endpoint.capabilities, OpenGroupAPI.Capabilities.mockBatchSubResponse()), - (OpenGroupAPI.Endpoint.rooms, [OpenGroupAPI.Room].mockBatchSubResponse()) + (Network.SOGS.Endpoint.capabilities, Network.SOGS.CapabilitiesResponse.mockBatchSubResponse()), + (Network.SOGS.Endpoint.rooms, [Network.SOGS.Room].mockBatchSubResponse()) ] ) @@ -2306,22 +2329,22 @@ extension Network.BatchResponse { static let mockCapabilitiesAndBanResponse: AnyPublisher<(ResponseInfoType, Data?), Error> = MockNetwork.batchResponseData( with: [ - (OpenGroupAPI.Endpoint.capabilities, OpenGroupAPI.Capabilities.mockBatchSubResponse()), - (OpenGroupAPI.Endpoint.userBan(""), NoResponse.mockBatchSubResponse()) + (Network.SOGS.Endpoint.capabilities, Network.SOGS.CapabilitiesResponse.mockBatchSubResponse()), + (Network.SOGS.Endpoint.userBan(""), NoResponse.mockBatchSubResponse()) ] ) static let mockBanAndRoomResponse: AnyPublisher<(ResponseInfoType, Data?), Error> = MockNetwork.batchResponseData( with: [ - (OpenGroupAPI.Endpoint.userBan(""), NoResponse.mockBatchSubResponse()), - (OpenGroupAPI.Endpoint.room("testRoom"), OpenGroupAPI.Room.mockBatchSubResponse()) + (Network.SOGS.Endpoint.userBan(""), NoResponse.mockBatchSubResponse()), + (Network.SOGS.Endpoint.room("testRoom"), Network.SOGS.Room.mockBatchSubResponse()) ] ) static let mockBanAndRoomsResponse: AnyPublisher<(ResponseInfoType, Data?), Error> = MockNetwork.batchResponseData( with: [ - (OpenGroupAPI.Endpoint.userBan(""), NoResponse.mockBatchSubResponse()), - (OpenGroupAPI.Endpoint.rooms, [OpenGroupAPI.Room].mockBatchSubResponse()) + (Network.SOGS.Endpoint.userBan(""), NoResponse.mockBatchSubResponse()), + (Network.SOGS.Endpoint.rooms, [Network.SOGS.Room].mockBatchSubResponse()) ] ) } diff --git a/SessionMessagingKitTests/Open Groups/Types/PersonalizationSpec.swift b/SessionNetworkingKitTests/SOGS/Types/PersonalizationSpec.swift similarity index 78% rename from SessionMessagingKitTests/Open Groups/Types/PersonalizationSpec.swift rename to SessionNetworkingKitTests/SOGS/Types/PersonalizationSpec.swift index ab6c201a14..5b24ce72de 100644 --- a/SessionMessagingKitTests/Open Groups/Types/PersonalizationSpec.swift +++ b/SessionNetworkingKitTests/SOGS/Types/PersonalizationSpec.swift @@ -5,7 +5,7 @@ import Foundation import Quick import Nimble -@testable import SessionMessagingKit +@testable import SessionNetworkingKit class PersonalizationSpec: QuickSpec { override class func spec() { @@ -13,9 +13,9 @@ class PersonalizationSpec: QuickSpec { describe("a Personalization") { // MARK: -- generates bytes correctly it("generates bytes correctly") { - expect(OpenGroupAPI.Personalization.sharedKeys.bytes) + expect(Network.SOGS.Personalization.sharedKeys.bytes) .to(equal([115, 111, 103, 115, 46, 115, 104, 97, 114, 101, 100, 95, 107, 101, 121, 115])) - expect(OpenGroupAPI.Personalization.authHeader.bytes) + expect(Network.SOGS.Personalization.authHeader.bytes) .to(equal([115, 111, 103, 115, 46, 97, 117, 116, 104, 95, 104, 101, 97, 100, 101, 114])) } } diff --git a/SessionMessagingKitTests/Open Groups/Types/SOGSEndpointSpec.swift b/SessionNetworkingKitTests/SOGS/Types/SOGSEndpointSpec.swift similarity index 56% rename from SessionMessagingKitTests/Open Groups/Types/SOGSEndpointSpec.swift rename to SessionNetworkingKitTests/SOGS/Types/SOGSEndpointSpec.swift index 88e568f21b..33b0929de7 100644 --- a/SessionMessagingKitTests/Open Groups/Types/SOGSEndpointSpec.swift +++ b/SessionNetworkingKitTests/SOGS/Types/SOGSEndpointSpec.swift @@ -1,13 +1,11 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation -import SessionSnodeKit -import SessionUtilitiesKit import Quick import Nimble -@testable import SessionMessagingKit +@testable import SessionNetworkingKit class SOGSEndpointSpec: QuickSpec { override class func spec() { @@ -15,12 +13,12 @@ class SOGSEndpointSpec: QuickSpec { describe("a SOGSEndpoint") { // MARK: -- provides the correct batch request variant it("provides the correct batch request variant") { - expect(OpenGroupAPI.Endpoint.batchRequestVariant).to(equal(.sogs)) + expect(Network.SOGS.Endpoint.batchRequestVariant).to(equal(.sogs)) } // MARK: -- excludes the correct headers from batch sub request it("excludes the correct headers from batch sub request") { - expect(OpenGroupAPI.Endpoint.excludedSubRequestHeaders).to(equal([ + expect(Network.SOGS.Endpoint.excludedSubRequestHeaders).to(equal([ HTTPHeader.sogsPubKey, HTTPHeader.sogsTimestamp, HTTPHeader.sogsNonce, @@ -32,53 +30,53 @@ class SOGSEndpointSpec: QuickSpec { it("generates the path value correctly") { // Utility - expect(OpenGroupAPI.Endpoint.onion.path).to(equal("oxen/v4/lsrpc")) - expect(OpenGroupAPI.Endpoint.batch.path).to(equal("batch")) - expect(OpenGroupAPI.Endpoint.sequence.path).to(equal("sequence")) - expect(OpenGroupAPI.Endpoint.capabilities.path).to(equal("capabilities")) + expect(Network.SOGS.Endpoint.onion.path).to(equal("oxen/v4/lsrpc")) + expect(Network.SOGS.Endpoint.batch.path).to(equal("batch")) + expect(Network.SOGS.Endpoint.sequence.path).to(equal("sequence")) + expect(Network.SOGS.Endpoint.capabilities.path).to(equal("capabilities")) // Rooms - expect(OpenGroupAPI.Endpoint.rooms.path).to(equal("rooms")) - expect(OpenGroupAPI.Endpoint.room("test").path).to(equal("room/test")) - expect(OpenGroupAPI.Endpoint.roomPollInfo("test", 123).path).to(equal("room/test/pollInfo/123")) + expect(Network.SOGS.Endpoint.rooms.path).to(equal("rooms")) + expect(Network.SOGS.Endpoint.room("test").path).to(equal("room/test")) + expect(Network.SOGS.Endpoint.roomPollInfo("test", 123).path).to(equal("room/test/pollInfo/123")) // Messages - expect(OpenGroupAPI.Endpoint.roomMessage("test").path).to(equal("room/test/message")) - expect(OpenGroupAPI.Endpoint.roomMessageIndividual("test", id: 123).path).to(equal("room/test/message/123")) - expect(OpenGroupAPI.Endpoint.roomMessagesRecent("test").path).to(equal("room/test/messages/recent")) - expect(OpenGroupAPI.Endpoint.roomMessagesBefore("test", id: 123).path).to(equal("room/test/messages/before/123")) - expect(OpenGroupAPI.Endpoint.roomMessagesSince("test", seqNo: 123).path) + expect(Network.SOGS.Endpoint.roomMessage("test").path).to(equal("room/test/message")) + expect(Network.SOGS.Endpoint.roomMessageIndividual("test", id: 123).path).to(equal("room/test/message/123")) + expect(Network.SOGS.Endpoint.roomMessagesRecent("test").path).to(equal("room/test/messages/recent")) + expect(Network.SOGS.Endpoint.roomMessagesBefore("test", id: 123).path).to(equal("room/test/messages/before/123")) + expect(Network.SOGS.Endpoint.roomMessagesSince("test", seqNo: 123).path) .to(equal("room/test/messages/since/123")) - expect(OpenGroupAPI.Endpoint.roomDeleteMessages("test", sessionId: "testId").path) + expect(Network.SOGS.Endpoint.roomDeleteMessages("test", sessionId: "testId").path) .to(equal("room/test/all/testId")) // Pinning - expect(OpenGroupAPI.Endpoint.roomPinMessage("test", id: 123).path).to(equal("room/test/pin/123")) - expect(OpenGroupAPI.Endpoint.roomUnpinMessage("test", id: 123).path).to(equal("room/test/unpin/123")) - expect(OpenGroupAPI.Endpoint.roomUnpinAll("test").path).to(equal("room/test/unpin/all")) + expect(Network.SOGS.Endpoint.roomPinMessage("test", id: 123).path).to(equal("room/test/pin/123")) + expect(Network.SOGS.Endpoint.roomUnpinMessage("test", id: 123).path).to(equal("room/test/unpin/123")) + expect(Network.SOGS.Endpoint.roomUnpinAll("test").path).to(equal("room/test/unpin/all")) // Files - expect(OpenGroupAPI.Endpoint.roomFile("test").path).to(equal("room/test/file")) - expect(OpenGroupAPI.Endpoint.roomFileIndividual("test", "123").path).to(equal("room/test/file/123")) + expect(Network.SOGS.Endpoint.roomFile("test").path).to(equal("room/test/file")) + expect(Network.SOGS.Endpoint.roomFileIndividual("test", "123").path).to(equal("room/test/file/123")) // Inbox/Outbox (Message Requests) - expect(OpenGroupAPI.Endpoint.inbox.path).to(equal("inbox")) - expect(OpenGroupAPI.Endpoint.inboxSince(id: 123).path).to(equal("inbox/since/123")) - expect(OpenGroupAPI.Endpoint.inboxFor(sessionId: "test").path).to(equal("inbox/test")) + expect(Network.SOGS.Endpoint.inbox.path).to(equal("inbox")) + expect(Network.SOGS.Endpoint.inboxSince(id: 123).path).to(equal("inbox/since/123")) + expect(Network.SOGS.Endpoint.inboxFor(sessionId: "test").path).to(equal("inbox/test")) - expect(OpenGroupAPI.Endpoint.outbox.path).to(equal("outbox")) - expect(OpenGroupAPI.Endpoint.outboxSince(id: 123).path).to(equal("outbox/since/123")) + expect(Network.SOGS.Endpoint.outbox.path).to(equal("outbox")) + expect(Network.SOGS.Endpoint.outboxSince(id: 123).path).to(equal("outbox/since/123")) // Users - expect(OpenGroupAPI.Endpoint.userBan("test").path).to(equal("user/test/ban")) - expect(OpenGroupAPI.Endpoint.userUnban("test").path).to(equal("user/test/unban")) - expect(OpenGroupAPI.Endpoint.userModerator("test").path).to(equal("user/test/moderator")) + expect(Network.SOGS.Endpoint.userBan("test").path).to(equal("user/test/ban")) + expect(Network.SOGS.Endpoint.userUnban("test").path).to(equal("user/test/unban")) + expect(Network.SOGS.Endpoint.userModerator("test").path).to(equal("user/test/moderator")) } } } diff --git a/SessionNetworkingKitTests/SOGS/Types/SOGSErrorSpec.swift b/SessionNetworkingKitTests/SOGS/Types/SOGSErrorSpec.swift new file mode 100644 index 0000000000..e994dea0b3 --- /dev/null +++ b/SessionNetworkingKitTests/SOGS/Types/SOGSErrorSpec.swift @@ -0,0 +1,24 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +import Quick +import Nimble + +@testable import SessionNetworkingKit + +class SOGSErrorSpec: QuickSpec { + override class func spec() { + // MARK: - a SOGSError + describe("a SOGSError") { + // MARK: -- generates the error description correctly + it("generates the error description correctly") { + expect(SOGSError.decryptionFailed.description).to(equal("Couldn't decrypt response.")) + expect(SOGSError.signingFailed.description).to(equal("Couldn't sign message.")) + expect(SOGSError.noPublicKey.description).to(equal("Couldn't find server public key.")) + expect(SOGSError.invalidEmoji.description).to(equal("The emoji is invalid.")) + expect(SOGSError.invalidPoll.description).to(equal("Poller in invalid state.")) + } + } + } +} diff --git a/SessionSnodeKitTests/SessionSnodeKit.xctestplan b/SessionNetworkingKitTests/SessionNetworkingKit.xctestplan similarity index 90% rename from SessionSnodeKitTests/SessionSnodeKit.xctestplan rename to SessionNetworkingKitTests/SessionNetworkingKit.xctestplan index ea699b6efd..52ef80ee0c 100644 --- a/SessionSnodeKitTests/SessionSnodeKit.xctestplan +++ b/SessionNetworkingKitTests/SessionNetworkingKit.xctestplan @@ -17,7 +17,7 @@ "target" : { "containerPath" : "container:Session.xcodeproj", "identifier" : "FDB5DAF92A981C42002C8721", - "name" : "SessionSnodeKitTests" + "name" : "SessionNetworkingKitTests" } } ], diff --git a/SessionSnodeKitTests/Types/BatchRequestSpec.swift b/SessionNetworkingKitTests/Types/BatchRequestSpec.swift similarity index 99% rename from SessionSnodeKitTests/Types/BatchRequestSpec.swift rename to SessionNetworkingKitTests/Types/BatchRequestSpec.swift index 36b33bd18a..05554a2be1 100644 --- a/SessionSnodeKitTests/Types/BatchRequestSpec.swift +++ b/SessionNetworkingKitTests/Types/BatchRequestSpec.swift @@ -7,7 +7,7 @@ import SessionUtilitiesKit import Quick import Nimble -@testable import SessionSnodeKit +@testable import SessionNetworkingKit class BatchRequestSpec: QuickSpec { override class func spec() { diff --git a/SessionSnodeKitTests/Types/BatchResponseSpec.swift b/SessionNetworkingKitTests/Types/BatchResponseSpec.swift similarity index 99% rename from SessionSnodeKitTests/Types/BatchResponseSpec.swift rename to SessionNetworkingKitTests/Types/BatchResponseSpec.swift index 3fe9ea5575..eac736ac8b 100644 --- a/SessionSnodeKitTests/Types/BatchResponseSpec.swift +++ b/SessionNetworkingKitTests/Types/BatchResponseSpec.swift @@ -7,7 +7,7 @@ import SessionUtilitiesKit import Quick import Nimble -@testable import SessionSnodeKit +@testable import SessionNetworkingKit class BatchResponseSpec: QuickSpec { override class func spec() { diff --git a/SessionSnodeKitTests/Types/BencodeResponseSpec.swift b/SessionNetworkingKitTests/Types/BencodeResponseSpec.swift similarity index 99% rename from SessionSnodeKitTests/Types/BencodeResponseSpec.swift rename to SessionNetworkingKitTests/Types/BencodeResponseSpec.swift index e0e6add5f9..0bd213f4ec 100644 --- a/SessionSnodeKitTests/Types/BencodeResponseSpec.swift +++ b/SessionNetworkingKitTests/Types/BencodeResponseSpec.swift @@ -6,7 +6,7 @@ import SessionUtilitiesKit import Quick import Nimble -@testable import SessionSnodeKit +@testable import SessionNetworkingKit class BencodeResponseSpec: QuickSpec { override class func spec() { diff --git a/SessionSnodeKitTests/Types/DestinationSpec.swift b/SessionNetworkingKitTests/Types/DestinationSpec.swift similarity index 98% rename from SessionSnodeKitTests/Types/DestinationSpec.swift rename to SessionNetworkingKitTests/Types/DestinationSpec.swift index 6e17460ac5..e226a25f21 100644 --- a/SessionSnodeKitTests/Types/DestinationSpec.swift +++ b/SessionNetworkingKitTests/Types/DestinationSpec.swift @@ -5,7 +5,7 @@ import Foundation import Quick import Nimble -@testable import SessionSnodeKit +@testable import SessionNetworkingKit class DestinationSpec: QuickSpec { override class func spec() { diff --git a/SessionSnodeKitTests/Types/HeaderSpec.swift b/SessionNetworkingKitTests/Types/HeaderSpec.swift similarity index 93% rename from SessionSnodeKitTests/Types/HeaderSpec.swift rename to SessionNetworkingKitTests/Types/HeaderSpec.swift index ef25b0c5a7..9f73a41944 100644 --- a/SessionSnodeKitTests/Types/HeaderSpec.swift +++ b/SessionNetworkingKitTests/Types/HeaderSpec.swift @@ -6,7 +6,7 @@ import SessionUtilitiesKit import Quick import Nimble -@testable import SessionSnodeKit +@testable import SessionNetworkingKit class HeaderSpec: QuickSpec { override class func spec() { diff --git a/SessionSnodeKitTests/Types/PreparedRequestSendingSpec.swift b/SessionNetworkingKitTests/Types/PreparedRequestSendingSpec.swift similarity index 99% rename from SessionSnodeKitTests/Types/PreparedRequestSendingSpec.swift rename to SessionNetworkingKitTests/Types/PreparedRequestSendingSpec.swift index a75966aa6b..a1ad6f438b 100644 --- a/SessionSnodeKitTests/Types/PreparedRequestSendingSpec.swift +++ b/SessionNetworkingKitTests/Types/PreparedRequestSendingSpec.swift @@ -7,7 +7,7 @@ import SessionUtilitiesKit import Quick import Nimble -@testable import SessionSnodeKit +@testable import SessionNetworkingKit class PreparedRequestSendingSpec: QuickSpec { override class func spec() { diff --git a/SessionSnodeKitTests/Types/PreparedRequestSpec.swift b/SessionNetworkingKitTests/Types/PreparedRequestSpec.swift similarity index 99% rename from SessionSnodeKitTests/Types/PreparedRequestSpec.swift rename to SessionNetworkingKitTests/Types/PreparedRequestSpec.swift index 7ba2a676ac..7b949b5fde 100644 --- a/SessionSnodeKitTests/Types/PreparedRequestSpec.swift +++ b/SessionNetworkingKitTests/Types/PreparedRequestSpec.swift @@ -7,7 +7,7 @@ import SessionUtilitiesKit import Quick import Nimble -@testable import SessionSnodeKit +@testable import SessionNetworkingKit class PreparedRequestSpec: QuickSpec { override class func spec() { diff --git a/SessionSnodeKitTests/Types/RequestSpec.swift b/SessionNetworkingKitTests/Types/RequestSpec.swift similarity index 99% rename from SessionSnodeKitTests/Types/RequestSpec.swift rename to SessionNetworkingKitTests/Types/RequestSpec.swift index 9888d30083..0b951cf1d3 100644 --- a/SessionSnodeKitTests/Types/RequestSpec.swift +++ b/SessionNetworkingKitTests/Types/RequestSpec.swift @@ -6,7 +6,7 @@ import SessionUtilitiesKit import Quick import Nimble -@testable import SessionSnodeKit +@testable import SessionNetworkingKit class RequestSpec: QuickSpec { override class func spec() { diff --git a/SessionNetworkingKitTests/_TestUtilities/CommonSSKMockExtensions.swift b/SessionNetworkingKitTests/_TestUtilities/CommonSSKMockExtensions.swift new file mode 100644 index 0000000000..bbd0834908 --- /dev/null +++ b/SessionNetworkingKitTests/_TestUtilities/CommonSSKMockExtensions.swift @@ -0,0 +1,132 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +@testable import SessionNetworkingKit + +extension NoResponse: Mocked { + static var mock: NoResponse = NoResponse() +} + +extension Network.BatchSubResponse: MockedGeneric where T: Mocked { + typealias Generic = T + + static func mock(type: T.Type) -> Network.BatchSubResponse { + return Network.BatchSubResponse( + code: 200, + headers: [:], + body: Generic.mock, + failedToParseBody: false + ) + } +} + +extension Network.BatchSubResponse { + static func mockArrayValue(type: M.Type) -> Network.BatchSubResponse> { + return Network.BatchSubResponse( + code: 200, + headers: [:], + body: [M.mock], + failedToParseBody: false + ) + } +} + +extension Network.Destination: Mocked { + static var mock: Network.Destination = try! Network.Destination.server( + server: "testServer", + headers: [:], + x25519PublicKey: "" + ).withGeneratedUrl(for: MockEndpoint.mock) +} + +extension Network.SOGS.CapabilitiesResponse: Mocked { + static var mock: Network.SOGS.CapabilitiesResponse = Network.SOGS.CapabilitiesResponse(capabilities: [], missing: nil) +} + +extension Network.SOGS.Room: Mocked { + static var mock: Network.SOGS.Room = Network.SOGS.Room( + token: "test", + name: "testRoom", + roomDescription: nil, + infoUpdates: 1, + messageSequence: 1, + created: 1, + activeUsers: 1, + activeUsersCutoff: 1, + imageId: nil, + pinnedMessages: nil, + admin: false, + globalAdmin: false, + admins: [], + hiddenAdmins: nil, + moderator: false, + globalModerator: false, + moderators: [], + hiddenModerators: nil, + read: true, + defaultRead: nil, + defaultAccessible: nil, + write: true, + defaultWrite: nil, + upload: true, + defaultUpload: nil + ) +} + +extension Network.SOGS.RoomPollInfo: Mocked { + static var mock: Network.SOGS.RoomPollInfo = Network.SOGS.RoomPollInfo( + token: "test", + activeUsers: 1, + admin: false, + globalAdmin: false, + moderator: false, + globalModerator: false, + read: true, + defaultRead: nil, + defaultAccessible: nil, + write: true, + defaultWrite: nil, + upload: true, + defaultUpload: false, + details: .mock + ) +} + +extension Network.SOGS.Message: Mocked { + static var mock: Network.SOGS.Message = Network.SOGS.Message( + id: 100, + sender: TestConstants.blind15PublicKey, + posted: 1, + edited: nil, + deleted: nil, + seqNo: 1, + whisper: false, + whisperMods: false, + whisperTo: nil, + base64EncodedData: nil, + base64EncodedSignature: nil, + reactions: nil + ) +} + +extension Network.SOGS.SendDirectMessageResponse: Mocked { + static var mock: Network.SOGS.SendDirectMessageResponse = Network.SOGS.SendDirectMessageResponse( + id: 1, + sender: TestConstants.blind15PublicKey, + recipient: "testRecipient", + posted: 1122, + expires: 2233 + ) +} + +extension Network.SOGS.DirectMessage: Mocked { + static var mock: Network.SOGS.DirectMessage = Network.SOGS.DirectMessage( + id: 101, + sender: TestConstants.blind15PublicKey, + recipient: "testRecipient", + posted: 1212, + expires: 2323, + base64EncodedMessage: "TestMessage".data(using: .utf8)!.base64EncodedString() + ) +} diff --git a/SessionSnodeKitTests/_TestUtilities/MockNetwork.swift b/SessionNetworkingKitTests/_TestUtilities/MockNetwork.swift similarity index 98% rename from SessionSnodeKitTests/_TestUtilities/MockNetwork.swift rename to SessionNetworkingKitTests/_TestUtilities/MockNetwork.swift index efecb6d9ac..eff21161ac 100644 --- a/SessionSnodeKitTests/_TestUtilities/MockNetwork.swift +++ b/SessionNetworkingKitTests/_TestUtilities/MockNetwork.swift @@ -4,7 +4,7 @@ import Foundation import Combine import SessionUtilitiesKit -@testable import SessionSnodeKit +@testable import SessionNetworkingKit // MARK: - MockNetwork @@ -47,7 +47,7 @@ class MockNetwork: Mock, NetworkType { return mock(args: [body, destination, requestTimeout, requestAndPathBuildTimeout]) } - func checkClientVersion(ed25519SecretKey: [UInt8]) -> AnyPublisher<(ResponseInfoType, AppVersionResponse), Error> { + func checkClientVersion(ed25519SecretKey: [UInt8]) -> AnyPublisher<(ResponseInfoType, Network.FileServer.AppVersionResponse), Error> { return mock(args: [ed25519SecretKey]) } } diff --git a/SessionSnodeKitTests/_TestUtilities/MockSnodeAPICache.swift b/SessionNetworkingKitTests/_TestUtilities/MockSnodeAPICache.swift similarity index 97% rename from SessionSnodeKitTests/_TestUtilities/MockSnodeAPICache.swift rename to SessionNetworkingKitTests/_TestUtilities/MockSnodeAPICache.swift index 75111962b8..90be7933ed 100644 --- a/SessionSnodeKitTests/_TestUtilities/MockSnodeAPICache.swift +++ b/SessionNetworkingKitTests/_TestUtilities/MockSnodeAPICache.swift @@ -6,7 +6,7 @@ import Foundation import Combine import SessionUtilitiesKit -@testable import SessionSnodeKit +@testable import SessionNetworkingKit class MockSnodeAPICache: Mock, SnodeAPICacheType { var hardfork: Int { diff --git a/SessionNotificationServiceExtension/NotificationResolution.swift b/SessionNotificationServiceExtension/NotificationResolution.swift index aef0805a55..c291628c84 100644 --- a/SessionNotificationServiceExtension/NotificationResolution.swift +++ b/SessionNotificationServiceExtension/NotificationResolution.swift @@ -4,11 +4,12 @@ import Foundation import SessionUIKit +import SessionNetworkingKit import SessionMessagingKit import SessionUtilitiesKit enum NotificationResolution: CustomStringConvertible { - case success(PushNotificationAPI.NotificationMetadata) + case success(Network.PushNotification.NotificationMetadata) case successCall case ignoreDueToMainAppRunning @@ -20,15 +21,15 @@ enum NotificationResolution: CustomStringConvertible { case ignoreDueToMessageRequest case ignoreDueToDuplicateMessage case ignoreDueToDuplicateCall - case ignoreDueToContentSize(PushNotificationAPI.NotificationMetadata) + case ignoreDueToContentSize(Network.PushNotification.NotificationMetadata) case errorTimeout case errorNotReadyForExtensions case errorLegacyPushNotification case errorCallFailure - case errorNoContent(PushNotificationAPI.NotificationMetadata) - case errorProcessing(PushNotificationAPI.ProcessResult) - case errorMessageHandling(MessageReceiverError, PushNotificationAPI.NotificationMetadata) + case errorNoContent(Network.PushNotification.NotificationMetadata) + case errorProcessing(Network.PushNotification.ProcessResult) + case errorMessageHandling(MessageReceiverError, Network.PushNotification.NotificationMetadata) case errorOther(Error) public var description: String { @@ -88,7 +89,7 @@ enum NotificationResolution: CustomStringConvertible { } } -internal extension PushNotificationAPI.NotificationMetadata { +internal extension Network.PushNotification.NotificationMetadata { var messageOriginString: String { guard self != .invalid else { return "decryption failure" } diff --git a/SessionNotificationServiceExtension/NotificationServiceExtension.swift b/SessionNotificationServiceExtension/NotificationServiceExtension.swift index 70d2273560..54fc5cd9a9 100644 --- a/SessionNotificationServiceExtension/NotificationServiceExtension.swift +++ b/SessionNotificationServiceExtension/NotificationServiceExtension.swift @@ -5,7 +5,7 @@ import CallKit import UserNotifications import SessionUIKit import SessionMessagingKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit // MARK: - Log.Category @@ -62,7 +62,8 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension do { let mainAppUnreadCount: Int = try performSetup(notificationInfo) - notificationInfo = try extractNotificationInfo(notificationInfo, mainAppUnreadCount) + notificationInfo = notificationInfo.with(mainAppUnreadCount: mainAppUnreadCount) + notificationInfo = try extractNotificationInfo(notificationInfo) try setupGroupIfNeeded(notificationInfo) processedNotification = try processNotification(notificationInfo) @@ -104,13 +105,11 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension /// Configure the different targets SNUtilitiesKit.configure( networkMaxFileSize: Network.maxFileSize, + maxValidImageDimention: ImageDataManager.DataSource.maxValidDimension, using: dependencies ) SNMessagingKit.configure(using: dependencies) - /// The `NotificationServiceExtension` needs custom behaviours for it's notification presenter so set it up here - dependencies.set(singleton: .notificationsManager, to: NSENotificationPresenter(using: dependencies)) - /// Cache the users secret key dependencies.mutate(cache: .general) { $0.setSecretKey(ed25519SecretKey: userMetadata.ed25519SecretKey) @@ -128,6 +127,12 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension ) dependencies.set(cache: .libSession, to: cache) + /// The `NotificationServiceExtension` needs custom behaviours for it's notification presenter so set it up here + /// + /// **Note:** This **MUST** happen after we have loaded the `libSession` cache as the notification settings are + /// stored in there + dependencies.set(singleton: .notificationsManager, to: NSENotificationPresenter(using: dependencies)) + return userMetadata.unreadCount } @@ -151,8 +156,8 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension // MARK: - Notification Handling - private func extractNotificationInfo(_ info: NotificationInfo, _ mainAppUnreadCount: Int) throws -> NotificationInfo { - let (maybeData, metadata, result) = PushNotificationAPI.processNotification( + private func extractNotificationInfo(_ info: NotificationInfo) throws -> NotificationInfo { + let (maybeData, metadata, result) = Network.PushNotification.processNotification( notificationContent: info.content, using: dependencies ) @@ -169,7 +174,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension contentHandler: info.contentHandler, metadata: metadata, data: data, - mainAppUnreadCount: mainAppUnreadCount + mainAppUnreadCount: info.mainAppUnreadCount ) default: throw NotificationError.processingError(result, metadata) @@ -274,7 +279,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension private func handleConfigMessage( _ notification: ProcessedNotification, swarmPublicKey: String, - namespace: SnodeAPI.Namespace, + namespace: Network.SnodeAPI.Namespace, serverHash: String, serverTimestampMs: Int64, data: Data @@ -801,7 +806,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension // // TODO: [Database Relocation] Need to de-database the 'preparedSubscribe' call for this to work (neeeds the AuthMethod logic to be de-databased) // /// Since this is an API call we need to wait for it to complete before we trigger the `completeSilently` logic // Log.info(.cat, "Group invitation was auto-approved, attempting to subscribe for PNs.") -// try? PushNotificationAPI +// try? Network.PushNotification // .preparedSubscribe( // db, // token: Data(hex: token), @@ -1279,7 +1284,7 @@ private extension NotificationServiceExtension { let content: UNMutableNotificationContent let requestId: String let contentHandler: ((UNNotificationContent) -> Void) - let metadata: PushNotificationAPI.NotificationMetadata + let metadata: Network.PushNotification.NotificationMetadata let data: Data let mainAppUnreadCount: Int @@ -1287,7 +1292,8 @@ private extension NotificationServiceExtension { requestId: String? = nil, content: UNMutableNotificationContent? = nil, contentHandler: ((UNNotificationContent) -> Void)? = nil, - metadata: PushNotificationAPI.NotificationMetadata? = nil + metadata: Network.PushNotification.NotificationMetadata? = nil, + mainAppUnreadCount: Int? = nil ) -> NotificationInfo { return NotificationInfo( content: (content ?? self.content), @@ -1295,7 +1301,7 @@ private extension NotificationServiceExtension { contentHandler: (contentHandler ?? self.contentHandler), metadata: (metadata ?? self.metadata), data: data, - mainAppUnreadCount: mainAppUnreadCount + mainAppUnreadCount: (mainAppUnreadCount ?? self.mainAppUnreadCount) ) } } @@ -1310,8 +1316,8 @@ private extension NotificationServiceExtension { enum NotificationError: Error { case notReadyForExtension - case processingErrorWithFallback(PushNotificationAPI.ProcessResult, PushNotificationAPI.NotificationMetadata) - case processingError(PushNotificationAPI.ProcessResult, PushNotificationAPI.NotificationMetadata) + case processingErrorWithFallback(Network.PushNotification.ProcessResult, Network.PushNotification.NotificationMetadata) + case processingError(Network.PushNotification.ProcessResult, Network.PushNotification.NotificationMetadata) case timeout } } diff --git a/SessionShareExtension/ShareNavController.swift b/SessionShareExtension/ShareNavController.swift index 475489dd60..b1f420316f 100644 --- a/SessionShareExtension/ShareNavController.swift +++ b/SessionShareExtension/ShareNavController.swift @@ -7,7 +7,7 @@ import CoreServices import UniformTypeIdentifiers import SignalUtilitiesKit import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit import SessionMessagingKit @@ -45,7 +45,6 @@ final class ShareNavController: UINavigationController { dependencies.warmCache(cache: .appVersion) AppSetup.setupEnvironment( - additionalMigrationTargets: [DeprecatedUIKitMigrationTarget.self], appSpecificBlock: { [dependencies] in // stringlint:ignore_start if !Log.loggerExists(withPrefix: "SessionShareExtension") { @@ -65,6 +64,7 @@ final class ShareNavController: UINavigationController { // Configure the different targets SNUtilitiesKit.configure( networkMaxFileSize: Network.maxFileSize, + maxValidImageDimention: ImageDataManager.DataSource.maxValidDimension, using: dependencies ) SNMessagingKit.configure(using: dependencies) diff --git a/SessionShareExtension/SimplifiedConversationCell.swift b/SessionShareExtension/SimplifiedConversationCell.swift index 0d421c8caf..d4ced9d635 100644 --- a/SessionShareExtension/SimplifiedConversationCell.swift +++ b/SessionShareExtension/SimplifiedConversationCell.swift @@ -40,7 +40,10 @@ final class SimplifiedConversationCell: UITableViewCell { }() private lazy var profilePictureView: ProfilePictureView = { - let view: ProfilePictureView = ProfilePictureView(size: .list, dataManager: nil) + let view: ProfilePictureView = ProfilePictureView( + size: .list, + dataManager: nil + ) view.translatesAutoresizingMaskIntoConstraints = false return view diff --git a/SessionShareExtension/ThreadPickerVC.swift b/SessionShareExtension/ThreadPickerVC.swift index 34246413a9..60295606b9 100644 --- a/SessionShareExtension/ThreadPickerVC.swift +++ b/SessionShareExtension/ThreadPickerVC.swift @@ -8,7 +8,7 @@ import DifferenceKit import SessionUIKit import SignalUtilitiesKit import SessionMessagingKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableViewDelegate, AttachmentApprovalViewControllerDelegate, ThemedNavigation { @@ -275,7 +275,7 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView dependencies[singleton: .network] .getSwarm(for: swarmPublicKey) .tryFlatMapWithRandomSnode(using: dependencies) { snode in - try SnodeAPI + try Network.SnodeAPI .preparedGetNetworkTime(from: snode, using: dependencies) .send(using: dependencies) } diff --git a/SessionSnodeKit/Configuration.swift b/SessionSnodeKit/Configuration.swift deleted file mode 100644 index af6ed9cc8d..0000000000 --- a/SessionSnodeKit/Configuration.swift +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import Foundation -import GRDB -import SessionUtilitiesKit - -public enum SNSnodeKit: MigratableTarget { // Just to make the external API nice - public static func migrations() -> TargetMigrations { - return TargetMigrations( - identifier: .snodeKit, - migrations: [ - [ - _001_InitialSetupMigration.self, - _002_SetupStandardJobs.self - ], // Initial DB Creation - [ - _003_YDBToGRDBMigration.self - ], // YDB to GRDB Migration - [ - _004_FlagMessageHashAsDeletedOrInvalid.self - ], // Legacy DB removal - [], // Add job priorities - [], // Fix thread FTS - [ - _005_AddSnodeReveivedMessageInfoPrimaryKey.self, - _006_DropSnodeCache.self, - _007_SplitSnodeReceivedMessageInfo.self, - _008_ResetUserConfigLastHashes.self - ], - [], // Renamed `Setting` to `KeyValueStore` - [] - ] - ) - } -} diff --git a/SessionSnodeKit/Meta/SessionSnodeKit.h b/SessionSnodeKit/Meta/SessionSnodeKit.h deleted file mode 100644 index 698aa516fd..0000000000 --- a/SessionSnodeKit/Meta/SessionSnodeKit.h +++ /dev/null @@ -1,4 +0,0 @@ -#import - -FOUNDATION_EXPORT double SessionSnodeKitVersionNumber; -FOUNDATION_EXPORT const unsigned char SessionSnodeKitVersionString[]; diff --git a/SessionSnodeKit/Models/AppVersionResponse.swift b/SessionSnodeKit/Models/AppVersionResponse.swift deleted file mode 100644 index ae5c33739b..0000000000 --- a/SessionSnodeKit/Models/AppVersionResponse.swift +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import Foundation - -public class AppVersionResponse: AppVersionInfo { - enum CodingKeys: String, CodingKey { - case prerelease - } - - public let prerelease: AppVersionInfo? - - public init( - version: String, - updated: TimeInterval?, - name: String?, - notes: String?, - assets: [Asset]?, - prerelease: AppVersionInfo? - ) { - self.prerelease = prerelease - - super.init( - version: version, - updated: updated, - name: name, - notes: notes, - assets: assets - ) - } - - required init(from decoder: Decoder) throws { - let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) - - self.prerelease = try? container.decode(AppVersionInfo?.self, forKey: .prerelease) - - try super.init(from: decoder) - } - - public override func encode(to encoder: Encoder) throws { - try super.encode(to: encoder) - - var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) - try container.encodeIfPresent(prerelease, forKey: .prerelease) - } -} - -// MARK: - AppVersionInfo - -public class AppVersionInfo: Codable { - enum CodingKeys: String, CodingKey { - case version = "result" - case updated - case name - case notes - case assets - } - - public struct Asset: Codable { - enum CodingKeys: String, CodingKey { - case name - case url - } - - public let name: String - public let url: String - } - - public let version: String - public let updated: TimeInterval? - public let name: String? - public let notes: String? - public let assets: [Asset]? - - public init( - version: String, - updated: TimeInterval?, - name: String?, - notes: String?, - assets: [Asset]? - ) { - self.version = version - self.updated = updated - self.name = name - self.notes = notes - self.assets = assets - } - - public func encode(to encoder: Encoder) throws { - var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) - - try container.encode(version, forKey: .version) - try container.encodeIfPresent(updated, forKey: .updated) - try container.encodeIfPresent(name, forKey: .name) - try container.encodeIfPresent(notes, forKey: .notes) - try container.encodeIfPresent(assets, forKey: .assets) - } -} diff --git a/SessionSnodeKit/Models/OxenDaemonRPCRequest.swift b/SessionSnodeKit/Models/OxenDaemonRPCRequest.swift deleted file mode 100644 index 1b9d6ea453..0000000000 --- a/SessionSnodeKit/Models/OxenDaemonRPCRequest.swift +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import Foundation - -public struct OxenDaemonRPCRequest: Encodable { - private enum CodingKeys: String, CodingKey { - case endpoint - case body = "params" - } - - private let endpoint: String - private let body: T - - public init( - endpoint: SnodeAPI.Endpoint, - body: T - ) { - self.endpoint = endpoint.path - self.body = body - } -} diff --git a/SessionSnodeKit/SnodeAPI/SnodeAPI.swift b/SessionSnodeKit/SnodeAPI/SnodeAPI.swift deleted file mode 100644 index af511bf056..0000000000 --- a/SessionSnodeKit/SnodeAPI/SnodeAPI.swift +++ /dev/null @@ -1,857 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. -// -// stringlint:disable - -import Foundation -import Combine -import GRDB -import Punycode -import SessionUtilitiesKit - -public final class SnodeAPI { - // MARK: - Settings - - public static let maxRetryCount: Int = 8 - - // MARK: - Batching & Polling - - public typealias PollResponse = [SnodeAPI.Namespace: (info: ResponseInfoType, data: PreparedGetMessagesResponse?)] - - public static func preparedPoll( - _ db: ObservingDatabase, - namespaces: [SnodeAPI.Namespace], - refreshingConfigHashes: [String] = [], - from snode: LibSession.Snode, - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest { - // Determine the maxSize each namespace in the request should take up - var requests: [any ErasedPreparedRequest] = [] - let namespaceMaxSizeMap: [SnodeAPI.Namespace: Int64] = SnodeAPI.Namespace.maxSizeMap(for: namespaces) - let fallbackSize: Int64 = (namespaceMaxSizeMap.values.min() ?? 1) - - // If we have any config hashes to refresh TTLs then add those requests first - if !refreshingConfigHashes.isEmpty { - let updatedExpiryMS: Int64 = ( - dependencies[cache: .snodeAPI].currentOffsetTimestampMs() + - (30 * 24 * 60 * 60 * 1000) // 30 days - ) - requests.append( - try SnodeAPI.preparedUpdateExpiry( - serverHashes: refreshingConfigHashes, - updatedExpiryMs: updatedExpiryMS, - extendOnly: true, - ignoreValidationFailure: true, - explicitTargetNode: snode, - authMethod: authMethod, - using: dependencies - ) - ) - } - - // Add the various 'getMessages' requests - requests.append( - contentsOf: try namespaces.map { namespace -> any ErasedPreparedRequest in - try SnodeAPI.preparedGetMessages( - db, - namespace: namespace, - snode: snode, - maxSize: namespaceMaxSizeMap[namespace] - .defaulting(to: fallbackSize), - authMethod: authMethod, - using: dependencies - ) - } - ) - - return try preparedBatch( - requests: requests, - requireAllBatchResponses: true, - snode: snode, - swarmPublicKey: try authMethod.swarmPublicKey, - using: dependencies - ) - .map { (_: ResponseInfoType, batchResponse: Network.BatchResponse) -> [SnodeAPI.Namespace: (info: ResponseInfoType, data: PreparedGetMessagesResponse?)] in - let messageResponses: [Network.BatchSubResponse] = batchResponse - .compactMap { $0 as? Network.BatchSubResponse } - - return zip(namespaces, messageResponses) - .reduce(into: [:]) { result, next in - guard let messageResponse: PreparedGetMessagesResponse = next.1.body else { return } - - result[next.0] = (next.1, messageResponse) - } - } - } - - public static func preparedBatch( - requests: [any ErasedPreparedRequest], - requireAllBatchResponses: Bool, - snode: LibSession.Snode? = nil, - swarmPublicKey: String, - requestTimeout: TimeInterval = Network.defaultTimeout, - requestAndPathBuildTimeout: TimeInterval? = nil, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest { - return try SnodeAPI - .prepareRequest( - request: { - switch snode { - case .none: - return try Request( - endpoint: .batch, - swarmPublicKey: swarmPublicKey, - body: Network.BatchRequest(requestsKey: .requests, requests: requests) - ) - - case .some(let snode): - return try Request( - endpoint: .batch, - snode: snode, - swarmPublicKey: swarmPublicKey, - body: Network.BatchRequest(requestsKey: .requests, requests: requests) - ) - } - }(), - responseType: Network.BatchResponse.self, - requireAllBatchResponses: requireAllBatchResponses, - requestTimeout: requestTimeout, - requestAndPathBuildTimeout: requestAndPathBuildTimeout, - using: dependencies - ) - } - - public static func preparedSequence( - requests: [any ErasedPreparedRequest], - requireAllBatchResponses: Bool, - swarmPublicKey: String, - snodeRetrievalRetryCount: Int, - requestTimeout: TimeInterval = Network.defaultTimeout, - requestAndPathBuildTimeout: TimeInterval? = nil, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest { - return try SnodeAPI - .prepareRequest( - request: Request( - endpoint: .sequence, - swarmPublicKey: swarmPublicKey, - body: Network.BatchRequest(requestsKey: .requests, requests: requests), - snodeRetrievalRetryCount: snodeRetrievalRetryCount - ), - responseType: Network.BatchResponse.self, - requireAllBatchResponses: requireAllBatchResponses, - requestTimeout: requestTimeout, - requestAndPathBuildTimeout: requestAndPathBuildTimeout, - using: dependencies - ) - } - - // MARK: - Retrieve - - public typealias PreparedGetMessagesResponse = (messages: [SnodeReceivedMessage], lastHash: String?) - - public static func preparedGetMessages( - _ db: ObservingDatabase, - namespace: SnodeAPI.Namespace, - snode: LibSession.Snode, - maxSize: Int64? = nil, - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest { - let maybeLastHash: String? = try SnodeReceivedMessageInfo - .fetchLastNotExpired( - db, - for: snode, - namespace: namespace, - swarmPublicKey: try authMethod.swarmPublicKey, - using: dependencies - )? - .hash - let preparedRequest: Network.PreparedRequest = try { - // Check if this namespace requires authentication - guard namespace.requiresReadAuthentication else { - return try SnodeAPI.prepareRequest( - request: Request( - endpoint: .getMessages, - swarmPublicKey: try authMethod.swarmPublicKey, - body: LegacyGetMessagesRequest( - pubkey: try authMethod.swarmPublicKey, - lastHash: (maybeLastHash ?? ""), - namespace: namespace, - maxCount: nil, - maxSize: maxSize - ) - ), - responseType: GetMessagesResponse.self, - using: dependencies - ) - } - - return try SnodeAPI.prepareRequest( - request: Request( - endpoint: .getMessages, - swarmPublicKey: try authMethod.swarmPublicKey, - body: GetMessagesRequest( - lastHash: (maybeLastHash ?? ""), - namespace: namespace, - authMethod: authMethod, - timestampMs: dependencies[cache: .snodeAPI].currentOffsetTimestampMs(), - maxSize: maxSize - ) - ), - responseType: GetMessagesResponse.self, - using: dependencies - ) - }() - - return preparedRequest - .tryMap { _, response -> (messages: [SnodeReceivedMessage], lastHash: String?) in - return ( - try response.messages.compactMap { rawMessage -> SnodeReceivedMessage? in - SnodeReceivedMessage( - snode: snode, - publicKey: try authMethod.swarmPublicKey, - namespace: namespace, - rawMessage: rawMessage - ) - }, - maybeLastHash - ) - } - } - - public static func getSessionID( - for onsName: String, - using dependencies: Dependencies - ) -> AnyPublisher { - let validationCount = 3 - - // The name must be lowercased - let onsName = onsName.lowercased().idnaEncoded ?? onsName.lowercased() - - // Hash the ONS name using BLAKE2b - guard - let nameHash = dependencies[singleton: .crypto].generate( - .hash(message: Array(onsName.utf8)) - ) - else { - return Fail(error: SnodeAPIError.onsHashingFailed) - .eraseToAnyPublisher() - } - - // Ask 3 different snodes for the Session ID associated with the given name hash - let base64EncodedNameHash = nameHash.toBase64() - - return dependencies[singleton: .network] - .getRandomNodes(count: validationCount) - .tryFlatMap { nodes in - Publishers.MergeMany( - try nodes.map { snode in - try SnodeAPI - .prepareRequest( - request: Request( - endpoint: .oxenDaemonRPCCall, - snode: snode, - body: OxenDaemonRPCRequest( - endpoint: .daemonOnsResolve, - body: ONSResolveRequest( - type: 0, // type 0 means Session - base64EncodedNameHash: base64EncodedNameHash - ) - ) - ), - responseType: ONSResolveResponse.self, - using: dependencies - ) - .tryMap { _, response -> String in - try dependencies[singleton: .crypto].tryGenerate( - .sessionId(name: onsName, response: response) - ) - } - .send(using: dependencies) - .map { _, sessionId in sessionId } - .eraseToAnyPublisher() - } - ) - } - .collect() - .tryMap { results -> String in - guard results.count == validationCount, Set(results).count == 1 else { - throw SnodeAPIError.onsValidationFailed - } - - return results[0] - } - .eraseToAnyPublisher() - } - - public static func preparedGetExpiries( - of serverHashes: [String], - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest { - return try SnodeAPI - .prepareRequest( - request: Request( - endpoint: .getExpiries, - swarmPublicKey: try authMethod.swarmPublicKey, - body: GetExpiriesRequest( - messageHashes: serverHashes, - authMethod: authMethod, - timestampMs: dependencies[cache: .snodeAPI].currentOffsetTimestampMs() - ) - ), - responseType: GetExpiriesResponse.self, - using: dependencies - ) - } - - // MARK: - Store - - public static func preparedSendMessage( - message: SnodeMessage, - in namespace: Namespace, - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest { - let request: Network.PreparedRequest = try { - // Check if this namespace requires authentication - guard namespace.requiresWriteAuthentication else { - return try SnodeAPI.prepareRequest( - request: Request( - endpoint: .sendMessage, - swarmPublicKey: try authMethod.swarmPublicKey, - body: LegacySendMessagesRequest( - message: message, - namespace: namespace - ), - snodeRetrievalRetryCount: 0 // The SendMessageJob already has a retry mechanism - ), - responseType: SendMessagesResponse.self, - requestAndPathBuildTimeout: Network.defaultTimeout, - using: dependencies - ) - } - - return try SnodeAPI.prepareRequest( - request: Request( - endpoint: .sendMessage, - swarmPublicKey: try authMethod.swarmPublicKey, - body: SendMessageRequest( - message: message, - namespace: namespace, - authMethod: authMethod, - timestampMs: dependencies[cache: .snodeAPI].currentOffsetTimestampMs() - ), - snodeRetrievalRetryCount: 0 // The SendMessageJob already has a retry mechanism - ), - responseType: SendMessagesResponse.self, - requestAndPathBuildTimeout: Network.defaultTimeout, - using: dependencies - ) - }() - - return request - .tryMap { _, response -> SendMessagesResponse in - try response.validateResultMap( - swarmPublicKey: try authMethod.swarmPublicKey, - using: dependencies - ) - - return response - } - } - - // MARK: - Edit - - public static func preparedUpdateExpiry( - serverHashes: [String], - updatedExpiryMs: Int64, - shortenOnly: Bool? = nil, - extendOnly: Bool? = nil, - ignoreValidationFailure: Bool = false, - explicitTargetNode: LibSession.Snode? = nil, - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest<[String: UpdateExpiryResponseResult]> { - // ShortenOnly and extendOnly cannot be true at the same time - guard shortenOnly == nil || extendOnly == nil else { throw NetworkError.invalidPreparedRequest } - - return try SnodeAPI - .prepareRequest( - request: Request( - endpoint: .expire, - swarmPublicKey: try authMethod.swarmPublicKey, - body: UpdateExpiryRequest( - messageHashes: serverHashes, - expiryMs: UInt64(updatedExpiryMs), - shorten: shortenOnly, - extend: extendOnly, - authMethod: authMethod - ) - ), - responseType: UpdateExpiryResponse.self, - using: dependencies - ) - .tryMap { _, response -> [String: UpdateExpiryResponseResult] in - do { - return try response.validResultMap( - swarmPublicKey: try authMethod.swarmPublicKey, - validationData: serverHashes, - using: dependencies - ) - } - catch { - guard ignoreValidationFailure else { throw error } - - return [:] - } - } - .handleEvents( - receiveOutput: { _, result in - /// Since we have updated the TTL we need to make sure we also update the local - /// `SnodeReceivedMessageInfo.expirationDateMs` values so they match the updated swarm, if - /// we had a specific `snode` we we're sending the request to then we should use those values, otherwise - /// we can just grab the first value from the response and use that - let maybeTargetResult: UpdateExpiryResponseResult? = { - guard let snode: LibSession.Snode = explicitTargetNode else { - return result.first?.value - } - - return result[snode.ed25519PubkeyHex] - }() - guard - let targetResult: UpdateExpiryResponseResult = maybeTargetResult, - let groupedExpiryResult: [UInt64: [String]] = targetResult.changed - .updated(with: targetResult.unchanged) - .groupedByValue() - .nullIfEmpty - else { return } - - dependencies[singleton: .storage].writeAsync { db in - try groupedExpiryResult.forEach { updatedExpiry, hashes in - try SnodeReceivedMessageInfo - .filter(hashes.contains(SnodeReceivedMessageInfo.Columns.hash)) - .updateAll( - db, - SnodeReceivedMessageInfo.Columns.expirationDateMs - .set(to: updatedExpiry) - ) - } - } - } - ) - } - - public static func preparedRevokeSubaccounts( - subaccountsToRevoke: [[UInt8]], - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest { - let timestampMs: UInt64 = dependencies[cache: .snodeAPI].currentOffsetTimestampMs() - - return try SnodeAPI - .prepareRequest( - request: Request( - endpoint: .revokeSubaccount, - swarmPublicKey: try authMethod.swarmPublicKey, - body: RevokeSubaccountRequest( - subaccountsToRevoke: subaccountsToRevoke, - authMethod: authMethod, - timestampMs: timestampMs - ) - ), - responseType: RevokeSubaccountResponse.self, - using: dependencies - ) - .tryMap { _, response -> Void in - try response.validateResultMap( - swarmPublicKey: try authMethod.swarmPublicKey, - validationData: (subaccountsToRevoke, timestampMs), - using: dependencies - ) - - return () - } - } - - public static func preparedUnrevokeSubaccounts( - subaccountsToUnrevoke: [[UInt8]], - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest { - let timestampMs: UInt64 = dependencies[cache: .snodeAPI].currentOffsetTimestampMs() - - return try SnodeAPI - .prepareRequest( - request: Request( - endpoint: .unrevokeSubaccount, - swarmPublicKey: try authMethod.swarmPublicKey, - body: UnrevokeSubaccountRequest( - subaccountsToUnrevoke: subaccountsToUnrevoke, - authMethod: authMethod, - timestampMs: timestampMs - ) - ), - responseType: UnrevokeSubaccountResponse.self, - using: dependencies - ) - .tryMap { _, response -> Void in - try response.validateResultMap( - swarmPublicKey: try authMethod.swarmPublicKey, - validationData: (subaccountsToUnrevoke, timestampMs), - using: dependencies - ) - - return () - } - } - - // MARK: - Delete - - public static func preparedDeleteMessages( - serverHashes: [String], - requireSuccessfulDeletion: Bool, - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest<[String: Bool]> { - return try SnodeAPI - .prepareRequest( - request: Request( - endpoint: .deleteMessages, - swarmPublicKey: try authMethod.swarmPublicKey, - body: DeleteMessagesRequest( - messageHashes: serverHashes, - requireSuccessfulDeletion: requireSuccessfulDeletion, - authMethod: authMethod - ) - ), - responseType: DeleteMessagesResponse.self, - using: dependencies - ) - .tryMap { _, response -> [String: Bool] in - let validResultMap: [String: Bool] = try response.validResultMap( - swarmPublicKey: try authMethod.swarmPublicKey, - validationData: serverHashes, - using: dependencies - ) - - // If `validResultMap` didn't throw then at least one service node - // deleted successfully so we should mark the hash as invalid so we - // don't try to fetch updates using that hash going forward (if we - // do we would end up re-fetching all old messages) - dependencies[singleton: .storage].writeAsync { db in - try? SnodeReceivedMessageInfo.handlePotentialDeletedOrInvalidHash( - db, - potentiallyInvalidHashes: serverHashes - ) - } - - return validResultMap - } - } - - - /// Clears all the user's data from their swarm. Returns a dictionary of snode public key to deletion confirmation. - public static func preparedDeleteAllMessages( - namespace: SnodeAPI.Namespace, - requestTimeout: TimeInterval = Network.defaultTimeout, - requestAndPathBuildTimeout: TimeInterval? = nil, - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest<[String: Bool]> { - return try SnodeAPI - .prepareRequest( - request: Request( - endpoint: .deleteAll, - swarmPublicKey: try authMethod.swarmPublicKey, - requiresLatestNetworkTime: true, - body: DeleteAllMessagesRequest( - namespace: namespace, - authMethod: authMethod, - timestampMs: dependencies[cache: .snodeAPI].currentOffsetTimestampMs() - ), - snodeRetrievalRetryCount: 0 - ), - responseType: DeleteAllMessagesResponse.self, - requestTimeout: requestTimeout, - requestAndPathBuildTimeout: requestAndPathBuildTimeout, - using: dependencies - ) - .tryMap { info, response -> [String: Bool] in - guard let targetInfo: LatestTimestampResponseInfo = info as? LatestTimestampResponseInfo else { - throw NetworkError.invalidResponse - } - - return try response.validResultMap( - swarmPublicKey: try authMethod.swarmPublicKey, - validationData: targetInfo.timestampMs, - using: dependencies - ) - } - } - - /// Clears all the user's data from their swarm. Returns a dictionary of snode public key to deletion confirmation. - public static func preparedDeleteAllMessages( - beforeMs: UInt64, - namespace: SnodeAPI.Namespace, - authMethod: AuthenticationMethod, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest<[String: Bool]> { - return try SnodeAPI - .prepareRequest( - request: Request( - endpoint: .deleteAllBefore, - swarmPublicKey: try authMethod.swarmPublicKey, - requiresLatestNetworkTime: true, - body: DeleteAllBeforeRequest( - beforeMs: beforeMs, - namespace: namespace, - authMethod: authMethod, - timestampMs: dependencies[cache: .snodeAPI].currentOffsetTimestampMs() - ) - ), - responseType: DeleteAllMessagesResponse.self, - retryCount: maxRetryCount, - using: dependencies - ) - .tryMap { _, response -> [String: Bool] in - try response.validResultMap( - swarmPublicKey: try authMethod.swarmPublicKey, - validationData: beforeMs, - using: dependencies - ) - } - } - - // MARK: - Internal API - - public static func preparedGetNetworkTime( - from snode: LibSession.Snode, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest { - return try SnodeAPI - .prepareRequest( - request: Request, Endpoint>( - endpoint: .getInfo, - snode: snode, - body: [:] - ), - responseType: GetNetworkTimestampResponse.self, - using: dependencies - ) - .map { _, response in - // Assume we've fetched the networkTime in order to send a message to the specified snode, in - // which case we want to update the 'clockOffsetMs' value for subsequent requests - let offset = (Int64(response.timestamp) - Int64(floor(dependencies.dateNow.timeIntervalSince1970 * 1000))) - dependencies.mutate(cache: .snodeAPI) { $0.setClockOffsetMs(offset) } - - return response.timestamp - } - } - - // MARK: - Convenience - - private static func prepareRequest( - request: Request, - responseType: R.Type, - requireAllBatchResponses: Bool = true, - retryCount: Int = 0, - requestTimeout: TimeInterval = Network.defaultTimeout, - requestAndPathBuildTimeout: TimeInterval? = nil, - using dependencies: Dependencies - ) throws -> Network.PreparedRequest { - return try Network.PreparedRequest( - request: request, - responseType: responseType, - requireAllBatchResponses: requireAllBatchResponses, - retryCount: retryCount, - requestTimeout: requestTimeout, - requestAndPathBuildTimeout: requestAndPathBuildTimeout, - using: dependencies - ) - .handleEvents( - receiveOutput: { _, response in - switch response { - case let snodeResponse as SnodeResponse: - // Update the network offset based on the response so subsequent requests have - // the correct network offset time - let offset = (Int64(snodeResponse.timeOffset) - Int64(floor(dependencies.dateNow.timeIntervalSince1970 * 1000))) - dependencies.mutate(cache: .snodeAPI) { - $0.setClockOffsetMs(offset) - - // Extract and store hard fork information if returned - guard snodeResponse.hardForkVersion.count > 1 else { return } - - if snodeResponse.hardForkVersion[1] > $0.softfork { - $0.softfork = snodeResponse.hardForkVersion[1] - dependencies[defaults: .standard, key: .hardfork] = $0.softfork - } - - if snodeResponse.hardForkVersion[0] > $0.hardfork { - $0.hardfork = snodeResponse.hardForkVersion[0] - dependencies[defaults: .standard, key: .hardfork] = $0.hardfork - $0.softfork = snodeResponse.hardForkVersion[1] - dependencies[defaults: .standard, key: .softfork] = $0.softfork - } - } - - default: break - } - } - ) - } -} - -// MARK: - Publisher Convenience - -public extension Publisher where Output == Set { - func tryMapWithRandomSnode( - using dependencies: Dependencies, - _ transform: @escaping (LibSession.Snode) throws -> T - ) -> AnyPublisher { - return self - .tryMap { swarm -> T in - var remainingSnodes: Set = swarm - let snode: LibSession.Snode = try dependencies.popRandomElement(&remainingSnodes) ?? { - throw SnodeAPIError.insufficientSnodes - }() - - return try transform(snode) - } - .eraseToAnyPublisher() - } - - func tryFlatMapWithRandomSnode( - maxPublishers: Subscribers.Demand = .unlimited, - retry retries: Int = 0, - drainBehaviour: ThreadSafeObject = .alwaysRandom, - using dependencies: Dependencies, - _ transform: @escaping (LibSession.Snode) throws -> P - ) -> AnyPublisher where T == P.Output, P: Publisher, P.Failure == Error { - return self - .mapError { $0 } - .flatMap(maxPublishers: maxPublishers) { swarm -> AnyPublisher in - // If we don't want to reuse a specific snode multiple times then just grab a - // random one from the swarm every time - var remainingSnodes: Set = drainBehaviour.performUpdateAndMap { behaviour in - switch behaviour { - case .alwaysRandom: return (behaviour, swarm) - case .limitedReuse(_, let targetSnode, _, let usedSnodes, let swarmHash): - // If we've used all of the snodes or the swarm has changed then reset the used list - guard swarmHash == swarm.hashValue && (targetSnode != nil || usedSnodes != swarm) else { - return (behaviour.reset(), swarm) - } - - return (behaviour, swarm.subtracting(usedSnodes)) - } - } - var lastError: Error? - - return Just(()) - .setFailureType(to: Error.self) - .tryFlatMap(maxPublishers: maxPublishers) { _ -> AnyPublisher in - let snode: LibSession.Snode = try drainBehaviour.performUpdateAndMap { behaviour in - switch behaviour { - case .limitedReuse(_, .some(let targetSnode), _, _, _): - return (behaviour.use(snode: targetSnode, from: swarm), targetSnode) - default: break - } - - // Select the next snode - let result: LibSession.Snode = try dependencies.popRandomElement(&remainingSnodes) ?? { - throw SnodeAPIError.ranOutOfRandomSnodes(lastError) - }() - - return (behaviour.use(snode: result, from: swarm), result) - } - - return try transform(snode) - .eraseToAnyPublisher() - } - .mapError { error in - // Prevent nesting the 'ranOutOfRandomSnodes' errors - switch error { - case SnodeAPIError.ranOutOfRandomSnodes: break - default: lastError = error - } - - return error - } - .retry(retries) - .eraseToAnyPublisher() - } - .eraseToAnyPublisher() - } -} - -// MARK: - SnodeAPI Cache - -public extension SnodeAPI { - class Cache: SnodeAPICacheType { - private let dependencies: Dependencies - public var hardfork: Int - public var softfork: Int - public var clockOffsetMs: Int64 = 0 - - init(using dependencies: Dependencies) { - self.dependencies = dependencies - self.hardfork = dependencies[defaults: .standard, key: .hardfork] - self.softfork = dependencies[defaults: .standard, key: .softfork] - } - - public func currentOffsetTimestampMs() -> T { - let timestampNowMs: Int64 = (Int64(floor(dependencies.dateNow.timeIntervalSince1970 * 1000)) + clockOffsetMs) - - guard let convertedTimestampNowMs: T = T(exactly: timestampNowMs) else { - Log.critical("[SnodeAPI.Cache] Failed to convert the timestamp to the desired type: \(type(of: T.self)).") - return 0 - } - - return convertedTimestampNowMs - } - - public func setClockOffsetMs(_ clockOffsetMs: Int64) { - self.clockOffsetMs = clockOffsetMs - } - } -} - -public extension Cache { - static let snodeAPI: CacheConfig = Dependencies.create( - identifier: "snodeAPI", - createInstance: { dependencies in SnodeAPI.Cache(using: dependencies) }, - mutableInstance: { $0 }, - immutableInstance: { $0 } - ) -} - -// MARK: - SnodeAPICacheType - -/// This is a read-only version of the Cache designed to avoid unintentionally mutating the instance in a non-thread-safe way -public protocol SnodeAPIImmutableCacheType: ImmutableCacheType { - /// The last seen storage server hard fork version. - var hardfork: Int { get } - - /// The last seen storage server soft fork version. - var softfork: Int { get } - - /// The offset between the user's clock and the Service Node's clock. Used in cases where the - /// user's clock is incorrect. - var clockOffsetMs: Int64 { get } - - /// Tthe current user clock timestamp in milliseconds offset by the difference between the user's clock and the clock of the most - /// recent Service Node's that was communicated with. - func currentOffsetTimestampMs() -> T -} - -public protocol SnodeAPICacheType: SnodeAPIImmutableCacheType, MutableCacheType { - /// The last seen storage server hard fork version. - var hardfork: Int { get set } - - /// The last seen storage server soft fork version. - var softfork: Int { get set } - - /// A function to update the offset between the user's clock and the Service Node's clock. - func setClockOffsetMs(_ clockOffsetMs: Int64) -} diff --git a/SessionSnodeKit/Types/Network.swift b/SessionSnodeKit/Types/Network.swift deleted file mode 100644 index 5913775129..0000000000 --- a/SessionSnodeKit/Types/Network.swift +++ /dev/null @@ -1,169 +0,0 @@ -// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. -// -// stringlint:disable - -import Foundation -import Combine -import SessionUtilitiesKit - -// MARK: - Singleton - -public extension Singleton { - static let network: SingletonConfig = Dependencies.create( - identifier: "network", - createInstance: { dependencies in LibSessionNetwork(using: dependencies) } - ) -} - -// MARK: - NetworkType - -public protocol NetworkType { - func getSwarm(for swarmPublicKey: String) -> AnyPublisher, Error> - func getRandomNodes(count: Int) -> AnyPublisher, Error> - - func send( - _ body: Data?, - to destination: Network.Destination, - requestTimeout: TimeInterval, - requestAndPathBuildTimeout: TimeInterval? - ) -> AnyPublisher<(ResponseInfoType, Data?), Error> - - func checkClientVersion(ed25519SecretKey: [UInt8]) -> AnyPublisher<(ResponseInfoType, AppVersionResponse), Error> -} - -// MARK: - Network Constants - -public class Network { - public static let defaultTimeout: TimeInterval = 10 - public static let fileUploadTimeout: TimeInterval = 60 - public static let fileDownloadTimeout: TimeInterval = 30 - - /// **Note:** The max file size is 10,000,000 bytes (rather than 10MiB which would be `(10 * 1024 * 1024)`), 10,000,000 - /// exactly will be fine but a single byte more will result in an error - public static let maxFileSize: UInt = 10_000_000 -} - -// MARK: - NetworkStatus - -public enum NetworkStatus { - case unknown - case connecting - case connected - case disconnected -} - -// MARK: - FileServer Convenience - -public extension Network { - enum NetworkAPI { - static let networkAPIServer = "http://networkv1.getsession.org" - static let networkAPIServerPublicKey = "cbf461a4431dc9174dceef4421680d743a2a0e1a3131fc794240bcb0bc3dd449" - - public enum Endpoint: EndpointType { - case info - case price - case token - - public static var name: String { "NetworkAPI.Endpoint" } - - public var path: String { - switch self { - case .info: return "info" - case .price: return "price" - case .token: return "token" - } - } - } - } - - enum FileServer { - fileprivate static let fileServer = "http://filev2.getsession.org" - fileprivate static let fileServerPublicKey = "da21e1d886c6fbaea313f75298bd64aab03a97ce985b46bb2dad9f2089c8ee59" - fileprivate static let legacyFileServer = "http://88.99.175.227" - fileprivate static let legacyFileServerPublicKey = "7cb31905b55cd5580c686911debf672577b3fb0bff81df4ce2d5c4cb3a7aaa69" - - public enum Endpoint: EndpointType { - case file - case fileIndividual(String) - case directUrl(URL) - case sessionVersion - - public static var name: String { "FileServerAPI.Endpoint" } - - public var path: String { - switch self { - case .file: return "file" - case .fileIndividual(let fileId): return "file/\(fileId)" - case .directUrl(let url): return url.path.removingPrefix("/") - case .sessionVersion: return "session_version" - } - } - } - - static func fileServerPubkey(url: String? = nil) -> String { - switch url?.contains(legacyFileServer) { - case true: return legacyFileServerPublicKey - default: return fileServerPublicKey - } - } - - static func isFileServerUrl(url: URL) -> Bool { - return ( - url.absoluteString.starts(with: fileServer) || - url.absoluteString.starts(with: legacyFileServer) - ) - } - - public static func downloadUrlString(for url: String, fileId: String) -> String { - switch url.contains(legacyFileServer) { - case true: return "\(fileServer)/\(Endpoint.fileIndividual(fileId).path)" - default: return downloadUrlString(for: fileId) - } - } - - public static func downloadUrlString(for fileId: String) -> String { - return "\(fileServer)/\(Endpoint.fileIndividual(fileId).path)" - } - } - - static func preparedUpload( - data: Data, - requestAndPathBuildTimeout: TimeInterval? = nil, - using dependencies: Dependencies - ) throws -> PreparedRequest { - return try PreparedRequest( - request: Request( - endpoint: FileServer.Endpoint.file, - destination: .serverUpload( - server: FileServer.fileServer, - x25519PublicKey: FileServer.fileServerPublicKey, - fileName: nil - ), - body: data - ), - responseType: FileUploadResponse.self, - requestTimeout: Network.fileUploadTimeout, - requestAndPathBuildTimeout: requestAndPathBuildTimeout, - using: dependencies - ) - } - - static func preparedDownload( - url: URL, - using dependencies: Dependencies - ) throws -> PreparedRequest { - return try PreparedRequest( - request: Request( - endpoint: FileServer.Endpoint.directUrl(url), - destination: .serverDownload( - url: url, - x25519PublicKey: FileServer.fileServerPublicKey, - fileName: nil - ) - ), - responseType: Data.self, - requestTimeout: Network.fileUploadTimeout, - using: dependencies - ) - } -} diff --git a/SessionSnodeKit/Utilities/Threading+SSK.swift b/SessionSnodeKit/Utilities/Threading+SSK.swift deleted file mode 100644 index 3424ad438e..0000000000 --- a/SessionSnodeKit/Utilities/Threading+SSK.swift +++ /dev/null @@ -1,6 +0,0 @@ -import Foundation -import SessionUtilitiesKit - -public extension Threading { - static let workQueue = DispatchQueue(label: "SessionSnodeKit.workQueue", qos: .userInitiated) // It's important that this is a serial queue -} diff --git a/SessionSnodeKitTests/_TestUtilities/CommonSSKMockExtensions.swift b/SessionSnodeKitTests/_TestUtilities/CommonSSKMockExtensions.swift deleted file mode 100644 index 85e05a67af..0000000000 --- a/SessionSnodeKitTests/_TestUtilities/CommonSSKMockExtensions.swift +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. - -import Foundation - -@testable import SessionSnodeKit - -extension NoResponse: Mocked { - static var mock: NoResponse = NoResponse() -} - -extension Network.BatchSubResponse: MockedGeneric where T: Mocked { - typealias Generic = T - - static func mock(type: T.Type) -> Network.BatchSubResponse { - return Network.BatchSubResponse( - code: 200, - headers: [:], - body: Generic.mock, - failedToParseBody: false - ) - } -} - -extension Network.BatchSubResponse { - static func mockArrayValue(type: M.Type) -> Network.BatchSubResponse> { - return Network.BatchSubResponse( - code: 200, - headers: [:], - body: [M.mock], - failedToParseBody: false - ) - } -} - -extension Network.Destination: Mocked { - static var mock: Network.Destination = try! Network.Destination.server( - server: "testServer", - headers: [:], - x25519PublicKey: "" - ).withGeneratedUrl(for: MockEndpoint.mock) -} diff --git a/SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift b/SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift index dcf2b6ea26..beaaa7ed26 100644 --- a/SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift +++ b/SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift @@ -5,7 +5,7 @@ import GRDB import Quick import Nimble import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionMessagingKit import SessionUtilitiesKit @@ -21,12 +21,7 @@ class ThreadDisappearingMessagesSettingsViewModelSpec: AsyncSpec { } @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNSnodeKit.self, - SNMessagingKit.self, - DeprecatedUIKitMigrationTarget.self - ], + migrations: SNMessagingKit.migrations, using: dependencies, initialData: { db in try SessionThread( diff --git a/SessionTests/Conversations/Settings/ThreadNotificationSettingsViewModelSpec.swift b/SessionTests/Conversations/Settings/ThreadNotificationSettingsViewModelSpec.swift index b339cbe090..8d2532b0b0 100644 --- a/SessionTests/Conversations/Settings/ThreadNotificationSettingsViewModelSpec.swift +++ b/SessionTests/Conversations/Settings/ThreadNotificationSettingsViewModelSpec.swift @@ -5,7 +5,7 @@ import GRDB import Quick import Nimble import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionMessagingKit import SessionUtilitiesKit @@ -21,12 +21,7 @@ class ThreadNotificationSettingsViewModelSpec: AsyncSpec { } @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNSnodeKit.self, - SNMessagingKit.self, - DeprecatedUIKitMigrationTarget.self - ], + migrations: SNMessagingKit.migrations, using: dependencies, initialData: { db in try SessionThread( diff --git a/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift b/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift index 5f9fdb51ac..77514dedf8 100644 --- a/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift +++ b/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift @@ -5,7 +5,7 @@ import GRDB import Quick import Nimble import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionUtilitiesKit @testable import SessionUIKit @@ -29,12 +29,7 @@ class ThreadSettingsViewModelSpec: AsyncSpec { } @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNSnodeKit.self, - SNMessagingKit.self, - DeprecatedUIKitMigrationTarget.self - ], + migrations: SNMessagingKit.migrations, using: dependencies, initialData: { db in try Identity( diff --git a/SessionTests/Database/DatabaseSpec.swift b/SessionTests/Database/DatabaseSpec.swift index 6e0561e696..662bb23e3b 100644 --- a/SessionTests/Database/DatabaseSpec.swift +++ b/SessionTests/Database/DatabaseSpec.swift @@ -6,7 +6,7 @@ import Quick import Nimble import SessionUtil import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit @testable import Session @testable import SessionMessagingKit @@ -38,14 +38,7 @@ class DatabaseSpec: QuickSpec { @TestState var initialResult: Result! = nil @TestState var finalResult: Result! = nil - let allMigrations: [Storage.KeyedMigration] = SynchronousStorage.sortedMigrationInfo( - migrationTargets: [ - SNUtilitiesKit.self, - SNSnodeKit.self, - SNMessagingKit.self, - DeprecatedUIKitMigrationTarget.self - ] - ) + let allMigrations: [Migration.Type] = SNMessagingKit.migrations let dynamicTests: [MigrationTest] = MigrationTest.extractTests(allMigrations) let allTableTypes: [(TableRecord & FetchableRecord).Type] = MigrationTest.extractDatabaseTypes(allMigrations) MigrationTest.explicitValues = [ @@ -75,12 +68,7 @@ class DatabaseSpec: QuickSpec { // MARK: -- can be created from an empty state it("can be created from an empty state") { mockStorage.perform( - migrationTargets: [ - SNUtilitiesKit.self, - SNSnodeKit.self, - SNMessagingKit.self, - DeprecatedUIKitMigrationTarget.self - ], + migrations: allMigrations, async: false, onProgressUpdate: nil, onComplete: { result in initialResult = result } @@ -92,7 +80,7 @@ class DatabaseSpec: QuickSpec { // MARK: -- can still parse the database table types it("can still parse the database table types") { mockStorage.perform( - sortedMigrations: allMigrations, + migrations: allMigrations, async: false, onProgressUpdate: nil, onComplete: { result in initialResult = result } @@ -115,7 +103,7 @@ class DatabaseSpec: QuickSpec { // MARK: -- can still parse the database types setting null where possible it("can still parse the database types setting null where possible") { mockStorage.perform( - sortedMigrations: allMigrations, + migrations: allMigrations, async: false, onProgressUpdate: nil, onComplete: { result in initialResult = result } @@ -137,9 +125,9 @@ class DatabaseSpec: QuickSpec { // MARK: -- can migrate from X to Y dynamicTests.forEach { test in - it("can migrate from \(test.initialMigrationKey) to \(test.finalMigrationKey)") { + it("can migrate from \(test.initialMigrationIdentifier) to \(test.finalMigrationIdentifier)") { let initialStateResult: Result = { - if let cachedResult: Result = snapshotCache[test.initialMigrationKey] { + if let cachedResult: Result = snapshotCache[test.initialMigrationIdentifier] { return cachedResult } @@ -153,7 +141,7 @@ class DatabaseSpec: QuickSpec { // Generate dummy data (otherwise structural issues or invalid foreign keys won't error) var initialResult: Result! storage.perform( - sortedMigrations: test.initialMigrations, + migrations: test.initialMigrations, async: false, onProgressUpdate: nil, onComplete: { result in initialResult = result } @@ -163,10 +151,10 @@ class DatabaseSpec: QuickSpec { // Generate dummy data (otherwise structural issues or invalid foreign keys won't error) try MigrationTest.generateDummyData(storage, nullsWherePossible: false) - snapshotCache[test.initialMigrationKey] = .success(dbQueue) + snapshotCache[test.initialMigrationIdentifier] = .success(dbQueue) return .success(dbQueue) } catch { - snapshotCache[test.initialMigrationKey] = .failure(error) + snapshotCache[test.initialMigrationIdentifier] = .failure(error) return .failure(error) } }() @@ -175,7 +163,7 @@ class DatabaseSpec: QuickSpec { switch initialStateResult { case .success(let db): sourceDb = db case .failure(let error): - fail("Failed to prepare the initial state for '\(test.initialMigrationKey)'. Error: \(error)") + fail("Failed to prepare the initial state for '\(test.initialMigrationIdentifier)'. Error: \(error)") return } @@ -186,7 +174,7 @@ class DatabaseSpec: QuickSpec { // Peform the target migrations to ensure the migrations themselves worked correctly mockStorage.perform( - sortedMigrations: test.migrationsToTest, + migrations: test.migrationsToTest, async: false, onProgressUpdate: nil, onComplete: { result in finalResult = result } @@ -195,12 +183,69 @@ class DatabaseSpec: QuickSpec { switch finalResult { case .success: break case .failure(let error): - fail("Failed to migrate from '\(test.initialMigrationKey)' to '\(test.finalMigrationKey)'. Error: \(error)") + fail("Failed to migrate from '\(test.initialMigrationIdentifier)' to '\(test.finalMigrationIdentifier)'. Error: \(error)") case .none: - fail("Failed to migrate from '\(test.initialMigrationKey)' to '\(test.finalMigrationKey)'. Error: No result") + fail("Failed to migrate from '\(test.initialMigrationIdentifier)' to '\(test.finalMigrationIdentifier)'. Error: No result") } } } + + // MARK: -- migration order hasn't changed + it("migration order hasn't changed") { + expect(SNMessagingKit.migrations.map { $0.identifier }).to(equal([ + "utilitiesKit.initialSetup", + "utilitiesKit.SetupStandardJobs", + "utilitiesKit.YDBToGRDBMigration", + "snodeKit.initialSetup", + "snodeKit.SetupStandardJobs", + "messagingKit.initialSetup", + "messagingKit.SetupStandardJobs", + "snodeKit.YDBToGRDBMigration", + "messagingKit.YDBToGRDBMigration", + "snodeKit.FlagMessageHashAsDeletedOrInvalid", + "messagingKit.RemoveLegacyYDB", + "utilitiesKit.AddJobPriority", + "messagingKit.FixDeletedMessageReadState", + "messagingKit.FixHiddenModAdminSupport", + "messagingKit.HomeQueryOptimisationIndexes", + "uiKit.ThemePreferences", + "messagingKit.EmojiReacts", + "messagingKit.OpenGroupPermission", + "messagingKit.AddThreadIdToFTS", + "utilitiesKit.AddJobUniqueHash", + "snodeKit.AddSnodeReveivedMessageInfoPrimaryKey", + "snodeKit.DropSnodeCache", + "snodeKit.SplitSnodeReceivedMessageInfo", + "snodeKit.ResetUserConfigLastHashes", + "messagingKit.AddPendingReadReceipts", + "messagingKit.AddFTSIfNeeded", + "messagingKit.SessionUtilChanges", + "messagingKit.GenerateInitialUserConfigDumps", + "messagingKit.BlockCommunityMessageRequests", + "messagingKit.MakeBrokenProfileTimestampsNullable", + "messagingKit.RebuildFTSIfNeeded_2_4_5", + "messagingKit.DisappearingMessagesWithTypes", + "messagingKit.ScheduleAppUpdateCheckJob", + "messagingKit.AddMissingWhisperFlag", + "messagingKit.ReworkRecipientState", + "messagingKit.GroupsRebuildChanges", + "messagingKit.GroupsExpiredFlag", + "messagingKit.FixBustedInteractionVariant", + "messagingKit.DropLegacyClosedGroupKeyPairTable", + "messagingKit.MessageDeduplicationTable", + "utilitiesKit.RenameTableSettingToKeyValueStore", + "messagingKit.MoveSettingsToLibSession", + "messagingKit.RenameAttachments", + "messagingKit.AddProMessageFlag", + "LastProfileUpdateTimestamp" + ])) + } + + // MARK: -- there are no duplicate migration names + it("there are no duplicate migration names") { + expect(Set(SNMessagingKit.migrations.map { $0.identifier }).sorted()) + .to(equal(SNMessagingKit.migrations.map { $0.identifier }.sorted())) + } } } } @@ -236,15 +281,15 @@ private struct TableColumn: Hashable { private class MigrationTest { static var explicitValues: [TableColumn: (any DatabaseValueConvertible)] = [:] - let initialMigrations: [Storage.KeyedMigration] - let migrationsToTest: [Storage.KeyedMigration] + let initialMigrations: [Migration.Type] + let migrationsToTest: [Migration.Type] - var initialMigrationKey: String { return (initialMigrations.last?.key ?? "an empty database") } - var finalMigrationKey: String { return (migrationsToTest.last?.key ?? "invalid") } + var initialMigrationIdentifier: String { return (initialMigrations.last?.identifier ?? "an empty database") } + var finalMigrationIdentifier: String { return (migrationsToTest.last?.identifier ?? "invalid") } private init( - initialMigrations: [Storage.KeyedMigration], - migrationsToTest: [Storage.KeyedMigration] + initialMigrations: [Migration.Type], + migrationsToTest: [Migration.Type] ) { self.initialMigrations = initialMigrations self.migrationsToTest = migrationsToTest @@ -252,7 +297,7 @@ private class MigrationTest { // MARK: - Test Data - static func extractTests(_ allMigrations: [Storage.KeyedMigration]) -> [MigrationTest] { + static func extractTests(_ allMigrations: [Migration.Type]) -> [MigrationTest] { return (0..<(allMigrations.count - 1)) .flatMap { index -> [MigrationTest] in ((index + 1).. MigrationTest in @@ -264,10 +309,10 @@ private class MigrationTest { } } - static func extractDatabaseTypes(_ allMigrations: [Storage.KeyedMigration]) -> [(TableRecord & FetchableRecord).Type] { + static func extractDatabaseTypes(_ allMigrations: [Migration.Type]) -> [(TableRecord & FetchableRecord).Type] { return Array(allMigrations .reduce(into: [:]) { result, next in - next.migration.createdTables.forEach { table in + next.createdTables.forEach { table in result[ObjectIdentifier(table).hashValue] = table } } @@ -392,69 +437,3 @@ private class MigrationTest { } } } - -enum TestAllMigrationRequirementsReversedMigratableTarget: MigratableTarget { // Just to make the external API nice - public static func migrations() -> TargetMigrations { - return TargetMigrations( - identifier: .session, - migrations: [ - [ - TestRequiresAllMigrationRequirementsReversedMigration.self - ] - ] - ) - } -} - -enum TestRequiresLibSessionStateMigratableTarget: MigratableTarget { // Just to make the external API nice - public static func migrations() -> TargetMigrations { - return TargetMigrations( - identifier: .session, - migrations: [ - [ - TestRequiresLibSessionStateMigration.self - ] - ] - ) - } -} - -enum TestRequiresSessionIdCachedMigratableTarget: MigratableTarget { // Just to make the external API nice - public static func migrations() -> TargetMigrations { - return TargetMigrations( - identifier: .session, - migrations: [ - [ - TestRequiresSessionIdCachedMigration.self - ] - ] - ) - } -} - -enum TestRequiresAllMigrationRequirementsReversedMigration: Migration { - static let target: TargetMigrations.Identifier = .session - static let identifier: String = "test" // stringlint:ignore - static let minExpectedRunDuration: TimeInterval = 0.1 - static let createdTables: [(TableRecord & FetchableRecord).Type] = [] - - static func migrate(_ db: ObservingDatabase, using dependencies: Dependencies) throws {} -} - -enum TestRequiresLibSessionStateMigration: Migration { - static let target: TargetMigrations.Identifier = .session - static let identifier: String = "test" // stringlint:ignore - static let minExpectedRunDuration: TimeInterval = 0.1 - static let createdTables: [(TableRecord & FetchableRecord).Type] = [] - - static func migrate(_ db: ObservingDatabase, using dependencies: Dependencies) throws {} -} - -enum TestRequiresSessionIdCachedMigration: Migration { - static let target: TargetMigrations.Identifier = .session - static let identifier: String = "test" // stringlint:ignore - static let minExpectedRunDuration: TimeInterval = 0.1 - static let createdTables: [(TableRecord & FetchableRecord).Type] = [] - - static func migrate(_ db: ObservingDatabase, using dependencies: Dependencies) throws {} -} diff --git a/SessionTests/Onboarding/OnboardingSpec.swift b/SessionTests/Onboarding/OnboardingSpec.swift index e6a4e41559..d60698f19d 100644 --- a/SessionTests/Onboarding/OnboardingSpec.swift +++ b/SessionTests/Onboarding/OnboardingSpec.swift @@ -9,7 +9,7 @@ import SessionUIKit import SessionUtilitiesKit @testable import Session -@testable import SessionSnodeKit +@testable import SessionNetworkingKit @testable import SessionMessagingKit class OnboardingSpec: AsyncSpec { @@ -24,12 +24,7 @@ class OnboardingSpec: AsyncSpec { } @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self, - SNSnodeKit.self, - SNMessagingKit.self, - DeprecatedUIKitMigrationTarget.self - ], + migrations: SNMessagingKit.migrations, using: dependencies ) @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = MockCrypto( @@ -43,6 +38,14 @@ class OnboardingSpec: AsyncSpec { crypto .when { $0.generate(.randomBytes(.any)) } .thenReturn(Data([1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8])) + crypto + .when { $0.generate(.ed25519Seed(ed25519SecretKey: .any)) } + .thenReturn(Data([ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, + 1, 2 + ])) crypto .when { $0.generate(.ed25519KeyPair(seed: .any)) } .thenReturn( @@ -73,7 +76,8 @@ class OnboardingSpec: AsyncSpec { ) @TestState(defaults: .standard, in: dependencies) var mockUserDefaults: MockUserDefaults! = MockUserDefaults( initialSetup: { defaults in - defaults.when { $0.bool(forKey: .any) }.thenReturn(true) + defaults.when { $0.bool(forKey: UserDefaults.BoolKey.isMainAppActive.rawValue) }.thenReturn(true) + defaults.when { $0.bool(forKey: UserDefaults.BoolKey.isUsingFullAPNs.rawValue) }.thenReturn(false) defaults.when { $0.integer(forKey: .any) }.thenReturn(2) defaults.when { $0.set(true, forKey: .any) }.thenReturn(()) defaults.when { $0.set(false, forKey: .any) }.thenReturn(()) @@ -117,7 +121,7 @@ class OnboardingSpec: AsyncSpec { .thenReturn(MockNetwork.batchResponseData( with: [ ( - SnodeAPI.Endpoint.getMessages, + Network.SnodeAPI.Endpoint.getMessages, GetMessagesResponse( messages: (pendingPushes? .pushData @@ -190,9 +194,10 @@ class OnboardingSpec: AsyncSpec { } } - // MARK: -- without a stored key pair - context("without a stored key pair") { + // MARK: -- without a stored secret key + context("without a stored secret key") { beforeEach { + mockGeneralCache.when { $0.ed25519SecretKey }.thenReturn([]) mockCrypto .when { $0.generate(.ed25519KeyPair(seed: .any)) } .thenReturn(KeyPair(publicKey: [1, 2, 3], secretKey: [4, 5, 6])) @@ -214,19 +219,20 @@ class OnboardingSpec: AsyncSpec { } } - // MARK: -- with a stored key pair - context("with a stored key pair") { + // MARK: -- with a stored secret key + context("with a stored secret key") { beforeEach { - mockStorage.write { db in - try Identity( - variant: .ed25519PublicKey, - data: Data(hex: TestConstants.edPublicKey) - ).insert(db) - try Identity( - variant: .ed25519SecretKey, - data: Data(hex: TestConstants.edSecretKey) - ).insert(db) - } + mockGeneralCache + .when { $0.ed25519SecretKey } + .thenReturn(Array(Data(hex: TestConstants.edSecretKey))) + mockCrypto + .when { $0.generate(.ed25519KeyPair(seed: .any)) } + .thenReturn( + KeyPair( + publicKey: Array(Data(hex: TestConstants.edPublicKey)), + secretKey: Array(Data(hex: TestConstants.edSecretKey)) + ) + ) } // MARK: ---- does not generate a seed @@ -266,22 +272,23 @@ class OnboardingSpec: AsyncSpec { // MARK: ---- and failing to generate an x25519KeyPair context("and failing to generate an x25519KeyPair") { beforeEach { - mockStorage.write { db in - try Identity.deleteAll(db) - try Identity( - variant: .ed25519PublicKey, - data: Data(hex: "090807") - ).insert(db) - try Identity( - variant: .ed25519SecretKey, - data: Data(hex: TestConstants.edSecretKey) - ).insert(db) - } + mockCrypto.removeMocksFor { $0.generate(.ed25519KeyPair(seed: .any)) } + mockCrypto.removeMocksFor { $0.generate(.ed25519Seed(ed25519SecretKey: .any)) } mockCrypto - .when { $0.generate(.x25519(ed25519Pubkey: [9, 8, 7])) } - .thenReturn(nil) + .when { try $0.tryGenerate(.ed25519Seed(ed25519SecretKey: .any)) } + .thenThrow(MockError.mockedData) mockCrypto - .when { $0.generate(.ed25519KeyPair(seed: .any)) } + .when { + $0.generate(.ed25519KeyPair( + seed: Array(Data(hex: TestConstants.edSecretKey)) + )) + } + .thenReturn(KeyPair(publicKey: [1, 2, 3], secretKey: [9, 8, 7])) + mockCrypto + .when { + $0.generate(.ed25519KeyPair( + seed: [1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8] + )) } .thenReturn(KeyPair(publicKey: [1, 2, 3], secretKey: [4, 5, 6])) mockCrypto .when { $0.generate(.x25519(ed25519Pubkey: [1, 2, 3])) } @@ -289,6 +296,9 @@ class OnboardingSpec: AsyncSpec { mockCrypto .when { $0.generate(.x25519(ed25519Seckey: [4, 5, 6])) } .thenReturn([7, 6, 5, 4]) + mockCrypto + .when { $0.generate(.x25519(ed25519Pubkey: [9, 8, 7])) } + .thenReturn(nil) } // MARK: ------ generates new credentials @@ -321,6 +331,9 @@ class OnboardingSpec: AsyncSpec { // MARK: ---- and an existing display name context("and an existing display name") { beforeEach { + mockUserDefaults + .when { $0.bool(forKey: UserDefaults.BoolKey.isUsingFullAPNs.rawValue) } + .thenReturn(true) mockLibSession .when { $0.profile( @@ -359,26 +372,33 @@ class OnboardingSpec: AsyncSpec { // MARK: ------ after generating new credentials context("after generating new credentials") { beforeEach { - mockStorage.write { db in - try Identity.deleteAll(db) - try Identity( - variant: .ed25519PublicKey, - data: Data(hex: "090807") - ).insert(db) - try Identity( - variant: .ed25519SecretKey, - data: Data(hex: TestConstants.edSecretKey) - ).insert(db) - } + mockCrypto.removeMocksFor { $0.generate(.ed25519KeyPair(seed: .any)) } + mockCrypto.removeMocksFor { $0.generate(.ed25519Seed(ed25519SecretKey: .any)) } mockCrypto - .when { $0.generate(.x25519(ed25519Pubkey: [9, 8, 7])) } - .thenReturn(nil) + .when { try $0.tryGenerate(.ed25519Seed(ed25519SecretKey: .any)) } + .thenThrow(MockError.mockedData) + mockCrypto + .when { + $0.generate(.ed25519KeyPair( + seed: Array(Data(hex: TestConstants.edSecretKey)) + )) + } + .thenReturn(KeyPair(publicKey: [1, 2, 3], secretKey: [9, 8, 7])) + mockCrypto + .when { + $0.generate(.ed25519KeyPair( + seed: [1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8] + )) } + .thenReturn(KeyPair(publicKey: [1, 2, 3], secretKey: [4, 5, 6])) mockCrypto .when { $0.generate(.x25519(ed25519Pubkey: [1, 2, 3])) } .thenReturn([4, 3, 2, 1]) mockCrypto .when { $0.generate(.x25519(ed25519Seckey: [4, 5, 6])) } .thenReturn([7, 6, 5, 4]) + mockCrypto + .when { $0.generate(.x25519(ed25519Pubkey: [9, 8, 7])) } + .thenReturn(nil) } // MARK: -------- has an empty display name @@ -390,6 +410,12 @@ class OnboardingSpec: AsyncSpec { // MARK: ---- and a missing display name context("and a missing display name") { + beforeEach { + mockUserDefaults + .when { $0.bool(forKey: UserDefaults.BoolKey.isUsingFullAPNs.rawValue) } + .thenReturn(true) + } + // MARK: ------ has an empty display name it("has an empty display name") { expect(cache.displayName).to(equal("")) @@ -583,13 +609,11 @@ class OnboardingSpec: AsyncSpec { Profile( id: "0588672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b", name: "TestCompleteName", - lastNameUpdate: 1234567890, nickname: nil, displayPictureUrl: nil, displayPictureEncryptionKey: nil, - displayPictureLastUpdated: nil, - blocksCommunityMessageRequests: nil, - lastBlocksCommunityMessageRequests: nil + profileLastUpdated: 1234567890, + blocksCommunityMessageRequests: nil ) ])) } @@ -623,13 +647,11 @@ class OnboardingSpec: AsyncSpec { Profile( id: "0588672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b", name: "TestCompleteName", - lastNameUpdate: nil, nickname: nil, displayPictureUrl: nil, displayPictureEncryptionKey: nil, - displayPictureLastUpdated: nil, - blocksCommunityMessageRequests: nil, - lastBlocksCommunityMessageRequests: nil + profileLastUpdated: nil, + blocksCommunityMessageRequests: nil ) )) } @@ -639,16 +661,36 @@ class OnboardingSpec: AsyncSpec { let result: [ConfigDump]? = mockStorage.read { db in try ConfigDump.fetchAll(db) } - let expectedData: Data? = Data(base64Encoded: "ZDE6IWkxZTE6JDEwNDpkMTojaTFlMTomZDE6K2ktMWUxOm4xNjpUZXN0Q29tcGxldGVOYW1lZTE6PGxsaTBlMzI66hc7V77KivGMNRmnu/acPnoF0cBJ+pVYNB2Ou0iwyWVkZWVlMTo9ZDE6KzA6MTpuMDplZTE6KGxlMTopbGUxOipkZTE6K2RlZQ==") + try require(result).to(haveCount(1)) - expect(result).to(equal([ + /// Since the `UserProfile` data is not deterministic then the best we can do is compare the `ConfigDump` + /// without it's data to ensure everything else is correct, then check that the dump data contains expected values + let resultData: Data = result![0].data + let resultWithoutData: ConfigDump = ConfigDump( + variant: result![0].variant, + sessionId: result![0].sessionId.hexString, + data: Data(), + timestampMs: result![0].timestampMs + ) + var resultDataString: String = "" + + for i in (0.. Void)? + public var onToggleExpansion: (@MainActor () -> Void)? public var font: UIFont { get { label.font } @@ -164,7 +164,7 @@ public class ExpandableLabel: UIView { // MARK: - Interaction - private func toggleExpansion() { + @MainActor private func toggleExpansion() { isExpanded.toggle() buttonLabel.text = (isExpanded ? "viewLess".localized() : "viewMore".localized()) label.numberOfLines = isExpanded ? 0 : (maxNumberOfLines - 1) @@ -178,7 +178,7 @@ public class ExpandableLabel: UIView { toggleDebounceTimer?.invalidate() toggleDebounceTimer = Timer.scheduledTimer(withTimeInterval: 0.2, repeats: false) { [weak self] _ in - self?.toggleExpansion() + Task { @MainActor [weak self] in self?.toggleExpansion() } } } } diff --git a/SessionUIKit/Components/Input View/InputTextView.swift b/SessionUIKit/Components/Input View/InputTextView.swift index 1f9fd8c204..069dec0413 100644 --- a/SessionUIKit/Components/Input View/InputTextView.swift +++ b/SessionUIKit/Components/Input View/InputTextView.swift @@ -92,11 +92,11 @@ public final class InputTextView: UITextView, UITextViewDelegate { // MARK: - Updating - public func textViewDidChange(_ textView: UITextView) { + @MainActor public func textViewDidChange(_ textView: UITextView) { handleTextChanged() } - private func handleTextChanged() { + @MainActor private func handleTextChanged() { defer { snDelegate?.inputTextViewDidChangeContent(self) } placeholderLabel.isHidden = !(text ?? "").isEmpty @@ -118,7 +118,7 @@ public final class InputTextView: UITextView, UITextViewDelegate { // MARK: - InputTextViewDelegate public protocol InputTextViewDelegate: AnyObject { - func inputTextViewDidChangeSize(_ inputTextView: InputTextView) - func inputTextViewDidChangeContent(_ inputTextView: InputTextView) - func didPasteImageFromPasteboard(_ inputTextView: InputTextView, image: UIImage) + @MainActor func inputTextViewDidChangeSize(_ inputTextView: InputTextView) + @MainActor func inputTextViewDidChangeContent(_ inputTextView: InputTextView) + @MainActor func didPasteImageFromPasteboard(_ inputTextView: InputTextView, image: UIImage) } diff --git a/SessionUIKit/Components/Input View/InputViewButton.swift b/SessionUIKit/Components/Input View/InputViewButton.swift index 184d527fa4..37c90ead2f 100644 --- a/SessionUIKit/Components/Input View/InputViewButton.swift +++ b/SessionUIKit/Components/Input View/InputViewButton.swift @@ -192,8 +192,8 @@ public final class InputViewButton: UIView { // MARK: - Delegate public protocol InputViewButtonDelegate: AnyObject { - func handleInputViewButtonTapped(_ inputViewButton: InputViewButton) - func handleInputViewButtonLongPressBegan(_ inputViewButton: InputViewButton?) - func handleInputViewButtonLongPressMoved(_ inputViewButton: InputViewButton, with touch: UITouch?) - func handleInputViewButtonLongPressEnded(_ inputViewButton: InputViewButton, with touch: UITouch?) + @MainActor func handleInputViewButtonTapped(_ inputViewButton: InputViewButton) + @MainActor func handleInputViewButtonLongPressBegan(_ inputViewButton: InputViewButton?) + @MainActor func handleInputViewButtonLongPressMoved(_ inputViewButton: InputViewButton, with touch: UITouch?) + @MainActor func handleInputViewButtonLongPressEnded(_ inputViewButton: InputViewButton, with touch: UITouch?) } diff --git a/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift b/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift index 90be264afb..b22ff1e77a 100644 --- a/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift +++ b/SessionUIKit/Components/Modals & Toast/ConfirmationModal.swift @@ -38,6 +38,17 @@ public class ConfirmationModal: Modal, UITextFieldDelegate, UITextViewDelegate { return result }() + private lazy var proImageTapGestureRecognizer: UITapGestureRecognizer = { + let result: UITapGestureRecognizer = UITapGestureRecognizer( + target: self, + action: #selector(proImageTapped) + ) + proDescriptionLabelContainer.addGestureRecognizer(result) + result.isEnabled = false + + return result + }() + private lazy var titleLabel: UILabel = { let result: UILabel = UILabel() result.font = .boldSystemFont(ofSize: Values.mediumFontSize) @@ -174,6 +185,21 @@ public class ConfirmationModal: Modal, UITextFieldDelegate, UITextViewDelegate { return result }() + private lazy var proDescriptionLabel: UILabel = { + let result: UILabel = UILabel() + result.font = .systemFont(ofSize: Values.smallFontSize) + result.themeTextColor = .textSecondary + + return result + }() + + private lazy var proDescriptionLabelContainer: UIView = { + let result: UIView = UIView() + result.isHidden = true + + return result + }() + private lazy var imageViewContainer: UIView = { let result: UIView = UIView() result.isHidden = true @@ -181,7 +207,10 @@ public class ConfirmationModal: Modal, UITextFieldDelegate, UITextViewDelegate { return result }() - private lazy var profileView: ProfilePictureView = ProfilePictureView(size: .hero, dataManager: nil) + private lazy var profileView: ProfilePictureView = ProfilePictureView( + size: .modal, + dataManager: nil + ) private lazy var textToConfirmContainer: UIView = { let result: UIView = UIView() @@ -233,6 +262,7 @@ public class ConfirmationModal: Modal, UITextFieldDelegate, UITextViewDelegate { textToConfirmContainer, textViewContainer, textViewErrorLabel, + proDescriptionLabelContainer, imageViewContainer ] ) @@ -300,12 +330,6 @@ public class ConfirmationModal: Modal, UITextFieldDelegate, UITextViewDelegate { } public override func populateContentView() { - let gestureRecogniser: UITapGestureRecognizer = UITapGestureRecognizer( - target: self, - action: #selector(contentViewTapped) - ) - contentView.addGestureRecognizer(gestureRecogniser) - contentView.addSubview(mainStackView) contentView.addSubview(closeButton) @@ -341,8 +365,13 @@ public class ConfirmationModal: Modal, UITextFieldDelegate, UITextViewDelegate { imageViewContainer.addSubview(profileView) profileView.center(.horizontal, in: imageViewContainer) - profileView.pin(.top, to: .top, of: imageViewContainer) - profileView.pin(.bottom, to: .bottom, of: imageViewContainer) + profileView.pin(.top, to: .top, of: imageViewContainer, withInset: 20) + profileView.pin(.bottom, to: .bottom, of: imageViewContainer, withInset: -20) + + proDescriptionLabelContainer.addSubview(proDescriptionLabel) + proDescriptionLabel.center(.horizontal, in: proDescriptionLabelContainer) + proDescriptionLabel.pin(.top, to: .top, of: proDescriptionLabelContainer) + proDescriptionLabel.pin(.bottom, to: .bottom, of: proDescriptionLabelContainer) mainStackView.pin(to: contentView) closeButton.pin(.top, to: .top, of: contentView, withInset: 8) @@ -501,23 +530,28 @@ public class ConfirmationModal: Modal, UITextFieldDelegate, UITextViewDelegate { contentStackView.addArrangedSubview(radioButton) } - case .image(let source, let placeholder, let icon, let style, let accessibility, let dataManager, let onClick): + case .image(let source, let placeholder, let icon, let style, let description, let accessibility, let dataManager, _, let onClick): imageViewContainer.isAccessibilityElement = (accessibility != nil) imageViewContainer.accessibilityIdentifier = accessibility?.identifier imageViewContainer.accessibilityLabel = accessibility?.label mainStackView.spacing = 0 + contentStackView.spacing = Values.verySmallSpacing + proDescriptionLabelContainer.isHidden = (description == nil) + proDescriptionLabel.attributedText = description imageViewContainer.isHidden = false profileView.clipsToBounds = (style == .circular) profileView.setDataManager(dataManager) profileView.update( ProfilePictureView.Info( source: (source ?? placeholder), + animationBehaviour: .generic(true), // Force the animate the avatar in modals icon: icon ) ) internalOnBodyTap = onClick contentTapGestureRecognizer.isEnabled = false imageViewTapGestureRecognizer.isEnabled = true + proImageTapGestureRecognizer.isEnabled = true case .inputConfirmation(let explanation, let textToConfirm): explanationLabel.themeAttributedText = explanation @@ -632,7 +666,7 @@ public class ConfirmationModal: Modal, UITextFieldDelegate, UITextViewDelegate { @objc private func imageViewTapped() { internalOnBodyTap?({ [weak self, info = self.info] valueUpdate in switch (valueUpdate, info.body) { - case (.image(let updatedIdentifier, let updatedData), .image(_, let placeholder, let icon, let style, let accessibility, let dataManager, let onClick)): + case (.image(let updatedIdentifier, let updatedData), .image(_, let placeholder, _, let style, let description, let accessibility, let dataManager, let onProBadgeTapped, let onClick)): self?.updateContent( with: info.with( body: .image( @@ -640,12 +674,15 @@ public class ConfirmationModal: Modal, UITextFieldDelegate, UITextViewDelegate { ImageDataManager.DataSource.data(updatedIdentifier, $0) }, placeholder: placeholder, - icon: icon, + icon: (updatedData == nil ? .rightPlus : .pencil), style: style, + description: description, accessibility: accessibility, dataManager: dataManager, + onProBageTapped: onProBadgeTapped, onClick: onClick - ) + ), + cancelTitle: "clear".localized() ) ) @@ -654,6 +691,11 @@ public class ConfirmationModal: Modal, UITextFieldDelegate, UITextViewDelegate { }) } + @objc private func proImageTapped() { + guard case .image(_, _, _, _, let description, _, _, let onProBadgeTapped, _) = info.body, (description != nil) else { return } + onProBadgeTapped?() + } + @objc internal func confirmationPressed() { internalOnConfirm?(self) } @@ -797,6 +839,7 @@ public extension ConfirmationModal { public func with( body: Body? = nil, + cancelTitle: String? = nil, onConfirm: ((ConfirmationModal) -> ())? = nil, onCancel: ((ConfirmationModal) -> ())? = nil, afterClosed: (() -> ())? = nil @@ -808,7 +851,7 @@ public extension ConfirmationModal { confirmTitle: self.confirmTitle, confirmStyle: self.confirmStyle, confirmEnabled: self.confirmEnabled, - cancelTitle: self.cancelTitle, + cancelTitle: (cancelTitle ?? self.cancelTitle), cancelStyle: self.cancelStyle, cancelEnabled: self.cancelEnabled, hasCloseButton: self.hasCloseButton, @@ -1009,8 +1052,10 @@ public extension ConfirmationModal.Info { placeholder: ImageDataManager.DataSource?, icon: ProfilePictureView.ProfileIcon = .none, style: ImageStyle, + description: NSAttributedString?, accessibility: Accessibility?, dataManager: ImageDataManagerType, + onProBageTapped: (() -> Void)?, onClick: (@MainActor (@escaping (ConfirmationModal.ValueUpdate) -> Void) -> Void) ) @@ -1045,12 +1090,13 @@ public extension ConfirmationModal.Info { lhsOptions == rhsOptions ) - case (.image(let lhsSource, let lhsPlaceholder, let lhsIcon, let lhsStyle, let lhsAccessibility, _, _), .image(let rhsSource, let rhsPlaceholder, let rhsIcon, let rhsStyle, let rhsAccessibility, _, _)): + case (.image(let lhsSource, let lhsPlaceholder, let lhsIcon, let lhsStyle, let lhsShowPro, let lhsAccessibility, _, _, _), .image(let rhsSource, let rhsPlaceholder, let rhsIcon, let rhsStyle, let rhsShowPro, let rhsAccessibility, _, _, _)): return ( lhsSource == rhsSource && lhsPlaceholder == rhsPlaceholder && lhsIcon == rhsIcon && lhsStyle == rhsStyle && + lhsShowPro == rhsShowPro && lhsAccessibility == rhsAccessibility ) @@ -1078,11 +1124,12 @@ public extension ConfirmationModal.Info { warning.hash(into: &hasher) options.hash(into: &hasher) - case .image(let source, let placeholder, let icon, let style, let accessibility, _, _): + case .image(let source, let placeholder, let icon, let style, let showPro, let accessibility, _, _, _): source.hash(into: &hasher) placeholder.hash(into: &hasher) icon.hash(into: &hasher) style.hash(into: &hasher) + showPro.hash(into: &hasher) accessibility.hash(into: &hasher) case .inputConfirmation(let explanation, let textToConfirm): diff --git a/SessionUIKit/Components/ProfilePictureView.swift b/SessionUIKit/Components/ProfilePictureView.swift index f7da0d828c..8310756f6c 100644 --- a/SessionUIKit/Components/ProfilePictureView.swift +++ b/SessionUIKit/Components/ProfilePictureView.swift @@ -2,10 +2,18 @@ import UIKit import Combine +import Lucide public final class ProfilePictureView: UIView { public struct Info { + public enum AnimationBehaviour { + case generic(Bool) // For communities and when Pro is not enabled + case contact(Bool) + case currentUser(SessionProManagerType) + } + let source: ImageDataManager.DataSource? + let animationBehaviour: AnimationBehaviour let renderingMode: UIImage.RenderingMode? let themeTintColor: ThemeValue? let inset: UIEdgeInsets @@ -15,6 +23,7 @@ public final class ProfilePictureView: UIView { public init( source: ImageDataManager.DataSource?, + animationBehaviour: AnimationBehaviour, renderingMode: UIImage.RenderingMode? = nil, themeTintColor: ThemeValue? = nil, inset: UIEdgeInsets = .zero, @@ -23,6 +32,7 @@ public final class ProfilePictureView: UIView { forcedBackgroundColor: ForcedThemeValue? = nil ) { self.source = source + self.animationBehaviour = animationBehaviour self.renderingMode = renderingMode self.themeTintColor = themeTintColor self.inset = inset @@ -37,12 +47,14 @@ public final class ProfilePictureView: UIView { case message case list case hero + case modal public var viewSize: CGFloat { switch self { case .navigation, .message: return 26 case .list: return 46 case .hero: return 110 + case .modal: return 90 } } @@ -51,6 +63,7 @@ public final class ProfilePictureView: UIView { case .navigation, .message: return 26 case .list: return 46 case .hero: return 80 + case .modal: return 90 } } @@ -59,6 +72,7 @@ public final class ProfilePictureView: UIView { case .navigation, .message: return 18 // Shouldn't be used case .list: return 32 case .hero: return 80 + case .modal: return 90 } } @@ -67,6 +81,7 @@ public final class ProfilePictureView: UIView { case .navigation, .message: return 10 // Intentionally not a multiple of 4 case .list: return 16 case .hero: return 24 + case .modal: return 24 // Shouldn't be used } } } @@ -76,6 +91,7 @@ public final class ProfilePictureView: UIView { case crown case rightPlus case letter(Character, Bool) + case pencil func iconVerticalInset(for size: Size) -> CGFloat { switch (self, size) { @@ -91,12 +107,13 @@ public final class ProfilePictureView: UIView { var isLeadingAligned: Bool { switch self { case .none, .crown, .letter: return true - case .rightPlus: return false + case .rightPlus, .pencil: return false } } } private var dataManager: ImageDataManagerType? + private var disposables: Set = Set() public var size: Size { didSet { widthConstraint.constant = (customWidth ?? size.viewSize) @@ -303,6 +320,7 @@ public final class ProfilePictureView: UIView { widthConstraint = self.set(.width, to: self.size.viewSize) heightConstraint = self.set(.height, to: self.size.viewSize) + .setting(priority: .defaultHigh) imageViewTopConstraint = imageContainerView.pin(.top, to: .top, of: self) imageViewLeadingConstraint = imageContainerView.pin(.leading, to: .leading, of: self) @@ -402,6 +420,7 @@ public final class ProfilePictureView: UIView { case .crown: imageView.image = UIImage(systemName: "crown.fill") + imageView.contentMode = .scaleAspectFit imageView.themeTintColor = .dynamicForPrimary( .green, use: .profileIcon_greenPrimaryColor, @@ -413,6 +432,7 @@ public final class ProfilePictureView: UIView { case .rightPlus: imageView.image = UIImage(systemName: "plus", withConfiguration: UIImage.SymbolConfiguration(weight: .semibold)) + imageView.contentMode = .scaleAspectFit imageView.themeTintColor = .black backgroundView.themeBackgroundColor = .primary imageView.isHidden = false @@ -423,18 +443,32 @@ public final class ProfilePictureView: UIView { backgroundView.themeBackgroundColor = (dangerMode ? .danger : .textPrimary) label.isHidden = false label.text = "\(character)" + + case .pencil: + imageView.image = Lucide.image(icon: .pencil, size: 14)?.withRenderingMode(.alwaysTemplate) + imageView.contentMode = .center + imageView.themeTintColor = .black + backgroundView.themeBackgroundColor = .primary + imageView.isHidden = false + label.isHidden = true + } } // MARK: - Content private func prepareForReuse() { + /// Reset the disposables in case this was called with different data/ + disposables = Set() + imageView.image = nil + imageView.shouldAnimateImage = false imageView.contentMode = .scaleAspectFill imageContainerView.clipsToBounds = clipsToBounds imageContainerView.themeBackgroundColor = .backgroundSecondary additionalImageContainerView.isHidden = true additionalImageView.image = nil + additionalImageView.shouldAnimateImage = false additionalImageContainerView.clipsToBounds = clipsToBounds imageViewTopConstraint.isActive = false @@ -478,7 +512,8 @@ public final class ProfilePictureView: UIView { case (.some(let source), .some(let renderingMode)) where source.directImage != nil: imageView.image = source.directImage?.withRenderingMode(renderingMode) - case (.some(let source), _): imageView.loadImage(source) + case (.some(let source), _): + imageView.loadImage(source) default: imageView.image = nil } @@ -497,6 +532,8 @@ public final class ProfilePictureView: UIView { } } + startAnimationIfNeeded(for: info, with: imageView) + // Check if there is a second image (if not then set the size and finish) guard let additionalInfo: Info = additionalInfo else { imageViewWidthConstraint.constant = size.imageSize @@ -550,6 +587,8 @@ public final class ProfilePictureView: UIView { } } + startAnimationIfNeeded(for: additionalInfo, with: additionalImageView) + imageViewTopConstraint.isActive = true imageViewLeadingConstraint.isActive = true imageViewCenterXConstraint.isActive = false @@ -566,6 +605,25 @@ public final class ProfilePictureView: UIView { ) additionalProfileIconBackgroundView.layer.cornerRadius = (size.iconSize / 2) } + + private func startAnimationIfNeeded(for info: Info, with targetImageView: SessionImageView) { + switch info.animationBehaviour { + case .generic(let enableAnimation), .contact(let enableAnimation): + targetImageView.shouldAnimateImage = enableAnimation + + case .currentUser(let currentUserSessionProState): + targetImageView.shouldAnimateImage = currentUserSessionProState.isSessionProSubject.value + currentUserSessionProState.isSessionProPublisher + .subscribe(on: DispatchQueue.main) + .receive(on: DispatchQueue.main) + .sink( + receiveValue: { [weak targetImageView] isPro in + targetImageView?.shouldAnimateImage = isPro + } + ) + .store(in: &disposables) + } + } } import SwiftUI @@ -591,7 +649,10 @@ public struct ProfilePictureSwiftUI: UIViewRepresentable { } public func makeUIView(context: Context) -> ProfilePictureView { - ProfilePictureView(size: size, dataManager: dataManager) + ProfilePictureView( + size: size, + dataManager: dataManager + ) } public func updateUIView(_ profilePictureView: ProfilePictureView, context: Context) { diff --git a/SessionUIKit/Components/Separator.swift b/SessionUIKit/Components/Separator.swift index d35add8fda..a4ccd6296d 100644 --- a/SessionUIKit/Components/Separator.swift +++ b/SessionUIKit/Components/Separator.swift @@ -68,17 +68,22 @@ public final class Separator: UIView { addSubview(rightLine) addSubview(titleLabel) - titleLabel.center(.horizontal, in: self) - titleLabel.center(.vertical, in: self) + titleLabel.pin(.top, to: .top, of: roundedLine, withInset: 6) + titleLabel.pin(.leading, to: .leading, of: roundedLine, withInset: 10) + titleLabel.pin(.trailing, to: .trailing, of: roundedLine, withInset: -10) + titleLabel.pin(.bottom, to: .bottom, of: roundedLine, withInset: -6) + roundedLine.pin(.top, to: .top, of: self) - roundedLine.pin(.top, to: .top, of: titleLabel, withInset: -6) - roundedLine.pin(.leading, to: .leading, of: titleLabel, withInset: -10) - roundedLine.pin(.trailing, to: .trailing, of: titleLabel, withInset: 10) - roundedLine.pin(.bottom, to: .bottom, of: titleLabel, withInset: 6) - roundedLine.pin(.bottom, to: .bottom, of: self) + roundedLine.pin(.bottom, to: .bottom, of: self).setting(priority: .defaultHigh) + roundedLine.center(.horizontal, in: self) + roundedLine.center(.vertical, in: self) + roundedLine.setContentHugging(.horizontal, to: .required) + roundedLine.setCompressionResistance(.horizontal, to: .required) + leftLine.pin(.leading, to: .leading, of: self) leftLine.pin(.trailing, to: .leading, of: roundedLine) leftLine.center(.vertical, in: self) + rightLine.pin(.leading, to: .trailing, of: roundedLine) rightLine.pin(.trailing, to: .trailing, of: self) rightLine.center(.vertical, in: self) diff --git a/SessionUIKit/Components/SessionImageView.swift b/SessionUIKit/Components/SessionImageView.swift index 7b50a4d73b..a768725213 100644 --- a/SessionUIKit/Components/SessionImageView.swift +++ b/SessionUIKit/Components/SessionImageView.swift @@ -8,9 +8,10 @@ public class SessionImageView: UIImageView { private var currentLoadIdentifier: String? private var imageLoadTask: Task? + private var streamConsumptionTask: Task? private var displayLink: CADisplayLink? - private var animationFrames: [UIImage]? + private var animationFrames: [UIImage?]? private var animationFrameDurations: [TimeInterval]? public private(set) var currentFrameIndex: Int = 0 public private(set) var accumulatedTime: TimeInterval = 0 @@ -32,6 +33,18 @@ public class SessionImageView: UIImageView { } } + public var shouldAnimateImage: Bool = true { + didSet { + guard oldValue != shouldAnimateImage else { return } + + if shouldAnimateImage { + startAnimationLoop() + } else { + stopAnimationLoop() + } + } + } + // MARK: - Initialization /// Use the `init(dataManager:)` initializer where possible to avoid explicitly needing to add the `dataManager` instance @@ -78,6 +91,7 @@ public class SessionImageView: UIImageView { deinit { imageLoadTask?.cancel() + streamConsumptionTask?.cancel() /// The documentation for `CADisplayLink` states: /// ``` @@ -124,11 +138,11 @@ public class SessionImageView: UIImageView { } @MainActor - public func loadImage(_ source: ImageDataManager.DataSource, onComplete: ((Bool) -> Void)? = nil) { + public func loadImage(_ source: ImageDataManager.DataSource, onComplete: (@MainActor (ImageDataManager.ProcessedImageData?) -> Void)? = nil) { /// If we are trying to load the image that is already displayed then no need to do anything if currentLoadIdentifier == source.identifier && (self.image == nil || isAnimating()) { /// If it was an animation that got paused then resume it - if let frames: [UIImage] = animationFrames, !frames.isEmpty, !isAnimating() { + if let frames: [UIImage?] = animationFrames, !frames.isEmpty, frames[0] != nil, !isAnimating() { startAnimationLoop() } return @@ -140,10 +154,13 @@ public class SessionImageView: UIImageView { /// No need to kick of an async task if we were given an image directly switch source { case .image(_, .some(let image)): - imageSizeMetadata = image.size - return handleLoadedImageData( - ImageDataManager.ProcessedImageData(type: .staticImage(image)) + let processedData: ImageDataManager.ProcessedImageData = ImageDataManager.ProcessedImageData( + type: .staticImage(image) ) + imageSizeMetadata = image.size + handleLoadedImageData(processedData) + onComplete?(processedData) + return default: break } @@ -167,7 +184,7 @@ public class SessionImageView: UIImageView { guard !Task.isCancelled && self?.currentLoadIdentifier == source.identifier else { return } self?.handleLoadedImageData(processedData) - onComplete?(processedData != nil) + onComplete?(processedData) } } } @@ -175,10 +192,11 @@ public class SessionImageView: UIImageView { @MainActor public func startAnimationLoop() { guard - let frames: [UIImage] = animationFrames, + shouldAnimateImage, + let frames: [UIImage?] = animationFrames, let durations: [TimeInterval] = animationFrameDurations, - frames.count > 1, - frames.count == durations.count + !frames.isEmpty, + !durations.isEmpty else { return stopAnimationLoop() } /// If it's already running (or paused) then no need to start the animation loop @@ -188,10 +206,11 @@ public class SessionImageView: UIImageView { } /// Just to be safe set the initial frame - if self.image == nil, frames.indices.contains(0) { + if self.image == nil, !frames.isEmpty, frames[0] != nil { self.image = frames[0] } + stopAnimationLoop() /// Make sure we don't unintentionally create extra `CADisplayLink` instances currentFrameIndex = 0 accumulatedTime = 0 @@ -207,7 +226,7 @@ public class SessionImageView: UIImageView { /// Stop animating if we don't have a valid animation state guard - let frames: [UIImage] = animationFrames, + let frames: [UIImage?] = animationFrames, let durations = animationFrameDurations, !frames.isEmpty, frames.count == durations.count, @@ -252,6 +271,7 @@ public class SessionImageView: UIImageView { @MainActor private func resetState(identifier: String?) { stopAnimationLoop() + streamConsumptionTask?.cancel() self.image = nil currentLoadIdentifier = identifier @@ -284,44 +304,69 @@ public class SessionImageView: UIImageView { self.currentFrameIndex = 0 self.accumulatedTime = 0 + guard self.shouldAnimateImage else { return } + switch frames.count { case 1...: startAnimationLoop() default: stopAnimationLoop() /// Treat as a static image } + + case .bufferedAnimatedImage(let firstFrame, let durations, let bufferedFrameStream): + self.image = firstFrame + self.animationFrameDurations = durations + self.animationFrames = Array(repeating: nil, count: durations.count) + self.animationFrames?[0] = firstFrame + + guard durations.count > 1 else { + stopAnimationLoop() + return + } + + streamConsumptionTask = Task { @MainActor in + for await event in bufferedFrameStream { + guard !Task.isCancelled else { break } + + switch event { + case .frame(let index, let frame): self.animationFrames?[index] = frame + case .readyToPlay: + guard self.shouldAnimateImage else { continue } + + startAnimationLoop() + } + } + } } } @objc private func updateFrame(displayLink: CADisplayLink) { /// Stop animating if we don't have a valid animation state guard - let frames: [UIImage] = animationFrames, + let frames: [UIImage?] = animationFrames, let durations = animationFrameDurations, !frames.isEmpty, - frames.count == durations.count, + !durations.isEmpty, currentFrameIndex < durations.count else { return stopAnimationLoop() } accumulatedTime += displayLink.duration - let currentFrameDuration: TimeInterval = durations[currentFrameIndex] + var currentFrameDuration: TimeInterval = durations[currentFrameIndex] /// It's possible for a long `CADisplayLink` tick to take longeer than a single frame so try to handle those cases while accumulatedTime >= currentFrameDuration { accumulatedTime -= currentFrameDuration - currentFrameIndex = (currentFrameIndex + 1) % frames.count + - /// Check if we need to break after advancing to the next frame - if currentFrameIndex < durations.count, accumulatedTime < durations[currentFrameIndex] { - break - } + let nextFrameIndex: Int = ((currentFrameIndex + 1) % durations.count) + + /// If the next frame hasn't been decoded yet, pause on the current frame, we'll re-evaluate on the next display tick. + guard nextFrameIndex < frames.count, frames[nextFrameIndex] != nil else { break } /// Prevent an infinite loop for all zero durations - if - durations[currentFrameIndex] <= 0.001 && - currentFrameIndex == (currentFrameIndex + 1) % frames.count - { - break - } + guard durations[nextFrameIndex] > 0.001 else { break } + + currentFrameIndex = nextFrameIndex + currentFrameDuration = durations[currentFrameIndex] } /// Make sure we don't cause an index-out-of-bounds somehow diff --git a/SessionUIKit/Components/SessionProBadge.swift b/SessionUIKit/Components/SessionProBadge.swift index 35e0b5837e..c5cce36801 100644 --- a/SessionUIKit/Components/SessionProBadge.swift +++ b/SessionUIKit/Components/SessionProBadge.swift @@ -44,7 +44,7 @@ public class SessionProBadge: UIView { public init(size: Size) { self.size = size - super.init(frame: .zero) + super.init(frame: CGRect(x: 0, y: 0, width: size.width, height: size.height)) self.setupView() } @@ -76,4 +76,14 @@ public class SessionProBadge: UIView { self.set(.width, to: self.size.width) self.set(.height, to: self.size.height) } + + public func toImage() -> UIImage { + self.proImageView.frame = CGRect( + x: (size.width - size.proFontWidth) / 2, + y: (size.height - size.proFontHeight) / 2, + width: size.proFontWidth, + height: size.proFontHeight + ) + return self.toImage(isOpaque: self.isOpaque, scale: UIScreen.main.scale) + } } diff --git a/SessionUIKit/Components/SwiftUI/ProCTAModal.swift b/SessionUIKit/Components/SwiftUI/ProCTAModal.swift index 01e48b7034..f4196cd942 100644 --- a/SessionUIKit/Components/SwiftUI/ProCTAModal.swift +++ b/SessionUIKit/Components/SwiftUI/ProCTAModal.swift @@ -8,7 +8,7 @@ public struct ProCTAModal: View { public enum Variant { case generic case longerMessages - case animatedProfileImage + case animatedProfileImage(isSessionProActivated: Bool) case morePinnedConvos(isGrandfathered: Bool) case groupLimit(isAdmin: Bool) @@ -20,7 +20,7 @@ public struct ProCTAModal: View { case .longerMessages: return "HigherCharLimitCTA.webp" case .animatedProfileImage: - return "session_pro_modal_background_animated_profile_image" + return "AnimatedProfileCTA.webp" case .morePinnedConvos: return "PinnedConversationsCTA.webp" case .groupLimit(let isAdmin): @@ -30,11 +30,23 @@ public struct ProCTAModal: View { // stringlint:ignore_contents public var animatedAvatarImageURL: URL? { switch self { - case .generic: - return Bundle.main.url(forResource: "GenericCTAAnimation", withExtension: "webp") + case .generic, .animatedProfileImage: + return Bundle.main.url(forResource: "AnimatedProfileCTAAnimationCropped", withExtension: "webp") default: return nil } } + /// Note: This is a hack to manually position the animated avatar in the CTA background image to prevent heavy loading for the + /// animated webp. These coordinates are based on the full size image and get scaled during rendering based on the actual size + /// of the modal. + public var animatedAvatarImagePadding: (leading: CGFloat, top: CGFloat) { + switch self { + case .generic: + return (1313.5, 753) + case .animatedProfileImage: + return (690, 363) + default: return (0, 0) + } + } public var subtitle: String { switch self { @@ -47,10 +59,12 @@ public struct ProCTAModal: View { return "proCallToActionLongerMessages" .put(key: "app_pro", value: Constants.app_pro) .localized() - case .animatedProfileImage: - return "proAnimatedDisplayPictureCallToActionDescription" - .put(key: "app_pro", value: Constants.app_pro) - .localized() + case .animatedProfileImage(let isSessionProActivated): + return isSessionProActivated ? + "proAnimatedDisplayPicture".localized() : + "proAnimatedDisplayPictureCallToActionDescription" + .put(key: "app_pro", value: Constants.app_pro) + .localized() case .morePinnedConvos(let isGrandfathered): return isGrandfathered ? "proCallToActionPinnedConversations" @@ -105,6 +119,7 @@ public struct ProCTAModal: View { } @EnvironmentObject var host: HostWrapper + @State var proCTAImageHeight: CGFloat = 0 private var delegate: SessionProManagerType? private let variant: ProCTAModal.Variant @@ -137,29 +152,45 @@ public struct ProCTAModal: View { afterClosed: afterClosed ) { close in VStack(spacing: 0) { + // Background images ZStack { - SessionAsyncImage( - source: ( - variant.animatedAvatarImageURL.map { .url($0) } ?? - .image( - variant.backgroundImageName, - UIImage(named: variant.backgroundImageName) ?? - UIImage() + if let animatedAvatarImageURL = variant.animatedAvatarImageURL { + GeometryReader { geometry in + let size: CGFloat = geometry.size.width / 1522.0 * 187.0 + let scale: CGFloat = geometry.size.width / 1522.0 + SessionAsyncImage( + source: .url(animatedAvatarImageURL), + dataManager: dataManager, + content: { image in + image + .resizable() + .aspectRatio(1, contentMode: .fit) + .frame(width: size, height: size) + }, + placeholder: { + if let data = try? Data(contentsOf: animatedAvatarImageURL) { + Image(uiImage: UIImage(data: data) ?? UIImage()) + .resizable() + .aspectRatio(1, contentMode: .fit) + .frame(width: size, height: size) + } else { + EmptyView() + } + } ) - ), - dataManager: dataManager, - content: { image in - image - .resizable() - .aspectRatio((1522.0/1258.0), contentMode: .fit) - .frame(maxWidth: .infinity) - }, - placeholder: { - ThemeColor(.alert_background) - .aspectRatio((1522.0/1258.0), contentMode: .fit) - .frame(maxWidth: .infinity) + .padding(.leading, variant.animatedAvatarImagePadding.leading * scale) + .padding(.top, variant.animatedAvatarImagePadding.top * scale) + .onAppear { + proCTAImageHeight = geometry.size.width / 1522.0 * 1258.0 + } } - ) + .frame(height: proCTAImageHeight) + } + + Image(uiImage: UIImage(named: variant.backgroundImageName) ?? UIImage()) + .resizable() + .aspectRatio((1522.0/1258.0), contentMode: .fit) + .frame(maxWidth: .infinity) } .backgroundColor(themeColor: .primary) .overlay(alignment: .bottom, content: { @@ -180,71 +211,110 @@ public struct ProCTAModal: View { maxWidth: .infinity, alignment: .bottom ) - + // Content VStack(spacing: Values.largeSpacing) { // Title - HStack(spacing: Values.smallSpacing) { - Text("upgradeTo".localized()) - .font(.system(size: Values.largeFontSize)) - .bold() - .foregroundColor(themeColor: .textPrimary) - - SessionProBadge_SwiftUI(size: .large) + if case .animatedProfileImage(let isSessionProActivated) = variant, isSessionProActivated { + HStack(spacing: Values.smallSpacing) { + SessionProBadge_SwiftUI(size: .large) + + Text("proActivated".localized()) + .font(.system(size: Values.largeFontSize)) + .bold() + .foregroundColor(themeColor: .textPrimary) + } + } else { + HStack(spacing: Values.smallSpacing) { + Text("upgradeTo".localized()) + .font(.system(size: Values.largeFontSize)) + .bold() + .foregroundColor(themeColor: .textPrimary) + + SessionProBadge_SwiftUI(size: .large) + } } + // Description, Subtitle - VStack(spacing: Values.smallSpacing) { + VStack(spacing: 0) { + if case .animatedProfileImage(let isSessionProActivated) = variant, isSessionProActivated { + HStack(spacing: Values.verySmallSpacing) { + Text("proAlreadyPurchased".localized()) + .font(.system(size: Values.smallFontSize)) + .foregroundColor(themeColor: .textSecondary) + + SessionProBadge_SwiftUI(size: .small) + } + } + Text(variant.subtitle) .font(.system(size: Values.smallFontSize)) .foregroundColor(themeColor: .textSecondary) .multilineTextAlignment(.center) .fixedSize(horizontal: false, vertical: true) } + // Benefits - VStack(alignment: .leading, spacing: Values.mediumSmallSpacing) { - ForEach( - 0.. { get } var isSessionProPublisher: AnyPublisher { get } func upgradeToPro(completion: ((_ result: Bool) -> Void)?) } diff --git a/SessionUIKit/Components/SwiftUI/SessionAsyncImage.swift b/SessionUIKit/Components/SwiftUI/SessionAsyncImage.swift index 306fcc14e9..59ddb996e9 100644 --- a/SessionUIKit/Components/SwiftUI/SessionAsyncImage.swift +++ b/SessionUIKit/Components/SwiftUI/SessionAsyncImage.swift @@ -6,7 +6,7 @@ import NaturalLanguage public struct SessionAsyncImage: View { @State private var loadedImage: UIImage? = nil - @State private var animationFrames: [UIImage]? + @State private var animationFrames: [UIImage?]? @State private var animationFrameDurations: [TimeInterval]? @State private var isAnimating: Bool = false @@ -16,6 +16,7 @@ public struct SessionAsyncImage: View { private let source: ImageDataManager.DataSource private let dataManager: ImageDataManagerType + private let shouldAnimateImage: Bool private let content: (Image) -> Content private let placeholder: () -> Placeholder @@ -23,11 +24,13 @@ public struct SessionAsyncImage: View { public init( source: ImageDataManager.DataSource, dataManager: ImageDataManagerType, + shouldAnimateImage: Bool = true, @ViewBuilder content: @escaping (Image) -> Content, @ViewBuilder placeholder: @escaping () -> Placeholder ) { self.source = source self.dataManager = dataManager + self.shouldAnimateImage = shouldAnimateImage self.content = content self.placeholder = placeholder } @@ -55,23 +58,22 @@ public struct SessionAsyncImage: View { .task(id: source.identifier) { await loadAndProcessData() } + .onChange(of: shouldAnimateImage) { newValue in + if let frames = animationFrames, !frames.isEmpty { + isAnimating = newValue + } + } } // MARK: - Internal Functions private func loadAndProcessData() async { + /// Reset the state before loading new data + await MainActor.run { resetAnimationState() } + let processedData = await dataManager.load(source) - /// Reset the state before loading new data - await MainActor.run { - self.loadedImage = nil - self.animationFrames = nil - self.animationFrameDurations = nil - self.isAnimating = false - self.currentFrameIndex = 0 - self.accumulatedTime = 0.0 - self.lastFrameDate = .now - } + guard !Task.isCancelled else { return } switch processedData?.type { case .staticImage(let image): @@ -84,13 +86,43 @@ public struct SessionAsyncImage: View { self.animationFrames = frames self.animationFrameDurations = durations self.loadedImage = frames.first - self.isAnimating = true /// Activate the `TimelineView` + + if self.shouldAnimateImage { + self.isAnimating = true /// Activate the `TimelineView` + } + } + + case .bufferedAnimatedImage(let firstFrame, let durations, let bufferedFrameStream) where durations.count > 1: + await MainActor.run { + self.loadedImage = firstFrame + self.animationFrameDurations = durations + self.animationFrames = Array(repeating: nil, count: durations.count) + self.animationFrames?[0] = firstFrame + } + + for await event in bufferedFrameStream { + guard !Task.isCancelled else { break } + + await MainActor.run { + switch event { + case .frame(let index, let frame): self.animationFrames?[index] = frame + case .readyToPlay: + guard self.shouldAnimateImage else { return } + + self.isAnimating = true + } + } } case .animatedImage(let frames, _): await MainActor.run { self.loadedImage = frames.first } + + case .bufferedAnimatedImage(let firstFrame, _, _): + await MainActor.run { + self.loadedImage = firstFrame + } default: await MainActor.run { @@ -99,41 +131,53 @@ public struct SessionAsyncImage: View { } } + @MainActor + private func resetAnimationState() { + self.loadedImage = nil + self.animationFrames = nil + self.animationFrameDurations = nil + self.isAnimating = false + self.currentFrameIndex = 0 + self.accumulatedTime = 0.0 + self.lastFrameDate = .now + } + private func updateAnimationFrame(at date: Date) { guard isAnimating, - let frames: [UIImage] = animationFrames, + let frames: [UIImage?] = animationFrames, let durations = animationFrameDurations, !frames.isEmpty, - frames.count == durations.count, + !durations.isEmpty, currentFrameIndex < durations.count, let lastDate = lastFrameDate - else { return } + else { + isAnimating = false + return + } /// Calculate elapsed time since the last frame let elapsed: TimeInterval = date.timeIntervalSince(lastDate) self.lastFrameDate = date accumulatedTime += elapsed - let currentFrameDuration: TimeInterval = durations[currentFrameIndex] + var currentFrameDuration: TimeInterval = durations[currentFrameIndex] // Advance frames if the accumulated time exceeds the current frame's duration while accumulatedTime >= currentFrameDuration { accumulatedTime -= currentFrameDuration - currentFrameIndex = (currentFrameIndex + 1) % frames.count + let nextFrameIndex: Int = ((currentFrameIndex + 1) % durations.count) - /// Check if we need to break after advancing to the next frame - if currentFrameIndex < durations.count, accumulatedTime < durations[currentFrameIndex] { - break - } + /// If the next frame hasn't been decoded yet, pause on the current frame, we'll re-evaluate on the next display tick. + guard nextFrameIndex < frames.count, frames[nextFrameIndex] != nil else { break } + + /// Prevent an infinite loop for all zero durations - if - durations[currentFrameIndex] <= 0.001 && - currentFrameIndex == (currentFrameIndex + 1) % frames.count - { - break - } + guard durations[nextFrameIndex] > 0.001 else { break } + + currentFrameIndex = nextFrameIndex + currentFrameDuration = durations[currentFrameIndex] } /// Make sure we don't cause an index-out-of-bounds somehow diff --git a/SessionUIKit/Components/SwiftUI/SessionTextField.swift b/SessionUIKit/Components/SwiftUI/SessionTextField.swift index b83b95b417..44288b0b4b 100644 --- a/SessionUIKit/Components/SwiftUI/SessionTextField.swift +++ b/SessionUIKit/Components/SwiftUI/SessionTextField.swift @@ -11,6 +11,8 @@ public struct SessionTextField: View where ExplanationView: Vie @State var textThemeColor: ThemeValue = .textPrimary @State fileprivate var textChanged: ((String) -> Void)? + @FocusState private var isFirstResponder: Bool + public enum SessionTextFieldType { case thin case normal @@ -72,6 +74,7 @@ public struct SessionTextField: View where ExplanationView: Vie Text(placeholder) .font(.system(size: Values.smallFontSize)) .foregroundColor(themeColor: .textSecondary) + .allowsHitTesting(false) } if #available(iOS 16.0, *) { @@ -83,6 +86,8 @@ public struct SessionTextField: View where ExplanationView: Vie .font(font) .foregroundColor(themeColor: textThemeColor) .accessibility(self.accessibility) + .focused($isFirstResponder) + } else { ZStack { TextEditor(text: $text) @@ -92,6 +97,7 @@ public struct SessionTextField: View where ExplanationView: Vie .accessibility(self.accessibility) .frame(maxHeight: self.height) .padding(.all, -4) + .focused($isFirstResponder) // FIXME: This is a workaround for dynamic height of the TextEditor. Text(text.isEmpty ? placeholder : text) @@ -117,6 +123,8 @@ public struct SessionTextField: View where ExplanationView: Vie RoundedRectangle(cornerRadius: self.cornerRadius) .stroke(themeColor: isErrorMode ? .danger : .borderSeparator) ) + .contentShape(RoundedRectangle(cornerRadius: self.cornerRadius)) + .onTapGesture { isFirstResponder = !isFirstResponder } // Added hit test to launch keyboard, currently textfield's hit area is too small .onChange(of: text) { newText in error = inputChecker?(newText) textThemeColor = ((newText == lastErroredText || error?.isEmpty == false) ? .danger : .textPrimary) @@ -127,7 +135,7 @@ public struct SessionTextField: View where ExplanationView: Vie textThemeColor = .danger } } - + // Error message switch self.type { case .thin: diff --git a/SessionUIKit/Style Guide/Constants+URLs.swift b/SessionUIKit/Style Guide/Constants+Apple.swift similarity index 69% rename from SessionUIKit/Style Guide/Constants+URLs.swift rename to SessionUIKit/Style Guide/Constants+Apple.swift index e00ba52de6..5accc01675 100644 --- a/SessionUIKit/Style Guide/Constants+URLs.swift +++ b/SessionUIKit/Style Guide/Constants+Apple.swift @@ -2,8 +2,14 @@ // // stringlint:disable public extension Constants { + // MARK: - URL static let session_network_url = "https://docs.getsession.org/session-network" static let session_staking_url = "https://docs.getsession.org/session-network/staking" static let session_token_url = "https://token.getsession.org" static let session_donations_url = "https://session.foundation/donate#app" + static let session_feedback_url = "https://getsession.org/feedback" + + // MARK: - Names + static let store_name = "App Store" + static let platform_name = "iOS" } diff --git a/SessionUIKit/Style Guide/Constants.swift b/SessionUIKit/Style Guide/Constants.swift index 7ff7fa8564..85ccadd231 100644 --- a/SessionUIKit/Style Guide/Constants.swift +++ b/SessionUIKit/Style Guide/Constants.swift @@ -15,4 +15,6 @@ public enum Constants { public static let usd_name_short: String = "USD" public static let session_network_data_price: String = "Price data powered by CoinGecko
Accurate at {date_time}" public static let app_pro: String = "Session Pro" + public static let session_foundation: String = "Session Foundation" + public static let pro: String = "Pro" } diff --git a/SessionUIKit/Style Guide/ThemeManager.swift b/SessionUIKit/Style Guide/ThemeManager.swift index 3b018a8008..d5faf2f774 100644 --- a/SessionUIKit/Style Guide/ThemeManager.swift +++ b/SessionUIKit/Style Guide/ThemeManager.swift @@ -53,10 +53,6 @@ public enum ThemeManager { _primaryColor = targetPrimaryColor _hasLoadedTheme = true - if !hasSetInitialSystemTrait || themeChanged { - updateAllUI() - } - if matchSystemChanged { _matchSystemNightModeSetting = targetMatchSystemNightModeSetting @@ -65,8 +61,19 @@ public enum ThemeManager { SNUIKit.mainWindow?.overrideUserInterfaceStyle = .unspecified } - // If the theme was changed then trigger the callback for the theme settings change (so it gets persisted) - guard themeChanged || matchSystemChanged else { return } + // If the theme was changed then trigger a UI update and the callback for the theme settings + // change (so it gets persisted) + guard themeChanged || matchSystemChanged else { + if !hasSetInitialSystemTrait { + updateAllUI() + } + + return + } + + if !hasSetInitialSystemTrait || themeChanged { + updateAllUI() + } SNUIKit.themeSettingsChanged(targetTheme, targetPrimaryColor, targetMatchSystemNightModeSetting) } @@ -82,10 +89,10 @@ public enum ThemeManager { // Swap to the appropriate light/dark mode switch (currentUserInterfaceStyle, ThemeManager.currentTheme) { - case (.light, .classicDark): updateThemeState(theme: .classicLight) - case (.light, .oceanDark): updateThemeState(theme: .oceanLight) - case (.dark, .classicLight): updateThemeState(theme: .classicDark) - case (.dark, .oceanLight): updateThemeState(theme: .oceanDark) + case (.light, .classicDark): updateThemeState(theme: .classicLight, primaryColor: _primaryColor) + case (.light, .oceanDark): updateThemeState(theme: .oceanLight, primaryColor: _primaryColor) + case (.dark, .classicLight): updateThemeState(theme: .classicDark, primaryColor: _primaryColor) + case (.dark, .oceanLight): updateThemeState(theme: .oceanDark, primaryColor: _primaryColor) default: break } } @@ -214,7 +221,7 @@ public enum ThemeManager { SNUIKit.mainWindow?.backgroundColor = color(for: .backgroundPrimary, in: currentTheme, with: primaryColor) } - public static func onThemeChange(observer: AnyObject, callback: @escaping (Theme, Theme.PrimaryColor, (ThemeValue) -> UIColor?) -> ()) { + @MainActor public static func onThemeChange(observer: AnyObject, callback: @escaping @MainActor (Theme, Theme.PrimaryColor, (ThemeValue) -> UIColor?) -> ()) { ThemeManager.uiRegistry.setObject( ThemeApplier( existingApplier: ThemeManager.get(for: observer), @@ -234,14 +241,19 @@ public enum ThemeManager { with primaryColor: Theme.PrimaryColor ) -> T? { switch value { - case .value(let value, let alpha): return T.resolve(value, for: theme)?.alpha(alpha) + case .value(let value, let alpha): + let color: T? = color(for: value, in: theme, with: primaryColor) + return color?.alpha(alpha) as? T + case .primary: return T.resolve(primaryColor) case .explicitPrimary(let explicitPrimary): return T.resolve(explicitPrimary) case .highlighted(let value, let alwaysDarken): + let color: T? = color(for: value, in: theme, with: primaryColor)! + switch (currentTheme.interfaceStyle, alwaysDarken) { - case (.light, _), (_, true): return T.resolve(value, for: theme)?.brighten(-0.06) - default: return T.resolve(value, for: theme)?.brighten(0.08) + case (.light, _), (_, true): return color?.brighten(-0.06) as? T + default: return color?.brighten(0.08) as? T } case .dynamicForInterfaceStyle(let light, let dark): @@ -299,7 +311,7 @@ public enum ThemeManager { } } - internal static func set( + @MainActor internal static func set( _ view: T, keyPath: ReferenceWritableKeyPath, to value: ThemeValue? @@ -335,7 +347,7 @@ public enum ThemeManager { ThemeManager.uiRegistry.setObject(updatedApplier, forKey: view) } - internal static func set( + @MainActor internal static func set( _ view: T, keyPath: ReferenceWritableKeyPath, to value: ThemeValue? @@ -369,7 +381,7 @@ public enum ThemeManager { ) } - internal static func set( + @MainActor internal static func set( _ view: T, keyPath: ReferenceWritableKeyPath, to value: ThemedAttributedString? @@ -444,14 +456,14 @@ internal class ThemeApplier { case controlState } - private let applyTheme: (Theme) -> () + private let applyTheme: @MainActor (Theme) -> () private let info: [AnyHashable] private var otherAppliers: [ThemeApplier]? - init( + @MainActor init( existingApplier: ThemeApplier?, info: [AnyHashable], - applyTheme: @escaping (Theme) -> () + applyTheme: @escaping @MainActor (Theme) -> () ) { self.applyTheme = applyTheme self.info = info @@ -466,7 +478,7 @@ internal class ThemeApplier { // Automatically apply the theme immediately (if the database has been setup) if SNUIKit.config?.isStorageValid == true || ThemeManager.hasLoadedTheme { - self.apply(theme: ThemeManager.currentTheme, isInitialApplication: true) + apply(theme: ThemeManager.currentTheme, isInitialApplication: true) } } @@ -497,7 +509,7 @@ internal class ThemeApplier { return self } - fileprivate func apply(theme: Theme, isInitialApplication: Bool = false) { + @MainActor fileprivate func apply(theme: Theme, isInitialApplication: Bool = false) { self.applyTheme(theme) // For the initial application of a ThemeApplier we don't want to apply the other @@ -526,21 +538,28 @@ extension Array { // MARK: - ColorType internal protocol ColorType { + /// Apple have done some odd schenanigans with `UIColor` where some types aren't _actually_ `UIColor` but a special + /// type (eg. `UIColor.black` and `UIColor.white` are `UICachedDeviceWhiteColor`), due to this casting to + /// `Self` in an extension on `UIColor` ends up failing (because calling `alpha(_)` on a `UICachedDeviceWhiteColor` + /// expects you to return a `UICachedDeviceWhiteColor`, but the alpha-applied output is a standard `UIColor` which can't + /// convert to `Self`), by defining an explicit `BaseColorType` we return an explicit type and avoid weird private types + associatedtype BaseColorType + var isPrimary: Bool { get } - func alpha(_ alpha: Double) -> Self? - func brighten(_ amount: Double) -> Self? + func alpha(_ alpha: Double) -> BaseColorType? + func brighten(_ amount: Double) -> BaseColorType? } extension UIColor: ColorType { internal var isPrimary: Bool { self == UIColor.primary() } - internal func alpha(_ alpha: Double) -> Self? { - return self.withAlphaComponent(CGFloat(alpha)) as? Self + internal func alpha(_ alpha: Double) -> UIColor? { + return self.withAlphaComponent(CGFloat(alpha)) } - internal func brighten(_ amount: Double) -> Self? { - return self.brighten(by: amount) as? Self + internal func brighten(_ amount: Double) -> UIColor? { + return self.brighten(by: amount) } } diff --git a/SessionUIKit/Style Guide/Themes/UIKit+Theme.swift b/SessionUIKit/Style Guide/Themes/UIKit+Theme.swift index 9f79609489..885320824d 100644 --- a/SessionUIKit/Style Guide/Themes/UIKit+Theme.swift +++ b/SessionUIKit/Style Guide/Themes/UIKit+Theme.swift @@ -35,7 +35,14 @@ public extension UIView { } var themeTintColor: ThemeValue? { - set { ThemeManager.set(self, keyPath: \.tintColor, to: newValue) } + set { + /// The `UIActivityIndicatorView` uses a `color` value instead of `tintColor` so redirect it in case this + /// is mistakenly used + switch self { + case let indicator as UIActivityIndicatorView: indicator.themeColor = newValue + default: ThemeManager.set(self, keyPath: \.tintColor, to: newValue) + } + } get { return nil } } @@ -337,6 +344,13 @@ public extension UIPageControl { } } +public extension UIActivityIndicatorView { + var themeColor: ThemeValue? { + set { ThemeManager.set(self, keyPath: \.color, to: newValue) } + get { return nil } + } +} + public extension GradientView { var themeBackgroundGradient: [ThemeValue]? { set { @@ -375,7 +389,7 @@ public extension GradientView { } public extension CAShapeLayer { - var themeStrokeColor: ThemeValue? { + @MainActor var themeStrokeColor: ThemeValue? { set { ThemeManager.set(self, keyPath: \.strokeColor, to: newValue) } get { return nil } } @@ -399,7 +413,7 @@ public extension CAShapeLayer { get { return self.strokeColor.map { .color(UIColor(cgColor: $0)) } } } - var themeFillColor: ThemeValue? { + @MainActor var themeFillColor: ThemeValue? { set { ThemeManager.set(self, keyPath: \.fillColor, to: newValue) } get { return nil } } @@ -425,7 +439,7 @@ public extension CAShapeLayer { } public extension CALayer { - var themeBackgroundColor: ThemeValue? { + @MainActor var themeBackgroundColor: ThemeValue? { set { ThemeManager.set(self, keyPath: \.backgroundColor, to: newValue) } get { return nil } } @@ -449,19 +463,19 @@ public extension CALayer { get { return self.backgroundColor.map { .color(UIColor(cgColor: $0)) } } } - var themeBorderColor: ThemeValue? { + @MainActor var themeBorderColor: ThemeValue? { set { ThemeManager.set(self, keyPath: \.borderColor, to: newValue) } get { return nil } } - var themeShadowColor: ThemeValue? { + @MainActor var themeShadowColor: ThemeValue? { set { ThemeManager.set(self, keyPath: \.shadowColor, to: newValue) } get { return nil } } } public extension CATextLayer { - var themeForegroundColor: ThemeValue? { + @MainActor var themeForegroundColor: ThemeValue? { set { ThemeManager.set(self, keyPath: \.foregroundColor, to: newValue) } get { return nil } } @@ -508,7 +522,7 @@ extension AttributedTextAssignable { get { attributedTextValue.map { ThemedAttributedString(attributedString: $0) } } set { attributedTextValue = newValue?.value } } - public var themeAttributedText: ThemedAttributedString? { + @MainActor public var themeAttributedText: ThemedAttributedString? { set { ThemeManager.set(self, keyPath: \.themeAttributedTextValue, to: newValue) } get { return nil } } @@ -520,7 +534,7 @@ extension UITextField: DirectAttributedTextAssignable { get { attributedPlaceholder.map { ThemedAttributedString(attributedString: $0) } } set { attributedPlaceholder = newValue?.value } } - public var themeAttributedPlaceholder: ThemedAttributedString? { + @MainActor public var themeAttributedPlaceholder: ThemedAttributedString? { set { ThemeManager.set(self, keyPath: \.themeAttributedPlaceholderValue, to: newValue) } get { return nil } } diff --git a/SessionUIKit/Style Guide/Values.swift b/SessionUIKit/Style Guide/Values.swift index ffd825c2af..d6a6894e9f 100644 --- a/SessionUIKit/Style Guide/Values.swift +++ b/SessionUIKit/Style Guide/Values.swift @@ -8,7 +8,7 @@ public enum Values { public static let veryLowOpacity = CGFloat(0.12) public static let lowOpacity = CGFloat(0.4) public static let mediumOpacity = CGFloat(0.6) - public static let highOpacity = CGFloat(0.75) + public static let highOpacity = CGFloat(0.7) // MARK: - Font Sizes public static let miniFontSize = isIPhone5OrSmaller ? CGFloat(8) : CGFloat(10) @@ -31,6 +31,8 @@ public enum Values { public static let accentLineThickness = CGFloat(4) public static let searchBarHeight = CGFloat(36) + + public static let gradientPaletteWidth = CGFloat(12) public static var separatorThickness: CGFloat { return 1 / UIScreen.main.scale } diff --git a/SessionUIKit/Types/ImageDataManager.swift b/SessionUIKit/Types/ImageDataManager.swift index 84472c7ad0..9e0dd29674 100644 --- a/SessionUIKit/Types/ImageDataManager.swift +++ b/SessionUIKit/Types/ImageDataManager.swift @@ -11,6 +11,10 @@ public actor ImageDataManager: ImageDataManagerType { attributes: .concurrent ) + /// Max memory size for a decoded animation to be considered "small" enough to be fully cached + private static let decodedAnimationCacheLimit: Int = 20 * 1024 * 1024 // 20 M + private static let maxAnimatedImageDownscaleDimention: CGFloat = 4096 + /// `NSCache` has more nuanced memory management systems than just listening for `didReceiveMemoryWarningNotification` /// and can clear out values gradually, it can also remove items based on their "cost" so is better suited than our custom `LRUCache` private let cache: NSCache = { @@ -47,17 +51,18 @@ public actor ImageDataManager: ImageDataManagerType { /// Wait for the result then cache and return it let processedData: ProcessedImageData? = await newTask.value - if let data: ProcessedImageData = processedData { - self.cache.setObject(data, forKey: identifier as NSString, cost: data.estimatedCost) + if let data: ProcessedImageData = processedData, data.isCacheable { + self.cache.setObject(data, forKey: identifier as NSString, cost: data.estimatedCacheCost) } self.activeLoadTasks[identifier] = nil return processedData } - nonisolated public func load( + @MainActor + public func load( _ source: ImageDataManager.DataSource, - onComplete: @escaping (ImageDataManager.ProcessedImageData?) -> Void + onComplete: @MainActor @escaping (ImageDataManager.ProcessedImageData?) -> Void ) { Task { [weak self] in let result: ImageDataManager.ProcessedImageData? = await self?.load(source) @@ -95,8 +100,15 @@ public actor ImageDataManager: ImageDataManagerType { /// Custom handle `videoUrl` values since it requires thumbnail generation case .videoUrl(let url, let mimeType, let sourceFilename, let thumbnailManager): /// If we had already generated a thumbnail then use that - if let existingThumbnail: UIImage = thumbnailManager.existingThumbnailImage(url: url, size: .large) { - let decodedImage: UIImage = (existingThumbnail.predecodedImage() ?? existingThumbnail) + if + let existingThumbnail: UIImage = thumbnailManager.existingThumbnailImage(url: url, size: .large), + let existingThumbCgImage: CGImage = existingThumbnail.cgImage, + let decodingContext: CGContext = createDecodingContext( + width: existingThumbCgImage.width, + height: existingThumbCgImage.height + ), + let decodedImage: UIImage = predecode(cgImage: existingThumbCgImage, using: decodingContext) + { let processedData: ProcessedImageData = ProcessedImageData( type: .staticImage(decodedImage) ) @@ -120,12 +132,15 @@ public actor ImageDataManager: ImageDataManagerType { let generator: AVAssetImageGenerator = AVAssetImageGenerator(asset: asset) generator.appliesPreferredTrackTransform = true - guard let cgImage: CGImage = try? generator.copyCGImage(at: time, actualTime: nil) else { - return nil - } + guard + let cgImage: CGImage = try? generator.copyCGImage(at: time, actualTime: nil), + let decodingContext: CGContext = createDecodingContext( + width: cgImage.width, + height: cgImage.height + ), + let decodedImage: UIImage = predecode(cgImage: cgImage, using: decodingContext) + else { return nil } - let image: UIImage = UIImage(cgImage: cgImage) - let decodedImage: UIImage = (image.predecodedImage() ?? image) let processedData: ProcessedImageData = ProcessedImageData( type: .staticImage(decodedImage) ) @@ -144,8 +159,15 @@ public actor ImageDataManager: ImageDataManagerType { /// Custom handle `urlThumbnail` generation case .urlThumbnail(let url, let size, let thumbnailManager): /// If we had already generated a thumbnail then use that - if let existingThumbnail: UIImage = thumbnailManager.existingThumbnailImage(url: url, size: .large) { - let decodedImage: UIImage = (existingThumbnail.predecodedImage() ?? existingThumbnail) + if + let existingThumbnail: UIImage = thumbnailManager.existingThumbnailImage(url: url, size: .large), + let existingThumbCgImage: CGImage = existingThumbnail.cgImage, + let decodingContext: CGContext = createDecodingContext( + width: existingThumbCgImage.width, + height: existingThumbCgImage.height + ), + let decodedImage: UIImage = predecode(cgImage: existingThumbCgImage, using: decodingContext) + { let processedData: ProcessedImageData = ProcessedImageData( type: .staticImage(decodedImage) ) @@ -156,21 +178,23 @@ public actor ImageDataManager: ImageDataManagerType { /// Otherwise we need to generate a new one let maxDimensionInPixels: CGFloat = await size.pixelDimension() let options: [CFString: Any] = [ + kCGImageSourceShouldCache: false, + kCGImageSourceShouldCacheImmediately: false, kCGImageSourceCreateThumbnailFromImageAlways: true, kCGImageSourceCreateThumbnailWithTransform: true, kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels ] guard - let format: SUIKImageFormat = dataSource.dataForGuessingImageFormat?.suiKitGuessedImageFormat, - format != .unknown, - let imageSource: CGImageSource = CGImageSourceCreateWithURL(url as CFURL, nil), - let thumbnail: CGImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary) + let source: CGImageSource = dataSource.createImageSource(options: options), + let cgImage: CGImage = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary), + let decodingContext: CGContext = createDecodingContext( + width: cgImage.width, + height: cgImage.height + ), + let decodedImage: UIImage = predecode(cgImage: cgImage, using: decodingContext) else { return nil } - let image: UIImage = UIImage(cgImage: thumbnail) - let decodedImage: UIImage = (image.predecodedImage() ?? image) - /// Since we generated a new thumbnail we should save it to disk saveThumbnailToDisk( image: decodedImage, @@ -183,39 +207,49 @@ public actor ImageDataManager: ImageDataManagerType { type: .staticImage(decodedImage) ) - case .closureThumbnail(_, _, let imageRetrier): - guard let image: UIImage = await imageRetrier() else { return nil } - - /// Since there is likely custom (external) logic used to retrieve this thumbnail we don't save it to disk as there - /// is no way to know if it _should_ change between generations/launches or not - let decodedImage: UIImage = (image.predecodedImage() ?? image) - - return ProcessedImageData( - type: .staticImage(decodedImage) - ) - /// Custom handle `placeholderIcon` generation case .placeholderIcon(let seed, let text, let size): let image: UIImage = PlaceholderIcon.generate(seed: seed, text: text, size: size) - let decodedImage: UIImage = (image.predecodedImage() ?? image) + + guard + let cgImage: CGImage = image.cgImage, + let decodingContext: CGContext = createDecodingContext( + width: cgImage.width, + height: cgImage.height + ), + let decodedImage: UIImage = predecode(cgImage: cgImage, using: decodingContext) + else { + return ProcessedImageData( + type: .staticImage(image) + ) + } return ProcessedImageData( type: .staticImage(decodedImage) ) + case .asyncSource(_, let sourceRetriever): + guard let source: DataSource = await sourceRetriever() else { return nil } + + return await processSource(source) + default: break } /// Otherwise load the data as either a static or animated image (do quick validation checks here - other checks /// require loading the image source anyway so don't bother to include them) guard - let imageData: Data = dataSource.imageData, - let imageFormat: SUIKImageFormat = imageData.suiKitGuessedImageFormat.nullIfUnknown, - (imageFormat != .gif || imageData.suiKitHasValidGifSize), - let source: CGImageSource = CGImageSourceCreateWithData(imageData as CFData, nil), - CGImageSourceGetCount(source) > 0 + let source: CGImageSource = dataSource.createImageSource(), + let properties: [String: Any] = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [String: Any], + let sourceWidth: Int = properties[kCGImagePropertyPixelWidth as String] as? Int, + let sourceHeight: Int = properties[kCGImagePropertyPixelHeight as String] as? Int, + sourceWidth > 0, + sourceWidth < ImageDataManager.DataSource.maxValidDimension, + sourceHeight > 0, + sourceHeight < ImageDataManager.DataSource.maxValidDimension else { return nil } - + + /// Get the number of frames in the image let count: Int = CGImageSourceGetCount(source) switch count { @@ -224,66 +258,197 @@ public actor ImageDataManager: ImageDataManagerType { /// Static image case 1: - guard let cgImage: CGImage = CGImageSourceCreateImageAtIndex(source, 0, nil) else { - return nil - } - /// Extract image orientation if present var orientation: UIImage.Orientation = .up if - let imageProperties: [CFString: Any] = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any], - let rawCgOrientation: UInt32 = imageProperties[kCGImagePropertyOrientation] as? UInt32, + let rawCgOrientation: UInt32 = properties[kCGImagePropertyOrientation as String] as? UInt32, let cgOrientation: CGImagePropertyOrientation = CGImagePropertyOrientation(rawValue: rawCgOrientation) { orientation = UIImage.Orientation(cgOrientation) } - let image: UIImage = UIImage(cgImage: cgImage, scale: 1, orientation: orientation) - let decodedImage: UIImage = (image.predecodedImage() ?? image) + /// Try to decode the image direct from the `CGImage` + let options: [CFString: Any] = [ + kCGImageSourceShouldCache: false, + kCGImageSourceShouldCacheImmediately: false + ] + + guard + let cgImage: CGImage = CGImageSourceCreateImageAtIndex(source, 0, options as CFDictionary), + let decodingContext = createDecodingContext(width: cgImage.width, height: cgImage.height), + let decodedImage: UIImage = predecode(cgImage: cgImage, using: decodingContext), + let decodedCgImage: CGImage = decodedImage.cgImage + else { return nil } + + let finalImage: UIImage = UIImage(cgImage: decodedCgImage, scale: 1, orientation: orientation) return ProcessedImageData( - type: .staticImage(decodedImage) + type: .staticImage(finalImage) ) /// Animated Image default: - var framesArray: [UIImage] = [] - var durationsArray: [TimeInterval] = [] - - for i in 0.. decodedAnimationCacheLimit else { + var frames: [UIImage] = [decodedFirstFrameImage] - let image: UIImage = UIImage(cgImage: cgImage) - let decodedImage: UIImage = (image.predecodedImage() ?? image) - let duration: TimeInterval = ImageDataManager.getFrameDuration(from: source, at: i) + for i in 1.. = AsyncStream { continuation in + let task = Task.detached(priority: .userInitiated) { + var (frameIndexesToBuffer, probeFrames) = await self.calculateHeuristicBuffer( + startIndex: 1, /// We have already decoded the first frame so skip it + source: source, + durations: durations, + using: decodingContext + ) + let lastBufferedFrameIndex: Int = ( + frameIndexesToBuffer.max() ?? + probeFrames.count + ) + + /// Immediately yield the frames decoded when calculating the buffer size + for (index, frame) in probeFrames.enumerated() { + if Task.isCancelled { break } + + /// We `+ 1` because the first frame is always manually assigned + continuation.yield(.frame(index: index + 1, frame: frame)) + } + + /// Clear out the `proveFrames` array so we don't use the extra memory + probeFrames.removeAll(keepingCapacity: false) + + /// Load in any additional buffer frames needed + for i in frameIndexesToBuffer { + guard !Task.isCancelled else { + continuation.finish() + return + } + + var decodedFrame: UIImage? + autoreleasepool { + decodedFrame = predecode( + cgImage: CGImageSourceCreateImageAtIndex(source, i, nil), + using: decodingContext + ) + } + + if let frame: UIImage = decodedFrame { + continuation.yield(.frame(index: i, frame: frame)) + } + } + + /// Now that we have buffered enough frames we can start the animation + if !Task.isCancelled { + continuation.yield(.readyToPlay) + } + + /// Start loading the remaining frames (`+ 1` as we want to start from the index after the last buffered index) + if lastBufferedFrameIndex < count { + for i in (lastBufferedFrameIndex + 1).. CGContext? { + guard width > 0 && height > 0 else { return nil } + + return CGContext( + data: nil, + width: width, + height: height, + bitsPerComponent: 8, + bytesPerRow: (width * 4), + space: CGColorSpaceCreateDeviceRGB(), + bitmapInfo: (CGImageAlphaInfo.premultipliedFirst.rawValue | CGBitmapInfo.byteOrder32Little.rawValue) + ) + } + + private static func predecode(cgImage: CGImage?, using context: CGContext) -> UIImage? { + guard let cgImage: CGImage = cgImage else { return nil } + + let width: Int = context.width + let height: Int = context.height + context.clear(CGRect(x: 0, y: 0, width: width, height: height)) + context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height)) + + return context.makeImage().map { UIImage(cgImage: $0) } + } + + private static func getFrameDurations(from imageSource: CGImageSource, count: Int) -> [TimeInterval] { + return (0.. TimeInterval { guard let properties = CGImageSourceCopyPropertiesAtIndex(source, index, nil) as? [String: Any] else { return 0.1 @@ -328,6 +493,50 @@ public actor ImageDataManager: ImageDataManagerType { return 0.1 /// Fallback } + private static func calculateHeuristicBuffer( + startIndex: Int, + source: CGImageSource, + durations: [TimeInterval], + using context: CGContext + ) async -> (frameIndexesToBuffer: [Int], probeFrames: [UIImage]) { + let probeFrameCount: Int = 5 /// Number of frames to decode in order to calculate the approx. time to load each frame + let safetyMargin: Double = 2 /// Number of extra frames to be buffered just in case + + guard durations.count > (startIndex + probeFrameCount) else { + return (Array(startIndex.. 0.001 else { return ([], probeFrames) } + + let decodeToDisplayRatio: Double = (avgDecodeTime / avgDisplayDuration) + let calculatedBufferSize: Double = ceil(decodeToDisplayRatio) + safetyMargin + let finalFramesToBuffer: Int = Int(max(Double(probeFrameCount), min(calculatedBufferSize, 60.0))) + + guard finalFramesToBuffer > (startIndex + probeFrameCount) else { return ([], probeFrames) } + + return (Array((startIndex + probeFrameCount).. UIImage?) case placeholderIcon(seed: String, text: String, size: CGFloat) + case asyncSource(String, @Sendable () async -> DataSource?) public var identifier: String { switch self { @@ -364,9 +573,6 @@ public extension ImageDataManager { case .urlThumbnail(let url, let size, _): return "\(url.absoluteString)-\(size)" - case .closureThumbnail(let identifier, let size, _): - return "\(identifier)-\(size)" - case .placeholderIcon(let seed, let text, let size): let content: (intSeed: Int, initials: String) = PlaceholderIcon.content( seed: seed, @@ -374,6 +580,9 @@ public extension ImageDataManager { ) return "\(seed)-\(content.initials)-\(Int(floor(size)))" + + /// We will use the identifier from the loaded source for caching purposes + case .asyncSource(let identifier, _): return identifier } } @@ -384,30 +593,34 @@ public extension ImageDataManager { case .image(_, let image): return image?.pngData() case .videoUrl: return nil case .urlThumbnail: return nil - case .closureThumbnail: return nil case .placeholderIcon: return nil + case .asyncSource: return nil } } - public var dataForGuessingImageFormat: Data? { + public var directImage: UIImage? { switch self { - case .url(let url), .urlThumbnail(let url, _, _): - guard let fileHandle: FileHandle = try? FileHandle(forReadingFrom: url) else { - return nil - } - - defer { fileHandle.closeFile() } - return fileHandle.readData(ofLength: 12) - - case .data(_, let data): return data - case .image, .videoUrl, .closureThumbnail, .placeholderIcon: return nil + case .image(_, let image): return image + default: return nil } } - public var directImage: UIImage? { + fileprivate func createImageSource(options: [CFString: Any]? = nil) -> CGImageSource? { + let finalOptions: CFDictionary = ( + options ?? + [ + kCGImageSourceShouldCache: false, + kCGImageSourceShouldCacheImmediately: false + ] + ) as CFDictionary + switch self { - case .image(_, let image): return image - default: return nil + case .url(let url): return CGImageSourceCreateWithURL(url as CFURL, finalOptions) + case .data(_, let data): return CGImageSourceCreateWithData(data as CFData, finalOptions) + case .urlThumbnail(let url, _, _): return CGImageSourceCreateWithURL(url as CFURL, finalOptions) + + // These cases have special handling which doesn't use `createImageSource` + case .image, .videoUrl, .placeholderIcon, .asyncSource: return nil } } @@ -419,6 +632,7 @@ public extension ImageDataManager { lhsIdentifier == rhsIdentifier && lhsData == rhsData ) + case (.image(let lhsIdentifier, _), .image(let rhsIdentifier, _)): /// `UIImage` is not _really_ equatable so we need to use a separate identifier to use instead return (lhsIdentifier == rhsIdentifier) @@ -436,12 +650,6 @@ public extension ImageDataManager { lhsSize == rhsSize ) - case (.closureThumbnail(let lhsIdentifier, let lhsSize, _), .closureThumbnail(let rhsIdentifier, let rhsSize, _)): - return ( - lhsIdentifier == rhsIdentifier && - lhsSize == rhsSize - ) - case (.placeholderIcon(let lhsSeed, let lhsText, let lhsSize), .placeholderIcon(let rhsSeed, let rhsText, let rhsSize)): return ( lhsSeed == rhsSeed && @@ -449,6 +657,9 @@ public extension ImageDataManager { lhsSize == rhsSize ) + case (.asyncSource(let lhsIdentifier, _), .asyncSource(let rhsIdentifier, _)): + return (lhsIdentifier == rhsIdentifier) + default: return false } } @@ -473,14 +684,13 @@ public extension ImageDataManager { url.hash(into: &hasher) size.hash(into: &hasher) - case .closureThumbnail(let identifier, let size, _): - identifier.hash(into: &hasher) - size.hash(into: &hasher) - case .placeholderIcon(let seed, let text, let size): seed.hash(into: &hasher) text.hash(into: &hasher) size.hash(into: &hasher) + + case .asyncSource(let identifier, _): + identifier.hash(into: &hasher) } } } @@ -491,7 +701,29 @@ public extension ImageDataManager { public extension ImageDataManager { enum DataType { case staticImage(UIImage) - case animatedImage(frames: [UIImage], frameDurations: [TimeInterval]) + case animatedImage(frames: [UIImage], durations: [TimeInterval]) + case bufferedAnimatedImage( + firstFrame: UIImage, + durations: [TimeInterval], + bufferedFrameStream: AsyncStream + ) + } + + enum BufferedFrameStreamEvent { + case frame(index: Int, frame: UIImage) + case readyToPlay + } +} + +// MARK: - ImageDataManager.isAnimatedImage + +public extension ImageDataManager { + static func isAnimatedImage(_ imageData: Data?) -> Bool { + guard let data: Data = imageData, let imageSource = CGImageSourceCreateWithData(data as CFData, nil) else { + return false + } + let frameCount = CGImageSourceGetCount(imageSource) + return frameCount > 1 } } @@ -501,7 +733,14 @@ public extension ImageDataManager { class ProcessedImageData: @unchecked Sendable { public let type: DataType public let frameCount: Int - public let estimatedCost: Int + public let estimatedCacheCost: Int + + public var isCacheable: Bool { + switch type { + case .staticImage, .animatedImage: return true + case .bufferedAnimatedImage: return false + } + } init(type: DataType) { self.type = type @@ -509,11 +748,15 @@ public extension ImageDataManager { switch type { case .staticImage(let image): frameCount = 1 - estimatedCost = ProcessedImageData.calculateCost(for: [image]) + estimatedCacheCost = ProcessedImageData.calculateCost(for: [image]) case .animatedImage(let frames, _): frameCount = frames.count - estimatedCost = ProcessedImageData.calculateCost(for: frames) + estimatedCacheCost = ProcessedImageData.calculateCost(for: frames) + + case .bufferedAnimatedImage(_, let durations, _): + frameCount = durations.count + estimatedCacheCost = 0 } } @@ -535,44 +778,6 @@ public extension ImageDataManager { /// Needed for `actor` usage (ie. assume safe access) extension UIImage: @unchecked Sendable {} -extension UIImage { - /// When loading an image the OS doesn't immediately decompress the entire image in order to be efficient but since that - /// decompressing could happen on the main thread it would defeat the purpose of our background processing potentially - /// re-introducing the jitteriness this class was designed to resolve, so instead this function will decompress the image directly - func predecodedImage() -> UIImage? { - guard let cgImage = self.cgImage else { return self } - - let width: Int = cgImage.width - let height: Int = cgImage.height - - /// Avoid `CGBitmapContextCreate` error with 0 dimension - guard width > 0 && height > 0 else { return self } - - let colorSpace: CGColorSpace = CGColorSpaceCreateDeviceRGB() - let bitmapInfo: UInt32 = ( - CGImageAlphaInfo.premultipliedFirst.rawValue | - CGBitmapInfo.byteOrder32Little.rawValue - ) - - guard - let context: CGContext = CGContext( - data: nil, - width: width, - height: height, - bitsPerComponent: 8, - bytesPerRow: (width * 4), - space: colorSpace, - bitmapInfo: bitmapInfo - ) - else { return self } - - context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height)) - guard let drawnImage: CGImage = context.makeImage() else { return self } - - return UIImage(cgImage: drawnImage, scale: self.scale, orientation: self.imageOrientation) - } -} - extension AVAsset { var isValidVideo: Bool { var maxTrackSize = CGSize.zero @@ -593,6 +798,9 @@ extension AVAsset { } public extension ImageDataManager.DataSource { + /// We need to ensure that the image size is "reasonable", otherwise trying to load it could cause out-of-memory crashes + static let maxValidDimension: Int = 1 << 18 // 262,144 pixels + @MainActor var sizeFromMetadata: CGSize? { /// There are a number of types which have fixed sizes, in those cases we should return the target size rather than try to @@ -603,39 +811,28 @@ public extension ImageDataManager.DataSource { return image.size - case .urlThumbnail(_, let size, _), .closureThumbnail(_, let size, _): + case .urlThumbnail(_, let size, _): let dimension: CGFloat = size.pixelDimension() return CGSize(width: dimension, height: dimension) case .placeholderIcon(_, _, let size): return CGSize(width: size, height: size) - case .url, .data, .videoUrl: break + case .url, .data, .videoUrl, .asyncSource: break } /// Since we don't have a direct size, try to extract it from the data guard - let imageData: Data = imageData, - let imageFormat: SUIKImageFormat = imageData.suiKitGuessedImageFormat.nullIfUnknown + let source: CGImageSource = createImageSource(), + let properties: [String: Any] = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [String: Any], + let sourceWidth: Int = properties[kCGImagePropertyPixelWidth as String] as? Int, + let sourceHeight: Int = properties[kCGImagePropertyPixelHeight as String] as? Int, + sourceWidth > 0, + sourceWidth < ImageDataManager.DataSource.maxValidDimension, + sourceHeight > 0, + sourceHeight < ImageDataManager.DataSource.maxValidDimension else { return nil } - /// We can extract the size of a `GIF` directly so do that - if imageFormat == .gif, let gifSize: CGSize = imageData.suiKitGifSize { - guard gifSize.suiKitIsValidGifSize else { return nil } - - return gifSize - } - - guard - let source: CGImageSource = CGImageSourceCreateWithData(imageData as CFData, nil), - CGImageSourceGetCount(source) > 0, - let imageProperties: [CFString: Any] = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any], - let pixelWidth = imageProperties[kCGImagePropertyPixelWidth] as? Int, - let pixelHeight = imageProperties[kCGImagePropertyPixelHeight] as? Int, - pixelWidth > 0, - pixelHeight > 0 - else { return nil } - - return CGSize(width: pixelWidth, height: pixelHeight) + return CGSize(width: sourceWidth, height: sourceHeight) } } @@ -667,9 +864,11 @@ public extension ImageDataManager { public protocol ImageDataManagerType { @discardableResult func load(_ source: ImageDataManager.DataSource) async -> ImageDataManager.ProcessedImageData? - nonisolated func load( + + @MainActor + func load( _ source: ImageDataManager.DataSource, - onComplete: @escaping (ImageDataManager.ProcessedImageData?) -> Void + onComplete: @MainActor @escaping (ImageDataManager.ProcessedImageData?) -> Void ) func cachedImage(identifier: String) async -> ImageDataManager.ProcessedImageData? diff --git a/SessionUIKit/Utilities/Data+Utilities.swift b/SessionUIKit/Utilities/Data+Utilities.swift deleted file mode 100644 index ade5ca6593..0000000000 --- a/SessionUIKit/Utilities/Data+Utilities.swift +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright © 2024 Rangeproof Pty Ltd. All rights reserved. - -import Foundation - -/// **Note:** The below code **MUST** match the equivalent in `SessionUtilitiesKit.Data+Utilities` -internal extension Data { - var suiKitGuessedImageFormat: SUIKImageFormat { - let twoBytesLength: Int = 2 - - guard count > twoBytesLength else { return .unknown } - - var bytes: [UInt8] = [UInt8](repeating: 0, count: twoBytesLength) - self.copyBytes(to: &bytes, from: (self.startIndex.. bufferLength else { return nil } - - var bytes: [UInt8] = [UInt8](repeating: 0, count: bufferLength) - self.copyBytes(to: &bytes, from: (self.startIndex.. 0 && - Int(width) < maxValidSize && - Int(height) > 0 && - Int(height) < maxValidSize - ) - } -} diff --git a/SessionUIKit/Utilities/Date+Utilities.swift b/SessionUIKit/Utilities/Date+Utilities.swift index 689977cf39..f7e1a02057 100644 --- a/SessionUIKit/Utilities/Date+Utilities.swift +++ b/SessionUIKit/Utilities/Date+Utilities.swift @@ -48,6 +48,14 @@ public extension Date { var formattedForBanner: String { return Date.localTimeAndDateFormatter.string(from: self) } + + static func fromHTTPExpiresHeaders(_ expiresValue: String?) -> Date? { + guard let expiresValue else { return nil } + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.dateFormat = "EEE',' dd MMM yyyy HH:mm:ss zzz" + return formatter.date(from: expiresValue) + } } // MARK: - Formatters diff --git a/SessionUIKit/Utilities/Localization+Style.swift b/SessionUIKit/Utilities/Localization+Style.swift index 788e777c10..43dce7671e 100644 --- a/SessionUIKit/Utilities/Localization+Style.swift +++ b/SessionUIKit/Utilities/Localization+Style.swift @@ -20,6 +20,9 @@ public extension ThemedAttributedString { case strikethrough = "s" case primaryTheme = "span" case icon = "icon" + case warningTheme = "warn" + case dangerTheme = "error" + case disabledTheme = "disabled" // MARK: - Functions @@ -53,6 +56,9 @@ public extension ThemedAttributedString { case .strikethrough: return [.strikethroughStyle: NSUnderlineStyle.single.rawValue] case .primaryTheme: return [.themeForegroundColor: ThemeValue.sessionButton_text] case .icon: return Lucide.attributes(for: font) + case .warningTheme: return [.themeForegroundColor: ThemeValue.warning] + case .dangerTheme: return [.themeForegroundColor: ThemeValue.danger] + case .disabledTheme: return [.themeForegroundColor: ThemeValue.disabled] } } } @@ -184,6 +190,9 @@ private extension Collection where Element == ThemedAttributedString.HTMLTag { case .icon: result[.font] = fontWith(Lucide.font(ofSize: (font.pointSize + 1)), traits: []) result[.baselineOffset] = Lucide.defaultBaselineOffset + case .warningTheme: result[.themeForegroundColor] = ThemeValue.warning + case .dangerTheme: result[.themeForegroundColor] = ThemeValue.danger + case .disabledTheme: result[.themeForegroundColor] = ThemeValue.disabled } } } diff --git a/SessionUIKit/Utilities/String+SessionProBadge.swift b/SessionUIKit/Utilities/String+SessionProBadge.swift new file mode 100644 index 0000000000..15f36582cc --- /dev/null +++ b/SessionUIKit/Utilities/String+SessionProBadge.swift @@ -0,0 +1,40 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import UIKit + +public extension String { + enum SessionProBadgePosition { + case leading, trailing + } + + func addProBadge( + at postion: SessionProBadgePosition, + font: UIFont, + textColor: ThemeValue = .textPrimary, + proBadgeSize: SessionProBadge.Size, + spacing: String = " " + ) -> NSMutableAttributedString { + let image: UIImage = SessionProBadge(size: proBadgeSize).toImage() + let base = NSMutableAttributedString() + let attachment = NSTextAttachment() + attachment.image = image + + // Vertical alignment tweak to align to baseline + let cap = font.capHeight + let dy = (cap - image.size.height) / 2 + attachment.bounds = CGRect(x: 0, y: dy, width: image.size.width, height: image.size.height) + + switch postion { + case .leading: + base.append(NSAttributedString(attachment: attachment)) + base.append(NSAttributedString(string: spacing)) + base.append(NSAttributedString(string: self, attributes: [.font: font, .themeForegroundColor: textColor])) + case .trailing: + base.append(NSAttributedString(string: self, attributes: [.font: font, .themeForegroundColor: textColor])) + base.append(NSAttributedString(string: spacing)) + base.append(NSAttributedString(attachment: attachment)) + } + + return base + } +} diff --git a/SessionUIKit/Utilities/UIImage+Utilities.swift b/SessionUIKit/Utilities/UIImage+Utilities.swift index b5e04176c7..0c2e894c15 100644 --- a/SessionUIKit/Utilities/UIImage+Utilities.swift +++ b/SessionUIKit/Utilities/UIImage+Utilities.swift @@ -62,4 +62,29 @@ public extension UIImage { UIGraphicsEndImageContext() return imageWithGradient } + + func withCircularBackground(backgroundColor: UIColor) -> UIImage? { + let originalSize = self.size + let diameter = max(originalSize.width, originalSize.height) * 2 + let newSize = CGSize(width: diameter, height: diameter) + + let renderer = UIGraphicsImageRenderer(size: newSize) + let renderedImage = renderer.image { context in + let ctx = context.cgContext + + // Draw the circular background + let circleRect = CGRect(origin: .zero, size: newSize) + ctx.setFillColor(backgroundColor.cgColor) + ctx.fillEllipse(in: circleRect) + + // Draw the original image centered + let imageOrigin = CGPoint( + x: (newSize.width - originalSize.width) / 2, + y: (newSize.height - originalSize.height) / 2 + ) + self.draw(at: imageOrigin) + } + + return renderedImage + } } diff --git a/SessionUIKit/Utilities/UIView+Constraints.swift b/SessionUIKit/Utilities/UIView+Constraints.swift index 0570ed3325..6d905e234a 100644 --- a/SessionUIKit/Utilities/UIView+Constraints.swift +++ b/SessionUIKit/Utilities/UIView+Constraints.swift @@ -211,16 +211,22 @@ public extension UIView { } } - func pin(to view: UIView) { - [ HorizontalEdge.leading, HorizontalEdge.trailing ].forEach { pin($0, to: $0, of: view) } - [ VerticalEdge.top, VerticalEdge.bottom ].forEach { pin($0, to: $0, of: view) } + @discardableResult + func pin(to view: UIView) -> [NSLayoutConstraint] { + return [ + [ HorizontalEdge.leading, HorizontalEdge.trailing ].map { pin($0, to: $0, of: view) }, + [ VerticalEdge.top, VerticalEdge.bottom ].map { pin($0, to: $0, of: view) } + ].flatMap { $0 } } - func pin(to view: UIView, withInset inset: CGFloat) { - pin(.leading, to: .leading, of: view, withInset: inset) - pin(.top, to: .top, of: view, withInset: inset) - view.pin(.trailing, to: .trailing, of: self, withInset: inset) - view.pin(.bottom, to: .bottom, of: self, withInset: inset) + @discardableResult + func pin(to view: UIView, withInset inset: CGFloat) -> [NSLayoutConstraint] { + return [ + pin(.leading, to: .leading, of: view, withInset: inset), + pin(.top, to: .top, of: view, withInset: inset), + view.pin(.trailing, to: .trailing, of: self, withInset: inset), + view.pin(.bottom, to: .bottom, of: self, withInset: inset) + ] } @discardableResult diff --git a/SessionUIKit/Utilities/UIView+Utilities.swift b/SessionUIKit/Utilities/UIView+Utilities.swift index b5518ad1f0..28a13ba074 100644 --- a/SessionUIKit/Utilities/UIView+Utilities.swift +++ b/SessionUIKit/Utilities/UIView+Utilities.swift @@ -3,7 +3,7 @@ import UIKit public extension UIView { - func toImage(isOpaque: Bool, scale: CGFloat) -> UIImage? { + func toImage(isOpaque: Bool, scale: CGFloat) -> UIImage { let format = UIGraphicsImageRendererFormat() format.scale = scale format.opaque = isOpaque diff --git a/SessionUtilitiesKit/Configuration.swift b/SessionUtilitiesKit/Configuration.swift index ce7ef571ee..445a3594ef 100644 --- a/SessionUtilitiesKit/Configuration.swift +++ b/SessionUtilitiesKit/Configuration.swift @@ -1,49 +1,22 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation -import UIKit.UIFont -import GRDB -public enum SNUtilitiesKit: MigratableTarget { // Just to make the external API nice - public static var maxFileSize: UInt = 0 +public enum SNUtilitiesKit { + public private(set) static var maxFileSize: UInt = 0 + public private(set) static var maxValidImageDimension: Int = 0 + public static var isRunningTests: Bool { ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil // stringlint:ignore } - public static func migrations() -> TargetMigrations { - return TargetMigrations( - identifier: .utilitiesKit, - migrations: [ - [ - // Intentionally including the '_003_YDBToGRDBMigration' in the first migration - // set to ensure the 'Identity' data is migrated before any other migrations are - // run (some need access to the users publicKey) - _001_InitialSetupMigration.self, - _002_SetupStandardJobs.self, - _003_YDBToGRDBMigration.self - ], // Initial DB Creation - [], // YDB to GRDB Migration - [], // Legacy DB removal - [ - _004_AddJobPriority.self - ], // Add job priorities - [], // Fix thread FTS - [ - _005_AddJobUniqueHash.self - ], - [ - _006_RenameTableSettingToKeyValueStore.self - ], // Renamed `Setting` to `KeyValueStore` - [] - ] - ) - } - public static func configure( networkMaxFileSize: UInt, + maxValidImageDimention: Int, using dependencies: Dependencies ) { self.maxFileSize = networkMaxFileSize + self.maxValidImageDimension = maxValidImageDimention // Register any recurring jobs to ensure they are actually scheduled dependencies[singleton: .jobRunner].registerRecurringJobs( diff --git a/SessionUtilitiesKit/Database/Models/KeyValueStore.swift b/SessionUtilitiesKit/Database/Models/KeyValueStore.swift index d7bac86112..539f2c15c2 100644 --- a/SessionUtilitiesKit/Database/Models/KeyValueStore.swift +++ b/SessionUtilitiesKit/Database/Models/KeyValueStore.swift @@ -150,7 +150,7 @@ public extension ObservingDatabase { return nil } - return try? KeyValueStore(key: key, value: value)?.saved(self) + return try? KeyValueStore(key: key, value: value)?.upserted(self) } private subscript(key: String) -> KeyValueStore? { @@ -161,7 +161,7 @@ public extension ObservingDatabase { return } - try? newValue.save(self) + try? newValue.upsert(self) } } diff --git a/SessionUtilitiesKit/Database/Storage.swift b/SessionUtilitiesKit/Database/Storage.swift index b162e27c99..09817afb74 100644 --- a/SessionUtilitiesKit/Database/Storage.swift +++ b/SessionUtilitiesKit/Database/Storage.swift @@ -246,8 +246,6 @@ open class Storage { // MARK: - Migrations - public typealias KeyedMigration = (key: String, identifier: TargetMigrations.Identifier, migration: Migration.Type) - public static func appliedMigrationIdentifiers(_ db: ObservingDatabase) -> Set { let migrator: DatabaseMigrator = DatabaseMigrator() @@ -255,47 +253,11 @@ open class Storage { .defaulting(to: []) } - public static func sortedMigrationInfo(migrationTargets: [MigratableTarget.Type]) -> [KeyedMigration] { - typealias MigrationInfo = (identifier: TargetMigrations.Identifier, migrations: TargetMigrations.MigrationSet) - - return migrationTargets - .map { target -> TargetMigrations in target.migrations() } - .sorted() - .reduce(into: [[MigrationInfo]]()) { result, next in - next.migrations.enumerated().forEach { index, migrationSet in - if result.count <= index { - result.append([]) - } - - result[index] = (result[index] + [(next.identifier, migrationSet)]) - } - } - .reduce(into: []) { result, next in - next.forEach { identifier, migrations in - result.append(contentsOf: migrations.map { (identifier.key(with: $0), identifier, $0) }) - } - } - } - public func perform( - migrationTargets: [MigratableTarget.Type], + migrations: [Migration.Type], async: Bool = true, onProgressUpdate: ((CGFloat, TimeInterval) -> ())?, onComplete: @escaping (Result) -> () - ) { - perform( - sortedMigrations: Storage.sortedMigrationInfo(migrationTargets: migrationTargets), - async: async, - onProgressUpdate: onProgressUpdate, - onComplete: onComplete - ) - } - - internal func perform( - sortedMigrations: [KeyedMigration], - async: Bool, - onProgressUpdate: ((CGFloat, TimeInterval) -> ())?, - onComplete: @escaping (Result) -> () ) { guard isValid, let dbWriter: DatabaseWriter = dbWriter else { let error: Error = (startupError ?? StorageError.startupFailed) @@ -306,36 +268,33 @@ open class Storage { // Setup and run any required migrations var migrator: DatabaseMigrator = DatabaseMigrator() - sortedMigrations.forEach { _, identifier, migration in - migrator.registerMigration( - self, - targetIdentifier: identifier, - migration: migration, - using: dependencies - ) + migrations.forEach { migration in + migrator.registerMigration(migration.identifier) { [dependencies] db in + let migration = migration.loggedMigrate(using: dependencies) + try migration(ObservingDatabase.create(db, using: dependencies)) + } } // Determine which migrations need to be performed and gather the relevant settings needed to // inform the app of progress/states let completedMigrations: [String] = (try? dbWriter.read { db in try migrator.completedMigrations(db) }) .defaulting(to: []) - let unperformedMigrations: [KeyedMigration] = sortedMigrations + let unperformedMigrations: [Migration.Type] = migrations .reduce(into: []) { result, next in - guard !completedMigrations.contains(next.key) else { return } + guard !completedMigrations.contains(next.identifier) else { return } result.append(next) } let migrationToDurationMap: [String: TimeInterval] = unperformedMigrations .reduce(into: [:]) { result, next in - result[next.key] = next.migration.minExpectedRunDuration + result[next.identifier] = next.minExpectedRunDuration } - let unperformedMigrationDurations: [TimeInterval] = unperformedMigrations - .map { _, _, migration in migration.minExpectedRunDuration } + let unperformedMigrationDurations: [TimeInterval] = unperformedMigrations.map { $0.minExpectedRunDuration } let totalMinExpectedDuration: TimeInterval = migrationToDurationMap.values.reduce(0, +) // Store the logic to handle migration progress and completion let progressUpdater: (String, CGFloat) -> Void = { (targetKey: String, progress: CGFloat) in - guard let migrationIndex: Int = unperformedMigrations.firstIndex(where: { key, _, _ in key == targetKey }) else { + guard let migrationIndex: Int = unperformedMigrations.firstIndex(where: { $0.identifier == targetKey }) else { return } @@ -352,8 +311,8 @@ open class Storage { let migrationCompleted: (Result) -> () = { [weak self, migrator, dbWriter, dependencies] result in // Make sure to transition the progress updater to 100% for the final migration (just // in case the migration itself didn't update to 100% itself) - if let lastMigrationKey: String = unperformedMigrations.last?.key { - MigrationExecution.current?.progressUpdater(lastMigrationKey, 1) + if let lastMigrationIdentifier: String = unperformedMigrations.last?.identifier { + MigrationExecution.current?.progressUpdater(lastMigrationIdentifier, 1) } self?.hasCompletedMigrations = true @@ -401,8 +360,8 @@ open class Storage { let migrationContext: MigrationExecution.Context = MigrationExecution.Context(progressUpdater: progressUpdater) // If we have an unperformed migration then trigger the progress updater immediately - if let firstMigrationKey: String = unperformedMigrations.first?.key { - migrationContext.progressUpdater(firstMigrationKey, 0) + if let firstMigrationIdentifier: String = unperformedMigrations.first?.identifier { + migrationContext.progressUpdater(firstMigrationIdentifier, 0) } MigrationExecution.$current.withValue(migrationContext) { @@ -494,6 +453,8 @@ open class Storage { .defaulting(to: "N/A") Log.verbose(.storage, "Database suspended successfully for \(id) (db: \(dbFileSize), shm: \(dbShmFileSize), wal: \(dbWalFileSize)).") } + + dependencies.notifyAsync(key: .databaseLifecycle(.suspended)) } /// This method reverses the database suspension used to prevent the `0xdead10cc` exception (see `suspendDatabaseAccess()` @@ -503,6 +464,7 @@ open class Storage { isSuspended = false Log.info(.storage, "Database access resumed.") + dependencies.notifyAsync(key: .databaseLifecycle(.resumed)) } public func checkpoint(_ mode: Database.CheckpointMode) throws { diff --git a/SessionUtilitiesKit/Database/Types/Migration.swift b/SessionUtilitiesKit/Database/Types/Migration.swift index 36a9026a8f..4609edfbdd 100644 --- a/SessionUtilitiesKit/Database/Types/Migration.swift +++ b/SessionUtilitiesKit/Database/Types/Migration.swift @@ -12,7 +12,6 @@ public extension Log.Category { // MARK: - Migration public protocol Migration { - static var target: TargetMigrations.Identifier { get } static var identifier: String { get } static var minExpectedRunDuration: TimeInterval { get } static var createdTables: [(TableRecord & FetchableRecord).Type] { get } @@ -21,17 +20,12 @@ public protocol Migration { } public extension Migration { - static func loggedMigrate( - _ storage: Storage?, - targetIdentifier: TargetMigrations.Identifier, - using dependencies: Dependencies - ) -> ((_ db: ObservingDatabase) throws -> ()) { + static func loggedMigrate(using dependencies: Dependencies) -> ((_ db: ObservingDatabase) throws -> ()) { return { (db: ObservingDatabase) in - Log.info(.migration, "Starting \(targetIdentifier.key(with: self))") + Log.info(.migration, "Starting \(identifier)") /// Store the `currentlyRunningMigration` in case it's useful MigrationExecution.current?.currentlyRunningMigration = MigrationExecution.CurrentlyRunningMigration( - identifier: targetIdentifier, migration: self ) defer { MigrationExecution.current?.currentlyRunningMigration = nil } @@ -44,7 +38,7 @@ public extension Migration { MigrationExecution.current?.observedEvents.append(contentsOf: db.events) MigrationExecution.current?.postCommitActions.merge(db.postCommitActions) { old, _ in old } - Log.info(.migration, "Completed \(targetIdentifier.key(with: self))") + Log.info(.migration, "Completed \(identifier)") } } } @@ -53,10 +47,9 @@ public extension Migration { public enum MigrationExecution { public struct CurrentlyRunningMigration: ThreadSafeType { - public let identifier: TargetMigrations.Identifier public let migration: Migration.Type - public var key: String { identifier.key(with: migration) } + public var key: String { migration.identifier } } public final class Context { @@ -83,6 +76,7 @@ public enum MigrationExecution { @TaskLocal public static var current: Context? + // stringlint:ignore_contents public static func updateProgress(_ progress: CGFloat) { // In test builds ignore any migration progress updates (we run in a custom database writer anyway) guard !SNUtilitiesKit.isRunningTests else { return } diff --git a/SessionUtilitiesKit/Database/Types/TargetMigrations.swift b/SessionUtilitiesKit/Database/Types/TargetMigrations.swift deleted file mode 100644 index b320516d5c..0000000000 --- a/SessionUtilitiesKit/Database/Types/TargetMigrations.swift +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import Foundation -import GRDB - -public protocol MigratableTarget { - static func migrations() -> TargetMigrations -} - -public struct TargetMigrations: Comparable { - /// This identifier is used to determine the order each set of migrations should run in. - /// - /// All migrations within a specific set will run first, followed by all migrations for the same set index in - /// the next `Identifier` before moving on to the next `MigrationSet`. So given the migrations: - /// - /// `{a: [1], [2, 3]}, {b: [4, 5], [6]}` - /// - /// the migrations will run in the following order: - /// - /// `a1, b4, b5, a2, a3, b6` - public enum Identifier: String, CaseIterable, Comparable { - // WARNING: The string version of these cases are used as migration identifiers so - // changing them will result in the migrations running again - case session - case utilitiesKit - case snodeKit - case messagingKit - case _deprecatedUIKit = "uiKit" - case test - - public static func < (lhs: Self, rhs: Self) -> Bool { - let lhsIndex: Int = (Identifier.allCases.firstIndex(of: lhs) ?? Identifier.allCases.count) - let rhsIndex: Int = (Identifier.allCases.firstIndex(of: rhs) ?? Identifier.allCases.count) - - return (lhsIndex < rhsIndex) - } - - public func key(with migration: Migration.Type) -> String { - return "\(self.rawValue).\(migration.identifier)" - } - } - - public typealias MigrationSet = [Migration.Type] - - let identifier: Identifier - let migrations: [MigrationSet] - - // MARK: - Initialization - - public init( - identifier: Identifier, - migrations: [MigrationSet] - ) { - guard !migrations.contains(where: { migration in migration.contains(where: { $0.target != identifier }) }) else { - preconditionFailure("Attempted to register a migration with the wrong target") - } - - self.identifier = identifier - self.migrations = migrations - } - - // MARK: - Equatable - - public static func == (lhs: TargetMigrations, rhs: TargetMigrations) -> Bool { - return ( - lhs.identifier == rhs.identifier && - lhs.migrations.count == rhs.migrations.count - ) - } - - // MARK: - Comparable - - public static func < (lhs: Self, rhs: Self) -> Bool { - return (lhs.identifier < rhs.identifier) - } -} diff --git a/SessionUtilitiesKit/Database/Utilities/DatabaseMigrator+Utilities.swift b/SessionUtilitiesKit/Database/Utilities/DatabaseMigrator+Utilities.swift deleted file mode 100644 index 748175ca8d..0000000000 --- a/SessionUtilitiesKit/Database/Utilities/DatabaseMigrator+Utilities.swift +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import Foundation -import GRDB - -public extension DatabaseMigrator { - mutating func registerMigration( - _ storage: Storage?, - targetIdentifier: TargetMigrations.Identifier, - migration: Migration.Type, - foreignKeyChecks: ForeignKeyChecks = .deferred, - using dependencies: Dependencies - ) { - self.registerMigration( - targetIdentifier.key(with: migration), - migrate: { db in - let migration = migration.loggedMigrate(storage, targetIdentifier: targetIdentifier, using: dependencies) - try migration(ObservingDatabase.create(db, using: dependencies)) - } - ) - } -} diff --git a/SessionUtilitiesKit/Dependency Injection/Dependencies.swift b/SessionUtilitiesKit/Dependency Injection/Dependencies.swift index acdca842a6..4c6578106a 100644 --- a/SessionUtilitiesKit/Dependency Injection/Dependencies.swift +++ b/SessionUtilitiesKit/Dependency Injection/Dependencies.swift @@ -13,6 +13,9 @@ public class Dependencies { @ThreadSafeObject private static var cachedIsRTLRetriever: (requiresMainThread: Bool, retriever: () -> Bool) = (false, { false }) @ThreadSafeObject private var storage: DependencyStorage = DependencyStorage() + private typealias DependencyChange = (Dependencies.DependencyStorage.Key, DependencyStorage.Value?) + private let dependencyChangeStream: CancellationAwareAsyncStream = CancellationAwareAsyncStream() + // MARK: - Subscript Access public subscript(singleton singleton: SingletonConfig) -> S { getOrCreate(singleton) } @@ -112,7 +115,7 @@ public class Dependencies { return elements.popRandomElement() } - // MARK: - Instance replacing + // MARK: - Instance management public func warmCache(cache: CacheConfig) { _ = getOrCreate(cache) @@ -134,6 +137,26 @@ public class Dependencies { public static func setIsRTLRetriever(requiresMainThread: Bool, isRTLRetriever: @escaping () -> Bool) { _cachedIsRTLRetriever.set(to: (requiresMainThread, isRTLRetriever)) } + + private func waitUntilInitialised(targetKey: Dependencies.DependencyStorage.Key) async throws { + /// If we already have an instance (which isn't a `NoopDependency`) then no need to observe the stream + guard !_storage.performMap({ $0.instances[targetKey]?.isNoop == false }) else { return } + + for await (key, instance) in dependencyChangeStream.stream { + /// If the target instance has been set (and isn't a `NoopDependency`) then we can stop waiting (observing the stream) + if key == targetKey && instance?.isNoop == false { + break + } + } + } + + public func waitUntilInitialised(singleton: SingletonConfig) async throws { + try await waitUntilInitialised(targetKey: DependencyStorage.Key.Variant.singleton.key(singleton.identifier)) + } + + public func waitUntilInitialised(cache: CacheConfig) async throws { + try await waitUntilInitialised(targetKey: DependencyStorage.Key.Variant.cache.key(cache.identifier)) + } } // MARK: - Cache Management @@ -196,11 +219,17 @@ public extension Dependencies { removeValue(feature.identifier, of: .feature) /// Notify observers + + Task { await dependencyChangeStream.send((key, nil)) } notifyAsync(events: [ ObservedEvent(key: .feature(feature), value: nil), ObservedEvent(key: .featureGroup(feature), value: nil) ]) } + + func defaultValue(feature: FeatureConfig) -> T? { + return feature.createInstance(self).defaultOption + } } // MARK: - DependenciesError @@ -244,6 +273,15 @@ private extension Dependencies { case userDefaults(UserDefaultsType) case feature(any FeatureType) + var isNoop: Bool { + switch self { + case .singleton(let value): return value is NoopDependency + case .userDefaults(let value): return value is NoopDependency + case .feature(let value): return value is NoopDependency + case .cache(let value): return value.performMap { $0 is NoopDependency } + } + } + func distinctKey(for identifier: String) -> Key { switch self { case .singleton: return Key(identifier, of: .singleton) @@ -340,18 +378,30 @@ private extension Dependencies { /// Convenience method to store a dependency instance in memory in a thread-safe way @discardableResult private func setValue(_ value: T, typedStorage: DependencyStorage.Value, key: String) -> T { - return _storage.performUpdateAndMap { storage in - storage.instances[typedStorage.distinctKey(for: key)] = typedStorage + let finalKey: DependencyStorage.Key = typedStorage.distinctKey(for: key) + let result: T = _storage.performUpdateAndMap { storage in + storage.instances[finalKey] = typedStorage return (storage, value) } + + /// We generally _shouldn't_ be setting a dependency to a no-op value so log a warning when we do so + if typedStorage.isNoop { + Log.warn("Setting noop dependency for \(key)") + } + + Task { await dependencyChangeStream.send((finalKey, typedStorage)) } + return result } /// Convenience method to remove a dependency instance from memory in a thread-safe way private func removeValue(_ key: String, of variant: DependencyStorage.Key.Variant) { + let finalKey: DependencyStorage.Key = variant.key(key) _storage.performUpdate { storage in - storage.instances.removeValue(forKey: variant.key(key)) + storage.instances.removeValue(forKey: finalKey) return storage } + + Task { await dependencyChangeStream.send((finalKey, nil)) } } } diff --git a/SessionUtilitiesKit/Dependency Injection/FeatureConfig.swift b/SessionUtilitiesKit/Dependency Injection/FeatureConfig.swift index 58b4b12c59..30440e19ab 100644 --- a/SessionUtilitiesKit/Dependency Injection/FeatureConfig.swift +++ b/SessionUtilitiesKit/Dependency Injection/FeatureConfig.swift @@ -25,7 +25,6 @@ public class FeatureConfig: FeatureStorage { self.createInstance = { _ in Feature( identifier: identifier, - options: Array(T.allCases), defaultOption: defaultOption, automaticChangeBehaviour: automaticChangeBehaviour ) diff --git a/SessionUtilitiesKit/Dependency Injection/NoopDependency.swift b/SessionUtilitiesKit/Dependency Injection/NoopDependency.swift new file mode 100644 index 0000000000..7907a37de6 --- /dev/null +++ b/SessionUtilitiesKit/Dependency Injection/NoopDependency.swift @@ -0,0 +1,5 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public protocol NoopDependency {} diff --git a/SessionUtilitiesKit/General/AppContext.swift b/SessionUtilitiesKit/General/AppContext.swift index 867627f3a1..cf47d1f0c3 100644 --- a/SessionUtilitiesKit/General/AppContext.swift +++ b/SessionUtilitiesKit/General/AppContext.swift @@ -54,7 +54,7 @@ public extension AppContext { func endBackgroundTask(_ backgroundTaskIdentifier: UIBackgroundTaskIdentifier) {} } -private final class NoopAppContext: AppContext { +private final class NoopAppContext: AppContext, NoopDependency { let mainWindow: UIWindow? = nil let frontMostViewController: UIViewController? = nil diff --git a/SessionUtilitiesKit/General/Feature+ServiceNetwork.swift b/SessionUtilitiesKit/General/Feature+ServiceNetwork.swift index 6797926834..7ae5469b20 100644 --- a/SessionUtilitiesKit/General/Feature+ServiceNetwork.swift +++ b/SessionUtilitiesKit/General/Feature+ServiceNetwork.swift @@ -15,7 +15,7 @@ public extension FeatureStorage { // MARK: - ServiceNetwork Feature -public enum ServiceNetwork: Int, Sendable, FeatureOption { +public enum ServiceNetwork: Int, Sendable, FeatureOption, CaseIterable { case mainnet = 1 case testnet = 2 diff --git a/SessionUtilitiesKit/General/Feature.swift b/SessionUtilitiesKit/General/Feature.swift index 9f73288a61..55998cbed9 100644 --- a/SessionUtilitiesKit/General/Feature.swift +++ b/SessionUtilitiesKit/General/Feature.swift @@ -41,6 +41,11 @@ public extension FeatureStorage { identifier: "forceSlowDatabaseQueries" ) + static let communityPollLimit: FeatureConfig = Dependencies.create( + identifier: "communityPollLimit", + defaultOption: 100 + ) + static let updatedGroupsDisableAutoApprove: FeatureConfig = Dependencies.create( identifier: "updatedGroupsDisableAutoApprove" ) @@ -88,16 +93,26 @@ public extension FeatureStorage { static let treatAllIncomingMessagesAsProMessages: FeatureConfig = Dependencies.create( identifier: "treatAllIncomingMessagesAsProMessages" ) + + static let shortenFileTTL: FeatureConfig = Dependencies.create( + identifier: "shortenFileTTL" + ) + + static let simulateAppReviewLimit: FeatureConfig = Dependencies.create( + identifier: "simulateAppReviewLimit" + ) } // MARK: - FeatureOption -public protocol FeatureOption: RawRepresentable, CaseIterable, Equatable, Hashable { +public protocol FeatureOption: RawRepresentable, Equatable, Hashable { static var defaultOption: Self { get } var isValidOption: Bool { get } var title: String { get } var subtitle: String? { get } + + static func validateOptions(defaultOption: Self) } public extension FeatureOption { @@ -127,7 +142,6 @@ public struct Feature: FeatureType { } private let identifier: String - public let options: [T] public let defaultOption: T public let automaticChangeBehaviour: ChangeBehaviour? @@ -135,17 +149,12 @@ public struct Feature: FeatureType { public init( identifier: String, - options: [T], defaultOption: T, automaticChangeBehaviour: ChangeBehaviour? = nil ) { - guard - T.self == Bool.self || - !options.appending(defaultOption).contains(where: { ($0.rawValue as? Int) == 0 }) - else { preconditionFailure("A rawValue of '0' is a protected value (it indicates unset)") } + T.validateOptions(defaultOption: defaultOption) self.identifier = identifier - self.options = options self.defaultOption = defaultOption self.automaticChangeBehaviour = automaticChangeBehaviour } @@ -263,3 +272,59 @@ extension Bool: FeatureOption { return (self ? "Enabled" : "Disabled") } } + +// MARK: - Int FeatureOption + +extension Int: @retroactive RawRepresentable {} +extension Int: FeatureOption { + // MARK: - Initialization + + public var rawValue: Int { return self } + + public init?(rawValue: Int) { + self = rawValue + } + + // MARK: - Feature Option + + public static var defaultOption: Int = 0 + + public var title: String { + return "\(self)" + } + + public var subtitle: String? { + return "\(self)" + } +} + +// MARK: - FeatureOption Validation + +extension FeatureOption { + public static func validateOptions(defaultOption: Self) { + guard (defaultOption.rawValue as? Int) != 0 else { + preconditionFailure("A rawValue of '0' is a protected value (it indicates unset)") + } + } +} + +extension FeatureOption where Self == Bool { + /// A `Bool` feature is always valid + public static func validateOptions(defaultOption: Bool) {} +} + +extension FeatureOption where Self == Int { + public static func validateOptions(defaultOption: Int) { + guard defaultOption != 0 else { + preconditionFailure("A rawValue of '0' is a protected value (it indicates unset)") + } + } +} + +extension FeatureOption where Self: CaseIterable { + public static func validateOptions(defaultOption: Self) { + guard !Array(Self.allCases).appending(defaultOption).contains(where: { ($0.rawValue as? Int) == 0 }) else { + preconditionFailure("A rawValue of '0' is a protected value (it indicates unset)") + } + } +} diff --git a/SessionUtilitiesKit/General/Logging.swift b/SessionUtilitiesKit/General/Logging.swift index 2dff548f87..67b529d8ec 100644 --- a/SessionUtilitiesKit/General/Logging.swift +++ b/SessionUtilitiesKit/General/Logging.swift @@ -22,6 +22,14 @@ public extension FeatureStorage { ) } + static func logLevel(group: Log.Group) -> FeatureConfig { + return Dependencies.create( + identifier: "\(Log.Group.identifierPrefix)\(group.name)", + groupIdentifier: "logging", + defaultOption: group.defaultLevel + ) + } + static let allLogLevels: FeatureConfig = Dependencies.create( identifier: "allLogLevels", groupIdentifier: "logging" @@ -50,33 +58,66 @@ public enum Log { case off case `default` + + var label: String { + switch self { + case .off: return "off" + case .verbose: return "verbose" + case .debug: return "debug" + case .info: return "info" + case .warn: return "warn" + case .error: return "error" + case .critical: return "critical" + case .default: return "default" + } + } + } + + public struct Group: Hashable { + public let name: String + public let defaultLevel: Log.Level + + fileprivate static let identifierPrefix: String = "group:" + + private init(name: String, defaultLevel: Log.Level) { + self.name = name + + switch AllLoggingCategories.existingGroup(for: name) { + case .some(let existingGroup): self.defaultLevel = existingGroup.defaultLevel + case .none: + self.defaultLevel = defaultLevel + AllLoggingCategories.register(group: self) + } + } + + @discardableResult public static func create( + _ group: String, + defaultLevel: Log.Level + ) -> Log.Group { + return Log.Group(name: group, defaultLevel: defaultLevel) + } } public struct Category: Hashable { public let rawValue: String - fileprivate let customPrefix: String + fileprivate let group: Group? fileprivate let customSuffix: String public let defaultLevel: Log.Level fileprivate static let identifierPrefix: String = "logLevel-" fileprivate var identifier: String { "\(Category.identifierPrefix)\(rawValue)" } - private init(rawValue: String, customPrefix: String, customSuffix: String, defaultLevel: Log.Level) { + private init(rawValue: String, group: Group?, customSuffix: String, defaultLevel: Log.Level) { + self.rawValue = rawValue + self.group = group + self.customSuffix = customSuffix + /// If we've already registered this category then assume the original has the correct `defaultLevel` and only /// modify the `customPrefix` value switch AllLoggingCategories.existingCategory(for: rawValue) { - case .some(let existingCategory): - self.rawValue = existingCategory.rawValue - self.customPrefix = customPrefix - self.customSuffix = customSuffix - self.defaultLevel = existingCategory.defaultLevel - + case .some(let existingCategory): self.defaultLevel = existingCategory.defaultLevel case .none: - self.rawValue = rawValue - self.customPrefix = customPrefix - self.customSuffix = customSuffix self.defaultLevel = defaultLevel - AllLoggingCategories.register(category: self) } } @@ -86,25 +127,25 @@ public enum Log { self.init( rawValue: identifier.substring(from: Category.identifierPrefix.count), - customPrefix: "", + group: nil, customSuffix: "", defaultLevel: .default ) } - public init(rawValue: String, customPrefix: String = "", customSuffix: String = "") { - self.init(rawValue: rawValue, customPrefix: customPrefix, customSuffix: customSuffix, defaultLevel: .default) + public init(rawValue: String, group: Group? = nil, customSuffix: String = "") { + self.init(rawValue: rawValue, group: group, customSuffix: customSuffix, defaultLevel: .default) } @discardableResult public static func create( _ rawValue: String, - customPrefix: String = "", + group: Group? = nil, customSuffix: String = "", defaultLevel: Log.Level ) -> Log.Category { return Log.Category( rawValue: rawValue, - customPrefix: customPrefix, + group: group, customSuffix: customSuffix, defaultLevel: defaultLevel ) @@ -653,12 +694,15 @@ public actor Logger: LoggerType { let defaultLogLevel: Log.Level = dependencies[feature: .logLevel(cat: .default)] let lowestCatLevel: Log.Level = categories .reduce(into: [], { result, next in - guard dependencies[feature: .logLevel(cat: next)] != .default else { - result.append(defaultLogLevel) - return - } + let explicitLevel: Log.Level = dependencies[feature: .logLevel(cat: next)] + let groupLevel: Log.Level? = next.group.map { dependencies[feature: .logLevel(group: $0)] } - result.append(dependencies[feature: .logLevel(cat: next)]) + switch (explicitLevel, groupLevel) { + case (.default, .none): result.append(defaultLogLevel) + case (.default, .default): result.append(defaultLogLevel) + case (_, .none): result.append(explicitLevel) + case (_, .some(let groupLevel)): result.append(min(explicitLevel, groupLevel)) + } }) .min() .defaulting(to: defaultLogLevel) @@ -678,16 +722,15 @@ public actor Logger: LoggerType { /// No point doubling up but we want to allow categories which match the `primaryPrefix` so that we /// have a mechanism for providing a different "default" log level for a specific target .filter { $0.rawValue != primaryPrefix } - .map { "\($0.customPrefix)\($0.rawValue)\($0.customSuffix)" } + .map { "\($0.group.map { "\($0.name):" } ?? "")\($0.rawValue)\($0.customSuffix)" } ) .joined(separator: ", ") - return "[\(prefixes)] " + return "[\(prefixes)]" }() /// Clean up the message if needed (replace double periods with single, trim whitespace, truncate pubkeys) - let logMessage: String = logPrefix - .appending(message) + let cleanedMessage: String = message .replacingOccurrences(of: "...", with: "|||") .replacingOccurrences(of: "..", with: ".") .replacingOccurrences(of: "|||", with: "...") @@ -709,16 +752,17 @@ public actor Logger: LoggerType { return updatedText } - + let ddLogMessage: String = "\(logPrefix) ".appending(cleanedMessage) + let consoleLogMessage: String = "\(logPrefix)[\(level)] ".appending(cleanedMessage) switch level { case .off, .default: return - case .verbose: DDLogVerbose("💙 \(logMessage)", file: file, function: function, line: line) - case .debug: DDLogDebug("💚 \(logMessage)", file: file, function: function, line: line) - case .info: DDLogInfo("💛 \(logMessage)", file: file, function: function, line: line) - case .warn: DDLogWarn("🧡 \(logMessage)", file: file, function: function, line: line) - case .error: DDLogError("❤️ \(logMessage)", file: file, function: function, line: line) - case .critical: DDLogError("🔥 \(logMessage)", file: file, function: function, line: line) + case .verbose: DDLogVerbose("💙 \(ddLogMessage)", file: file, function: function, line: line) + case .debug: DDLogDebug("💚 \(ddLogMessage)", file: file, function: function, line: line) + case .info: DDLogInfo("💛 \(ddLogMessage)", file: file, function: function, line: line) + case .warn: DDLogWarn("🧡 \(ddLogMessage)", file: file, function: function, line: line) + case .error: DDLogError("❤️ \(ddLogMessage)", file: file, function: function, line: line) + case .critical: DDLogError("🔥 \(ddLogMessage)", file: file, function: function, line: line) } let mainCategory: String = (categories.first?.rawValue ?? "General") @@ -730,7 +774,7 @@ public actor Logger: LoggerType { } #if DEBUG - systemLogger?.log(level, logMessage) + systemLogger?.log(level, consoleLogMessage) #endif } } @@ -859,6 +903,7 @@ extension Log.Level: FeatureOption { public struct AllLoggingCategories: FeatureOption { public static let allCases: [AllLoggingCategories] = [] + @ThreadSafeObject private static var registeredGroupDefaults: Set = [] @ThreadSafeObject private static var registeredCategoryDefaults: Set = [] // MARK: - Initialization @@ -866,7 +911,21 @@ public struct AllLoggingCategories: FeatureOption { public let rawValue: Int public init(rawValue: Int) { - self.rawValue = -1 // `0` is a protected value so can't use it + _ = Log.Category.default // Access the `default` log category to ensure it exists + self.rawValue = -1 // `0` is a protected value so can't use it + } + + fileprivate static func register(group: Log.Group) { + guard + !registeredGroupDefaults.contains(where: { existingGroup in + /// **Note:** We only want to use the `rawValue` to distinguish between logging categories + /// as the `defaultLevel` can change via the dev settings and any additional metadata could + /// be file/class specific + group.name == existingGroup.name + }) + else { return } + + _registeredGroupDefaults.performUpdate { $0.inserting(group) } } fileprivate static func register(category: Log.Category) { @@ -882,10 +941,21 @@ public struct AllLoggingCategories: FeatureOption { _registeredCategoryDefaults.performUpdate { $0.inserting(category) } } + fileprivate static func existingGroup(for name: String) -> Log.Group? { + return AllLoggingCategories.registeredGroupDefaults.first(where: { $0.name == name }) + } + fileprivate static func existingCategory(for cat: String) -> Log.Category? { return AllLoggingCategories.registeredCategoryDefaults.first(where: { $0.rawValue == cat }) } + public func currentValues(using dependencies: Dependencies) -> [Log.Group: Log.Level] { + return AllLoggingCategories.registeredGroupDefaults + .reduce(into: [:]) { result, group in + result[group] = dependencies[feature: .logLevel(group: group)] + } + } + public func currentValues(using dependencies: Dependencies) -> [Log.Category: Log.Level] { return AllLoggingCategories.registeredCategoryDefaults .reduce(into: [:]) { result, cat in diff --git a/SessionUtilitiesKit/General/String+Utilities.swift b/SessionUtilitiesKit/General/String+Utilities.swift index bbe0503875..59509afca4 100644 --- a/SessionUtilitiesKit/General/String+Utilities.swift +++ b/SessionUtilitiesKit/General/String+Utilities.swift @@ -144,10 +144,10 @@ public extension String { dateComponentsFormatter.unitsStyle = .full return dateComponentsFormatter.string(from: duration) ?? "" - case .twoUnits: // 2 units, no localization, short version e.g 1w 1d + case .twoUnits: // 2 units, no localization, short version e.g 1w 1d, remove trailing 0's e.g 12h 0m -> 12h dateComponentsFormatter.maximumUnitCount = 2 dateComponentsFormatter.unitsStyle = .abbreviated - dateComponentsFormatter.zeroFormattingBehavior = .dropLeading + dateComponentsFormatter.zeroFormattingBehavior = .dropAll calendar.locale = Locale(identifier: "en-US") dateComponentsFormatter.calendar = calendar return dateComponentsFormatter.string(from: duration) ?? "" @@ -223,15 +223,6 @@ public extension String { return self.trimmingCharacters(in: .whitespacesAndNewlines) } - /// iOS strips anything that looks like a printf formatting character from the notification body, so if we want to dispay a literal "%" in - /// a notification it must be escaped. - /// - /// See https://developer.apple.com/documentation/usernotifications/unnotificationcontent/body for - /// more details. - var filteredForNotification: String { - self.replacingOccurrences(of: "%", with: "%%") - } - private var hasExcessiveDiacriticals: Bool { for char in self.enumerated() { let scalarCount = String(char.element).unicodeScalars.count diff --git a/SessionUtilitiesKit/LibSession/LibSession.swift b/SessionUtilitiesKit/LibSession/LibSession.swift index 916686f2f8..e787e7b4db 100644 --- a/SessionUtilitiesKit/LibSession/LibSession.swift +++ b/SessionUtilitiesKit/LibSession/LibSession.swift @@ -17,6 +17,10 @@ public extension Log.Category { static let libSession: Log.Category = .create("LibSession", defaultLevel: .info) } +public extension Log.Group { + static let libSession: Log.Group = .create("libSession", defaultLevel: .info) +} + // MARK: - Logging extension LibSession { @@ -29,7 +33,13 @@ extension LibSession { ObservationBuilder.observe(.featureGroup(.allLogLevels), using: dependencies) { [dependencies] _ in let currentLogLevels: [Log.Category: Log.Level] = dependencies[feature: .allLogLevels] .currentValues(using: dependencies) - let cDefaultLevel: LOG_LEVEL = (currentLogLevels[.default]?.libSession ?? LOG_LEVEL_OFF) + let currentGroupLogLevels: [Log.Group: Log.Level] = dependencies[feature: .allLogLevels] + .currentValues(using: dependencies) + let targetDefault: Log.Level? = min( + (currentLogLevels[.default] ?? .off), + (currentGroupLogLevels[.libSession] ?? .off) + ) + let cDefaultLevel: LOG_LEVEL = (targetDefault?.libSession ?? LOG_LEVEL_OFF) session_logger_set_level_default(cDefaultLevel) session_logger_reset_level(cDefaultLevel) @@ -57,20 +67,45 @@ extension LibSession { DispatchQueue.global(qos: .background).async { /// Logs from libSession come through in the format: /// `[yyyy-MM-dd hh:mm:ss] [+{lifetime}s] [{cat}:{lvl}|log.hpp:{line}] {message}` - /// We want to remove the extra data because it doesn't help the logs + /// + /// We want to simplify the message because our logging already includes category and timestamp information: + /// `[+{lifetime}s] {message}` let processedMessage: String = { - let logParts: [String] = msg.components(separatedBy: "] ") + let trimmedMsg = msg.trimmingCharacters(in: .whitespacesAndNewlines) - guard logParts.count == 4 else { return msg.trimmingCharacters(in: .whitespacesAndNewlines) } + guard + let timestampRegex: NSRegularExpression = LibSession.timestampRegex, + let messageStartRegex: NSRegularExpression = LibSession.messageStartRegex + else { return trimmedMsg } - let message: String = String(logParts[3]).trimmingCharacters(in: .whitespacesAndNewlines) + let fullRange = NSRange(trimmedMsg.startIndex..(_ value: T?, forKey key: ObservableKey) { addEvent(ObservedEvent(key: key, value: value)) } } +// MARK: - LoggingDatabaseRecord + +public enum LoggingDatabaseRecordContext { + /// This `TaskLocal` variable is set and accessible within the context of a single `Task` and allows any code running within + /// the task to access the isntance without running into threading issues or needing to manage multiple instances + @TaskLocal + public static var suppressLogs: Bool? +} + +public protocol LoggingDatabaseRecord { + func logDeletion() + static func logDeletion() +} + // MARK: - ObservationContext public enum ObservationContext { @@ -153,10 +167,6 @@ public extension PersistableRecord { func upsert(_ db: ObservingDatabase) throws { return try self.upsert(db.originalDb) } - - func save(_ db: ObservingDatabase, onConflict conflictResolution: Database.ConflictResolution? = nil) throws { - try self.save(db.originalDb, onConflict: conflictResolution) - } } public extension SQLRequest { @@ -182,16 +192,12 @@ public extension MutablePersistableRecord { try self.update(db.originalDb, onConflict: conflictResolution) } - mutating func save(_ db: ObservingDatabase, onConflict conflictResolution: Database.ConflictResolution? = nil) throws { - try self.save(db.originalDb, onConflict: conflictResolution) - } - - func saved(_ db: ObservingDatabase, onConflict conflictResolution: Database.ConflictResolution? = nil) throws -> Self { - return try self.saved(db.originalDb, onConflict: conflictResolution) - } - @discardableResult func delete(_ db: ObservingDatabase) throws -> Bool { + if LoggingDatabaseRecordContext.suppressLogs != true { + (self as? LoggingDatabaseRecord)?.logDeletion() + } + return try self.delete(db.originalDb) } } @@ -239,6 +245,10 @@ public extension QueryInterfaceRequest { @discardableResult func deleteAll(_ db: ObservingDatabase) throws -> Int { + if LoggingDatabaseRecordContext.suppressLogs != true { + (RowDecoder.self as? LoggingDatabaseRecord.Type)?.logDeletion() + } + return try self.deleteAll(db.originalDb) } } @@ -306,6 +316,10 @@ public extension TableRecord { @discardableResult static func deleteAll(_ db: ObservingDatabase) throws -> Int { + if LoggingDatabaseRecordContext.suppressLogs != true { + (self as? LoggingDatabaseRecord.Type)?.logDeletion() + } + return try self.deleteAll(db.originalDb) } } @@ -317,11 +331,19 @@ public extension TableRecord where Self: Identifiable, Self.ID: DatabaseValueCon @discardableResult static func deleteAll(_ db: ObservingDatabase, ids: some Collection) throws -> Int { + if LoggingDatabaseRecordContext.suppressLogs != true { + (self as? LoggingDatabaseRecord.Type)?.logDeletion() + } + return try self.deleteAll(db.originalDb, ids: ids) } @discardableResult static func deleteOne(_ db: ObservingDatabase, id: Self.ID) throws -> Bool { + if LoggingDatabaseRecordContext.suppressLogs != true { + (self as? LoggingDatabaseRecord.Type)?.logDeletion() + } + return try self.deleteOne(db.originalDb, id: id) } } diff --git a/SessionUtilitiesKit/Media/Data+Image.swift b/SessionUtilitiesKit/Media/Data+Image.swift deleted file mode 100644 index 028a20930b..0000000000 --- a/SessionUtilitiesKit/Media/Data+Image.swift +++ /dev/null @@ -1,369 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import UIKit -import AVKit -import ImageIO -import UniformTypeIdentifiers - -public extension Data { - private struct ImageDimensions { - let pixelSize: CGSize - let depthBytes: CGFloat - } - - var isValidImage: Bool { - let imageFormat: ImageFormat = self.guessedImageFormat - let isAnimated: Bool = (imageFormat == .gif) - let maxFileSize: UInt = (isAnimated ? - MediaUtils.maxFileSizeAnimatedImage : - MediaUtils.maxFileSizeImage - ) - - return ( - count < maxFileSize && - isValidImage(type: nil, format: imageFormat) && - hasValidImageDimensions(isAnimated: isAnimated) - ) - } - - var guessedImageFormat: ImageFormat { - let twoBytesLength: Int = 2 - - guard count > twoBytesLength else { return .unknown } - - var bytes: [UInt8] = [UInt8](repeating: 0, count: twoBytesLength) - self.copyBytes(to: &bytes, from: (self.startIndex.. bufferLength else { return false } - - var bytes: [UInt8] = [UInt8](repeating: 0, count: bufferLength) - self.copyBytes(to: &bytes, from: (self.startIndex.. 0 && width < maxValidSize && height > 0 && height < maxValidSize) - } - - var sizeForWebpData: CGSize { - guard - guessedImageFormat == .webp, - let source: CGImageSource = CGImageSourceCreateWithData(self as CFData, nil) - else { return .zero } - - // Check if there's at least one image - let count: Int = CGImageSourceGetCount(source) - guard count > 0 else { - return .zero - } - - // Get properties of the first frame - guard let properties: [CFString: Any] = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any] else { - return .zero - } - - // Try to get dimensions from properties - if - let width: Int = properties[kCGImagePropertyPixelWidth] as? Int, - let height: Int = properties[kCGImagePropertyPixelHeight] as? Int, - width > 0, - height > 0 - { - return CGSize(width: width, height: height) - } - - // If we can't get dimensions from properties, try creating an image - if let image: CGImage = CGImageSourceCreateImageAtIndex(source, 0, nil) { - return CGSize(width: image.width, height: image.height) - } - - return .zero - } - - // MARK: - Initialization - - init?(validImageDataAt path: String, type: UTType? = nil, using dependencies: Dependencies) throws { - let fileUrl: URL = URL(fileURLWithPath: path) - - guard - let type: UTType = type, - let fileSize: UInt64 = dependencies[singleton: .fileManager].fileSize(of: path), - fileSize <= SNUtilitiesKit.maxFileSize, - (type.isImage || type.isAnimated) - else { return nil } - - self = try Data(contentsOf: fileUrl, options: [.dataReadingMapped]) - } - - // MARK: - Functions - - func hasValidImageDimensions(isAnimated: Bool) -> Bool { - guard - let dataPtr: CFData = CFDataCreate(kCFAllocatorDefault, self.bytes, self.count), - let imageSource = CGImageSourceCreateWithData(dataPtr, nil) - else { return false } - - return Data.hasValidImageDimension(source: imageSource, isAnimated: isAnimated) - } - - func isValidImage(type: UTType?) -> Bool { - return isValidImage(type: type, format: self.guessedImageFormat) - } - - func isValidImage(type: UTType?, format: ImageFormat) -> Bool { - // Don't trust the file extension; iOS (e.g. UIKit, Core Graphics) will happily - // load a .gif with a .png file extension - // - // Instead, use the "magic numbers" in the file data to determine the image format - // - // If the image has a declared MIME type, ensure that agrees with the - // deduced image format - switch format { - case .unknown: return false - case .png: return (type == nil || type == .png) - case .jpeg: return (type == nil || type == .jpeg) - - case .gif: - guard hasValidGifSize else { return false } - - return (type == nil || type == .gif) - - case .tiff: return (type == nil || type == .tiff || type == .xTiff) - case .bmp: return (type == nil || type == .bmp || type == .xWinBpm) - case .webp: return (type == nil || type == .webP) - } - } - - static func isValidImage(at path: String, type: UTType? = nil, using dependencies: Dependencies) -> Bool { - guard let data: Data = try? Data(validImageDataAt: path, type: type, using: dependencies) else { - return false - } - - return data.hasValidImageDimensions(isAnimated: type?.isAnimated == true) - } - - static func hasValidImageDimension(source: CGImageSource, isAnimated: Bool) -> Bool { - guard let dimensions: ImageDimensions = imageDimensions(source: source) else { return false } - - // We only support (A)RGB and (A)Grayscale, so worst case is 4. - let worseCastComponentsPerPixel: CGFloat = 4 - let bytesPerPixel: CGFloat = (worseCastComponentsPerPixel * dimensions.depthBytes) - let expectedBytePerPixel: CGFloat = 4 - let maxValidImageDimension: CGFloat = CGFloat(isAnimated ? - MediaUtils.maxAnimatedImageDimensions : - MediaUtils.maxStillImageDimensions - ) - let maxBytes: CGFloat = (maxValidImageDimension * maxValidImageDimension * expectedBytePerPixel) - let actualBytes: CGFloat = (dimensions.pixelSize.width * dimensions.pixelSize.height * bytesPerPixel) - - return (actualBytes <= maxBytes) - } - - static func hasAlpha(forValidImageFilePath filePath: String) -> Bool { - let fileUrl: URL = URL(fileURLWithPath: filePath) - let options: [String: Any] = [kCGImageSourceShouldCache as String: NSNumber(booleanLiteral: false)] - - guard - let imageSource = CGImageSourceCreateWithURL(fileUrl as CFURL, nil), - let properties: [CFString: Any] = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, options as CFDictionary) as? [CFString: Any], - let hasAlpha: Bool = properties[kCGImagePropertyHasAlpha] as? Bool - else { return false } - - return hasAlpha - } - - private static func imageDimensions(source: CGImageSource) -> ImageDimensions? { - guard - let properties: [CFString: Any] = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any], - let width: Double = properties[kCGImagePropertyPixelWidth] as? Double, - let height: Double = properties[kCGImagePropertyPixelHeight] as? Double, - // The number of bits in each color sample of each pixel. The value of this key is a CFNumberRef - let depthBits: UInt = properties[kCGImagePropertyDepth] as? UInt - else { return nil } - - // This should usually be 1. - let depthBytes: CGFloat = ceil(CGFloat(depthBits) / 8.0) - - // The color model of the image such as "RGB", "CMYK", "Gray", or "Lab" - // The value of this key is CFStringRef - guard - let colorModel = properties[kCGImagePropertyColorModel] as? String, - ( - colorModel != (kCGImagePropertyColorModelRGB as String) || - colorModel != (kCGImagePropertyColorModelGray as String) - ) - else { return nil } - - return ImageDimensions(pixelSize: CGSize(width: width, height: height), depthBytes: depthBytes) - } - - static func mediaSize( - for path: String, - type: UTType?, - mimeType: String?, - sourceFilename: String?, - using dependencies: Dependencies - ) -> CGSize { - let fileUrl: URL = URL(fileURLWithPath: path) - let maybePixelSize: CGSize? = extractSize( - from: path, - type: type, - mimeType: mimeType, - sourceFilename: sourceFilename, - using: dependencies - ) - - guard let pixelSize: CGSize = maybePixelSize else { return .zero } - - // WebP and videos shouldn't have orientations so no need for any logic to rotate the size - switch (type, type?.isVideo, type?.isAnimated) { - case (.webP, _, _), (_, true, _), (_, _, true): return pixelSize - default: break - } - - // With CGImageSource we avoid loading the whole image into memory. - let options: [String: Any] = [kCGImageSourceShouldCache as String: NSNumber(booleanLiteral: false)] - - guard - let imageSource = CGImageSourceCreateWithURL(fileUrl as CFURL, nil), - let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, options as CFDictionary) as? [AnyHashable: Any], - let width: CGFloat = properties[kCGImagePropertyPixelWidth as String] as? CGFloat, - let height: CGFloat = properties[kCGImagePropertyPixelHeight as String] as? CGFloat - else { return .zero } - - guard - let rawCgOrientation: UInt32 = properties[kCGImagePropertyOrientation] as? UInt32, - let cgOrientation: CGImagePropertyOrientation = CGImagePropertyOrientation(rawValue: rawCgOrientation) - else { - return CGSize(width: width, height: height) - } - - return apply( - orientation: UIImage.Orientation(cgOrientation), - to: CGSize(width: width, height: height) - ) - } - - private static func apply(orientation: UIImage.Orientation, to imageSize: CGSize) -> CGSize { - switch orientation { - case .up, .upMirrored, .down, .downMirrored: return imageSize - case .leftMirrored, .left, .rightMirrored, .right: - return CGSize(width: imageSize.height, height: imageSize.width) - - @unknown default: return imageSize - } - } - - private static func extractSize( - from path: String, - type: UTType?, - mimeType: String?, - sourceFilename: String?, - using dependencies: Dependencies - ) -> CGSize? { - let fileUrl: URL = URL(fileURLWithPath: path) - - switch (type, type?.isVideo) { - case (.webP, _): - // Need to custom handle WebP images - guard let targetData: Data = try? Data(contentsOf: fileUrl, options: [.dataReadingMapped]) else { - return nil - } - - let imageSize: CGSize = targetData.sizeForWebpData - - guard imageSize.width > 0, imageSize.height > 0 else { return nil } - - return imageSize - - case (_, true): - // Videos don't have the same metadata as images so also need custom handling - let assetInfo: (asset: AVURLAsset, cleanup: () -> Void)? = AVURLAsset.asset( - for: path, - mimeType: mimeType, - sourceFilename: sourceFilename, - using: dependencies - ) - - guard - let asset: AVURLAsset = assetInfo?.asset, - let track: AVAssetTrack = asset.tracks(withMediaType: .video).first - else { return nil } - - let size: CGSize = track.naturalSize - let transformedSize: CGSize = size.applying(track.preferredTransform) - let videoSize: CGSize = CGSize( - width: abs(transformedSize.width), - height: abs(transformedSize.height) - ) - - guard videoSize.width > 0, videoSize.height > 0 else { return nil } - - return videoSize - - default: - // Otherwise use our custom code - guard - let imageSource = CGImageSourceCreateWithURL(fileUrl as CFURL, nil), - let dimensions: ImageDimensions = imageDimensions(source: imageSource), - dimensions.pixelSize.width > 0, - dimensions.pixelSize.height > 0, - dimensions.depthBytes > 0 - else { return nil } - - return dimensions.pixelSize - } - } -} - -private extension UIImage.Orientation { - init(_ cgOrientation: CGImagePropertyOrientation) { - switch cgOrientation { - case .up: self = .up - case .upMirrored: self = .upMirrored - case .down: self = .down - case .downMirrored: self = .downMirrored - case .left: self = .left - case .leftMirrored: self = .leftMirrored - case .right: self = .right - case .rightMirrored: self = .rightMirrored - } - } -} diff --git a/SessionUtilitiesKit/Media/DataSource.swift b/SessionUtilitiesKit/Media/DataSource.swift index 31730867dc..84fcddb595 100644 --- a/SessionUtilitiesKit/Media/DataSource.swift +++ b/SessionUtilitiesKit/Media/DataSource.swift @@ -1,11 +1,14 @@ // Copyright © 2024 Rangeproof Pty Ltd. All rights reserved. import Foundation +import CoreGraphics +import ImageIO import UniformTypeIdentifiers // MARK: - DataSource public protocol DataSource: Equatable { + var dependencies: Dependencies { get } var data: Data { get } var dataUrl: URL? { get } @@ -20,17 +23,55 @@ public protocol DataSource: Equatable { var dataLength: Int { get } var sourceFilename: String? { get set } + var fileExtension: String { get } var mimeType: String? { get } var shouldDeleteOnDeinit: Bool { get } - var isValidImage: Bool { get } - var isValidVideo: Bool { get } - // MARK: - Functions func write(to path: String) throws } +public extension DataSource { + var imageSize: CGSize? { + let type: UTType? = UTType(sessionFileExtension: fileExtension) + let options: CFDictionary = [ + kCGImageSourceShouldCache: false, + kCGImageSourceShouldCacheImmediately: false + ] as CFDictionary + let maybeSource: CGImageSource? = { + switch self.dataPathIfOnDisk { + case .some(let path): return CGImageSourceCreateWithURL(URL(fileURLWithPath: path) as CFURL, options) + case .none: return CGImageSourceCreateWithData(data as CFData, options) + } + }() + + guard let source: CGImageSource = maybeSource else { return nil } + + return MediaUtils.MediaMetadata(source: source)?.pixelSize + } + + var isValidImage: Bool { + let type: UTType? = UTType(sessionFileExtension: fileExtension) + + switch self.dataPathIfOnDisk { + case .some(let path): return MediaUtils.isValidImage(at: path, type: type, using: dependencies) + case .none: return MediaUtils.isValidImage(data: data, type: type) + } + } + + var isValidVideo: Bool { + guard let dataUrl: URL = self.dataUrl else { return false } + + return MediaUtils.isValidVideo( + path: dataUrl.path, + mimeType: mimeType, + sourceFilename: sourceFilename, + using: dependencies + ) + } +} + // MARK: - DataSourceValue public class DataSourceValue: DataSource { @@ -38,10 +79,10 @@ public class DataSourceValue: DataSource { return DataSourceValue(data: Data(), fileExtension: UTType.fileExtensionText, using: dependencies) } - private let dependencies: Dependencies + public let dependencies: Dependencies public var data: Data public var sourceFilename: String? - var fileExtension: String + public var fileExtension: String var cachedFilePath: String? public var shouldDeleteOnDeinit: Bool @@ -68,28 +109,6 @@ public class DataSourceValue: DataSource { } } - public var isValidImage: Bool { - guard let dataPath: String = self.dataPathIfOnDisk else { - return self.data.isValidImage - } - - // if ows_isValidImage is given a file path, it will - // avoid loading most of the data into memory, which - // is considerably more performant, so try to do that. - return Data.isValidImage(at: dataPath, type: UTType(sessionFileExtension: fileExtension), using: dependencies) - } - - public var isValidVideo: Bool { - guard let dataUrl: URL = self.dataUrl else { return false } - - return MediaUtils.isValidVideo( - path: dataUrl.path, - mimeType: UTType.sessionMimeType(for: fileExtension), - sourceFilename: sourceFilename, - using: dependencies - ) - } - // MARK: - Initialization public init(data: Data, fileExtension: String, using dependencies: Dependencies) { @@ -157,9 +176,10 @@ public class DataSourceValue: DataSource { // MARK: - DataSourcePath public class DataSourcePath: DataSource { - private let dependencies: Dependencies + public let dependencies: Dependencies public var filePath: String public var sourceFilename: String? + public var fileExtension: String { URL(fileURLWithPath: filePath).pathExtension } var cachedData: Data? var cachedDataLength: Int? public var shouldDeleteOnDeinit: Bool @@ -197,32 +217,6 @@ public class DataSourcePath: DataSource { } public var mimeType: String? { UTType.sessionMimeType(for: URL(fileURLWithPath: filePath).pathExtension) } - - public var isValidImage: Bool { - guard let dataPath: String = self.dataPathIfOnDisk else { - return self.data.isValidImage - } - - // if ows_isValidImage is given a file path, it will - // avoid loading most of the data into memory, which - // is considerably more performant, so try to do that. - return Data.isValidImage( - at: dataPath, - type: UTType(sessionFileExtension: URL(fileURLWithPath: filePath).pathExtension), - using: dependencies - ) - } - - public var isValidVideo: Bool { - guard let dataUrl: URL = self.dataUrl else { return false } - - return MediaUtils.isValidVideo( - path: dataUrl.path, - mimeType: mimeType, - sourceFilename: sourceFilename, - using: dependencies - ) - } // MARK: - Initialization diff --git a/SessionUtilitiesKit/Media/MediaUtils.swift b/SessionUtilitiesKit/Media/MediaUtils.swift index dee568b0ed..ae3dec2938 100644 --- a/SessionUtilitiesKit/Media/MediaUtils.swift +++ b/SessionUtilitiesKit/Media/MediaUtils.swift @@ -18,16 +18,130 @@ public enum MediaError: Error { // MARK: - MediaUtils public enum MediaUtils { - public static var maxFileSizeAnimatedImage: UInt { SNUtilitiesKit.maxFileSize } - public static var maxFileSizeImage: UInt { SNUtilitiesKit.maxFileSize } - public static var maxFileSizeVideo: UInt { SNUtilitiesKit.maxFileSize } - public static var maxFileSizeAudio: UInt { SNUtilitiesKit.maxFileSize } - public static var maxFileSizeGeneric: UInt { SNUtilitiesKit.maxFileSize } - - public static let maxAnimatedImageDimensions: UInt = 1 * 1024 - public static let maxStillImageDimensions: UInt = 8 * 1024 - public static let maxVideoDimensions: CGFloat = 3 * 1024 - + public struct MediaMetadata { + public let pixelSize: CGSize + public let frameCount: Int + public let depthBytes: CGFloat? + public let hasAlpha: Bool? + public let colorModel: String? + public let orientation: UIImage.Orientation? + + public var hasValidPixelSize: Bool { + pixelSize.width > 0 && + pixelSize.width < CGFloat(SNUtilitiesKit.maxValidImageDimension) && + pixelSize.height > 0 && + pixelSize.height < CGFloat(SNUtilitiesKit.maxValidImageDimension) + } + + // MARK: - Initialization + + public init?(source: CGImageSource) { + let count: Int = CGImageSourceGetCount(source) + + guard + count > 0, + let properties: [CFString: Any] = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any], + let width: Double = properties[kCGImagePropertyPixelWidth] as? Double, + let height: Double = properties[kCGImagePropertyPixelHeight] as? Double + else { return nil } + + self.pixelSize = CGSize(width: width, height: height) + self.frameCount = count + self.depthBytes = { + /// The number of bits in each color sample of each pixel. The value of this key is a CFNumberRef + guard let depthBits: UInt = properties[kCGImagePropertyDepth] as? UInt else { return nil } + + /// This should usually be 1 + return ceil(CGFloat(depthBits) / 8.0) + }() + self.hasAlpha = (properties[kCGImagePropertyHasAlpha] as? Bool) + /// The color model of the image such as "RGB", "CMYK", "Gray", or "Lab", the value of this key is CFStringRef + self.colorModel = (properties[kCGImagePropertyColorModel] as? String) + self.orientation = { + guard + let rawCgOrientation: UInt32 = properties[kCGImagePropertyOrientation] as? UInt32, + let cgOrientation: CGImagePropertyOrientation = CGImagePropertyOrientation(rawValue: rawCgOrientation) + else { return nil } + + return UIImage.Orientation(cgOrientation) + }() + } + + public init( + pixelSize: CGSize, + depthBytes: CGFloat? = nil, + hasAlpha: Bool? = nil, + colorModel: String? = nil, + orientation: UIImage.Orientation? = nil + ) { + self.pixelSize = pixelSize + self.frameCount = 1 + self.depthBytes = depthBytes + self.hasAlpha = hasAlpha + self.colorModel = colorModel + self.orientation = orientation + } + + public init?( + from path: String, + type: UTType?, + mimeType: String?, + sourceFilename: String?, + using dependencies: Dependencies + ) { + /// Videos don't have the same metadata as images so need custom handling + guard type?.isVideo != true else { + let assetInfo: (asset: AVURLAsset, cleanup: () -> Void)? = AVURLAsset.asset( + for: path, + mimeType: mimeType, + sourceFilename: sourceFilename, + using: dependencies + ) + + guard + let asset: AVURLAsset = assetInfo?.asset, + let track: AVAssetTrack = asset.tracks(withMediaType: .video).first + else { return nil } + + let size: CGSize = track.naturalSize + let transformedSize: CGSize = size.applying(track.preferredTransform) + let videoSize: CGSize = CGSize( + width: abs(transformedSize.width), + height: abs(transformedSize.height) + ) + + guard videoSize.width > 0, videoSize.height > 0 else { return nil } + + self.pixelSize = videoSize + self.frameCount = -1 /// Rather than try to extract the frames, or give it an "incorrect" value, make it explicitly invalid + self.depthBytes = nil + self.hasAlpha = false + self.colorModel = nil + self.orientation = nil + return + } + + guard + let imageSource = CGImageSourceCreateWithURL(URL(fileURLWithPath: path) as CFURL, nil), + let metadata: MediaMetadata = MediaMetadata(source: imageSource) + else { return nil } + + self = metadata + } + + // MARK: - Functions + + public func apply(orientation: UIImage.Orientation) -> CGSize { + switch orientation { + case .up, .upMirrored, .down, .downMirrored: return pixelSize + case .leftMirrored, .left, .rightMirrored, .right: + return CGSize(width: pixelSize.height, height: pixelSize.width) + + @unknown default: return pixelSize + } + } + } + public static func isVideoOfValidContentTypeAndSize(path: String, type: String?, using dependencies: Dependencies) -> Bool { guard dependencies[singleton: .fileManager].fileExists(atPath: path) else { Log.error(.media, "Media file missing.") @@ -37,30 +151,24 @@ public enum MediaUtils { Log.error(.media, "Media file has invalid content type.") return false } - + guard let fileSize: UInt64 = dependencies[singleton: .fileManager].fileSize(of: path) else { Log.error(.media, "Media file has unknown length.") return false } return UInt(fileSize) <= SNUtilitiesKit.maxFileSize } - + public static func isValidVideo(asset: AVURLAsset) -> Bool { var maxTrackSize = CGSize.zero + for track: AVAssetTrack in asset.tracks(withMediaType: .video) { let trackSize: CGSize = track.naturalSize maxTrackSize.width = max(maxTrackSize.width, trackSize.width) maxTrackSize.height = max(maxTrackSize.height, trackSize.height) } - if maxTrackSize.width < 1.0 || maxTrackSize.height < 1.0 { - Log.error(.media, "Invalid video size: \(maxTrackSize)") - return false - } - if maxTrackSize.width > maxVideoDimensions || maxTrackSize.height > maxVideoDimensions { - Log.error(.media, "Invalid video dimensions: \(maxTrackSize)") - return false - } - return true + + return MediaMetadata(pixelSize: maxTrackSize).hasValidPixelSize } /// Use `isValidVideo(asset: AVURLAsset)` if the `AVURLAsset` needs to be generated elsewhere in the code, @@ -80,4 +188,98 @@ public enum MediaUtils { return result } + + public static func isValidImage(data: Data, type: UTType? = nil) -> Bool { + let options: CFDictionary = [ + kCGImageSourceShouldCache: false, + kCGImageSourceShouldCacheImmediately: false + ] as CFDictionary + + guard + data.count < SNUtilitiesKit.maxFileSize, + let type: UTType = type, + (type.isImage || type.isAnimated), + let source: CGImageSource = CGImageSourceCreateWithData(data as CFData, options), + let metadata: MediaMetadata = MediaMetadata(source: source) + else { return false } + + return metadata.hasValidPixelSize + } + + public static func isValidImage(at path: String, type: UTType? = nil, using dependencies: Dependencies) -> Bool { + let options: CFDictionary = [ + kCGImageSourceShouldCache: false, + kCGImageSourceShouldCacheImmediately: false + ] as CFDictionary + + guard + let type: UTType = type, + let fileSize: UInt64 = dependencies[singleton: .fileManager].fileSize(of: path), + fileSize <= SNUtilitiesKit.maxFileSize, + (type.isImage || type.isAnimated), + let source: CGImageSource = CGImageSourceCreateWithURL(URL(fileURLWithPath: path) as CFURL, options), + let metadata: MediaMetadata = MediaMetadata(source: source) + else { return false } + + return metadata.hasValidPixelSize + } + + public static func unrotatedSize( + for path: String, + type: UTType?, + mimeType: String?, + sourceFilename: String?, + using dependencies: Dependencies + ) -> CGSize { + guard + let metadata: MediaMetadata = MediaMetadata( + from: path, + type: type, + mimeType: mimeType, + sourceFilename: sourceFilename, + using: dependencies + ) + else { return .zero } + + /// If the metadata doesn't ahve an orientation then don't rotate the size (WebP and videos shouldn't have orientations) + guard let orientation: UIImage.Orientation = metadata.orientation else { return metadata.pixelSize } + + return metadata.apply(orientation: orientation) + } + + public static func guessedImageFormat(data: Data) -> ImageFormat { + let twoBytesLength: Int = 2 + + guard data.count > twoBytesLength else { return .unknown } + + var bytes: [UInt8] = [UInt8](repeating: 0, count: twoBytesLength) + data.copyBytes(to: &bytes, from: (data.startIndex.. ObservableKey { + ObservableKey("appLifecycle-\(event)", .appLifecycle) + } + + static func databaseLifecycle(_ event: DatabaseLifecycle) -> ObservableKey { + ObservableKey("databaseLifecycle-\(event)", .databaseLifecycle) + } + static func feature(_ key: FeatureConfig) -> ObservableKey { ObservableKey(key.identifier, .feature) } @@ -17,6 +25,26 @@ public extension ObservableKey { } public extension GenericObservableKey { + static let appLifecycle: GenericObservableKey = "appLifecycle" + static let databaseLifecycle: GenericObservableKey = "databaseLifecycle" static let feature: GenericObservableKey = "feature" static let featureGroup: GenericObservableKey = "featureGroup" } + +// MARK: - AppLifecycle + +public enum AppLifecycle: String, Sendable { + case didEnterBackground + case willEnterForeground + case didBecomeActive + case willResignActive + case didReceiveMemoryWarning + case willTerminate +} + +// MARK: - DatabaseLifecycle + +public enum DatabaseLifecycle: String, Sendable { + case suspended + case resumed +} diff --git a/SessionUtilitiesKit/Observations/ObservationManager.swift b/SessionUtilitiesKit/Observations/ObservationManager.swift index 4bff64a107..e0b8388818 100644 --- a/SessionUtilitiesKit/Observations/ObservationManager.swift +++ b/SessionUtilitiesKit/Observations/ObservationManager.swift @@ -1,22 +1,48 @@ // Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. import Foundation +import UIKit.UIApplication // MARK: - Singleton public extension Singleton { static let observationManager: SingletonConfig = Dependencies.create( identifier: "observationManager", - createInstance: { dependencies in ObservationManager() } + createInstance: { dependencies in ObservationManager(using: dependencies) } ) } // MARK: - ObservationManager public actor ObservationManager { + private let lifecycleObservations: [any NSObjectProtocol] private var store: [ObservableKey: [UUID: AsyncStream<(event: ObservedEvent, priority: Priority)>.Continuation]] = [:] + // MARK: - Initialization + + init(using dependencies: Dependencies) { + let notifications: [Notification.Name: AppLifecycle] = [ + UIApplication.didEnterBackgroundNotification: .didEnterBackground, + UIApplication.willEnterForegroundNotification: .willEnterForeground, + UIApplication.didBecomeActiveNotification: .didBecomeActive, + UIApplication.willResignActiveNotification: .willResignActive, + UIApplication.didReceiveMemoryWarningNotification: .didReceiveMemoryWarning, + UIApplication.willTerminateNotification: .willTerminate + ] + + lifecycleObservations = notifications.reduce(into: []) { [dependencies] result, next in + let value: AppLifecycle = next.value + + result.append( + NotificationCenter.default.addObserver(forName: next.key, object: nil, queue: .current) { [dependencies] _ in + dependencies.notifyAsync(key: .appLifecycle(value)) + } + ) + } + } + deinit { + NotificationCenter.default.removeObserver(self) store.values.forEach { $0.values.forEach { $0.finish() } } } diff --git a/SessionUtilitiesKit/Types/CancellationAwareAsyncStream.swift b/SessionUtilitiesKit/Types/CancellationAwareAsyncStream.swift new file mode 100644 index 0000000000..42a56bace5 --- /dev/null +++ b/SessionUtilitiesKit/Types/CancellationAwareAsyncStream.swift @@ -0,0 +1,72 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +// MARK: - CancellationAwareAsyncStream + +public actor CancellationAwareAsyncStream: CancellationAwareStreamType { + private let lifecycleManager: StreamLifecycleManager = StreamLifecycleManager() + + // MARK: - Initialization + + public init() {} + + // MARK: - Functions + + public func send(_ newValue: Element) async { + lifecycleManager.send(newValue) + } + + public func finishCurrentStreams() async { + lifecycleManager.finishCurrentStreams() + } + + public func beforeYield(to continuation: AsyncStream.Continuation) async { + // No-op - no initial value + } + + public func makeTrackedStream() -> AsyncStream { + lifecycleManager.makeTrackedStream().stream + } +} + +// MARK: - CancellationAwareStreamType + +public protocol CancellationAwareStreamType: Actor { + associatedtype Element: Sendable + + func send(_ newValue: Element) async + func finishCurrentStreams() async + + /// This function gets called when a stream is initially created but before the inner stream is created, it shouldn't be called directly + func beforeYield(to continuation: AsyncStream.Continuation) async + + /// This is an internal function which shouldn't be called directly + func makeTrackedStream() async -> AsyncStream +} + +public extension CancellationAwareStreamType { + /// Every time `stream` is accessed it will create a **new** stream + /// + /// **Note:** This is non-isolated so it can be exposed via protocols without `async`, this is safe because `AsyncStream` is + /// thread-safe internally and `Element` is `Sendable` so it's verified to be safe to send concurrently + nonisolated var stream: AsyncStream { + AsyncStream { continuation in + let bridgingTask = Task { + await self.beforeYield(to: continuation) + + let internalStream: AsyncStream = await self.makeTrackedStream() + + for await element in internalStream { + continuation.yield(element) + } + + continuation.finish() + } + + continuation.onTermination = { @Sendable _ in + bridgingTask.cancel() + } + } + } +} diff --git a/SessionUtilitiesKit/Types/CurrentValueAsyncStream.swift b/SessionUtilitiesKit/Types/CurrentValueAsyncStream.swift index 26c6b9245e..b2f9f13e1a 100644 --- a/SessionUtilitiesKit/Types/CurrentValueAsyncStream.swift +++ b/SessionUtilitiesKit/Types/CurrentValueAsyncStream.swift @@ -2,34 +2,34 @@ import Foundation -public actor CurrentValueAsyncStream { - private var _currentValue: Element - private let continuation: AsyncStream.Continuation - public let stream: AsyncStream - - public var currentValue: Element { _currentValue } +public actor CurrentValueAsyncStream: CancellationAwareStreamType { + private let lifecycleManager: StreamLifecycleManager = StreamLifecycleManager() + + /// This is the most recently emitted value + public private(set) var currentValue: Element // MARK: - Initialization public init(_ initialValue: Element) { - self._currentValue = initialValue - - /// We use `.bufferingNewest(1)` to ensure that the stream always holds the most recent value. When a new iterator is - /// created for the stream, it will receive this buffered value first. - let (stream, continuation) = AsyncStream.makeStream(of: Element.self, bufferingPolicy: .bufferingNewest(1)) - self.stream = stream - self.continuation = continuation - self.continuation.yield(initialValue) + self.currentValue = initialValue } // MARK: - Functions - public func send(_ newValue: Element) { - _currentValue = newValue - continuation.yield(newValue) + public func send(_ newValue: Element) async { + currentValue = newValue + lifecycleManager.send(newValue) } - public func finish() { - continuation.finish() + public func finishCurrentStreams() async { + lifecycleManager.finishCurrentStreams() + } + + public func beforeYield(to continuation: AsyncStream.Continuation) async { + continuation.yield(currentValue) + } + + public func makeTrackedStream() -> AsyncStream { + lifecycleManager.makeTrackedStream().stream } } diff --git a/SessionUtilitiesKit/Types/DocumentPickerHandler.swift b/SessionUtilitiesKit/Types/DocumentPickerHandler.swift new file mode 100644 index 0000000000..4b51e24e43 --- /dev/null +++ b/SessionUtilitiesKit/Types/DocumentPickerHandler.swift @@ -0,0 +1,28 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import UIKit + +public class DocumentPickerHandler: NSObject, UIDocumentPickerDelegate { + private let didPickDocumentsAt: ((UIDocumentPickerViewController, [URL]) -> Void)? + private let wasCancelled: ((UIDocumentPickerViewController) -> Void)? + + // MARK: - Initialization + + public init( + didPickDocumentsAt: ((UIDocumentPickerViewController, [URL]) -> Void)? = nil, + wasCancelled: ((UIDocumentPickerViewController) -> Void)? = nil + ) { + self.didPickDocumentsAt = didPickDocumentsAt + self.wasCancelled = wasCancelled + } + + // MARK: - UIDocumentPickerDelegate + + public func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { + didPickDocumentsAt?(controller, urls) + } + + public func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) { + wasCancelled?(controller) + } +} diff --git a/SessionUtilitiesKit/Types/StreamLifecycleManager.swift b/SessionUtilitiesKit/Types/StreamLifecycleManager.swift new file mode 100644 index 0000000000..444e431525 --- /dev/null +++ b/SessionUtilitiesKit/Types/StreamLifecycleManager.swift @@ -0,0 +1,61 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public final class StreamLifecycleManager: @unchecked Sendable { + private let lock: NSLock = NSLock() + private var continuations: [UUID: AsyncStream.Continuation] = [:] + + // MARK: - Initialization + + public init() {} + + deinit { + finishCurrentStreams() + } + + // MARK: - Functions + + func makeTrackedStream() -> (stream: AsyncStream, id: UUID) { + let (stream, continuation) = AsyncStream.makeStream(of: Element.self) + let id: UUID = UUID() + + lock.withLock { continuations[id] = continuation } + + continuation.onTermination = { @Sendable [self] _ in + self.finishStream(id: id) + } + + return (stream, id) + } + + func send(_ value: Element) { + /// Capture current continuations before sending to avoid deadlocks where yielding could result in a new continuation being + /// added while the lock is held + let currentContinuations: [UUID: AsyncStream.Continuation] = lock.withLock { continuations } + + for continuation in currentContinuations.values { + continuation.yield(value) + } + } + + func finishStream(id: UUID) { + lock.withLock { + if let continuation: AsyncStream.Continuation = continuations.removeValue(forKey: id) { + continuation.finish() + } + } + } + + func finishCurrentStreams() { + let currentContinuations: [UUID: AsyncStream.Continuation] = lock.withLock { + let continuationsToFinish: [UUID: AsyncStream.Continuation] = continuations + continuations.removeAll() + return continuationsToFinish + } + + for continuation in currentContinuations.values { + continuation.finish() + } + } +} diff --git a/SessionUtilitiesKit/Types/UserDefaultsType.swift b/SessionUtilitiesKit/Types/UserDefaultsType.swift index 34e12df7b7..51aaa0ffe3 100644 --- a/SessionUtilitiesKit/Types/UserDefaultsType.swift +++ b/SessionUtilitiesKit/Types/UserDefaultsType.swift @@ -165,12 +165,30 @@ public extension UserDefaults.BoolKey { /// Indicates whether the local notification for token bonus is scheduled static let isSessionNetworkPageNotificationScheduled: UserDefaults.BoolKey = "isSessionNetworkPageNotificationScheduled" + + /// Indicates whether the user visited the Path screen + static let hasVisitedPathScreen: UserDefaults.BoolKey = "hasVisitedPathScreen" + + /// Indicates whether the user changed the app theme + static let hasChangedTheme: UserDefaults.BoolKey = "hasChangedTheme" + + /// Indicates whether the user pressed the donate button + static let hasPressedDonateButton: UserDefaults.BoolKey = "hasPressedDonateButton" + + /// Indicates wheter app has already presented the user the app review prompt dialog + static let didShowAppReviewPrompt: UserDefaults.BoolKey = "didShowAppReviewPrompt" + + /// Idicates whether app review prompt was ignored or no iteraction was done to dismiss it (closed app) + static let didActionAppReviewPrompt: UserDefaults.BoolKey = "didActionAppReviewPrompt" } public extension UserDefaults.DateKey { /// The date/time when the users profile picture was last uploaded to the server (used to rate-limit re-uploading) static let lastProfilePictureUpload: UserDefaults.DateKey = "lastProfilePictureUpload" + /// The date/time when the users profile picture expires on the server + static let profilePictureExpiresDate: UserDefaults.DateKey = "profilePictureExpiresDate" + /// The date/time when any open group last had a successful poll (used as a fallback date/time if the open group hasn't been polled /// this session) static let lastOpen: UserDefaults.DateKey = "lastOpen" @@ -180,6 +198,9 @@ public extension UserDefaults.DateKey { /// The date/time when we received a call pre-offer (used to suppress call notifications which are too old) static let lastCallPreOffer: UserDefaults.DateKey = "lastCallPreOffer" + + /// The date/time when app review prompt will appear again + static let rateAppRetryDate: UserDefaults.DateKey = "rateAppRetryDate" } public extension UserDefaults.DoubleKey { @@ -196,6 +217,9 @@ public extension UserDefaults.IntKey { /// The id of the message that was just shared to static let lastSharedMessageId: UserDefaults.IntKey = "lastSharedMessageId" + + /// The number of attempts made to retry showing of app rating prompt + static let rateAppRetryAttemptCount: UserDefaults.IntKey = "rateAppRetryAttemptCount" } public extension UserDefaults.StringKey { @@ -207,6 +231,9 @@ public extension UserDefaults.StringKey { /// The id of the thread that a message was just shared to static let lastSharedThreadId: UserDefaults.StringKey = "lastSharedThreadId" + + /// The app-icon name of the previously selected app icon disguise + static let lastSelectedAppIconDisguise: UserDefaults.StringKey = "lastSelectedAppIconDisguise" } // MARK: - Keys diff --git a/SessionUtilitiesKit/Utilities/AVURLAsset+Utilities.swift b/SessionUtilitiesKit/Utilities/AVURLAsset+Utilities.swift index 962970910e..4a9b41b448 100644 --- a/SessionUtilitiesKit/Utilities/AVURLAsset+Utilities.swift +++ b/SessionUtilitiesKit/Utilities/AVURLAsset+Utilities.swift @@ -55,7 +55,7 @@ public extension AVURLAsset { finalExtension = fileExtension } - let tmpPath: String = URL(fileURLWithPath: NSTemporaryDirectory()) + let tmpPath: String = URL(fileURLWithPath: dependencies[singleton: .fileManager].temporaryDirectory) .appendingPathComponent(URL(fileURLWithPath: path).lastPathComponent) .appendingPathExtension(finalExtension) .path diff --git a/SessionUtilitiesKitTests/Database/Models/IdentitySpec.swift b/SessionUtilitiesKitTests/Database/Models/IdentitySpec.swift index a6c60c58b8..5101300fc7 100644 --- a/SessionUtilitiesKitTests/Database/Models/IdentitySpec.swift +++ b/SessionUtilitiesKitTests/Database/Models/IdentitySpec.swift @@ -15,9 +15,7 @@ class IdentitySpec: QuickSpec { @TestState var dependencies: TestDependencies! = TestDependencies() @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self - ], + migrations: [_001_SUK_InitialSetupMigration.self], using: dependencies ) diff --git a/SessionUtilitiesKitTests/JobRunner/JobRunnerSpec.swift b/SessionUtilitiesKitTests/JobRunner/JobRunnerSpec.swift index 82b6fe25e2..bfd9d50009 100644 --- a/SessionUtilitiesKitTests/JobRunner/JobRunnerSpec.swift +++ b/SessionUtilitiesKitTests/JobRunner/JobRunnerSpec.swift @@ -47,14 +47,12 @@ class JobRunnerSpec: QuickSpec { } @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), - migrationTargets: [ - SNUtilitiesKit.self + migrations: [ + _001_SUK_InitialSetupMigration.self, + _012_AddJobPriority.self, + _020_AddJobUniqueHash.self ], - using: dependencies, - initialData: { db in - // Migrations add jobs which we don't want so delete them - try Job.deleteAll(db) - } + using: dependencies ) @TestState(singleton: .jobRunner, in: dependencies) var jobRunner: JobRunnerType! = JobRunner( isTestingJobRunner: true, diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift index 2d09e83f81..8a2b9917ad 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift @@ -276,14 +276,14 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC // MARK: - Contents - private func updateContents() { + @MainActor private func updateContents() { updateNavigationBar() updateInputAccessory() } // MARK: - Input Accessory - public func updateInputAccessory() { + @MainActor public func updateInputAccessory() { var currentPageViewController: AttachmentPrepViewController? if pageViewControllers?.count == 1 { @@ -430,7 +430,8 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC animated: animated ) { [weak self] finished in completion?(finished) - self?.updateContents() + + Task { @MainActor [weak self] in self?.updateContents() } } } @@ -524,7 +525,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC // Image editor has no changes. return attachmentItem.attachment } - guard let dstImage = ImageEditorCanvasView.renderForOutput(model: imageEditorModel, transform: imageEditorModel.currentTransform()) else { + guard let dstImage = ImageEditorCanvasView.renderForOutput(model: imageEditorModel, transform: imageEditorModel.currentTransform(), using: dependencies) else { Log.error(.cat, "Could not render for output.") return attachmentItem.attachment } @@ -639,7 +640,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC // MARK: - Session Pro CTA - @discardableResult func showSessionProCTAIfNeeded() -> Bool { + @discardableResult @MainActor func showSessionProCTAIfNeeded() -> Bool { guard dependencies[feature: .sessionProEnabled] && (!isSessionPro) else { return false } @@ -660,7 +661,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC return true } - func showModalForMessagesExceedingCharacterLimit(isSessionPro: Bool) { + @MainActor func showModalForMessagesExceedingCharacterLimit(isSessionPro: Bool) { guard !showSessionProCTAIfNeeded() else { return } self.hideInputAccessoryView() @@ -687,7 +688,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC // MARK: - AttachmentTextToolbarDelegate extension AttachmentApprovalViewController: AttachmentTextToolbarDelegate { - func attachmentTextToolBarDidTapCharacterLimitLabel(_ attachmentTextToolbar: AttachmentTextToolbar) { + @MainActor func attachmentTextToolBarDidTapCharacterLimitLabel(_ attachmentTextToolbar: AttachmentTextToolbar) { guard !showSessionProCTAIfNeeded() else { return } self.hideInputAccessoryView() let confirmationModal: ConfirmationModal = ConfirmationModal( @@ -709,7 +710,7 @@ extension AttachmentApprovalViewController: AttachmentTextToolbarDelegate { present(confirmationModal, animated: true, completion: nil) } - func attachmentTextToolbarDidTapSend(_ attachmentTextToolbar: AttachmentTextToolbar) { + @MainActor func attachmentTextToolbarDidTapSend(_ attachmentTextToolbar: AttachmentTextToolbar) { guard let text = attachmentTextToolbar.text, LibSession.numberOfCharactersLeft( @@ -737,7 +738,7 @@ extension AttachmentApprovalViewController: AttachmentTextToolbarDelegate { ) } - func attachmentTextToolbarDidChange(_ attachmentTextToolbar: AttachmentTextToolbar) { + @MainActor func attachmentTextToolbarDidChange(_ attachmentTextToolbar: AttachmentTextToolbar) { approvalDelegate?.attachmentApproval(self, didChangeMessageText: attachmentTextToolbar.text) } } @@ -745,11 +746,11 @@ extension AttachmentApprovalViewController: AttachmentTextToolbarDelegate { // MARK: - extension AttachmentApprovalViewController: AttachmentPrepViewControllerDelegate { - func prepViewControllerUpdateNavigationBar() { + @MainActor func prepViewControllerUpdateNavigationBar() { updateNavigationBar() } - func prepViewControllerUpdateControls() { + @MainActor func prepViewControllerUpdateControls() { updateInputAccessory() } } @@ -758,11 +759,39 @@ extension AttachmentApprovalViewController: AttachmentPrepViewControllerDelegate extension SignalAttachmentItem: GalleryRailItem { func buildRailItemView(using dependencies: Dependencies) -> UIView { - let imageView = UIImageView() - imageView.image = getThumbnailImage(using: dependencies) - imageView.themeBackgroundColor = .backgroundSecondary + let imageView: SessionImageView = SessionImageView(dataManager: dependencies[singleton: .imageDataManager]) imageView.contentMode = .scaleAspectFill + imageView.themeBackgroundColor = .backgroundSecondary + if let path: String = (attachment.dataSource.dataPathIfOnDisk ?? attachment.dataUrl?.absoluteString) { + let source: ImageDataManager.DataSource = { + /// Can't thumbnail animated images so just load the full file in this case + if attachment.isAnimatedImage { + return .url(URL(fileURLWithPath: path)) + } + + /// Videos have a custom method for generating their thumbnails so use that instead + if attachment.isVideo { + return .videoUrl( + URL(fileURLWithPath: path), + attachment.mimeType, + attachment.sourceFilename, + dependencies[singleton: .attachmentManager] + ) + } + + return .urlThumbnail( + URL(fileURLWithPath: path), + .small, + dependencies[singleton: .attachmentManager] + ) + }() + + Task(priority: .userInitiated) { + await imageView.loadImage(source) + } + } + return imageView } diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentItemCollection.swift b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentItemCollection.swift index dc8c384ae3..b636a474fc 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentItemCollection.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentItemCollection.swift @@ -60,10 +60,6 @@ class SignalAttachmentItem: Equatable { return attachment.captionText } - func getThumbnailImage(using dependencies: Dependencies) -> UIImage? { - return attachment.staticThumbnail(using: dependencies) - } - // MARK: Equatable static func == (lhs: SignalAttachmentItem, rhs: SignalAttachmentItem) -> Bool { diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentPrepViewController.swift b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentPrepViewController.swift index 40ee9e0643..3aee88512b 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentPrepViewController.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentPrepViewController.swift @@ -9,9 +9,8 @@ import SessionMessagingKit import SessionUtilitiesKit protocol AttachmentPrepViewControllerDelegate: AnyObject { - func prepViewControllerUpdateNavigationBar() - - func prepViewControllerUpdateControls() + @MainActor func prepViewControllerUpdateNavigationBar() + @MainActor func prepViewControllerUpdateControls() } // MARK: - @@ -74,7 +73,7 @@ public class AttachmentPrepViewController: OWSViewController { private lazy var imageEditorView: ImageEditorView? = { guard let imageEditorModel = attachmentItem.imageEditorModel else { return nil } - let view: ImageEditorView = ImageEditorView(model: imageEditorModel, delegate: self) + let view: ImageEditorView = ImageEditorView(model: imageEditorModel, delegate: self, using: dependencies) view.translatesAutoresizingMaskIntoConstraints = false guard view.configureSubviews() else { return nil } @@ -201,7 +200,7 @@ public class AttachmentPrepViewController: OWSViewController { ]) if attachment.isImage, let editorView: ImageEditorView = imageEditorView { - let size: CGSize = (attachment.image()?.size ?? CGSize.zero) + let size: CGSize = (attachment.imageSize ?? CGSize.zero) let isPortrait: Bool = (size.height > size.width) NSLayoutConstraint.activate([ diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentTextToolbar.swift b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentTextToolbar.swift index 26204358e0..c516ed9a2a 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentTextToolbar.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentTextToolbar.swift @@ -10,9 +10,9 @@ import Combine let kMaxMessageBodyCharacterCount = 2000 protocol AttachmentTextToolbarDelegate: AnyObject { - func attachmentTextToolbarDidTapSend(_ attachmentTextToolbar: AttachmentTextToolbar) - func attachmentTextToolbarDidChange(_ attachmentTextToolbar: AttachmentTextToolbar) - func attachmentTextToolBarDidTapCharacterLimitLabel(_ attachmentTextToolbar: AttachmentTextToolbar) + @MainActor func attachmentTextToolbarDidTapSend(_ attachmentTextToolbar: AttachmentTextToolbar) + @MainActor func attachmentTextToolbarDidChange(_ attachmentTextToolbar: AttachmentTextToolbar) + @MainActor func attachmentTextToolBarDidTapCharacterLimitLabel(_ attachmentTextToolbar: AttachmentTextToolbar) } // MARK: - @@ -204,17 +204,15 @@ extension AttachmentTextToolbar: InputViewButtonDelegate { } extension AttachmentTextToolbar: InputTextViewDelegate { - func inputTextViewDidChangeSize(_ inputTextView: InputTextView) { + @MainActor func inputTextViewDidChangeSize(_ inputTextView: InputTextView) { invalidateIntrinsicContentSize() self.bottomStackView?.alignment = (inputTextView.contentSize.height > inputTextView.minHeight) ? .top : .center } - func inputTextViewDidChangeContent(_ inputTextView: InputTextView) { + @MainActor func inputTextViewDidChangeContent(_ inputTextView: InputTextView) { updateNumberOfCharactersLeft(text ?? "") delegate?.attachmentTextToolbarDidChange(self) } - func didPasteImageFromPasteboard(_ inputTextView: InputTextView, image: UIImage) { - - } + @MainActor func didPasteImageFromPasteboard(_ inputTextView: InputTextView, image: UIImage) {} } diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorBrushViewController.swift b/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorBrushViewController.swift index b8adcf5717..35a39417a9 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorBrushViewController.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorBrushViewController.swift @@ -63,7 +63,8 @@ public class ImageEditorBrushViewController: OWSViewController { paletteView.delegate = self self.view.addSubview(paletteView) paletteView.center(.vertical, in: self.view, withInset: -(bottomInset / 2)) - paletteView.pin(.trailing, to: .trailing, of: self.view) + paletteView.pin(.trailing, to: .trailing, of: self.view, withInset: -Values.smallSpacing) + paletteView.set(.width, to: Values.gradientPaletteWidth) self.view.isUserInteractionEnabled = true diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorCanvasView.swift b/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorCanvasView.swift index 9a48afaeb0..3fd75252e0 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorCanvasView.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorCanvasView.swift @@ -501,10 +501,12 @@ public class ImageEditorCanvasView: UIView { ] ) let layer = EditorTextLayer(itemId: item.itemId) - layer.string = attributedString - layer.themeForegroundColorForced = .color(item.color.color) - layer.font = CGFont(item.font.fontName as CFString) + // Set as .strings, passing only attributed string does not display text + // `attributedString` is now only used to compute sizes + layer.string = attributedString.string + layer.font = item.font layer.fontSize = fontSize + layer.themeForegroundColorForced = .color(item.color.color) layer.isWrapped = true layer.alignmentMode = CATextLayerAlignmentMode.center // I don't think we need to enable allowsFontSubpixelQuantization @@ -595,16 +597,22 @@ public class ImageEditorCanvasView: UIView { // We render using the transform parameter, not the transform from the model. // This allows this same method to be used for rendering "previews" for the // crop tool and the final output. - public class func renderForOutput(model: ImageEditorModel, transform: ImageEditorTransform) -> UIImage? { - // TODO: Do we want to render off the main thread? - Log.assertOnMainThread() - + @MainActor public class func renderForOutput( + model: ImageEditorModel, + transform: ImageEditorTransform, + using dependencies: Dependencies + ) -> UIImage? { // Render output at same size as source image. let dstSizePixels = transform.outputSizePixels let dstScale: CGFloat = 1.0 // The size is specified in pixels, not in points. let viewSize = dstSizePixels - - let hasAlpha = Data.hasAlpha(forValidImageFilePath: model.srcImagePath) + let hasAlpha: Bool = (MediaUtils.MediaMetadata( + from: model.srcImagePath, + type: nil, + mimeType: nil, + sourceFilename: nil, + using: dependencies + )?.hasAlpha == true) // We use an UIImageView + UIView.renderAsImage() instead of a CGGraphicsContext // Because CALayer.renderInContext() doesn't honor CALayer properties like frame, diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorModel.swift b/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorModel.swift index 8f0478b432..1e6e22dfc1 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorModel.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorModel.swift @@ -70,7 +70,7 @@ public class ImageEditorModel { throw ImageEditorError.invalidInput } - let srcImageSizePixels = Data.mediaSize( + let srcImageSizePixels = MediaUtils.unrotatedSize( for: srcImagePath, type: type, mimeType: nil, @@ -278,10 +278,11 @@ public class ImageEditorModel { // MARK: - Utilities // Returns nil on error. - private class func crop(imagePath: String, unitCropRect: CGRect) -> UIImage? { - // TODO: Do we want to render off the main thread? - Log.assertOnMainThread() - + @MainActor private class func crop( + imagePath: String, + unitCropRect: CGRect, + using dependencies: Dependencies + ) -> UIImage? { guard let srcImage = UIImage(contentsOfFile: imagePath) else { Log.error("[ImageEditorModel] Could not load image") return nil @@ -307,8 +308,13 @@ public class ImageEditorModel { Log.warn("[ImageEditorModel] Empty crop rectangle.") return nil } - - let hasAlpha = Data.hasAlpha(forValidImageFilePath: imagePath) + let hasAlpha: Bool = (MediaUtils.MediaMetadata( + from: imagePath, + type: nil, + mimeType: nil, + sourceFilename: nil, + using: dependencies + )?.hasAlpha == true) UIGraphicsBeginImageContextWithOptions(cropRect.size, !hasAlpha, srcImage.scale) defer { UIGraphicsEndImageContext() } diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorPaletteView.swift b/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorPaletteView.swift index 2d7ec71558..b98469d677 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorPaletteView.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorPaletteView.swift @@ -227,11 +227,13 @@ public class ImageEditorPaletteView: UIView { } addSubview(imageView) // We use an invisible margin to expand the hot area of this control. - let margin: CGFloat = 20 - imageView.pin(.top, to: .top, of: self, withInset: margin) - imageView.pin(.leading, to: .leading, of: self, withInset: -margin) - imageView.pin(.trailing, to: .trailing, of: self, withInset: margin) - imageView.pin(.bottom, to: .bottom, of: self, withInset: -margin) + let verticalMargin: CGFloat = 20 + let horizontalMargin: CGFloat = 8 + + imageView.pin(.top, to: .top, of: self, withInset: verticalMargin) + imageView.pin(.leading, to: .leading, of: self, withInset: -horizontalMargin) + imageView.pin(.trailing, to: .trailing, of: self, withInset: horizontalMargin) + imageView.pin(.bottom, to: .bottom, of: self, withInset: -verticalMargin) imageView.themeBorderColor = .white imageView.layer.borderWidth = 1 diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorTextViewController.swift b/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorTextViewController.swift index 77d8f261bb..4febd32c7b 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorTextViewController.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorTextViewController.swift @@ -190,8 +190,12 @@ public class ImageEditorTextViewController: OWSViewController, VAlignTextViewDel paletteView.delegate = self self.view.addSubview(paletteView) - paletteView.center(.horizontal, in: textView) - paletteView.pin(.trailing, to: .trailing, of: self.view) + paletteView.center(.vertical, in: self.view, withInset: -((bottomInset / 2) + Values.largeSpacing)) + paletteView.pin(.trailing, to: .trailing, of: self.view, withInset: -Values.smallSpacing) + + // Size of gradient image and touchable area + paletteView.set(.width, to: Values.gradientPaletteWidth) + // This will determine the text view's size. paletteView.pin(.leading, to: .trailing, of: textView) diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorView.swift b/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorView.swift index 09a508e811..be0b023167 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorView.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorView.swift @@ -20,15 +20,16 @@ public class ImageEditorView: UIView { weak var delegate: ImageEditorViewDelegate? + private let dependencies: Dependencies private let model: ImageEditorModel - private let canvasView: ImageEditorCanvasView // TODO: We could hang this on the model or make this static // if we wanted more color continuity. private var currentColor = ImageEditorColor.defaultColor() - public required init(model: ImageEditorModel, delegate: ImageEditorViewDelegate) { + public required init(model: ImageEditorModel, delegate: ImageEditorViewDelegate, using dependencies: Dependencies) { + self.dependencies = dependencies self.model = model self.delegate = delegate self.canvasView = ImageEditorCanvasView(model: model) @@ -463,7 +464,7 @@ public class ImageEditorView: UIView { // into the background image without applying the transform (e.g. rotating, etc.), so we // use a default transform. let previewTransform = ImageEditorTransform.defaultTransform(srcImageSizePixels: model.srcImageSizePixels) - guard let previewImage = ImageEditorCanvasView.renderForOutput(model: model, transform: previewTransform) else { + guard let previewImage = ImageEditorCanvasView.renderForOutput(model: model, transform: previewTransform, using: dependencies) else { Log.error("[ImageEditorView] Couldn't generate preview image.") return } diff --git a/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift b/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift index c2e0ab5444..4feb892afb 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift @@ -22,42 +22,6 @@ public class MediaMessageView: UIView { public let mode: Mode public let attachment: SignalAttachment private let disableLinkPreviewImageDownload: Bool - - private lazy var validImageData: Data? = { - guard - attachment.isValidImage, - let dataUrl: URL = attachment.dataUrl, - let imageData: Data = try? Data(contentsOf: dataUrl), ( - ( - attachment.dataType == .gif && - attachment.isAnimatedImage && - imageData.hasValidGifSize - ) || ( - attachment.dataType == .webP && - attachment.isAnimatedImage && - imageData.sizeForWebpData != .zero - ) || ( - imageData.hasValidImageDimensions(isAnimated: false) - ) - ) - else { return nil } - - return imageData - }() - private lazy var validVideoImage: UIImage? = { - if attachment.isVideo { - guard - attachment.isValidVideo, - let image: UIImage = attachment.videoPreview(using: dependencies), - image.size.width > 0, - image.size.height > 0 - else { return nil } - - return image - } - - return nil - }() private lazy var duration: TimeInterval? = attachment.duration(using: dependencies) private var linkPreviewInfo: (url: String, draft: LinkPreviewDraft?)? @@ -146,10 +110,19 @@ public class MediaMessageView: UIView { // Override the image to the correct one if attachment.isImage || attachment.isAnimatedImage { - if let imageData: Data = validImageData, let dataUrl: URL = attachment.dataUrl { + let maybeSource: ImageDataManager.DataSource? = { + guard attachment.isValidImage else { return nil } + + return ( + attachment.dataSource.dataPathIfOnDisk.map { .url(URL(fileURLWithPath: $0)) } ?? + attachment.dataSource.dataUrl.map { .url($0) } + ) + }() + + if let source: ImageDataManager.DataSource = maybeSource { view.layer.minificationFilter = .trilinear view.layer.magnificationFilter = .trilinear - view.loadImage(.data(dataUrl.absoluteString, imageData)) + view.loadImage(source) } else { view.contentMode = .scaleAspectFit @@ -158,10 +131,28 @@ public class MediaMessageView: UIView { } } else if attachment.isVideo { - if let validImage: UIImage = validVideoImage { + let maybeSource: ImageDataManager.DataSource? = { + guard attachment.isValidVideo else { return nil } + + return attachment.dataSource.dataUrl.map { url in + .videoUrl( + url, + attachment.mimeType, + attachment.sourceFilename, + dependencies[singleton: .attachmentManager] + ) + } + }() + + if let source: ImageDataManager.DataSource = maybeSource { view.layer.minificationFilter = .trilinear view.layer.magnificationFilter = .trilinear - view.image = validImage + view.loadImage(source) + } + else { + view.contentMode = .scaleAspectFit + view.image = UIImage(named: "FileLarge")?.withRenderingMode(.alwaysTemplate) + view.themeTintColor = .textPrimary } } else if attachment.isUrl { @@ -331,6 +322,7 @@ public class MediaMessageView: UIView { titleStackView.addArrangedSubview(subtitleLabel) imageView.alpha = 1 + imageView.set(.width, to: .width, of: stackView) imageView.addSubview(fileTypeImageView) // Type-specific configurations @@ -378,12 +370,12 @@ public class MediaMessageView: UIView { let maybeImageSize: CGFloat? = { if attachment.isImage || attachment.isAnimatedImage { - if validImageData != nil { return nil } + guard attachment.isValidImage else { return nil } // If we don't have a valid image then use the 'generic' case } else if attachment.isValidVideo { - if validVideoImage != nil { return nil } + guard attachment.isValidVideo else { return nil } // If we don't have a valid image then use the 'generic' case } @@ -414,12 +406,7 @@ public class MediaMessageView: UIView { ) : stackView.widthAnchor.constraint(lessThanOrEqualTo: widthAnchor) ), - - imageView.widthAnchor.constraint( - equalTo: imageView.heightAnchor, - multiplier: clampedRatio - ), - + (maybeImageSize != nil ? imageView.widthAnchor.constraint(equalToConstant: imageSize) : imageView.widthAnchor.constraint(lessThanOrEqualTo: widthAnchor) @@ -434,11 +421,9 @@ public class MediaMessageView: UIView { equalTo: imageView.centerYAnchor, constant: ceil(imageSize * 0.15) ), - fileTypeImageView.widthAnchor.constraint( - equalTo: fileTypeImageView.heightAnchor, - multiplier: ((fileTypeImageView.image?.size.width ?? 1) / (fileTypeImageView.image?.size.height ?? 1)) - ), - fileTypeImageView.widthAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 0.5), + + fileTypeImageView.widthAnchor.constraint(equalToConstant: imageSize * 0.5), + fileTypeImageView.heightAnchor.constraint(equalToConstant: imageSize * 0.5), loadingView.centerXAnchor.constraint(equalTo: imageView.centerXAnchor), loadingView.centerYAnchor.constraint(equalTo: imageView.centerYAnchor), @@ -446,6 +431,16 @@ public class MediaMessageView: UIView { loadingView.heightAnchor.constraint(equalToConstant: ceil(imageSize / 3)) ]) + if imageView.image?.size == nil { + // Handle `clampedRatio` ratio when image is from data + NSLayoutConstraint.activate([ + imageView.widthAnchor.constraint( + equalTo: imageView.heightAnchor, + multiplier: clampedRatio + ) + ]) + } + // No inset for the text for URLs but there is for all other layouts if !attachment.isUrl { NSLayoutConstraint.activate([ diff --git a/SignalUtilitiesKit/Utilities/AppSetup.swift b/SignalUtilitiesKit/Utilities/AppSetup.swift index c8daacfd89..1a3f2f7558 100644 --- a/SignalUtilitiesKit/Utilities/AppSetup.swift +++ b/SignalUtilitiesKit/Utilities/AppSetup.swift @@ -3,14 +3,13 @@ import Foundation import GRDB import SessionUIKit -import SessionSnodeKit +import SessionNetworkingKit import SessionMessagingKit import SessionUtilitiesKit public enum AppSetup { public static func setupEnvironment( requestId: String? = nil, - additionalMigrationTargets: [MigratableTarget.Type] = [], appSpecificBlock: (() -> ())? = nil, migrationProgressChanged: ((CGFloat, TimeInterval) -> ())? = nil, migrationsCompletion: @escaping (Result) -> (), @@ -43,7 +42,6 @@ public enum AppSetup { runPostSetupMigrations( requestId: requestId, backgroundTask: backgroundTask, - additionalMigrationTargets: additionalMigrationTargets, migrationProgressChanged: migrationProgressChanged, migrationsCompletion: migrationsCompletion, using: dependencies @@ -57,7 +55,6 @@ public enum AppSetup { public static func runPostSetupMigrations( requestId: String? = nil, backgroundTask: SessionBackgroundTask? = nil, - additionalMigrationTargets: [MigratableTarget.Type] = [], migrationProgressChanged: ((CGFloat, TimeInterval) -> ())? = nil, migrationsCompletion: @escaping (Result) -> (), using dependencies: Dependencies @@ -65,12 +62,7 @@ public enum AppSetup { var backgroundTask: SessionBackgroundTask? = (backgroundTask ?? SessionBackgroundTask(label: #function, using: dependencies)) dependencies[singleton: .storage].perform( - migrationTargets: additionalMigrationTargets - .appending(contentsOf: [ - SNUtilitiesKit.self, - SNSnodeKit.self, - SNMessagingKit.self - ]), + migrations: SNMessagingKit.migrations, onProgressUpdate: migrationProgressChanged, onComplete: { originalResult in // Now that the migrations are complete there are a few more states which need diff --git a/_SharedTestUtilities/SynchronousStorage.swift b/_SharedTestUtilities/SynchronousStorage.swift index fef7fd8aae..59510cf436 100644 --- a/_SharedTestUtilities/SynchronousStorage.swift +++ b/_SharedTestUtilities/SynchronousStorage.swift @@ -11,8 +11,7 @@ class SynchronousStorage: Storage, DependenciesSettable, InitialSetupable { public init( customWriter: DatabaseWriter? = nil, - migrationTargets: [MigratableTarget.Type]? = nil, - migrations: [Storage.KeyedMigration]? = nil, + migrations: [Migration.Type]? = nil, using dependencies: Dependencies, initialData: ((ObservingDatabase) throws -> ())? = nil ) { @@ -21,20 +20,9 @@ class SynchronousStorage: Storage, DependenciesSettable, InitialSetupable { super.init(customWriter: customWriter, using: dependencies) - // Process any migration targets first - if let migrationTargets: [MigratableTarget.Type] = migrationTargets { + if let migrations: [Migration.Type] = migrations { perform( - migrationTargets: migrationTargets, - async: false, - onProgressUpdate: nil, - onComplete: { _ in } - ) - } - - // Then process any provided migration info - if let migrations: [Storage.KeyedMigration] = migrations { - perform( - sortedMigrations: migrations, + migrations: migrations, async: false, onProgressUpdate: nil, onComplete: { _ in }