diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eed88ef68..3c2277f70 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,10 @@ on: # Allows you to run this workflow manually from the Actions tab workflow_dispatch: +env: + # Make the git branch for a PR available to our Fastfile + MX_GIT_BRANCH: ${{ github.event.pull_request.head.ref }} + jobs: build: name: Build @@ -39,7 +43,7 @@ jobs: bundle config path vendor/bundle bundle install --jobs 4 --retry 3 - name: Use right MatrixKit and MatrixSDK versions - run: bundle exec fastlane point_dependencies_to_pending_releases + run: bundle exec fastlane point_dependencies_to_related_branches # Main step - name: Build iOS simulator @@ -75,7 +79,7 @@ jobs: bundle config path vendor/bundle bundle install --jobs 4 --retry 3 - name: Use right MatrixKit and MatrixSDK versions - run: bundle exec fastlane point_dependencies_to_pending_releases + run: bundle exec fastlane point_dependencies_to_related_branches # Main step - name: Unit tests diff --git a/CHANGES.rst b/CHANGES.rst index ffd02addb..6d4065d85 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,42 @@ +Changes in 1.3.6 (2021-05-07) +================================================= + +✨ Features + * + +🙌 Improvements + * Jitsi: Use Jitsi server from homeserver's Well Known, if present, to create conferences (#3158). + * RoomMemberDetailsVC: Enable / disable "Hide all messages from this user" from settings (#4281). + * RoomVC: Show / Hide More and Report Content contextual menu from settings (#4285). + * SettingsVC: Show / hide NSFW and decrypted content options from build settings (#4290). + * RoomVC: Tweaked Scroll to Bottom FAB button (#4272). + * DesignKit: Introduce a new framework to manage design components. + * Add Jitsi widget remove banner for privileged users. + * Update "Jump to unread" banner to a pill style button. + * CallVC: Add transfer button. + +🐛 Bugfix + * RoomVC: Avoid navigation to integration management using integration popup with settings set to integration disabled (#4261). + * RiotSettings: Logging out resets RiotSettings (#4259). + * RoomVC: Crash in `setScrollToBottomHidden` method (#4270). + * Notifications: Make them work in debug mode (#4274). + * VoIP: Fix call bar layout issue (#4300). + +⚠️ API Changes + * + +🗣 Translations + * + +🧱 Build + * GH Actions: Make jobs use the right version of MatrixKit and MatrixSDK. + +Others + * + +Improvements: + * Upgrade MatrixKit version ([v0.14.11](https://github.com/matrix-org/matrix-ios-kit/releases/tag/v0.14.11)). + Changes in 1.3.5 (2021-04-22) ================================================= diff --git a/Config/AppConfiguration.swift b/Config/AppConfiguration.swift index 7a8259772..597876852 100644 --- a/Config/AppConfiguration.swift +++ b/Config/AppConfiguration.swift @@ -44,6 +44,9 @@ class AppConfiguration: CommonConfiguration { // Each room member will be considered as a potential contact. MXKContactManager.shared().contactManagerMXRoomSource = MXKContactManagerMXRoomSource.all + // Use UIKit BackgroundTask for handling background tasks in the SDK + MXSDKOptions.sharedInstance().backgroundModeHandler = MXUIKitBackgroundModeHandler() + // Enable key backup on app MXSDKOptions.sharedInstance().enableKeyBackupWhenStartingMXCrypto = true } diff --git a/Config/AppIdentifiers.xcconfig b/Config/AppIdentifiers.xcconfig index fbb194172..d8a4e9d4d 100644 --- a/Config/AppIdentifiers.xcconfig +++ b/Config/AppIdentifiers.xcconfig @@ -22,8 +22,8 @@ APPLICATION_GROUP_IDENTIFIER = group.im.vector APPLICATION_SCHEME = element // Version -MARKETING_VERSION = 1.3.5 -CURRENT_PROJECT_VERSION = 1.3.5 +MARKETING_VERSION = 1.3.6 +CURRENT_PROJECT_VERSION = 1.3.6 // Team @@ -32,13 +32,13 @@ DEVELOPMENT_TEAM = 7J4U792NQT // Provisioning profiles RIOT_PROVISIONING_PROFILE_SPECIFIER = Vector App Store -RIOT_PROVISIONING_PROFILE = f65e7447-b8a3-46cc-8fba-fa60e55e2511 +RIOT_PROVISIONING_PROFILE = 4b43c1ca-3246-4984-828f-165838f5715a NSE_PROVISIONING_PROFILE_SPECIFIER = "Vector NSE: App Store" -NSE_PROVISIONING_PROFILE = 31dc9316-e029-47fd-81f5-778db07d76a2 +NSE_PROVISIONING_PROFILE = de44ca91-4318-4c23-8611-b531793505c2 SHARE_EXTENSION_PROVISIONING_PROFILE_SPECIFIER = "Vector Share Extension: App Store" -SHARE_EXTENSION_PROVISIONING_PROFILE = 1a3be143-50c7-4ae2-834e-00596a053141 +SHARE_EXTENSION_PROVISIONING_PROFILE = 546090a2-77ca-4bc2-b904-da5bd97a2f37 SIRI_INTENTS_PROVISIONING_PROFILE_SPECIFIER = "Vector Siri Intents: App Store" -SIRI_INTENTS_PROVISIONING_PROFILE = 18a66f93-ffe1-4008-b343-58350cc65023 +SIRI_INTENTS_PROVISIONING_PROFILE = 6951ad31-4850-445a-89c8-b64bca0a1c44 diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift index 0df5a4c8b..e7d771d1f 100644 --- a/Config/BuildSettings.swift +++ b/Config/BuildSettings.swift @@ -180,7 +180,7 @@ final class BuildSettings: NSObject { "https://scalar-staging.riot.im/scalar/api", ] // Jitsi server used outside integrations to create conference calls from the call button in the timeline - static let jitsiServerUrl = NSURL(string: "https://jitsi.riot.im") + static let jitsiServerUrl: URL = URL(string: "https://jitsi.riot.im")! // MARK: - Features @@ -242,6 +242,8 @@ final class BuildSettings: NSObject { static let settingsScreenShowChangePassword:Bool = true static let settingsScreenShowInviteFriends:Bool = true static let settingsScreenShowEnableStunServerFallback: Bool = true + static let settingsScreenShowNotificationDecodedContentOption: Bool = true + static let settingsScreenShowNsfwRoomsOption: Bool = true static let settingsSecurityScreenShowSessions:Bool = true static let settingsSecurityScreenShowSetupBackup:Bool = true static let settingsSecurityScreenShowRestoreBackup:Bool = true @@ -268,6 +270,12 @@ final class BuildSettings: NSObject { static let roomScreenAllowMediaLibraryAction: Bool = true static let roomScreenAllowStickerAction: Bool = true static let roomScreenAllowFilesAction: Bool = true + + // MARK: - Room Contextual Menu + + static let roomContextualMenuShowMoreOptionForMessages: Bool = true + static let roomContextualMenuShowMoreOptionForStates: Bool = true + static let roomContextualMenuShowReportContentOption: Bool = true // MARK: - Room Info Screen @@ -284,6 +292,10 @@ final class BuildSettings: NSObject { static let roomSettingsScreenShowAdvancedSettings: Bool = true static let roomSettingsScreenAdvancedShowEncryptToVerifiedOption: Bool = true + // MARK: - Room Member Screen + + static let roomMemberScreenShowIgnore: Bool = true + // MARK: - Message static let messageDetailsAllowShare: Bool = true static let messageDetailsAllowPermalink: Bool = true diff --git a/Config/CommonConfiguration.swift b/Config/CommonConfiguration.swift index 57fdb56a9..1583fdc78 100644 --- a/Config/CommonConfiguration.swift +++ b/Config/CommonConfiguration.swift @@ -63,9 +63,6 @@ class CommonConfiguration: NSObject, Configurable { // Disable identicon use sdkOptions.disableIdenticonUseForUserAvatar = true - // Use UIKit BackgroundTask for handling background tasks in the SDK - sdkOptions.backgroundModeHandler = MXUIKitBackgroundModeHandler() - // Pass httpAdditionalHeaders to the SDK sdkOptions.httpAdditionalHeaders = BuildSettings.httpAdditionalHeaders diff --git a/DesignKit/Common.xcconfig b/DesignKit/Common.xcconfig new file mode 100644 index 000000000..40cf7124b --- /dev/null +++ b/DesignKit/Common.xcconfig @@ -0,0 +1,27 @@ +// +// Copyright 2021 Vector Creations Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +// Configuration settings file format documentation can be found at: +// https://help.apple.com/xcode/#/dev745c5c974 + +#include "Config/AppIdentifiers.xcconfig" + +PRODUCT_NAME = DesignKit +PRODUCT_BUNDLE_IDENTIFIER = $(BASE_BUNDLE_IDENTIFIER).designkit + +INFOPLIST_FILE = DesignKit/Info.plist + +SKIP_INSTALL = YES diff --git a/DesignKit/Debug.xcconfig b/DesignKit/Debug.xcconfig new file mode 100644 index 000000000..11a7288a4 --- /dev/null +++ b/DesignKit/Debug.xcconfig @@ -0,0 +1,20 @@ +// +// Copyright 2021 Vector Creations Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +// Configuration settings file format documentation can be found at: +// https://help.apple.com/xcode/#/dev745c5c974 + +#include "Common.xcconfig" diff --git a/DesignKit/DesignKit.h b/DesignKit/DesignKit.h new file mode 100644 index 000000000..4ff68e722 --- /dev/null +++ b/DesignKit/DesignKit.h @@ -0,0 +1,27 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import + +//! Project version number for DesignKit. +FOUNDATION_EXPORT double DesignKitVersionNumber; + +//! Project version string for DesignKit. +FOUNDATION_EXPORT const unsigned char DesignKitVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/DesignKit/Info.plist b/DesignKit/Info.plist new file mode 100644 index 000000000..c0701c6d7 --- /dev/null +++ b/DesignKit/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/DesignKit/Release.xcconfig b/DesignKit/Release.xcconfig new file mode 100644 index 000000000..11a7288a4 --- /dev/null +++ b/DesignKit/Release.xcconfig @@ -0,0 +1,20 @@ +// +// Copyright 2021 Vector Creations Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +// Configuration settings file format documentation can be found at: +// https://help.apple.com/xcode/#/dev745c5c974 + +#include "Common.xcconfig" diff --git a/DesignKit/Source/Colors.swift b/DesignKit/Source/Colors.swift new file mode 100644 index 000000000..b5dc66261 --- /dev/null +++ b/DesignKit/Source/Colors.swift @@ -0,0 +1,64 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import UIKit + +/// Colors at https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=1255%3A1104 +@objc public protocol Colors { + + /// - Focused/Active states + /// - CTAs + var accent: UIColor { get } + + /// - Error messages + /// - Content requiring user attention + /// - Notification, alerts + var alert: UIColor { get } + + /// - Text + /// - Icons + var primaryContent: UIColor { get } + + /// - Text + /// - Icons + var secondaryContent: UIColor { get } + + /// - Text + /// - Icons + var tertiaryContent: UIColor { get } + + /// - Text + /// - Icons + var quarterlyContent: UIColor { get } + + /// Separating line + var separator: UIColor { get } + + // Cards, tiles + var tile: UIColor { get } + + /// Top navigation background on iOS + var navigation: UIColor { get } + + /// Background UI color + var background: UIColor { get } + + /// - Names in chat timeline + /// - Avatars default states that include first name letter + var namesAndAvatars: [UIColor] { get } + +} diff --git a/DesignKit/Source/ThemeV2.swift b/DesignKit/Source/ThemeV2.swift new file mode 100644 index 000000000..48f310992 --- /dev/null +++ b/DesignKit/Source/ThemeV2.swift @@ -0,0 +1,28 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import UIKit + +/// Theme v2. May be named again as `Theme` when the migration completed. +@objc public protocol ThemeV2 { + + /// Colors object + var colors: Colors { get } + + /// may contain more design components in future, like icons, audio files etc. + +} diff --git a/DesignKit/Variants/Dark/DarkColors.swift b/DesignKit/Variants/Dark/DarkColors.swift new file mode 100644 index 000000000..a1cf2ac54 --- /dev/null +++ b/DesignKit/Variants/Dark/DarkColors.swift @@ -0,0 +1,56 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import UIKit + +/// Dark theme colors. Will be a struct when things are more Swifty. +public class DarkColors: Colors { + + public let accent: UIColor = UIColor(rgb: 0x0DBD8B) + + public let alert: UIColor = UIColor(rgb: 0xFF4B55) + + public let primaryContent: UIColor = UIColor(rgb: 0xFFFFFF) + + public let secondaryContent: UIColor = UIColor(rgb: 0xA9B2BC) + + public let tertiaryContent: UIColor = UIColor(rgb: 0x8E99A4) + + public let quarterlyContent: UIColor = UIColor(rgb: 0x6F7882) + + public let separator: UIColor = UIColor(rgb: 0x21262C) + + public let tile: UIColor = UIColor(rgb: 0x394049) + + public let navigation: UIColor = UIColor(rgb: 0x21262C) + + public let background: UIColor = UIColor(rgb: 0x15191E) + + public let namesAndAvatars: [UIColor] = [ + UIColor(rgb: 0x368BD6), + UIColor(rgb: 0xAC3BA8), + UIColor(rgb: 0x03B381), + UIColor(rgb: 0xE64F7A), + UIColor(rgb: 0xFF812D), + UIColor(rgb: 0x2DC2C5), + UIColor(rgb: 0x5C56F5), + UIColor(rgb: 0x74D12C) + ] + + public init() {} + +} diff --git a/DesignKit/Variants/Light/LightColors.swift b/DesignKit/Variants/Light/LightColors.swift new file mode 100644 index 000000000..d8b5e108b --- /dev/null +++ b/DesignKit/Variants/Light/LightColors.swift @@ -0,0 +1,56 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import UIKit + +/// Light theme colors. Will be a struct when things are more Swifty. +public class LightColors: Colors { + + public let accent: UIColor = UIColor(rgb: 0x0DBD8B) + + public let alert: UIColor = UIColor(rgb: 0xFF4B55) + + public let primaryContent: UIColor = UIColor(rgb: 0x17191C) + + public let secondaryContent: UIColor = UIColor(rgb: 0x737D8C) + + public let tertiaryContent: UIColor = UIColor(rgb: 0x8D97A5) + + public let quarterlyContent: UIColor = UIColor(rgb: 0xC1C6CD) + + public let separator: UIColor = UIColor(rgb: 0xE3E8F0) + + public let tile: UIColor = UIColor(rgb: 0xF3F8FD) + + public let navigation: UIColor = UIColor(rgb: 0xF4F6FA) + + public let background: UIColor = UIColor(rgb: 0xFFFFFF) + + public let namesAndAvatars: [UIColor] = [ + UIColor(rgb: 0x368BD6), + UIColor(rgb: 0xAC3BA8), + UIColor(rgb: 0x03B381), + UIColor(rgb: 0xE64F7A), + UIColor(rgb: 0xFF812D), + UIColor(rgb: 0x2DC2C5), + UIColor(rgb: 0x5C56F5), + UIColor(rgb: 0x74D12C) + ] + + public init() {} + +} diff --git a/DesignKit/target.yml b/DesignKit/target.yml new file mode 100644 index 000000000..e10f76f12 --- /dev/null +++ b/DesignKit/target.yml @@ -0,0 +1,33 @@ +name: DesignKit + +schemes: + DesignKit: + analyze: + config: Debug + archive: + config: Release + build: + targets: + DesignKit: + - running + - profiling + - analyzing + - archiving + profile: + config: Release + run: + config: Debug + disableMainThreadChecker: true + +targets: + DesignKit: + type: framework + platform: iOS + + configFiles: + Debug: Debug.xcconfig + Release: Release.xcconfig + + sources: + - path: . + - path: ../Riot/Categories/UIColor.swift diff --git a/INSTALL.md b/INSTALL.md index 3e382eb82..5800848d0 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -68,7 +68,7 @@ If you want to modify MatrixKit and/or MatrixSDK locally and see the result in E But before you have to checkout [MatrixKit](https://github.com/matrix-org/matrix-ios-kit) repository in `../matrix-ios-kit` and [MatrixSDK](https://github.com/matrix-org/matrix-ios-sdk) in `../matrix-ios-sdk` locally relatively to your Element iOS project folder. Be sure to use compatible branches for Element iOS, MatrixKit and MatrixSDK. For example, if you want to modify Element iOS from develop branch, use MatrixKit and MatrixSDK develop branches and then make your modifications. -**Important**: By working with local pods (development pods) you will need to use legacy build system in Xcode, to have your local changes taken into account. To enable it go to Xcode menu and select `File > Workspace Settings… > Build System` and then choose `Legacy Build System`. +**Important**: By working with [XcodeGen](https://github.com/yonaskolb/XcodeGen) you will need to use the _New Build System_ in Xcode, to have your some of the xcconfig variables taken into account. It should be enabled by default on the latest Xcode versions, but if you need to enable it go to Xcode menu and select `File > Workspace Settings… > Build System` and then choose `New Build System`. ### `$matrixKitVersion` Modification diff --git a/Podfile b/Podfile index ad722b0aa..faff847c4 100644 --- a/Podfile +++ b/Podfile @@ -11,7 +11,7 @@ use_frameworks! # - `{ {kit spec hash} => {sdk spec hash}` to depend on specific pod options (:git => …, :podspec => …) for each repo. Used by Fastfile during CI # # Warning: our internal tooling depends on the name of this variable name, so be sure not to change it -$matrixKitVersion = '= 0.14.10' +$matrixKitVersion = '= 0.14.11' # $matrixKitVersion = :local # $matrixKitVersion = {'develop' => 'develop'} diff --git a/Podfile.lock b/Podfile.lock index 9ab02bf3f..6d2878f14 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -55,28 +55,28 @@ PODS: - MatomoTracker (7.4.1): - MatomoTracker/Core (= 7.4.1) - MatomoTracker/Core (7.4.1) - - MatrixKit (0.14.10): + - MatrixKit (0.14.11): - Down (~> 0.10.0) - DTCoreText (~> 1.6.25) - HPGrowingTextView (~> 1.1) - libPhoneNumber-iOS (~> 0.9.13) - - MatrixKit/Core (= 0.14.10) - - MatrixSDK (= 0.18.10) - - MatrixKit/Core (0.14.10): + - MatrixKit/Core (= 0.14.11) + - MatrixSDK (= 0.18.11) + - MatrixKit/Core (0.14.11): - Down (~> 0.10.0) - DTCoreText (~> 1.6.25) - HPGrowingTextView (~> 1.1) - libPhoneNumber-iOS (~> 0.9.13) - - MatrixSDK (= 0.18.10) - - MatrixSDK (0.18.10): - - MatrixSDK/Core (= 0.18.10) - - MatrixSDK/Core (0.18.10): + - MatrixSDK (= 0.18.11) + - MatrixSDK (0.18.11): + - MatrixSDK/Core (= 0.18.11) + - MatrixSDK/Core (0.18.11): - AFNetworking (~> 4.0.0) - GZIP (~> 1.3.0) - libbase58 (~> 0.1.4) - OLMKit (~> 3.2.2) - Realm (= 10.7.2) - - MatrixSDK/JingleCallStack (0.18.10): + - MatrixSDK/JingleCallStack (0.18.11): - JitsiMeetSDK (= 3.2.0) - MatrixSDK/Core - OLMKit (3.2.2): @@ -115,7 +115,7 @@ DEPENDENCIES: - KeychainAccess (~> 4.2.2) - KTCenterFlowLayout (~> 1.3.1) - MatomoTracker (~> 7.4.1) - - MatrixKit (= 0.14.10) + - MatrixKit (= 0.14.11) - MatrixSDK - MatrixSDK/JingleCallStack - OLMKit @@ -186,8 +186,8 @@ SPEC CHECKSUMS: LoggerAPI: ad9c4a6f1e32f518fdb43a1347ac14d765ab5e3d Logging: beeb016c9c80cf77042d62e83495816847ef108b MatomoTracker: 24a846c9d3aa76933183fe9d47fd62c9efa863fb - MatrixKit: fa06a06a1e40105d1ef3b753797f12d6024d2f4f - MatrixSDK: 2d37dc430394d752bbbb7f10fca835e7fc452c66 + MatrixKit: b4427e22aed3ae4a3ef5175bb017fc2b9786269c + MatrixSDK: f3cecb8e8ca7624dc52940dfae95ae8b81ebee1a OLMKit: 20d1c564033a1ae7148f8f599378d4c798363905 ReadMoreTextView: 19147adf93abce6d7271e14031a00303fe28720d Realm: e523da9ade306c5ae87e85dc09fdef148d3e1cc1 @@ -199,6 +199,6 @@ SPEC CHECKSUMS: zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 323458da7e25e73dfa41fd662e2a55f532ce2668 +PODFILE CHECKSUM: eafd5b002866b4fb5d6814a0b07f1c3d99c5744a COCOAPODS: 1.10.1 diff --git a/Riot/Assets/Images.xcassets/Call/call_audio_mute_off_icon.imageset/Microphone.png b/Riot/Assets/Images.xcassets/Call/call_audio_mute_off_icon.imageset/Microphone.png index 699255712..a808cf5bd 100644 Binary files a/Riot/Assets/Images.xcassets/Call/call_audio_mute_off_icon.imageset/Microphone.png and b/Riot/Assets/Images.xcassets/Call/call_audio_mute_off_icon.imageset/Microphone.png differ diff --git a/Riot/Assets/Images.xcassets/Call/call_audio_mute_off_icon.imageset/Microphone@2x.png b/Riot/Assets/Images.xcassets/Call/call_audio_mute_off_icon.imageset/Microphone@2x.png index a73e07451..32681bc51 100644 Binary files a/Riot/Assets/Images.xcassets/Call/call_audio_mute_off_icon.imageset/Microphone@2x.png and b/Riot/Assets/Images.xcassets/Call/call_audio_mute_off_icon.imageset/Microphone@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Call/call_audio_mute_off_icon.imageset/Microphone@3x.png b/Riot/Assets/Images.xcassets/Call/call_audio_mute_off_icon.imageset/Microphone@3x.png index b292d11c5..310c13a63 100644 Binary files a/Riot/Assets/Images.xcassets/Call/call_audio_mute_off_icon.imageset/Microphone@3x.png and b/Riot/Assets/Images.xcassets/Call/call_audio_mute_off_icon.imageset/Microphone@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Call/call_audio_mute_on_icon.imageset/Microphone Off.png b/Riot/Assets/Images.xcassets/Call/call_audio_mute_on_icon.imageset/Microphone Off.png index c194f59b3..fcfe16211 100644 Binary files a/Riot/Assets/Images.xcassets/Call/call_audio_mute_on_icon.imageset/Microphone Off.png and b/Riot/Assets/Images.xcassets/Call/call_audio_mute_on_icon.imageset/Microphone Off.png differ diff --git a/Riot/Assets/Images.xcassets/Call/call_audio_mute_on_icon.imageset/Microphone Off@2x.png b/Riot/Assets/Images.xcassets/Call/call_audio_mute_on_icon.imageset/Microphone Off@2x.png index a4bdab618..6cd4af09b 100644 Binary files a/Riot/Assets/Images.xcassets/Call/call_audio_mute_on_icon.imageset/Microphone Off@2x.png and b/Riot/Assets/Images.xcassets/Call/call_audio_mute_on_icon.imageset/Microphone Off@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Call/call_audio_mute_on_icon.imageset/Microphone Off@3x.png b/Riot/Assets/Images.xcassets/Call/call_audio_mute_on_icon.imageset/Microphone Off@3x.png index cb777a3fa..d3423152d 100644 Binary files a/Riot/Assets/Images.xcassets/Call/call_audio_mute_on_icon.imageset/Microphone Off@3x.png and b/Riot/Assets/Images.xcassets/Call/call_audio_mute_on_icon.imageset/Microphone Off@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Call/call_dialpad_backspace_icon.imageset/Contents.json b/Riot/Assets/Images.xcassets/Call/call_dialpad_backspace_icon.imageset/Contents.json index df240cf11..713667a8c 100644 --- a/Riot/Assets/Images.xcassets/Call/call_dialpad_backspace_icon.imageset/Contents.json +++ b/Riot/Assets/Images.xcassets/Call/call_dialpad_backspace_icon.imageset/Contents.json @@ -19,5 +19,8 @@ "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" } } diff --git a/Riot/Assets/Images.xcassets/Call/call_go_to_chat_icon.imageset/Contents.json b/Riot/Assets/Images.xcassets/Call/call_go_to_chat_icon.imageset/Contents.json new file mode 100644 index 000000000..96e141816 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Call/call_go_to_chat_icon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "back.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "back@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "back@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Call/call_go_to_chat_icon.imageset/back.png b/Riot/Assets/Images.xcassets/Call/call_go_to_chat_icon.imageset/back.png new file mode 100644 index 000000000..38a571045 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Call/call_go_to_chat_icon.imageset/back.png differ diff --git a/Riot/Assets/Images.xcassets/Call/call_go_to_chat_icon.imageset/back@2x.png b/Riot/Assets/Images.xcassets/Call/call_go_to_chat_icon.imageset/back@2x.png new file mode 100644 index 000000000..cd85e3085 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Call/call_go_to_chat_icon.imageset/back@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Call/call_go_to_chat_icon.imageset/back@3x.png b/Riot/Assets/Images.xcassets/Call/call_go_to_chat_icon.imageset/back@3x.png new file mode 100644 index 000000000..8c3dc1625 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Call/call_go_to_chat_icon.imageset/back@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Call/call_hangup_large.imageset/End Call.png b/Riot/Assets/Images.xcassets/Call/call_hangup_large.imageset/End Call.png index b1934caaf..d1f21e64e 100644 Binary files a/Riot/Assets/Images.xcassets/Call/call_hangup_large.imageset/End Call.png and b/Riot/Assets/Images.xcassets/Call/call_hangup_large.imageset/End Call.png differ diff --git a/Riot/Assets/Images.xcassets/Call/call_hangup_large.imageset/End Call@2x.png b/Riot/Assets/Images.xcassets/Call/call_hangup_large.imageset/End Call@2x.png index 8a471f94d..7b4433f3f 100644 Binary files a/Riot/Assets/Images.xcassets/Call/call_hangup_large.imageset/End Call@2x.png and b/Riot/Assets/Images.xcassets/Call/call_hangup_large.imageset/End Call@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Call/call_hangup_large.imageset/End Call@3x.png b/Riot/Assets/Images.xcassets/Call/call_hangup_large.imageset/End Call@3x.png index 0a7195c07..46bec1ae1 100644 Binary files a/Riot/Assets/Images.xcassets/Call/call_hangup_large.imageset/End Call@3x.png and b/Riot/Assets/Images.xcassets/Call/call_hangup_large.imageset/End Call@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Call/call_video_icon.imageset/Contents.json b/Riot/Assets/Images.xcassets/Call/call_video_icon.imageset/Contents.json index f69f28085..453722c5e 100644 --- a/Riot/Assets/Images.xcassets/Call/call_video_icon.imageset/Contents.json +++ b/Riot/Assets/Images.xcassets/Call/call_video_icon.imageset/Contents.json @@ -19,5 +19,8 @@ "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" } } diff --git a/Riot/Assets/Images.xcassets/Call/call_video_mute_off_icon.imageset/Video.png b/Riot/Assets/Images.xcassets/Call/call_video_mute_off_icon.imageset/Video.png index e379143ca..afcd4d1f8 100644 Binary files a/Riot/Assets/Images.xcassets/Call/call_video_mute_off_icon.imageset/Video.png and b/Riot/Assets/Images.xcassets/Call/call_video_mute_off_icon.imageset/Video.png differ diff --git a/Riot/Assets/Images.xcassets/Call/call_video_mute_off_icon.imageset/Video@2x.png b/Riot/Assets/Images.xcassets/Call/call_video_mute_off_icon.imageset/Video@2x.png index f458c0c9e..17f48c56d 100644 Binary files a/Riot/Assets/Images.xcassets/Call/call_video_mute_off_icon.imageset/Video@2x.png and b/Riot/Assets/Images.xcassets/Call/call_video_mute_off_icon.imageset/Video@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Call/call_video_mute_off_icon.imageset/Video@3x.png b/Riot/Assets/Images.xcassets/Call/call_video_mute_off_icon.imageset/Video@3x.png index e847ec334..e3bede731 100644 Binary files a/Riot/Assets/Images.xcassets/Call/call_video_mute_off_icon.imageset/Video@3x.png and b/Riot/Assets/Images.xcassets/Call/call_video_mute_off_icon.imageset/Video@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Call/call_video_mute_on_icon.imageset/Video Off.png b/Riot/Assets/Images.xcassets/Call/call_video_mute_on_icon.imageset/Video Off.png index 243928c1a..89052530e 100644 Binary files a/Riot/Assets/Images.xcassets/Call/call_video_mute_on_icon.imageset/Video Off.png and b/Riot/Assets/Images.xcassets/Call/call_video_mute_on_icon.imageset/Video Off.png differ diff --git a/Riot/Assets/Images.xcassets/Call/call_video_mute_on_icon.imageset/Video Off@2x.png b/Riot/Assets/Images.xcassets/Call/call_video_mute_on_icon.imageset/Video Off@2x.png index ef951c8e0..7671bde13 100644 Binary files a/Riot/Assets/Images.xcassets/Call/call_video_mute_on_icon.imageset/Video Off@2x.png and b/Riot/Assets/Images.xcassets/Call/call_video_mute_on_icon.imageset/Video Off@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Call/call_video_mute_on_icon.imageset/Video Off@3x.png b/Riot/Assets/Images.xcassets/Call/call_video_mute_on_icon.imageset/Video Off@3x.png index 975156a50..894447ebc 100644 Binary files a/Riot/Assets/Images.xcassets/Call/call_video_mute_on_icon.imageset/Video Off@3x.png and b/Riot/Assets/Images.xcassets/Call/call_video_mute_on_icon.imageset/Video Off@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Call/camera_switch.imageset/Contents.json b/Riot/Assets/Images.xcassets/Call/camera_switch.imageset/Contents.json index a1d4a004a..67ecee5d6 100644 --- a/Riot/Assets/Images.xcassets/Call/camera_switch.imageset/Contents.json +++ b/Riot/Assets/Images.xcassets/Call/camera_switch.imageset/Contents.json @@ -19,8 +19,5 @@ "info" : { "author" : "xcode", "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "template" } } diff --git a/Riot/Assets/Images.xcassets/Call/camera_switch.imageset/camera_switch.png b/Riot/Assets/Images.xcassets/Call/camera_switch.imageset/camera_switch.png index 27d63753e..1c362a52b 100644 Binary files a/Riot/Assets/Images.xcassets/Call/camera_switch.imageset/camera_switch.png and b/Riot/Assets/Images.xcassets/Call/camera_switch.imageset/camera_switch.png differ diff --git a/Riot/Assets/Images.xcassets/Call/camera_switch.imageset/camera_switch@2x.png b/Riot/Assets/Images.xcassets/Call/camera_switch.imageset/camera_switch@2x.png index 3f526d9a8..a1a67cd00 100644 Binary files a/Riot/Assets/Images.xcassets/Call/camera_switch.imageset/camera_switch@2x.png and b/Riot/Assets/Images.xcassets/Call/camera_switch.imageset/camera_switch@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Call/camera_switch.imageset/camera_switch@3x.png b/Riot/Assets/Images.xcassets/Call/camera_switch.imageset/camera_switch@3x.png index 872210582..ba278efe3 100644 Binary files a/Riot/Assets/Images.xcassets/Call/camera_switch.imageset/camera_switch@3x.png and b/Riot/Assets/Images.xcassets/Call/camera_switch.imageset/camera_switch@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Activities/new_close.imageset/Close.png b/Riot/Assets/Images.xcassets/Room/Activities/new_close.imageset/Close.png new file mode 100644 index 000000000..35a24620d Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Activities/new_close.imageset/Close.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Activities/new_close.imageset/Close@2x.png b/Riot/Assets/Images.xcassets/Room/Activities/new_close.imageset/Close@2x.png new file mode 100644 index 000000000..9ee4ab4f0 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Activities/new_close.imageset/Close@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Activities/new_close.imageset/Close@3x.png b/Riot/Assets/Images.xcassets/Room/Activities/new_close.imageset/Close@3x.png new file mode 100644 index 000000000..5220dd2f9 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Activities/new_close.imageset/Close@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Activities/new_close.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Activities/new_close.imageset/Contents.json new file mode 100644 index 000000000..90d19c308 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Activities/new_close.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "filename" : "Close.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Close@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Close@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Activities/room_scroll_up.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Activities/room_scroll_up.imageset/Contents.json new file mode 100644 index 000000000..92d19c27b --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Activities/room_scroll_up.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "filename" : "Up.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Up@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Up@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Activities/room_scroll_up.imageset/Up.png b/Riot/Assets/Images.xcassets/Room/Activities/room_scroll_up.imageset/Up.png new file mode 100644 index 000000000..c9556b53d Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Activities/room_scroll_up.imageset/Up.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Activities/room_scroll_up.imageset/Up@2x.png b/Riot/Assets/Images.xcassets/Room/Activities/room_scroll_up.imageset/Up@2x.png new file mode 100644 index 000000000..f029fd55b Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Activities/room_scroll_up.imageset/Up@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Activities/room_scroll_up.imageset/Up@3x.png b/Riot/Assets/Images.xcassets/Room/Activities/room_scroll_up.imageset/Up@3x.png new file mode 100644 index 000000000..90d3fc8ac Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Activities/room_scroll_up.imageset/Up@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Activities/scrolldown.imageset/scrolldown.png b/Riot/Assets/Images.xcassets/Room/Activities/scrolldown.imageset/scrolldown.png index 4e639fb9b..992d1edaf 100644 Binary files a/Riot/Assets/Images.xcassets/Room/Activities/scrolldown.imageset/scrolldown.png and b/Riot/Assets/Images.xcassets/Room/Activities/scrolldown.imageset/scrolldown.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Activities/scrolldown.imageset/scrolldown@2x.png b/Riot/Assets/Images.xcassets/Room/Activities/scrolldown.imageset/scrolldown@2x.png index aabb2b121..c21846293 100644 Binary files a/Riot/Assets/Images.xcassets/Room/Activities/scrolldown.imageset/scrolldown@2x.png and b/Riot/Assets/Images.xcassets/Room/Activities/scrolldown.imageset/scrolldown@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Activities/scrolldown.imageset/scrolldown@3x.png b/Riot/Assets/Images.xcassets/Room/Activities/scrolldown.imageset/scrolldown@3x.png index 81685ed6b..936e57707 100644 Binary files a/Riot/Assets/Images.xcassets/Room/Activities/scrolldown.imageset/scrolldown@3x.png and b/Riot/Assets/Images.xcassets/Room/Activities/scrolldown.imageset/scrolldown@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Activities/scrolldown_dark.imageset/scrolldown_dark.png b/Riot/Assets/Images.xcassets/Room/Activities/scrolldown_dark.imageset/scrolldown_dark.png index 740a562f4..5d3228686 100644 Binary files a/Riot/Assets/Images.xcassets/Room/Activities/scrolldown_dark.imageset/scrolldown_dark.png and b/Riot/Assets/Images.xcassets/Room/Activities/scrolldown_dark.imageset/scrolldown_dark.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Activities/scrolldown_dark.imageset/scrolldown_dark@2x.png b/Riot/Assets/Images.xcassets/Room/Activities/scrolldown_dark.imageset/scrolldown_dark@2x.png index 1dfe3ab3d..14a8900e0 100644 Binary files a/Riot/Assets/Images.xcassets/Room/Activities/scrolldown_dark.imageset/scrolldown_dark@2x.png and b/Riot/Assets/Images.xcassets/Room/Activities/scrolldown_dark.imageset/scrolldown_dark@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Activities/scrolldown_dark.imageset/scrolldown_dark@3x.png b/Riot/Assets/Images.xcassets/Room/Activities/scrolldown_dark.imageset/scrolldown_dark@3x.png index 14ae3aa1b..729c73bb2 100644 Binary files a/Riot/Assets/Images.xcassets/Room/Activities/scrolldown_dark.imageset/scrolldown_dark@3x.png and b/Riot/Assets/Images.xcassets/Room/Activities/scrolldown_dark.imageset/scrolldown_dark@3x.png differ diff --git a/Riot/Assets/Riot-Defaults.plist b/Riot/Assets/Riot-Defaults.plist index 28d0d04b8..7ef98b242 100644 --- a/Riot/Assets/Riot-Defaults.plist +++ b/Riot/Assets/Riot-Defaults.plist @@ -20,8 +20,6 @@ syncLocalContacts - createConferenceCallsWithJitsi - enableRageShake maxAllowedMediaCacheSize @@ -34,5 +32,7 @@ 15020851 enableBotCreation + enableRingingForGroupCalls + diff --git a/Riot/Assets/SharedImages.xcassets/Common/cancel.imageset/Contents.json b/Riot/Assets/SharedImages.xcassets/Common/cancel.imageset/Contents.json index 2f43e0b91..92357aede 100644 --- a/Riot/Assets/SharedImages.xcassets/Common/cancel.imageset/Contents.json +++ b/Riot/Assets/SharedImages.xcassets/Common/cancel.imageset/Contents.json @@ -1,23 +1,26 @@ { "images" : [ { - "idiom" : "universal", "filename" : "cancel.png", + "idiom" : "universal", "scale" : "1x" }, { - "idiom" : "universal", "filename" : "cancel@2x.png", + "idiom" : "universal", "scale" : "2x" }, { - "idiom" : "universal", "filename" : "cancel@3x.png", + "idiom" : "universal", "scale" : "3x" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" } -} \ No newline at end of file +} diff --git a/Riot/Assets/ar.lproj/Vector.strings b/Riot/Assets/ar.lproj/Vector.strings index 389af1cc9..310cc5f70 100644 --- a/Riot/Assets/ar.lproj/Vector.strings +++ b/Riot/Assets/ar.lproj/Vector.strings @@ -129,3 +129,11 @@ "callbar_only_multiple_paused" = "عَدَد %@ مُكالَمَة مُعلَّقة"; "callbar_active_and_single_paused" = "مُكالَمَةٌ واحدةٌ نَشِطة (%@) · مُكالَمَةٌ واحدةٌ مُعلَّقة"; "callbar_only_single_paused" = "مُكالَمَة مُعلَّقة"; +"auth_softlogout_reason" = "لَقَد سَجَّلَ مُدير الخادِم الرَّئيس الخَّاصِّ بِك (%1$@) الخُرُوج مِن الحِساب الخَّاصِّ بِك %2$@ (%3$@)."; +"auth_softlogout_signed_out" = "لَقَد سَجَّلتَ الخُرُوج"; +"auth_autodiscover_invalid_response" = "اِستِجابَة اِكتِشاف الخادِم الرَّئيس غَير صَالِحة"; +"auth_accept_policies" = "يُرجَى مُراجَعَة سِياسَات هَذَا الخادِم الرَّئيس وقُبُولِهَا:"; +"auth_add_email_and_phone_warning" = "التَّسجِيل بِواسِطَة البَريد الإلِكتُرونيّ وَرَقم الهَاتِف معًا غَير مَدعوم حَتَّى تَتَوَفَر بيئة بَرمجة التَّطبيقات (API). سَوفَ يَتِّم أخذ رَقم الهَاتِف فَقَط فِي الاِعتِبار. يُمكِنُكَ إضافَة البَريد الإلِكتُرونيّ الخَّاصّ بِك إلَى مَلَفّ التَّعريف الخَّاصِّ بِك فِي الإعدَادَات."; +"auth_reset_password_success_message" = "لَقَد تمَّ إعادَةُ ضَبطِ كَلِمَةِ المُرُور الخَّاصَّةِ بِك.\n\nلَقَد تمَّ تَسجِيلُ خُرُوجِك مِن جَميعِ الجَلَسات وَلَن تَستَلِمَ بَعد الآن دَفعَ الإِشعَارات. لِإعادَةِ تَفعِيل الإِشعَارات، أعِد تَسجِيلَ الدُّخول عَلَى كُلِّ جِهاز."; +"auth_reset_password_error_not_found" = "لَا يَبدو أنَّ عُنوان البَريد الإلِكتُرونيّ الخَّاصِّ بِك مُقتَرِنٌ بِمُعَرِّف Matrix عَلَى الخادِمِ الرَّئيس هَذَا."; +"auth_reset_password_error_unauthorized" = "فَشَلَ التَّحَقُق مِن عُنوان البَريد الإلِكتُرونيّ: تَأكَّد مِن نَقرِكَ عَلَى الرَّابِط الَّذي فِي البَريد الإلِكتُرونيّ"; diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index 96493461a..e2f8ed08d 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -1327,8 +1327,8 @@ "callbar_only_single_active" = "Laufender Anruf (%@)"; "room_event_action_delete_confirmation_message" = "Möchtest Du die nicht gesendete Nachricht wirklich löschen?"; "room_event_action_delete_confirmation_title" = "Nicht gesendete Nachricht löschen"; -"room_details_integrations" = "Einbindungen"; -"room_details_search" = "Raum suchen"; +"room_details_integrations" = "Integrationen"; +"room_details_search" = "Raum durchsuchen"; "room_multiple_typing_notification" = "%@ und andere"; "room_accessibility_video_call" = "Videoanruf"; "room_message_replying_to" = "%@ anworten"; diff --git a/Riot/Assets/en.lproj/Localizable.strings b/Riot/Assets/en.lproj/Localizable.strings index 9d7213925..6541338bd 100644 --- a/Riot/Assets/en.lproj/Localizable.strings +++ b/Riot/Assets/en.lproj/Localizable.strings @@ -123,6 +123,12 @@ /* Incoming named video conference invite from a specific person */ "VIDEO_CONF_NAMED_FROM_USER" = "Video group call from %@: '%@'"; +/* A user added a Jitsi call to a room */ +"GROUP_CALL_STARTED" = "Group call started"; + +/* Group call from user, CallKit caller name */ +"GROUP_CALL_FROM_USER" = "%@ (Group call)"; + /** Key verification **/ "KEY_VERIFICATION_REQUEST_FROM_USER" = "%@ wants to verify"; diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 6b58da5c1..277d820ce 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -65,12 +65,13 @@ "less" = "Less"; // Call Bar -"callbar_only_single_active" = "Active call (%@)"; +"callbar_only_single_active" = "Tap to return to the call (%@)"; "callbar_active_and_single_paused" = "1 active call (%@) · 1 paused call"; "callbar_active_and_multiple_paused" = "1 active call (%@) · %@ paused calls"; "callbar_only_single_paused" = "Paused call"; "callbar_only_multiple_paused" = "%@ paused calls"; "callbar_return" = "Return"; +"callbar_only_single_active_group" = "Tap to Join the group call (%@)"; // Accessibility "accessibility_checkbox_label" = "checkbox"; @@ -312,7 +313,8 @@ Tap the + to start adding people."; "room_member_power_level_short_custom" = "Custom"; // Chat -"room_jump_to_first_unread" = "Jump to first unread message"; +"room_slide_to_end_group_call" = "Slide to end the call for everyone"; +"room_jump_to_first_unread" = "Jump to unread"; "room_accessiblity_scroll_to_bottom" = "Scroll to bottom"; "room_new_message_notification" = "%d new message"; "room_new_messages_notification" = "%d new messages"; @@ -395,6 +397,8 @@ Tap the + to start adding people."; "room_accessibility_hangup" = "Hang up"; "room_place_voice_call" = "Voice call"; "room_open_dialpad" = "Dial pad"; +"room_join_group_call" = "Join"; +"room_no_privileges_to_create_group_call" = "You need to be an admin or a moderator to start a call."; "media_type_accessibility_image" = "Image"; "media_type_accessibility_audio" = "Audio"; @@ -526,6 +530,7 @@ Tap the + to start adding people."; "settings_labs_e2e_encryption_prompt_message" = "To finish setting up encryption you must log in again."; "settings_labs_create_conference_with_jitsi" = "Create conference calls with jitsi"; "settings_labs_message_reaction" = "React to messages with emoji"; +"settings_labs_enable_ringing_for_group_calls" = "Ring for group calls"; "settings_version" = "Version %@"; "settings_olm_version" = "Olm Version %@"; @@ -823,10 +828,22 @@ Tap the + to start adding people."; "event_formatter_message_edited_mention" = "(edited)"; "event_formatter_call_voice" = "Voice call"; "event_formatter_call_video" = "Video call"; -"event_formatter_call_has_ended" = "This call has ended"; -"event_formatter_call_you_currently_in" = "You're currently in this call"; +"event_formatter_call_connecting" = "Connecting…"; +"event_formatter_call_ringing" = "Ringing…"; +"event_formatter_call_has_ended" = "Ended %@"; +"event_formatter_call_you_currently_in" = "Active call"; "event_formatter_call_you_declined" = "You declined this call"; +"event_formatter_call_you_missed" = "You missed this call"; +"event_formatter_call_connection_failed" = "Connection failed"; "event_formatter_call_back" = "Call back"; +"event_formatter_call_decline" = "Decline"; +"event_formatter_call_answer" = "Answer"; +"event_formatter_call_retry" = "Retry"; +"event_formatter_call_end_call" = "End call"; +"event_formatter_group_call" = "Group call"; +"event_formatter_group_call_join" = "Join"; +"event_formatter_group_call_leave" = "Leave"; +"event_formatter_group_call_incoming" = "%@ in %@"; // Events formatter with you "event_formatter_widget_added_by_you" = "You added the widget: %@"; diff --git a/Riot/Assets/es.lproj/InfoPlist.strings b/Riot/Assets/es.lproj/InfoPlist.strings index 0a79c4758..db8c99db1 100644 --- a/Riot/Assets/es.lproj/InfoPlist.strings +++ b/Riot/Assets/es.lproj/InfoPlist.strings @@ -1,7 +1,7 @@ // Permissions usage explanations -"NSCameraUsageDescription" = "La cámara se utiliza para tomar fotos y vídeos, realizar llamadas de vídeo."; -"NSPhotoLibraryUsageDescription" = "La biblioteca de fotos se utiliza para enviar fotos y vídeos."; -"NSMicrophoneUsageDescription" = "El micrófono se utiliza para tomar vídeos, realizar llamadas."; +"NSCameraUsageDescription" = "La cámara se usa para sacar fotos, vídeos y hacer videollamadas."; +"NSPhotoLibraryUsageDescription" = "La biblioteca de fotos se usa para enviar fotos y vídeos."; +"NSMicrophoneUsageDescription" = "El micrófono se usa para grabar vídeos y realizar llamadas."; "NSContactsUsageDescription" = "Para mostrarte cuáles de tus contactos ya utilizan Matrix, Element puede enviar las direcciones de correo electrónico y números telefónicos de tu agenda de contactos a tu Servidor de Identidad de Matrix. En los casos que se puede, tu información personal se cifra antes de ser enviada - por favor consulta la política de privacidad de tu Servidor de Identidad."; "NSFaceIDUsageDescription" = "Face ID se usa para acceder a tu aplicación."; "NSCalendarsUsageDescription" = "Mostrar tus reuniones en la aplicación."; diff --git a/Riot/Assets/es.lproj/Localizable.strings b/Riot/Assets/es.lproj/Localizable.strings index 2a6c23591..dbac976df 100644 --- a/Riot/Assets/es.lproj/Localizable.strings +++ b/Riot/Assets/es.lproj/Localizable.strings @@ -54,3 +54,19 @@ "MSG_FROM_USER_IN_ROOM_TITLE" = "%@ en %@"; /* Sticker from a specific person, not referencing a room. */ "STICKER_FROM_USER" = "%@ envió una pegatina"; + +/** Key verification **/ + +"KEY_VERIFICATION_REQUEST_FROM_USER" = "%@ quiere verificar"; + +/** Notification messages **/ + +/* New message indicator on unknown room */ +"MESSAGE" = "Mensaje"; + +/* New message indicator from a DM */ +"MESSAGE_FROM_X" = "Mensaje de %@"; + +/* New message indicator on a room */ +"MESSAGE_IN_X" = "Mensaje en %@"; +"MESSAGE_PROTECTED" = "Nuevo mensaje"; diff --git a/Riot/Assets/es.lproj/Vector.strings b/Riot/Assets/es.lproj/Vector.strings index de769fa05..c14cdda6b 100644 --- a/Riot/Assets/es.lproj/Vector.strings +++ b/Riot/Assets/es.lproj/Vector.strings @@ -51,7 +51,7 @@ "auth_repeat_password_placeholder" = "Repite la contraseña"; "auth_repeat_new_password_placeholder" = "Confirma tu contraseña nueva"; "auth_home_server_placeholder" = "URL (ej. https://matrix.org)"; -"auth_identity_server_placeholder" = "URL (ej. https://matrix.org)"; +"auth_identity_server_placeholder" = "URL (ej.: https://matrix.org)"; "auth_invalid_login_param" = "Nombre de usuario y/o contraseña incorrectos"; "auth_invalid_user_name" = "Los nombres de usuario solo pueden contener letras, números, puntos, guiones y guiones bajos"; "auth_invalid_password" = "Contraseña demasiado corta (mínimo 6)"; @@ -121,12 +121,12 @@ "auth_msisdn_validation_title" = "Verificación Pendiente"; "auth_msisdn_validation_message" = "Hemos enviado un SMS con un código de activación. Por favor, ingresa este código a continuación."; "auth_msisdn_validation_error" = "No se pudo verificar el número telefónico."; -"auth_recaptcha_message" = "Este Servidor Doméstico quiere asegurarse de que no eres un robot"; +"auth_recaptcha_message" = "Este servidor base quiere asegurarse de que no eres un robot"; "auth_reset_password_message" = "Para restablecer tu contraseña, ingresa la dirección de correo electrónico vinculada a tu cuenta:"; "auth_reset_password_missing_email" = "Debes ingresar la dirección de correo electrónico vinculada a tu cuenta."; "auth_reset_password_email_validation_message" = "Se envió un correo electrónico a %@. Una vez que hayas seguido el enlace que contiene, haz clic a continuación."; "auth_reset_password_error_unauthorized" = "No se pudo verificar la dirección de correo electrónico: asegúrate de hacer clic en el enlace del correo electrónico"; -"auth_reset_password_error_not_found" = "Tu dirección de correo electrónico no parece estar asociada a una ID de Matrix en este Servidor Doméstico."; +"auth_reset_password_error_not_found" = "Tu dirección de correo electrónico no parece estar asociada a una ID de Matrix en este servidor base."; "auth_reset_password_success_message" = "Tu contraseña fue restablecida.\n\nSe ha cerrado sesión en todos tus dispositivos y ya no recibirás notificaciones push. Para volver a habilitar las notificaciones, vuelve a iniciar sesión en cada dispositivo."; "auth_add_email_and_phone_warning" = "Todavía no es posible registrarse con correo electrónico y número telefónico a la vez, hasta que exista la API. Solo se tendrá en cuenta el número telefónico. Puedes añadir tu correo electrónico a tu perfil en ajustes."; // Chat creation @@ -550,3 +550,43 @@ "deactivate_account_forget_messages_information_part3" = ": esto provocará que los usuarios futuros vean conversaciones incompletas)"; // String for App Store "store_short_description" = "Chat/VoIP descentralizado y seguro"; +"room_participants_leave_prompt_title_for_dm" = "Salir"; +"people_empty_view_title" = "Personas"; +"social_login_button_title_sign_up" = "Registrarse con %@"; +"social_login_button_title_sign_in" = "Iniciar sesión con %@"; +"social_login_button_title_continue" = "Seguir con %@"; +"social_login_list_title_sign_up" = "O"; +"social_login_list_title_sign_in" = "O"; + +// Social login + +"social_login_list_title_continue" = "Seguir con"; +"auth_softlogout_clear_data_sign_out" = "Cerrar sesión"; +"auth_softlogout_clear_data_sign_out_title" = "¿Seguro?"; +"auth_softlogout_clear_data_button" = "Borrar todos los datos"; +"auth_softlogout_clear_data_message_2" = "Bórralos si vas a dejar de usar este dispositivo, o quieres iniciar sesión con otra cuenta."; +"auth_softlogout_clear_data_message_1" = "Cuidado: tus datos personales (incluyendo claves de cifrado) todavía están alojados en este dispositivo."; +"auth_softlogout_clear_data" = "Borrar datos personales"; +"auth_softlogout_reason" = "La administración de tu servidor base (%1$@) ha cerrado tu sesión %2$@ (%3$@)."; +"auth_softlogout_sign_in" = "Iniciar sesión"; +"auth_softlogout_signed_out" = "No tienes una sesión iniciada"; +"auth_accept_policies" = "Por favor, lee y acepta los términos de este servidor base:"; +"auth_add_phone_message_2" = "Pon un número de teléfono para que te descubran personas que te conozcan."; +"auth_add_email_message_2" = "Pon un correo para poder recuperar tu cuenta en el futuro, y, opcionalmente que las personas que te conozcan descubran tu cuenta."; +"auth_login_single_sign_on" = "Iniciar sesión"; + +// Accessibility +"accessibility_checkbox_label" = "casilla de selección"; +"callbar_return" = "Volver"; +"callbar_only_multiple_paused" = "%@ llamadas en espera"; +"callbar_only_single_paused" = "Llamada en espera"; +"callbar_active_and_multiple_paused" = "1 llamada en curso (%@) · %@ llamadas en espera"; +"callbar_active_and_single_paused" = "1 llamada en curso (%@) · 1 llamada en espera"; + +// Call Bar +"callbar_only_single_active" = "Llamada en curso (%@)"; +"less" = "Menos"; +"more" = "Más"; +"switch" = "Cambiar"; +"skip" = "Saltar"; +"close" = "Cerrar"; diff --git a/Riot/Assets/fa.lproj/InfoPlist.strings b/Riot/Assets/fa.lproj/InfoPlist.strings new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/Riot/Assets/fa.lproj/InfoPlist.strings @@ -0,0 +1 @@ + diff --git a/Riot/Assets/fa.lproj/Localizable.strings b/Riot/Assets/fa.lproj/Localizable.strings new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/Riot/Assets/fa.lproj/Localizable.strings @@ -0,0 +1 @@ + diff --git a/Riot/Assets/fa.lproj/Vector.strings b/Riot/Assets/fa.lproj/Vector.strings index 8b1378917..e0420eb2b 100644 --- a/Riot/Assets/fa.lproj/Vector.strings +++ b/Riot/Assets/fa.lproj/Vector.strings @@ -1 +1,17 @@ + + + +// Room Details +"room_details_title" = "جزئیات اتاق"; +"invite" = "دعوت"; + +// Actions +"view" = "مشاهده"; +"leave" = "ترک"; +"save" = "ذخیره"; +"cancel" = "لغو"; +"retry" = "تلاش مجدد"; +"continue" = "ادامه"; +"close" = "بستن"; +"back" = "بازگشت"; diff --git a/Riot/Assets/hu.lproj/Vector.strings b/Riot/Assets/hu.lproj/Vector.strings index a44ee794f..afb1f59a4 100644 --- a/Riot/Assets/hu.lproj/Vector.strings +++ b/Riot/Assets/hu.lproj/Vector.strings @@ -1350,3 +1350,9 @@ // Call Bar "callbar_only_single_active" = "Aktív hívás (%@)"; +"room_details_integrations" = "Integrációk"; +"room_details_search" = "Szoba keresése"; +"room_multiple_typing_notification" = "%@ és mások"; +"room_accessibility_video_call" = "Videohívás"; +"room_message_replying_to" = "Válasz erre: %@"; +"room_message_editing" = "Szerkesztés"; diff --git a/Riot/Assets/nb-NO.lproj/InfoPlist.strings b/Riot/Assets/nb-NO.lproj/InfoPlist.strings new file mode 100644 index 000000000..738bfe23a --- /dev/null +++ b/Riot/Assets/nb-NO.lproj/InfoPlist.strings @@ -0,0 +1,9 @@ + + +"NSContactsUsageDescription" = "For å finne kontakter som allerede bruker Matrix, kan Element sende e-postadresser og telefonnummer i din adressebok til utvalgte Matrix identitetstjenere. Der det støttes blir data anonymisert (hashet) - vennligst sjekk din identitetstjener for detaljer."; +"NSFaceIDUsageDescription" = "Face ID brukes for å få tilgang til appen."; +"NSCalendarsUsageDescription" = "Se dine avtalte møter i appen."; +"NSMicrophoneUsageDescription" = "Mikrofonen brukes til videoopptak, og i samtaler."; +"NSPhotoLibraryUsageDescription" = "Bildebiblioteket brukes for å sende bilder og videoer."; +// Permissions usage explanations +"NSCameraUsageDescription" = "Kameraet brukes til å ta bilder, spille inn video, og i videosamtaler."; diff --git a/Riot/Assets/nb-NO.lproj/Localizable.strings b/Riot/Assets/nb-NO.lproj/Localizable.strings new file mode 100644 index 000000000..26102b751 --- /dev/null +++ b/Riot/Assets/nb-NO.lproj/Localizable.strings @@ -0,0 +1,113 @@ + + + +/** Key verification **/ + +"KEY_VERIFICATION_REQUEST_FROM_USER" = "%@ ønsker å bekrefte"; + +/* Incoming named video conference invite from a specific person */ +"VIDEO_CONF_NAMED_FROM_USER" = "Video-konferansesamtale fra %@: '%@'"; + +/* Incoming named voice conference invite from a specific person */ +"VOICE_CONF_NAMED_FROM_USER" = "Konferansesamtale fra %@: '%@'"; + +/* Incoming unnamed voice conference invite from a specific person */ +"VOICE_CONF_FROM_USER" = "Konferansesamtale fra %@"; + +/* Incoming unnamed video conference invite from a specific person */ +"VIDEO_CONF_FROM_USER" = "Video-konferansesamtale fra %@"; + +/* Incoming one-to-one video call */ +"VIDEO_CALL_FROM_USER" = "Videosamtale fra %@"; + +/** Calls **/ + +/* Incoming one-to-one voice call */ +"VOICE_CALL_FROM_USER" = "%@ ringer"; + +/* A user has invited you to a named room */ +"USER_INVITE_TO_NAMED_ROOM" = "%@ har invitert deg til %@"; + +/* A user has invited you to an (unamed) group chat */ +"USER_INVITE_TO_CHAT_GROUP_CHAT" = "%@ har invitert deg til en gruppesamtale"; + +/** Invites **/ + +/* A user has invited you to a chat */ +"USER_INVITE_TO_CHAT" = "%@ har invitert deg til en samtale"; + +/* Look, stuff's happened, alright? Just open the app. */ +"MSGS_IN_TWO_PLUS_ROOMS" = "%@ nye meldinger i %@, %@ og andre"; + +/* Multiple messages in two rooms */ +"MSGS_IN_TWO_ROOMS" = "%@ nye meldinger i %@ og %@"; + +/* Multiple unread messages from two plus people (ie. for 4+ people: 'others' replaces the third person) */ +"MSGS_FROM_TWO_PLUS_USERS" = "%@ nye meldinger fra %@, %@ og andre"; + +/* Multiple unread messages from three people */ +"MSGS_FROM_THREE_USERS" = "%@ nye meldinger fra %@, %@ og %@"; + +/* Multiple unread messages from two people */ +"MSGS_FROM_TWO_USERS" = "%@ nye meldinger fra %@ og %@"; + +/* Multiple unread messages from a specific person, not referencing a room */ +"MSGS_FROM_USER" = "%@ nye meldinger i %@"; + +/** Coalesced messages **/ + +/* Multiple unread messages in a room */ +"UNREAD_IN_ROOM" = "%@ meldinger i %@"; +"MESSAGE_PROTECTED" = "Ny melding"; + +/* New message indicator on a room */ +"MESSAGE_IN_X" = "Melding i %@"; + +/* New message indicator from a DM */ +"MESSAGE_FROM_X" = "Melding fra %@"; + +/** Notification messages **/ + +/* New message indicator on unknown room */ +"MESSAGE" = "Melding"; + +/* Sticker from a specific person, not referencing a room. */ +"STICKER_FROM_USER" = "%@ sendte et klistremerke"; + +/* A single unread message */ +"SINGLE_UNREAD" = "Du mottok en melding"; + +/* A single unread message in a room */ +"SINGLE_UNREAD_IN_ROOM" = "Du mottok en melding i %@"; + +/* New action message from a specific person in a named room. */ +"IMAGE_FROM_USER_IN_ROOM" = "%@ lastet opp et bilde %@ i %@"; + +/** Image Messages **/ + +/* New action message from a specific person, not referencing a room. */ +"IMAGE_FROM_USER" = "%@ sendte et bilde %@"; + +/* New action message from a specific person in a named room. */ +"ACTION_FROM_USER_IN_ROOM" = "%@: * %@ %@"; + +/* New action message from a specific person, not referencing a room. */ +"ACTION_FROM_USER" = "* %@ %@"; + +/* New message from a specific person in a named room. Content included. */ +"MSG_FROM_USER_IN_ROOM_WITH_CONTENT" = "%@ i %@: %@"; + +/** Single, unencrypted messages (where we can include the content */ + +/* New message from a specific person, not referencing a room. Content included. */ +"MSG_FROM_USER_WITH_CONTENT" = "%@: %@"; + +/* New message from a specific person in a named room */ +"MSG_FROM_USER_IN_ROOM" = "%@ skrevet i %@"; + +/** Single, end-to-end encrypted messages (ie. we don't know what they say) */ + +/* New message from a specific person, not referencing a room */ +"MSG_FROM_USER" = "%@ sendte en melding"; +/* Message title for a specific person in a named room */ +"MSG_FROM_USER_IN_ROOM_TITLE" = "%@ i %@"; diff --git a/Riot/Assets/nb-NO.lproj/Vector.strings b/Riot/Assets/nb-NO.lproj/Vector.strings index eb35c73c6..482d7213a 100644 --- a/Riot/Assets/nb-NO.lproj/Vector.strings +++ b/Riot/Assets/nb-NO.lproj/Vector.strings @@ -557,7 +557,7 @@ "secrets_setup_recovery_key_loading" = "Laster…"; "secrets_setup_recovery_key_export_action" = "Lagre"; "secrets_setup_recovery_key_done_action" = "Fullført"; -"secrets_setup_recovery_key_storage_alert_title" = "Hold det trygt"; +"secrets_setup_recovery_key_storage_alert_title" = "Oppbevar trygt"; "secrets_setup_recovery_passphrase_validate_action" = "Fullført"; "secrets_setup_recovery_passphrase_confirm_passphrase_title" = "Bekreft"; "secrets_setup_recovery_passphrase_confirm_passphrase_placeholder" = "Bekreft passordfrasen"; @@ -630,7 +630,7 @@ "room_resource_usage_limit_reached_message_1_default" = "Denne hjemmeserveren har overskredet en av sine ressursgrenser så "; "room_resource_limit_exceeded_message_contact_3" = " for å fortsette å bruke denne tjenesten."; "room_resource_limit_exceeded_message_contact_2_link" = "kontakt tjenesteadministratoren"; -"room_resource_limit_exceeded_message_contact_1" = " Vær så snill "; +"room_resource_limit_exceeded_message_contact_1" = " Vennligst "; "room_predecessor_link" = "Trykk her for å se eldre meldinger."; "room_predecessor_information" = "Dette rommet er en fortsettelse av en annen samtale."; "room_replacement_information" = "Dette rommet er erstattet og er ikke lenger aktivt."; @@ -785,3 +785,78 @@ // Call Bar "callbar_only_single_active" = "Aktiv samtale (%@)"; +"secrets_setup_recovery_key_information" = "Oppbevar din sikkerhetsnøkkel trygt. Den kan brukes til å å tilgang til dine sikrede meldinger og data."; + +// MARK: - Secrets set up + +// Recovery Key + +"secrets_setup_recovery_key_title" = "Lagre din sikkerhetsnøkkel"; +"secrets_recovery_with_key_invalid_recovery_key_message" = "Vennligst sjekk at du har tastet riktig sikkerhetsnøkkel."; +"secrets_recovery_with_key_invalid_recovery_key_title" = "Ugyldig sikkerhetsnøkkel, ingen tilgang til sikret lagringsområde"; +"secrets_recovery_with_key_recover_action" = "Bruk sikkerhetsnøkkel"; +"secrets_setup_recovery_key_storage_alert_message" = "✓ Skriv ut og oppbevar på et trygt sted\n✓ Lagre på en USB minnepenn, eller ekstern sikkerhetskopi\n✓ Kopièr til din personlige skylagring"; +"secrets_recovery_with_passphrase_invalid_passphrase_title" = "Feil sikkerhetsfrase, ingen tilgang til sikret lagringsområde"; +"secrets_recovery_with_key_information_verify_device" = "Bruk din sikkerhetsnøkkel for å bekrefte denne enheten."; +"secrets_recovery_with_key_information_default" = "Få tilgang til dine sikrede meldinger og identitetsnøkkelen som brukes for å bekrefte andre sesjoner, ved å taste inn din sikkerhetsnøkkel."; +"secrets_recovery_with_passphrase_invalid_passphrase_message" = "Vennligst sjekk at du har tastet riktig sikkerhetsfrase."; +"room_open_dialpad" = "Nummertastatur"; +"settings_labs" = "LABS"; +"room_place_voice_call" = "Lydsamtale"; +"settings_pin_rooms_with_unread" = "Feste rom med uleste meldinger"; +"settings_global_settings_info" = "Globale varslingsinnstillinger er tilgjengelige på din %@ nettleserklient"; +"room_multiple_typing_notification" = "%@ og andre"; +"room_event_action_delete_confirmation_message" = "Er du sikker på at du vil slette denne usendte meldingen?"; +"external_link_confirmation_message" = "Lenken %@ bringer deg til et annet nettsted: %@\n\nEr du sikker på at du vil fortsette?"; +"room_accessibility_video_call" = "Videosamtale"; +"room_event_action_delete_confirmation_title" = "Slett usendt melding"; +"room_unsent_messages_cancel_message" = "Er du sikker på at du vil slette alle usendte meldinger i dette rommet?"; +"room_unsent_messages_cancel_title" = "Slett usendte meldinger"; +"room_message_replying_to" = "Svarer til %@"; +"room_message_editing" = "Redigerer"; +"room_member_power_level_custom_in" = "Tilpasset (%@) av %@"; +"pin_protection_confirm_pin_to_disable" = "Bekreft PIN-kode for å deaktivere PIN-kode"; +"pin_protection_choose_pin" = "Opprett en PIN-kode for sikkerhet"; +"device_verification_self_verify_alert_message" = "Verifiser den nye påloggingen som vil ha tilgang til kontoen din:% @"; + +// Service terms - Variant for identity server when displayed out of a context +"service_terms_modal_title_identity_server" = "Finne kontakter"; +"widget_integration_missing_room_id" = "Manglende rom_id i forespørsel."; +"rage_shake_prompt" = "Det ser ut til at du rister telefonen i frustrasjon. Ønsker du å sende inn rapport om feil?"; +"room_details_addresses_invalid_address_prompt_title" = "Ugyldig alias format"; +"security_settings_crypto_sessions_description_2" = "Om du ikke gjenkjenner en pålogging, endre ditt passord og tilbakestill sikkerhetskopi."; +"settings_key_backup_info_trust_signature_invalid_device_verified" = "Sikkerhetskopien har en ugyldig signatur fra %@"; +"settings_key_backup_info_trust_signature_valid_device_unverified" = "Sikkerhetskopi har signatur fra %@"; +"settings_key_backup_info_trust_signature_valid" = "Sikkerhetskopien har en gyldig signatur fra denne økten"; +"settings_key_backup_info_trust_signature_unknown" = "Sikkerhetskopi av økten har signatur med ID: %@"; +"settings_key_backup_info_progress_done" = "Alle nøkler sikkerhetskopiert"; +"settings_key_backup_info_progress" = "Sikkerhetskopierer %@ nøkler…"; +"settings_key_backup_info_not_valid" = "Denne økten sikkerhetskopierer ikke dine nøkler, men du har en eksisterende sikkerhetskopi du kan gjenopprette fra og legge til, for å gå videre."; +"settings_key_backup_info_valid" = "Denne økten sikkerhetskopierer dine nøkler."; +"settings_key_backup_info_version" = "Sikkerhetskopi av nøkler versjon : %@"; +"settings_key_backup_info_signout_warning" = "Før du logger ut, koble denne sesjonen til sikkerhetskopi av nøkler for å unngå tap av nøkler som kanskje bare er lagret på denne enheten."; +"settings_key_backup_info_none" = "Nøklene dine for denne sesjonen blir ikke sikkerhetskopiert"; +"settings_third_party_notices" = "Tredjepartsmerknader"; +"settings_labs_e2e_encryption_prompt_message" = "Vennligst logg inn igjen for å ferdigstille oppsett av kryptering."; +"settings_key_backup" = "Sikkerhetskopi av meldingsnøkler"; +"settings_ui_theme_picker_message" = "\"Auto\" bruker din enhets innstillinger for å bytte bakgrunnsfarge"; + +// MARK: - Favourites + +"favourites_empty_view_title" = "Favorittrom og personer"; +"create_room_placeholder_address" = "#testrom: matrix.org"; +"device_verification_emoji_santa" = "Julenisse"; +"device_verification_security_advice_emoji" = "Sammenlign de unike emojiene, og sjekk at de vises i samme rekkefølge."; + +// MARK: Sign out warning + +"sign_out_existing_key_backup_alert_title" = "Er du sikker på at du vil logge av?"; +"service_terms_modal_message_identity_server" = "Godta brukervilkårene til identitetsserveren (%@) for å finne kontakter."; +"event_formatter_call_back" = "Ring tilbake"; +"room_details_advanced_e2e_encryption_disabled" = "Kryptering er ikke aktivert i dette rommet."; +"room_details_flair_section" = "Vi brukervalg for samfunn"; +"room_details_photo_for_dm" = "Bilde"; +"room_details_photo" = "Rombilde"; +"settings_flair" = "Vis brukervalg hvor tillat"; +"settings_on_denied_notification" = "Varsler er ikke tillat for %@, vennligst tillat dem i enhetens innstillinger"; +"settings_pin_rooms_with_missed_notif" = "Fest rom med tapte varsler"; diff --git a/Riot/Assets/nl.lproj/Vector.strings b/Riot/Assets/nl.lproj/Vector.strings index 22b8683cc..87c8fb14f 100644 --- a/Riot/Assets/nl.lproj/Vector.strings +++ b/Riot/Assets/nl.lproj/Vector.strings @@ -1023,7 +1023,7 @@ // MARK: - Secrets reset -"secrets_reset_title" = "Alles terugzetten"; +"secrets_reset_title" = "Alles opnieuw instellen"; "secrets_setup_recovery_passphrase_confirm_passphrase_placeholder" = "Wachtwoord bevestigen"; "secrets_setup_recovery_passphrase_confirm_passphrase_title" = "Bevestigen"; "secrets_setup_recovery_passphrase_validate_action" = "Klaar"; @@ -1058,7 +1058,7 @@ // Recover with passphrase "secrets_recovery_with_passphrase_title" = "Herstelwachtwoord"; -"secrets_recovery_reset_action_part_2" = "Alles terugzetten"; +"secrets_recovery_reset_action_part_2" = "Alles opnieuw instellen"; // MARK: - Secrets recovery diff --git a/Riot/Assets/pl.lproj/InfoPlist.strings b/Riot/Assets/pl.lproj/InfoPlist.strings index cc8968571..b50e4e044 100644 --- a/Riot/Assets/pl.lproj/InfoPlist.strings +++ b/Riot/Assets/pl.lproj/InfoPlist.strings @@ -1,6 +1,7 @@ // Permissions usage explanations -"NSCameraUsageDescription" = "Aparat służy do robienia zdjęć i nagrywania filmów, prowadzenia rozmów wideo."; -"NSPhotoLibraryUsageDescription" = "Biblioteka zdjęć służy do wysyłania zdjęć i filmów."; -"NSMicrophoneUsageDescription" = "Mikrofon służy do robienia filmów, wykonywania połączeń."; -"NSContactsUsageDescription" = "Możemy pokazać Ci, które z Twoich kontaktów korzystają aktualnie z Element, bądź Matrix. Możemy wysyłać adresy e-mail i numery telefonów z Twojej książki adresowej na Twój serwer Matrix. New Vector nie przechowuje Twoich danych, ani nie wykorzystuje ich w żadnym celu. Aby uzyskać dodatkowe informacje zajrzyj do zakładki \"polityka prywatności\" w ustawieniach aplikacji."; +"NSCameraUsageDescription" = "Kamera wykorzystywana jest do robienia zdjęć, nagrywania filmów i prowadzenia rozmów wideo."; +"NSPhotoLibraryUsageDescription" = "Biblioteka zdjęć wykorzystywana jest do wysyłania zdjęć i filmów."; +"NSMicrophoneUsageDescription" = "Mikrofon wykorzystywany jest podczas nagrywania filmów i wykonywania połączeń."; +"NSContactsUsageDescription" = "Aby móc znaleźć osoby z Twoich kontaktów, które korzystają już z sieci Matrix, Element może wysłać adresy e-mail i numery telefonów z Twojej książki adresowej do wybranego serwera tożsamości Matrix. Tam, gdzie jest to obsługiwane, dane osobowe są szyfrowane przed wysłaniem - zapoznaj się z polityką prywatności Twojego serwera tożsamości, aby uzyskać więcej informacji."; "NSCalendarsUsageDescription" = "Zobacz swoje zaplanowane spotkania w aplikacji."; +"NSFaceIDUsageDescription" = "Face ID wykorzystywane jest do odblokowywania aplikacji."; diff --git a/Riot/Assets/pl.lproj/Localizable.strings b/Riot/Assets/pl.lproj/Localizable.strings index e8acae301..4888159c7 100644 --- a/Riot/Assets/pl.lproj/Localizable.strings +++ b/Riot/Assets/pl.lproj/Localizable.strings @@ -1,5 +1,5 @@ /* New message from a specific person, not referencing a room */ -"MSG_FROM_USER" = "%@ wysłał(a) wiadomość"; +"MSG_FROM_USER" = "%@ wysłał(-a) wiadomość"; /* New message from a specific person, not referencing a room. Content included. */ "MSG_FROM_USER_WITH_CONTENT" = "%@: %@"; /* New message from a specific person in a named room. Content included. */ @@ -23,27 +23,27 @@ /* Look, stuff's happened, alright? Just open the app. */ "MSGS_IN_TWO_PLUS_ROOMS" = "%@ nowych wiadomości w %@, %@ i innych"; /* New action message from a specific person, not referencing a room. */ -"IMAGE_FROM_USER" = "%@ wysłał(a) zdjęcie %@"; +"IMAGE_FROM_USER" = "%@ wysłał(-a) zdjęcie %@"; /* New action message from a specific person in a named room. */ -"IMAGE_FROM_USER_IN_ROOM" = "%@ wysłał(a) zdjęcie %@ w %@"; +"IMAGE_FROM_USER_IN_ROOM" = "%@ wysłał(-a) zdjęcie %@ w %@"; /* Multiple unread messages from a specific person, not referencing a room */ "MSGS_FROM_USER" = "%@ nowych wiadomości w %@"; /* Multiple messages in two rooms */ "MSGS_IN_TWO_ROOMS" = "%@ nowych wiadomości w %@ i %@"; /* A user has invited you to a named room */ -"USER_INVITE_TO_NAMED_ROOM" = "%@ zaprosił(a) Ciebie do %@"; +"USER_INVITE_TO_NAMED_ROOM" = "%@ zaprosił(-a) Ciebie do %@"; /* A user has invited you to an (unamed) group chat */ -"USER_INVITE_TO_CHAT_GROUP_CHAT" = "%@ zaprosił(a) Ciebie do rozmowy grupowej"; +"USER_INVITE_TO_CHAT_GROUP_CHAT" = "%@ zaprosił(-a) Ciebie do rozmowy grupowej"; /* A user has invited you to a chat */ -"USER_INVITE_TO_CHAT" = "%@ zaprosił(a) Ciebie do rozmowy"; +"USER_INVITE_TO_CHAT" = "%@ zaprosił(-a) Ciebie do rozmowy"; /* Message title for a specific person in a named room */ "MSG_FROM_USER_IN_ROOM_TITLE" = "%@ w %@"; /* New message from a specific person in a named room */ "MSG_FROM_USER_IN_ROOM" = "%@ opublikowany w %@"; /* Sticker from a specific person, not referencing a room. */ -"STICKER_FROM_USER" = "%@ wysłał(a) naklejkę"; +"STICKER_FROM_USER" = "%@ wysłał(-a) naklejkę"; /* Incoming one-to-one voice call */ -"VOICE_CALL_FROM_USER" = "Zadzwonił do Ciebie z %@"; +"VOICE_CALL_FROM_USER" = "Połączenie głosowe z %@"; /* Incoming one-to-one video call */ "VIDEO_CALL_FROM_USER" = "Połączenie wideo z %@"; /* Incoming unnamed voice conference invite from a specific person */ @@ -54,10 +54,11 @@ "VOICE_CONF_NAMED_FROM_USER" = "Połączenie grupowe z %@: '%@'"; /* Incoming named video conference invite from a specific person */ "VIDEO_CONF_NAMED_FROM_USER" = "Grupowe połączenie wideo z %@: '%@'"; -"KEY_VERIFICATION_REQUEST_FROM_USER" = "%@ wymaga weryfikacji"; +"KEY_VERIFICATION_REQUEST_FROM_USER" = "%@ chce się z Tobą zweryfikować"; /* New message indicator on unknown room */ "MESSAGE" = "Wiadomość"; /* New message indicator from a DM */ "MESSAGE_FROM_X" = "Wiadomość od %@"; /* New message indicator on a room */ "MESSAGE_IN_X" = "Wiadomość w %@"; +"MESSAGE_PROTECTED" = "Nowa Wiadomość"; diff --git a/Riot/Assets/pl.lproj/Vector.strings b/Riot/Assets/pl.lproj/Vector.strings index 9d4cb8526..c559c823d 100644 --- a/Riot/Assets/pl.lproj/Vector.strings +++ b/Riot/Assets/pl.lproj/Vector.strings @@ -1,12 +1,12 @@ // Titles "title_home" = "Strona startowa"; "title_favourites" = "Ulubione"; -"title_people" = "Ludzie"; +"title_people" = "Osoby"; "title_rooms" = "Pokoje"; "title_groups" = "Społeczności"; "warning" = "Ostrzeżenie"; // Actions -"view" = "Widok"; +"view" = "Wyświetl"; "back" = "Powrót"; "continue" = "Kontynuuj"; "create" = "Utwórz"; @@ -14,7 +14,7 @@ "remove" = "Usuń"; "invite" = "Zaproś"; "retry" = "Ponów"; -"off" = "Off"; +"off" = "Wyłączony"; "cancel" = "Anuluj"; "save" = "Zapisz"; "join" = "Dołącz"; @@ -22,7 +22,7 @@ "accept" = "Akceptuj"; "preview" = "Podgląd"; "camera" = "Kamera"; -"voice" = "Głos"; +"voice" = "Połączenie Głosowe"; "active_call" = "Aktywne połączenie"; "active_call_details" = "Aktywne połączenie (%@)"; "later" = "Później"; @@ -32,25 +32,25 @@ "sending" = "Wysyłanie"; // Authentication "auth_login" = "Zaloguj się"; -"auth_register" = "Zarejestruj"; +"auth_register" = "Rejestracja"; "auth_submit" = "Wyślij"; "auth_skip" = "Pomiń"; -"auth_send_reset_email" = "Wyślij e-mail przywracający"; +"auth_send_reset_email" = "Wyślij e-mail resetujący hasło"; "auth_return_to_login" = "Wróć do ekranu logowania"; -"auth_user_id_placeholder" = "Nazwa użytkownika lub e-mail"; +"auth_user_id_placeholder" = "E-mail lub nazwa użytkownika"; "auth_password_placeholder" = "Hasło"; "auth_new_password_placeholder" = "Nowe hasło"; "auth_user_name_placeholder" = "Nazwa użytkownika"; "auth_optional_email_placeholder" = "Adres e-mail (opcjonalnie)"; "auth_email_placeholder" = "Adres e-mail"; -"auth_optional_phone_placeholder" = "Numer telefonu komórkowego (opcjonalne)"; +"auth_optional_phone_placeholder" = "Numer telefonu komórkowego (opcjonalnie)"; "auth_phone_placeholder" = "Numer telefonu"; "auth_repeat_password_placeholder" = "Powtórz hasło"; "auth_repeat_new_password_placeholder" = "Potwierdź swoje nowe hasło"; "auth_home_server_placeholder" = "URL (np. https://matrix.org)"; -"auth_identity_server_placeholder" = "Adres URL (np. https://matrix.org)"; +"auth_identity_server_placeholder" = "URL (np. https://matrix.org)"; "auth_invalid_login_param" = "Nieprawidłowa nazwa użytkownika i/lub hasło"; -"auth_invalid_user_name" = "Nazwa użytkownika może zawierać tylko litery, cyfry, kropki, myślniki i podkreślenia"; +"auth_invalid_user_name" = "Nazwa użytkownika może jedynie zawierać litery, cyfry, kropki, myślniki i podkreślenia"; "auth_invalid_email" = "To nie wygląda na poprawny adres e-mail"; "auth_invalid_phone" = "To nie wygląda na poprawny numer telefonu"; "auth_missing_password" = "Brak hasła"; @@ -61,34 +61,34 @@ "auth_missing_email_or_phone" = "Brak adresu e-mail lub numeru telefonu"; "auth_email_in_use" = "Podany adres e-mail jest już w użyciu"; "auth_phone_in_use" = "Ten numer telefonu jest już używany"; -"auth_password_dont_match" = "Hasła nie zgadzają się"; +"auth_password_dont_match" = "Hasła się nie zgadzają"; "auth_username_in_use" = "Nazwa użytkownika jest już używana"; "auth_use_server_options" = "Użyj niestandardowych ustawień serwera (zaawansowane)"; "auth_email_validation_message" = "Sprawdź swój e-mail, aby kontynuować rejestrację"; "auth_msisdn_validation_title" = "Oczekiwanie na weryfikację"; "auth_msisdn_validation_message" = "Wysłaliśmy SMS z kodem aktywacyjnym. Podaj ten kod poniżej."; "auth_recaptcha_message" = "Ten serwer domowy chciałby się upewnić, że nie jesteś robotem"; -"auth_reset_password_message" = "Aby zresetować swoje hasło, wpisz adres e-mail powiązany z twoim kontem:"; +"auth_reset_password_message" = "Aby zresetować swoje hasło, wprowadź adres e-mail powiązany z twoim kontem:"; "auth_reset_password_missing_password" = "Należy wprowadzić nowe hasło."; -"auth_reset_password_next_step_button" = "Zweryfikowałem swój adres e-mail"; -"auth_reset_password_error_unauthorized" = "Nie udało się zweryfikować adresu e-mail: upewnij się że kliknąłeś w link w e-mailu"; -"auth_reset_password_error_not_found" = "Twój adres e-mail zdaje się nie być powiązany z żadnym Matrix ID na tym serwerze domowym."; +"auth_reset_password_next_step_button" = "Zweryfikowałem(-am) swój adres e-mail"; +"auth_reset_password_error_unauthorized" = "Nie udało się zweryfikować adresu e-mail: upewnij się że kliknąłeś(-aś) w link w e-mailu"; +"auth_reset_password_error_not_found" = "Twój adres e-mail zdaje się być nie powiązany z żadnym Matrix ID na tym serwerze domowym."; "auth_invalid_password" = "Zbyt krótkie hasło (co najmniej 6 znaków)"; "auth_add_email_message" = "Dodaj do konta adres e-mail, aby użytkownicy mogli Cię odnaleźć, oraz abyś mógł przywrócić swoje hasło."; "auth_add_phone_message" = "Dodaj numer telefonu, aby użytkownicy mogli Cię odnaleźć."; -"auth_reset_password_missing_email" = "Musi zostać wprowadzony e-mail powiązany z kontem."; -"auth_reset_password_email_validation_message" = "Wysłano email na adres %@. Po otwarciu znajdującego się tam linku, kliknij poniżej."; -"auth_reset_password_success_message" = "Twoje hasło zostało zresetowane.\n\nZostałeś wylogowany ze wszystkich urządzeń i nie będziesz więcej otrzymywać powiadomień push. Aby ponownie włączyć powiadomienia, zaloguj się ponownie na każdym urządzeniu."; -"auth_add_email_and_phone_warning" = "Rejestracja jednocześnie za pomocą numeru telefonu i adresu e-mail nie jest obsługiwana dopóki nie pojawi się odpowiednie API. Tylko numer telefonu będzie brany pod uwagę. Możesz dodać adres e-mail do swojego profilu w ustawieniach."; +"auth_reset_password_missing_email" = "Należy wprowadzić adres e-mail powiązany z Twoim kontem."; +"auth_reset_password_email_validation_message" = "Wysłano e-mail na adres %@. Po otwarciu znajdującego się tam linku, kliknij poniżej."; +"auth_reset_password_success_message" = "Twoje hasło zostało zresetowane.\n\nZostałeś wylogowany(-a) ze wszystkich sesji i nie będziesz więcej otrzymywać powiadomień push. Aby ponownie włączyć powiadomienia, zaloguj się ponownie na każdym urządzeniu."; +"auth_add_email_and_phone_warning" = "Jednoczesna rejestracja za pomocą numeru telefonu i adresu e-mail nie jest póki co obsługiwana. Uwzględniany będzie jedynie numer telefonu. Jednakże możesz dodać adres e-mail do swojego profilu w ustawieniach."; // Chat creation -"room_creation_title" = "Nowa rozmowa"; +"room_creation_title" = "Nowy pokój rozmów"; "room_creation_account" = "Konto"; "room_creation_appearance" = "Wygląd"; "room_creation_appearance_name" = "Nazwa"; "room_creation_privacy" = "Prywatność"; "room_recents_directory_section_network" = "Sieć"; "room_recents_favourites_section" = "ULUBIONE"; -"room_recents_people_section" = "LUDZIE"; +"room_recents_people_section" = "OSOBY"; "room_recents_conversations_section" = "POKOJE"; "room_recents_no_conversation" = "Brak pokojów"; "room_recents_low_priority_section" = "NISKI PRIORYTET"; @@ -109,7 +109,7 @@ // Search "search_rooms" = "Pokoje"; "search_messages" = "Wiadomości"; -"search_people" = "Ludzie"; +"search_people" = "Osoby"; "search_files" = "Pliki"; "search_default_placeholder" = "Szukaj"; "search_no_result" = "Brak wyników"; @@ -121,12 +121,12 @@ // Contacts "contacts_address_book_section" = "LOKALNE KONTAKTY"; "contacts_address_book_matrix_users_toggle" = "Tylko użytkownicy Matrixa"; -"contacts_address_book_permission_denied" = "Nie udzieliłeś(-aś) uprawnienia na dostęp do listy kontaktów"; +"contacts_address_book_permission_denied" = "Nie udzieliłeś(-aś) uprawnienia aplikacji Element na dostęp do listy kontaktów"; "contacts_user_directory_section" = "KATALOG UŻYTKOWNIKÓW"; "room_participants_leave_prompt_title" = "Opuść pokój"; "room_participants_remove_prompt_title" = "Potwierdzenie"; "room_participants_invite_prompt_title" = "Potwierdzenie"; -"room_participants_filter_room_members" = "Filtruj członków pokoju"; +"room_participants_filter_room_members" = "Filtruj uczestników pokoju"; "room_participants_idle" = "Bezczynny(-a)"; "room_participants_now" = "teraz"; "room_participants_ago" = "temu"; @@ -143,7 +143,7 @@ "room_participants_action_set_moderator" = "Uczyń moderatorem"; "room_participants_action_set_admin" = "Uczyń administratorem"; "room_participants_action_start_new_chat" = "Rozpocznij nową rozmowę"; -"room_participants_action_start_voice_call" = "Rozpocznij połączenie telefoniczne"; +"room_participants_action_start_voice_call" = "Rozpocznij połączenie głosowe"; "room_participants_action_start_video_call" = "Rozpocznij połączenie wideo"; "room_participants_action_mention" = "Wspomnij"; "room_many_users_are_typing" = "%@, %@ i inni piszą…"; @@ -155,7 +155,7 @@ "room_message_short_placeholder" = "Wyślij wiadomość…"; "room_message_reply_to_short_placeholder" = "Wyślij odpowiedź…"; "room_offline_notification" = "Połączenie z serwerem zostało utracone."; -"room_ongoing_conference_call_close" = "Zamknij"; +"room_ongoing_conference_call_close" = "Usuń"; "room_prompt_resend" = "Wyślij ponownie wszystko"; "room_prompt_cancel" = "anuluj wszystko"; "room_resend_unsent_messages" = "Wyślij niewysłane wiadomości ponownie"; @@ -173,41 +173,41 @@ "room_event_action_save" = "Zapisz"; "room_event_action_resend" = "Wyślij ponownie"; "room_event_action_delete" = "Usuń"; -"room_event_action_cancel_download" = "Anuluj pobieranie"; +"room_event_action_cancel_download" = "Anuluj Pobieranie"; "room_action_send_sticker" = "Wyślij naklejkę"; "room_replacement_information" = "Ten pokój został zamieniony i nie jest już aktywny."; "room_replacement_link" = "Konwersacja jest kontynuowana tutaj."; "room_predecessor_information" = "Ten pokój jest kontynuacją innej rozmowy."; "room_predecessor_link" = "Kliknij tutaj, aby zobaczyć starsze wiadomości."; "room_resource_limit_exceeded_message_contact_2_link" = "skontaktuj się z administratorem"; -"room_resource_limit_exceeded_message_contact_3" = " , aby korzystać dalej z funkcji."; +"room_resource_limit_exceeded_message_contact_3" = " , aby dalej korzystać z tej funkcji."; "room_resource_limit_exceeded_message_contact_1" = " Proszę "; "room_resource_usage_limit_reached_message_1_default" = "Ten serwer przekroczył jeden z limitów, więc "; "room_resource_usage_limit_reached_message_1_monthly_active_user" = "Ten serwer osiągnął miesięczny limit aktywnych użytkowników, więc "; "room_resource_usage_limit_reached_message_2" = "niektórzy użytkownicy nie będą mogli się zalogować."; "room_resource_usage_limit_reached_message_contact_3" = " , aby zwiększył ten limit."; // Unknown devices -"unknown_devices_alert_title" = "Pokój zawiera nieznane urządzenia"; +"unknown_devices_alert_title" = "Pokój zawiera nieznane sesje"; "room_event_action_cancel_send" = "Anuluj Wysyłanie"; "room_event_action_view_encryption" = "Informacje o szyfrowaniu"; "room_event_failed_to_send" = "Nie udało się wysłać"; "room_warning_about_encryption" = "Szyfrowanie End-to-End jest w fazie beta i nie jest pewne. \n\nNie powinieneś ufać mu w celu zabezpieczenia danych.\n\nUrządzenia nie będą w stanie na razie odszyfrować historii sprzed momentu dołączenia do pokoju.\n\nSzyfrowane wiadomości nie będą widoczne dla klientów którzy nie wspierają jeszcze szyfrowania."; -"unknown_devices_alert" = "Ten pokój zawiera nieznane urządzenia, które nie zostały zweryfikowane.\nOznacza to brak gwarancji, że urządzenia należą do użytkowników do których twierdzą, że należą.\nPrzed kontynuowaniem, zalecamy wykonanie procesu weryfikacji każdego urządzenia, ale możesz ponownie wysłać wiadomość bez weryfikacji, jeśli wolisz."; +"unknown_devices_alert" = "Ten pokój zawiera nieznane sesje, które nie zostały zweryfikowane.\nOznacza to brak gwarancji, że sesje należą do użytkowników do których twierdzą, że należą.\nPrzed kontynuowaniem, zalecamy wykonanie procesu weryfikacji każdej sesji, ale możesz ponownie wysłać wiadomość bez weryfikacji, jeśli wolisz."; "unknown_devices_send_anyway" = "Wyślij mimo to"; "unknown_devices_call_anyway" = "Zadzwoń mimo to"; "unknown_devices_answer_anyway" = "Odpowiedz mimo to"; "unknown_devices_verify" = "Zweryfikuj…"; -"unknown_devices_title" = "Nieznane urządzenia"; +"unknown_devices_title" = "Nieznane sesje"; // Room Title "room_title_new_room" = "Nowy pokój"; -"room_title_multiple_active_members" = "%@/%@ aktywnych członków"; -"room_title_invite_members" = "Zaproś członków"; -"room_title_members" = "%@ członków"; -"room_title_one_member" = "1 członek"; +"room_title_multiple_active_members" = "%@/%@ aktywnych uczestników"; +"room_title_invite_members" = "Zaproś uczestników"; +"room_title_members" = "%@ uczestników"; +"room_title_one_member" = "1 uczestnik"; // Room Preview "room_preview_invitation_format" = "Zostałeś(-aś) zaproszony(-a) do tego pokoju przez %@"; "room_preview_subtitle" = "To jest podgląd pokoju. Interakcje zostały zablokowane."; -"room_preview_unlinked_email_warning" = "Zaproszenie zostało wysłane do %@, który nie jest powiązany z tym kontem. Możesz zalogować się z wykorzystaniem innego konta, albo dodać ten adres email do swojego konta."; +"room_preview_unlinked_email_warning" = "Zaproszenie zostało wysłane do %@, który nie jest powiązany z tym kontem. Możesz zalogować się z wykorzystaniem innego konta, albo dodać ten adres e-mail do swojego konta."; "room_preview_try_join_an_unknown_room_default" = "pokój"; // Settings "settings_title" = "Ustawienia"; @@ -221,7 +221,7 @@ "settings_contacts" = "LOKALNE KONTAKTY"; "settings_advanced" = "ZAAWANSOWANE"; "settings_other" = "POZOSTAŁE"; -"settings_devices" = "URZĄDZENIA"; +"settings_devices" = "SESJE"; "settings_cryptography" = "KRYPTOGRAFIA"; "settings_deactivate_account" = "DEZAKTYWUJ KONTO"; "settings_sign_out" = "Wyloguj się"; @@ -253,10 +253,10 @@ "settings_new_password" = "nowe hasło"; "settings_confirm_password" = "potwierdź hasło"; "settings_password_updated" = "Twoje hasło zostało zmienione"; -"settings_crypto_device_name" = "Nazwa urządzenia: "; -"settings_crypto_device_id" = "\nIdentyfikator urządzenia: "; -"settings_crypto_device_key" = "\nKlucz urządzenia: "; -"settings_crypto_blacklist_unverified_devices" = "Szyfruj wiadomości tylko do zaufanych urządzeń"; +"settings_crypto_device_name" = "Nazwa Sesji: "; +"settings_crypto_device_id" = "\nID Sesji: "; +"settings_crypto_device_key" = "\nKlucz Sesji:\n"; +"settings_crypto_blacklist_unverified_devices" = "Szyfruj wiadomości tylko do zaufanych sesji"; "settings_deactivate_my_account" = "Dezaktywuj moje konto"; // Room Details "room_details_title" = "Szczegóły pokoju"; @@ -274,9 +274,9 @@ "room_details_access_section_directory_toggle" = "Wyświetlaj ten pokój na liście pokojów"; "room_details_history_section" = "Kto może przeczytać historię?"; "room_details_history_section_anyone" = "Każdy"; -"room_details_history_section_members_only" = "Tylko członkowie (od momentu włączenia tej opcji)"; -"room_details_history_section_members_only_since_invited" = "Tylko członkowie (od kiedy zostali zaproszeni)"; -"room_details_history_section_members_only_since_joined" = "Tylko członkowie (od kiedy dołączyli)"; +"room_details_history_section_members_only" = "Tylko uczestnicy (od momentu włączenia tej opcji)"; +"room_details_history_section_members_only_since_invited" = "Tylko uczestnicy (od kiedy zostali zaproszeni)"; +"room_details_history_section_members_only_since_joined" = "Tylko uczestnicy (od kiedy dołączyli)"; "room_details_history_section_prompt_title" = "Ostrzeżenie o prywatności"; "room_details_addresses_section" = "Adresy"; "room_details_no_local_addresses" = "Ten pokój nie ma lokalnych adresów"; @@ -288,10 +288,10 @@ "room_details_banned_users_section" = "Zbanowani użytkownicy"; "room_details_advanced_section" = "Zaawansowane"; "room_details_advanced_room_id" = "ID Pokoju:"; -"room_details_advanced_enable_e2e_encryption" = "Włącz szyfrowanie \n(ostrzeżenie: nie może zostać wyłączone!)"; +"room_details_advanced_enable_e2e_encryption" = "Włącz szyfrowanie (ostrzeżenie: nie może zostać wyłączone!)"; "room_details_advanced_e2e_encryption_enabled" = "Szyfrowanie jest włączone w tym pokoju"; "room_details_advanced_e2e_encryption_disabled" = "Szyfrowanie nie jest włączone w tym pokoju."; -"room_details_advanced_e2e_encryption_blacklist_unverified_devices" = "Szyfruj wiadomości tylko do zaufanych urządzeń"; +"room_details_advanced_e2e_encryption_blacklist_unverified_devices" = "Szyfruj wiadomości tylko do zaufanych sesji"; "room_details_set_main_address" = "Ustaw jako główny adres"; "room_details_unset_main_address" = "Nie ustawiaj jako główny adres"; "room_details_copy_room_id" = "Kopiuj ID pokoju"; @@ -299,23 +299,23 @@ "room_details_copy_room_url" = "Kopiuj URL Pokoju"; // Group Details "group_details_title" = "Informacje o społeczności"; -"group_details_people" = "Ludzie"; +"group_details_people" = "Osoby"; "group_details_rooms" = "Pokoje"; // Group Home -"group_home_one_member_format" = "1 członek"; -"group_home_multi_members_format" = "%tu członków"; +"group_home_one_member_format" = "1 uczestnik"; +"group_home_multi_members_format" = "%tu uczestników"; "group_home_one_room_format" = "1 pokój"; "group_home_multi_rooms_format" = "%tu pokojów"; -"group_invitation_format" = "%@ zaprosił(a) Cię do przyłączenia się do tej Społeczności"; +"group_invitation_format" = "%@ zaprosił(-a) Cię do przyłączenia się do tej społeczności"; "group_participants_leave_prompt_title" = "Opuść grupę"; "group_participants_remove_prompt_title" = "Potwierdzenie"; "group_participants_invite_prompt_title" = "Potwierdzenie"; -"group_participants_filter_members" = "Filtruj członków społeczności"; +"group_participants_filter_members" = "Filtruj uczestników społeczności"; "group_participants_invite_malformed_id" = "Uszkodzony ID. Matrix ID powinien być podobny do '@localpart:domain'"; // Group rooms "group_rooms_filter_rooms" = "Filtruj pokoje społeczności"; // Read Receipts -"read_receipts_list" = "Przeczytaj listę odbiorców"; +"read_receipts_list" = "Lista odbiorców"; "media_picker_select" = "Wybierz"; // Directory "directory_title" = "Katalog"; @@ -325,20 +325,20 @@ "directory_server_placeholder" = "matrix.org"; "event_formatter_widget_added" = "widżet %@ został dodany przez %@"; "event_formatter_widget_removed" = "widżet %@ został usunięty przez %@"; -"event_formatter_rerequest_keys_part1_link" = "Poproś ponownie o klucze szyfrujące"; -"event_formatter_rerequest_keys_part2" = " z innych Twoich urządzeń."; +"event_formatter_rerequest_keys_part1_link" = "Poproś o ponowne przesłanie kluczy szyfrujących"; +"event_formatter_rerequest_keys_part2" = " pozostałe swoje sesje."; // Others "or" = "lub"; "you" = "Ty"; "today" = "Dzisiaj"; "yesterday" = "Wczoraj"; // Room key request dialog -"e2e_room_key_request_title" = "Żądanie klucza szyfrującego"; -"e2e_room_key_request_message_new_device" = "Dodałeś(-aś) nowe urządzenie '%@', które żąda kluczy szyfrujących."; -"e2e_room_key_request_message" = "Twoje niezweryfikowane urządzenie '%@' żąda kluczy szyfrujących."; +"e2e_room_key_request_title" = "Zapytanie o klucz szyfrujący"; +"e2e_room_key_request_message_new_device" = "Dodałeś(-aś) nową sesję '%@', która prosi o klucze szyfrujące."; +"e2e_room_key_request_message" = "Twoja niezweryfikowana sesja '%@' prosi o klucze szyfrujące."; "e2e_room_key_request_start_verification" = "Rozpocznij weryfikację…"; "e2e_room_key_request_share_without_verifying" = "Udostępnij bez weryfikacji"; -"gdpr_consent_not_given_alert_review_now_action" = "Przejrzyj teraz"; +"gdpr_consent_not_given_alert_review_now_action" = "Zapoznaj się teraz"; "deactivate_account_title" = "Dezaktywuj konto"; "deactivate_account_informations_part1" = "To sprawi, że Twoje konto stanie się na stałe niezdatne do użytku. Nie będziesz mógł się zalogować i nikt nie będzie mógł ponownie zarejestrować tego samego identyfikatora użytkownika. Spowoduje to, że Twoje konto opuści wszystkie pokoje, w których uczestniczy, i usunie dane Twojego konta z serwera tożsamości. "; "deactivate_account_informations_part2_emphasize" = "Ta czynność jest nieodwracalna."; @@ -353,14 +353,14 @@ "deactivate_account_password_alert_message" = "Aby kontynuować, proszę wprowadzić swoje hasło"; // Re-request confirmation dialog "rerequest_keys_alert_title" = "Prośba wysłana"; -"rerequest_keys_alert_message" = "Uruchom proszę Element na innym urządzeniu, które może odszyfrować wiadomość, aby wysłać klucze do tego urządzenia."; -"on" = "Włączone"; -"video" = "Obraz"; +"rerequest_keys_alert_message" = "Uruchom Element na innym urządzeniu (innej aktywnej sesji) zdolnym do odszyfrowania wiadomości. Dzięki temu sesja zdolna do odszyfrowania wiadomości będzie mogła udostępnić klucze szyfrowania bieżącej sesji."; +"on" = "Włączony"; +"video" = "Połączenie Wideo"; "auth_forgot_password" = "Zapomniałeś(-aś) hasło?"; // Room recents "room_recents_directory_section" = "KATALOG POKOJÓW"; // Rooms tab -"room_directory_no_public_room" = "Nie są dostępne publiczne pokoje"; +"room_directory_no_public_room" = "Brak dostępnych publicznych pokojów"; "directory_cell_description" = "%tu pokojów"; // Chat participants "room_participants_title" = "Uczestnicy"; @@ -370,7 +370,7 @@ "room_participants_leave_prompt_msg" = "Czy napewno chcesz opuścić pokój?"; "room_participants_online" = "Dostępny(-a)"; "room_participants_offline" = "Niedostępny(-a)"; -"room_participants_unknown" = "Nieznane"; +"room_participants_unknown" = "Status nieznany"; "room_participants_action_section_other" = "Opcje"; // Chat "room_jump_to_first_unread" = "Przeskocz do pierwszej nieprzeczytanej wiadomości"; @@ -378,22 +378,22 @@ "room_new_messages_notification" = "%d nowych wiadomości"; "room_one_user_is_typing" = "%@ pisze…"; "room_two_users_are_typing" = "%@ i %@ piszą…"; -"room_ongoing_conference_call" = "Przychodzące połączenie grupowe. Dołącz z %@ lub %@."; -"room_title_one_active_member" = "%@/%@ aktywnych członków"; +"room_ongoing_conference_call" = "W tym pokoju dostępna jest konferencja. Dołącz wybierając %@ lub %@."; +"room_title_one_active_member" = "%@/%@ aktywnych uczestników"; "next" = "Następny"; -"auth_untrusted_id_server" = "Serwer tożsamości nie jest zaufany"; -"room_creation_appearance_picture" = "Obraz rozmowy (opcjonalne)"; -"room_creation_private_room" = "Ta rozmowa jest prywatna"; -"room_creation_public_room" = "Ta rozmowa jest publiczna"; -"room_creation_make_public" = "Zrób publiczny"; -"room_creation_make_public_prompt_title" = "Zrobić tą rozmowę publiczną?"; -"room_creation_make_private" = "Zrób prywatny"; +"auth_untrusted_id_server" = "Niezaufany serwer tożsamości"; +"room_creation_appearance_picture" = "Obraz pokoju rozmów (opcjonalnie)"; +"room_creation_private_room" = "Ten pokój rozmów jest prywatny"; +"room_creation_public_room" = "Ten pokój rozmów jest publiczny"; +"room_creation_make_public" = "Ustaw jako publiczny"; +"room_creation_make_public_prompt_title" = "Ustawić pokój rozmów jako publiczny?"; +"room_creation_make_private" = "Ustaw jako prywatny"; "search_in_progress" = "Wyszukiwanie…"; "contacts_address_book_no_contact" = "Brak lokalnych kontaktów"; "auth_msisdn_validation_error" = "Nie można zweryfikować numeru telefonu."; "room_participants_invite_malformed_id" = "Uszkodzony ID. Powinien być adres e-mail lub Matrix ID podobny do '@localpart:domain'"; "room_unsent_messages_notification" = "Wiadomość nie została wysłana."; -"room_ongoing_conference_call_with_close" = "Przychodzące połączenie grupowe. Dołącz z %@ lub z %@. %@ to."; +"room_ongoing_conference_call_with_close" = "W tym pokoju dostępna jest konferencja. Dołącz wybierając %@ lub %@. %@ konferencję z tego pokoju."; "directory_search_results_title" = "Przeglądaj wyniki katalogów"; "room_event_action_kick_prompt_reason" = "Powód wyrzucenia użytkownika"; "room_action_send_photo_or_video" = "Wyślij zdjęcie lub film"; @@ -410,16 +410,16 @@ "settings_show_decrypted_content" = "Pokaż odszyfrowaną zawartość"; "settings_fail_to_update_password" = "Nie udało się zaktualizować hasła"; "settings_crypto_export" = "Eksportuj klucze"; -"room_details_people" = "Członkowie"; -"room_details_access_section_invited_only" = "Tylko ludzie, którzy zostali zaproszeni"; +"room_details_people" = "Uczestnicy"; +"room_details_access_section_invited_only" = "Tylko osoby, które zostały zaproszone"; "room_details_new_flair_placeholder" = "Dodaj nowe ID społeczności (np. +foo%@)"; -"room_creation_keep_private" = "Zachowaj prywatny"; +"room_creation_keep_private" = "Zachowaj jako prywatny"; "auth_email_not_found" = "Nie udało się wysłać wiadomości e-mail: Adres e-mail nie został znaleziony"; "auth_accept_policies" = "Przeczytaj i zaakceptuj zasady tego serwera domowego:"; "room_creation_wait_for_creation" = "Pokój jest już tworzony. Proszę czekaj."; "room_creation_invite_another_user" = "Szukaj / zaproś przez ID Użytkownika, Nazwę lub e-mail"; "search_people_placeholder" = "Szukaj przez ID Użytkownika, Nazwę lub e-mail"; -"contacts_user_directory_offline_section" = "KATALOG UŻYTKOWNIKA (offline)"; +"contacts_user_directory_offline_section" = "KATALOG UŻYTKOWNIKÓW (offline)"; "room_participants_remove_prompt_msg" = "Czy na pewno chcesz usunąć %@ z tej rozmowy?"; "room_participants_invite_prompt_msg" = "Czy na pewno chcesz zaprosić %@ do tej rozmowy?"; "room_participants_invite_another_user" = "Szukaj / zaproś przez ID Użytkownika, Nazwę lub e-mail"; @@ -428,12 +428,12 @@ "settings_surname" = "Nazwisko"; "settings_first_name" = "Imię"; "settings_unignore_user" = "Pokazać wszystkie wiadomości od %@?"; -"settings_contacts_discover_matrix_users" = "Użyj e-maili i numerów telefonów do odkrycia użytkowników"; +"settings_contacts_discover_matrix_users" = "Użyj adresów e-mail i numerów telefonów do odkrycia użytkowników"; "settings_labs_e2e_encryption_prompt_message" = "Aby zakończyć konfigurowanie szyfrowania, musisz zalogować się ponownie."; "room_details_mute_notifs" = "Wycisz powiadomienia"; // GDPR -"gdpr_consent_not_given_alert_message" = "Aby kontynuować korzystanie z serwera domowego %@, musisz przejrzeć i zaakceptować warunki użytkowania."; -"e2e_room_key_request_ignore_request" = "Zignoruj żądanie"; +"gdpr_consent_not_given_alert_message" = "Aby kontynuować korzystanie z serwera domowego %@, musisz zapoznać się i zaakceptować warunki użytkowania."; +"e2e_room_key_request_ignore_request" = "Zignoruj prośbę"; "widget_integration_room_not_visible" = "Pokój %@ nie jest widoczny."; "widget_integration_missing_user_id" = "Brakujące user_id w żądaniu."; "widget_integration_missing_room_id" = "Brakujące room_id w żądaniu."; @@ -451,25 +451,25 @@ // Widget "widget_no_power_to_manage" = "Potrzebujesz uprawnień do zarządzania widżetami w tym pokoju"; "bug_report_send" = "Wyślij"; -"bug_report_progress_zipping" = "Zbieranie dzienników"; +"bug_report_progress_zipping" = "Zbieranie logów"; "bug_report_progress_uploading" = "Wysyłanie raportu"; "bug_report_send_screenshot" = "Wyślij zrzut ekranu"; "bug_report_logs_description" = "W celu zdiagnozowania problemów, logi z tego klienta zostaną wysłane wraz z tym raportem o błędzie. Jeśli wolisz wysłać tylko powyższy tekst, odznacz:"; "bug_report_send_logs" = "Wyślij logi"; // Bug report "bug_report_title" = "Zgłoś błąd"; -"e2e_need_log_in_again" = "Należy się ponownie zalogować w celu wygenerowania kluczy szyfrowania end-to-end dla tego urządzenia i wysłania klucza publicznego do Twojego serwera domowego.\nJest to jednorazowe działanie; przepraszamy za trudności."; +"e2e_need_log_in_again" = "Należy się ponownie zalogować w celu wygenerowania kluczy szyfrowania end-to-end dla tej sesji i wysłania klucza publicznego do Twojego serwera domowego.\nJest to jednorazowe działanie; przepraszamy za niedogodności."; // Crypto "e2e_enabling_on_app_update" = "Element obsługuje już szyfrowanie end-to-end (E2E), ale musisz zalogować się ponownie, aby je włączyć.\n\nMożesz to zrobić teraz lub później z poziomu ustawień aplikacji."; // No VoIP support "no_voip_title" = "Połączenie przychodzące"; "call_incoming_video" = "Przychodzące połączenie wideo…"; -"call_incoming_voice" = "Połączenie przychodzące…"; +"call_incoming_voice" = "Połączenie przychodzące …"; "call_incoming_video_prompt" = "Przychodzące połączenie wideo od %@"; // Call "call_incoming_voice_prompt" = "Przychodzące połączenie głosowe od %@"; "do_not_ask_again" = "Nie pytaj ponownie"; -"start" = "Rozpocznij"; +"start" = "Start"; "settings_config_home_server" = "Serwerem domowym jest %@"; "settings_config_identity_server" = "Serwerem tożsamości jest %@"; "settings_remove_email_prompt_msg" = "Czy na pewno chcesz usunąć adres e-mail %@?"; @@ -486,14 +486,14 @@ "large_badge_value_k_format" = "%.1fK"; "bug_crash_report_title" = "Raport o awarii"; // String for App Store -"store_short_description" = "Bezpieczny, zdecentralizowany czat/VoIP"; -"store_full_description" = "Komunikuj się, po swojemu.\n\nAplikacja do czatowania, pod Twoją kontrolą i całkowicie elastyczna. Element pozwala Ci komunikować się tak, jak chcesz. Stworzona dla [matrixa] - otwartego standardu, zdecentralizowanej komunikacji.\n\nZałóż darmowe konto na matrix.org, załatw swój własny serwer na https://ems.element.io lub skorzystaj z innego serwera Matrix.\n\nDlaczego warto wybrać Element?\n\nPEŁNA KOMUNIKACJA: Zbuduj pokoje wokół swoich zespołów, przyjaciół, społeczności - jak chcesz! Czat, udostępnianie plików, dodawanie widgetów i wykonywanie połączeń głosowych i wideo - wszystko to za darmo.\n\n\nPOTĘŻNA INTEGRACJA: Użyj Element z narzędziami, które znasz i kochasz. Dzięki Element możesz nawet rozmawiać z użytkownikami i grupami z innymi aplikacjami do czatowania.\n\nPRYWATNY I BEZPIECZNY: Trzymaj swoje rozmowy w tajemnicy. Najnowocześniejsze szyfrowanie typu end-to-end zapewnia prywatną komunikację.\n\nOTWARTY, NIE ZAMKNIĘTY: Open source i zbudowany na Matrixie. Miej swoje dane pod kontrolą poprzez hosting własnego serwera lub wybranie serwera, któremu ufasz.\n\nGDZIEKOLWIEK JESTEŚ: Bądź w kontakcie gdziekolwiek jesteś, dzięki w pełni zsynchronizowanej historii wiadomości na wszystkich Twoich urządzeniach i online na https://app.element.io."; -"room_creation_make_public_prompt_msg" = "Jesteś pewien, że chcesz zrobić ten czat publiczny? Każdy może czytać Twoje wiadomości i dołączyć do czatu."; +"store_short_description" = "Bezpieczny i zdecentralizowany czat/VoIP"; +"store_full_description" = "Element to nowy rodzaj czatu, komunikatora i aplikacji do współpracy, który:\n\n1. Daje Ci kontrolę nad ochroną prywatności\n2. Umożliwia komunikację z każdym w sieci Matrix, a nawet poza nią, poprzez integrację z aplikacjami, takimi jak Slack\n3. Chroni Ciebie przed reklamami, dataminingiem, i backdoorami\n4. Zabezpiecza Cię poprzez szyfrowanie end-to-end z wykorzystaniem cross-signing w celu weryfikacji sesji\n\nElement całkowicie różni się od innych komunikatorów internetowych i aplikacji umożliwiających współpracę, ponieważ jest zdecentralizowany i otwarto źródłowy.\n\nElement umożliwia postawienie własnego serwera lub wybór istniejącego - dzięki temu masz kontrolę nad prywatnością swoich danych i rozmów.\nDodatkowo system ten umożliwia integrację z innymi komunikatorami, dzięki czemu możesz komunikować się nie tylko z użytkownikami sieci matrix.\n\nElement jest w stanie to wszystko zrobić, ponieważ działa na Matrixie - standardzie otwartej, zdecentralizowanej komunikacji umożliwiającej połączenie serwerów domowych w sieć federacji.\n\nElement daje Ci dużą kontrolę nad danymi, ponieważ to Ty wybierasz swój serwer domowy na którym przechowywane są dane. Może to być istniejący w sieci Matrix serwer lub uruchomiona i skonfigurowana przez Ciebie instancja Matrixa, którą w zależności od upodobań możesz przyłączyć do sieci Matrix lub zintegrować z innymi komunikatorami takimi jak np. Slack.\n\nAplikacja Element umożliwia Ci wybranie rodzaju serwera na różne sposoby:\n\n1. Możesz założyć darmowe konto na publicznym serwerze matrix.org\n2. Możesz samodzielnie hostować swoje konto, uruchamiając serwer Matrix na własnym sprzęcie\n3. Możesz założyć konto na serwerze niestandardowym, po prostu subskrybując platformę hostingową Element Matrix Services\n\nDlaczego wybrać Element?\n\nMASZ KONTROLĘ NAD SWOIMI DANYMI: To Ty decydujesz, gdzie chcesz przechowywać swoje dane i wiadomości. To Ty jesteś ich właścicielem i masz nad nimi kontrole, a nie jakaś WIELKA KORPORACJA który wydobywa Twoje dane i udostępnia osobom trzecim.\n\nOTWARTA KOMUNIKACJA: Możesz rozmawiać z kimkolwiek w sieci Matrix, niezależnie od tego, czy używa Element, czy innej aplikacji Matrix, a nawet jeśli używa innego systemu przesyłania wiadomości, takiego jak Slack, IRC lub XMPP.\n\nBEZPIECZEŃSTWO: Prawdziwe szyfrowanie typu end-to-end (tylko uczestnicy rozmowy mogą odszyfrować wiadomości) i cross-signing w celu weryfikacji urządzeń uczestników konwersacji.\n\nPEŁNA KOMUNIKACJA: Wiadomości, rozmowy głosowe i wideo, udostępnianie plików, udostępnianie ekranu i cała masa integracji, botów i widżetów. Twórz pokoje, społeczności, bądź w kontakcie i załatwiaj sprawy.\n\nGDZIEKOLWIEK JESTEŚ: Pozostań w kontakcie, gdziekolwiek jesteś, dzięki w pełni zsynchronizowanej historii wiadomości na wszystkich urządzeniach oraz w Internecie pod adresem https://element.io/app."; +"room_creation_make_public_prompt_msg" = "Jesteś pewien, że chcesz ustawić ten pokój rozmów jako publiczny? Każdy będzie mógł czytać wiadomości i dołączyć do czatu."; "directory_search_fail" = "Nie udało się pobrać danych"; -"contacts_address_book_permission_required" = "Uprawnienie jest wymagane żeby uzyskać dostęp do kontaktów lokalnych"; +"contacts_address_book_permission_required" = "Aby aplikacja mogła uzyskać dostęp do lokalnych kontaktów wymagane jest przyznanie jej odpowiedniego uprawnienia"; "room_participants_invite_malformed_id_title" = "Błąd Zaproszenia"; -"room_participants_action_ban" = "Zbanuj z tego pokoju"; -"room_unsent_messages_unknown_devices_notification" = "Wiadomość nie została wysłana z powodu obecności nieznanych urządzeń. %@ lub %@@ teraz?"; +"room_participants_action_ban" = "Zbanuj w tym pokoju"; +"room_unsent_messages_unknown_devices_notification" = "Wiadomość nie została wysłana z powodu wykrycia nieznanych sesji."; "room_event_action_ban_prompt_reason" = "Powód, dla którego zbanowano tego użytkownika"; "room_event_action_reply" = "Odpowiedz"; "room_event_action_edit" = "Edytuj"; @@ -503,10 +503,10 @@ "settings_flair" = "Pokazuj wyznacznik społeczności gdzie jest to zezwolone"; "settings_key_backup" = "KOPIA ZAPASOWA KLUCZY"; "settings_enable_push_notif" = "Powiadomienia na tym urządzeniu"; -"settings_global_settings_info" = "Globalne ustawienia powiadomień są dostępne na Twoim kliencie internetowym %@"; -"settings_on_denied_notification" = "Powiadomienia są odrzucane w %@, proszę zezwól na nie w ustawieniach urządzenia"; +"settings_global_settings_info" = "Globalne i szczegółowe ustawienia powiadomień są dostępne z poziomu klienta webowego: %@"; +"settings_on_denied_notification" = "Powiadomienia dla aplikacji %@ są wyłączone. Proszę zezwól na nie w ustawieniach urządzenia"; "settings_callkit_info" = "Odbieraj połączenia przychodzące na ekranie blokady. Zobacz swoje połęczenia Element w historii połączeń w systemie. Jeśli usługa iCloud jest włączona, historia połączeń zostanie udostępniona Apple."; -"settings_ui_theme_picker_message" = "\"Auto\" używa ustawienia \"Odwróć kolory\" urządzenia"; +"settings_ui_theme_picker_message" = "\"Auto\" używa ustawienia \"Odwróć kolory\" Twojego urządzenia"; "close" = "Zamknij"; "auth_softlogout_sign_in" = "Zaloguj się"; "auth_softlogout_clear_data_button" = "Wyczyść wszystkie dane"; @@ -516,16 +516,16 @@ "room_action_send_file" = "Wyślij plik"; "room_message_edits_history_title" = "Edycje wiadomości"; "settings_enable_callkit" = "Zintegrowane połączenie"; -"settings_key_backup_info_checking" = "Sprawdzanie..."; +"settings_key_backup_info_checking" = "Sprawdzanie…"; "settings_key_backup_info_version" = "Wersja kopii zapasowej kluczy: %@"; "settings_key_backup_info_algorithm" = "Algorytm: %@"; -"settings_key_backup_info_progress" = "Tworzenie kopii zapasowej %@ kluczy..."; +"settings_key_backup_info_progress" = "Tworzenie kopii zapasowej %@ kluczy…"; "settings_key_backup_info_progress_done" = "Utworzono kopię zapasową wszystkich kluczy"; "settings_key_backup_button_delete" = "Usuń kopię zapasową"; "settings_key_backup_delete_confirmation_prompt_title" = "Usuń kopię zapasową"; -"auth_login_single_sign_on" = "Zaloguj się za pomocą pojedynczego logowania"; -"auth_autodiscover_invalid_response" = "Nieprawidłowa odpowiedź na wykrycie serwera domowego"; -"settings_key_backup_button_create" = "Rozpocznij z użyciem klucza kopii zapasowej"; +"auth_login_single_sign_on" = "Zaloguj się"; +"auth_autodiscover_invalid_response" = "Nie udało się odnaleźć serwera domowego"; +"settings_key_backup_button_create" = "Zacznij korzystać z kopii zapasowej kluczy"; "settings_key_backup_button_restore" = "Przywróć z kopii zapasowej"; "room_details_fail_to_update_avatar" = "Nie udało się zaaktualizować zdjęcia pokoju"; "room_details_fail_to_update_room_name" = "Nie udało się zaaktualizować nazwy pokoju"; @@ -540,94 +540,94 @@ "room_details_fail_to_update_room_communities" = "Nie udało się zaaktualizować powiązanych społeczności"; "room_details_fail_to_update_room_direct" = "Nie udało się zaaktualizować flagi tego pokoju"; "room_details_fail_to_enable_encryption" = "Nie udało się włączyć szyfrowania w tym pokoju"; -"group_details_home" = "Strona domowa"; -"key_backup_setup_intro_setup_action_without_existing_backup" = "Zacznij korzystać z klucza bezpieczeństwa"; -"key_backup_setup_intro_setup_connect_action_with_existing_backup" = "Podłącz to urządzenie do klucza bezpieczeństwa"; -"key_backup_setup_intro_manual_export_info" = "(Zaawansowany)"; +"group_details_home" = "Strona startowa"; +"key_backup_setup_intro_setup_action_without_existing_backup" = "Zacznij korzystać z Bezpiecznej Kopii Kluczy"; +"key_backup_setup_intro_setup_connect_action_with_existing_backup" = "Podłącz tę sesję do Bezpiecznej Kopii Kluczy"; +"key_backup_setup_intro_manual_export_info" = "(Zaawansowane)"; "key_backup_setup_intro_manual_export_action" = "Ręcznie eksportuj klucze"; -"key_backup_setup_passphrase_title" = "Zabezpiecz swoją kopię zapasową hasłem"; -"key_backup_setup_passphrase_info" = "Będziemy przechowywać zaszyfrowaną kopię Twoich kluczy na naszym serwerze. Zabezpiecz swoją kopię zapasową przy pomocy hasła, aby zapewnić jej bezpieczeństwo.\n\nDla maksymalnego bezpieczeństwa - to powinno być inne hasło, niż hasło do konta."; -"key_backup_setup_passphrase_passphrase_title" = "Wejdź"; +"key_backup_setup_passphrase_title" = "Zabezpiecz kopię zapasową za pomocą hasła"; +"key_backup_setup_passphrase_info" = "Będziemy przechowywać zaszyfrowaną kopię Twoich kluczy na naszym serwerze. Chroń swoją kopię zapasową hasłem, aby była bezpieczna.\n\nAby zapewnić maksymalne bezpieczeństwo, hasło kopii zapasowej powinno być inne niż hasło do konta."; +"key_backup_setup_passphrase_passphrase_title" = "Wprowadź"; "key_backup_setup_passphrase_passphrase_placeholder" = "Wprowadź hasło"; "key_backup_setup_passphrase_passphrase_valid" = "Świetnie!"; "key_backup_setup_passphrase_passphrase_invalid" = "Spróbuj dodać słowo"; "key_backup_setup_passphrase_confirm_passphrase_title" = "Potwierdź"; "key_backup_setup_passphrase_confirm_passphrase_placeholder" = "Potwierdź hasło"; "key_backup_setup_passphrase_confirm_passphrase_valid" = "Świetnie!"; -"key_backup_setup_passphrase_confirm_passphrase_invalid" = "Hasło nie pasuje"; +"key_backup_setup_passphrase_confirm_passphrase_invalid" = "Hasło się nie zgadza"; "key_backup_setup_passphrase_set_passphrase_action" = "Ustaw hasło"; -"key_backup_setup_passphrase_setup_recovery_key_info" = "Możesz też zabezpieczyć kopię zapasową przy pomocy klucza odzyskiwania, zapisując ją w bezpiecznym miejscu."; -"key_backup_setup_passphrase_setup_recovery_key_action" = "(Zaawansowany) Konfiguruj za pomocą klucza odzyskiwania"; +"key_backup_setup_passphrase_setup_recovery_key_info" = "Lub zabezpiecz kopię zapasową za pomocą klucza odzyskiwania, zapisując ją w bezpiecznym miejscu."; +"key_backup_setup_passphrase_setup_recovery_key_action" = "(Zaawansowane) Skonfiguruj za pomocą klucza odzyskiwania"; "key_backup_setup_success_title" = "Sukces!"; // Success from passphrase -"key_backup_setup_success_from_passphrase_info" = "Tworzone są kopie zapasowe kluczy.\n\nTwój klucz odzyskiwania to gwarant bezpieczeństwa - możesz użyć go, aby odzyskać dostęp do zaszyfrowanych wiadomości, jeśli zapomnisz hasła.\n\nTrzymaj ten klucz w bezpiecznym miejscu (np. w menedżerze haseł)."; +"key_backup_setup_success_from_passphrase_info" = "Tworzone są kopie zapasowe Twoich kluczy.\n\nTwój klucz odzyskiwania jest gwarancją bezpieczeństwa - możesz go użyć do przywrócenia dostępu do zaszyfrowanych wiadomości, jeśli zapomnisz hasła.\n\nPrzechowuj klucz odzyskiwania w bardzo bezpiecznym miejscu, np. w menedżerze haseł (lub sejfie)."; "key_backup_setup_success_from_passphrase_save_recovery_key_action" = "Zapisz klucz odzyskiwania"; "key_backup_setup_success_from_passphrase_done_action" = "Zrobione"; // Success from recovery key -"key_backup_setup_success_from_recovery_key_info" = "Tworzone są kopie zapasowe kluczy.\n\nZrób kopię tego klucza i przechowuj ją w bezpiecznym miejscu."; +"key_backup_setup_success_from_recovery_key_info" = "Twoje klucze są archiwizowane.\n\nZrób kopię tego klucza odzyskiwania i zachowaj go w bezpiecznym miejscu."; "key_backup_setup_success_from_recovery_key_recovery_key_title" = "Klucz odzyskiwania"; "key_backup_setup_success_from_recovery_key_make_copy_action" = "Zrób kopię"; -"key_backup_setup_success_from_recovery_key_made_copy_action" = "Zrobiłem kopię"; +"key_backup_setup_success_from_recovery_key_made_copy_action" = "Zrobiłem(-am) kopię"; "key_backup_recover_title" = "Bezpieczne wiadomości"; -"key_backup_recover_invalid_passphrase_title" = "Niepoprawne hasło odzyskiwania"; -"key_backup_recover_invalid_passphrase" = "Nie można odszyfrować kopii zapasowej przy użyciu tego hasła: Proszę, sprawdź czy wprowadziłeś(aś) prawidłowe hasło odzyskiwania."; -"key_backup_recover_invalid_recovery_key_title" = "Klucz odzyskiwania nie pasuje"; -"key_backup_recover_invalid_recovery_key" = "Nie można odszyfrować kopii zapasowej przy użycia tego klucza: Proszę, sprawdź czy wpisałeś go poprawnie."; -"key_backup_recover_from_passphrase_info" = "Skorzystaj ze swojego hasła odzyskiwania, aby odblokować historię bezpiecznych wiadomości"; -"key_backup_recover_from_passphrase_passphrase_title" = "Wejdź"; +"key_backup_recover_invalid_passphrase_title" = "Nieprawidłowe hasło odzyskiwania"; +"key_backup_recover_invalid_passphrase" = "Nie można odszyfrować kopii zapasowej przy użyciu tego hasła: sprawdź, czy zostało wprowadzone prawidłowe hasło odzyskiwania."; +"key_backup_recover_invalid_recovery_key_title" = "Niezgodność klucza odzyskiwania"; +"key_backup_recover_invalid_recovery_key" = "Nie można odszyfrować kopii zapasowej za pomocą tego klucza: sprawdź, czy został wprowadzony prawidłowy klucz odzyskiwania."; +"key_backup_recover_from_passphrase_info" = "Użyj hasło odzyskiwania, aby odszyfrować historyczne wiadomości"; +"key_backup_recover_from_passphrase_passphrase_title" = "Wprowadź"; "key_backup_recover_from_passphrase_passphrase_placeholder" = "Wprowadź hasło"; "key_backup_recover_from_passphrase_recover_action" = "Odblokuj historię"; -"key_backup_recover_from_passphrase_lost_passphrase_action_part1" = "Nie znasz hasła odzyskiwania? Możesz "; -"key_backup_recover_from_passphrase_lost_passphrase_action_part2" = "Użyj swojego klucza odzyskiwania"; +"key_backup_recover_from_passphrase_lost_passphrase_action_part1" = "Nie znasz swojego hasła odzyskiwania? Możesz "; +"key_backup_recover_from_passphrase_lost_passphrase_action_part2" = "użyć klucz odzyskiwania"; "key_backup_recover_from_passphrase_lost_passphrase_action_part3" = "."; -"key_backup_recover_from_recovery_key_info" = "Skorzystaj z klucza odzyskiwania, aby odblokować historię bezpiecznych wiadomości"; -"key_backup_recover_from_recovery_key_recovery_key_title" = "Wejdź"; -"key_backup_recover_from_recovery_key_recovery_key_placeholder" = "Wpisz klucz odzyskiwania"; +"key_backup_recover_from_recovery_key_info" = "Użyj klucz odzyskiwania, aby odszyfrować historyczne wiadomości"; +"key_backup_recover_from_recovery_key_recovery_key_title" = "Wprowadź"; +"key_backup_recover_from_recovery_key_recovery_key_placeholder" = "Wprowadź klucz odzyskiwania"; "key_backup_recover_from_recovery_key_recover_action" = "Odblokuj historię"; -"key_backup_recover_from_recovery_key_lost_recovery_key_action" = "Zgubiłeś swój klucz odzyskiwania? Możesz ustawić nowy w ustawieniach."; -"key_backup_recover_success_info" = "Kopia zapasowa została przywrócona!"; -"key_backup_recover_done_action" = "Zrobiono"; +"key_backup_recover_from_recovery_key_lost_recovery_key_action" = "Zgubiłeś(-aś) klucz odzyskiwania? Możesz skonfigurować nowy w ustawieniach."; +"key_backup_recover_success_info" = "Przywrócono kopię zapasową!"; +"key_backup_recover_done_action" = "Zrobione"; "key_backup_setup_banner_title" = "Nigdy nie trać zaszyfrowanych wiadomości"; "key_backup_setup_banner_subtitle" = "Zacznij korzystać z kopii zapasowej"; "key_backup_recover_banner_title" = "Nigdy nie trać zaszyfrowanych wiadomości"; "key_backup_recover_connent_banner_subtitle" = "Podłącz to urządzenie do kopii zapasowej"; -"sign_out_existing_key_backup_alert_title" = "Jesteś pewien, że chcesz się wylogować?"; +"sign_out_existing_key_backup_alert_title" = "Czy na pewno chcesz się wylogować?"; "sign_out_existing_key_backup_alert_sign_out_action" = "Wyloguj się"; -"sign_out_non_existing_key_backup_alert_title" = "Jeśli wylogujesz się teraz, stracisz dostęp do swoich zaszyfrowanych wiadomości"; +"sign_out_non_existing_key_backup_alert_title" = "Jeśli się teraz wylogujesz, utracisz dostęp do zaszyfrowanych wiadomości"; "sign_out_non_existing_key_backup_alert_setup_key_backup_action" = "Zacznij korzystać z kopii zapasowej"; -"sign_out_non_existing_key_backup_alert_discard_key_backup_action" = "Nie chcę swoich zaszyfrowanych wiadomości"; -"sign_out_non_existing_key_backup_sign_out_confirmation_alert_title" = "Utracisz zaszyfrowane wiadomości"; -"sign_out_non_existing_key_backup_sign_out_confirmation_alert_message" = "Utracisz dostęp do zaszyfrowanych wiadomości, jeżeli wylogujesz się przed utworzeniem kopii zapasowej."; -"sign_out_non_existing_key_backup_sign_out_confirmation_alert_sign_out_action" = "Wylogowanie"; -"sign_out_non_existing_key_backup_sign_out_confirmation_alert_backup_action" = "Kopia zapasowa"; -"sign_out_key_backup_in_progress_alert_title" = "Trwa tworzenie kopii zapasowej. Jeżeli wylogujesz się teraz - Stracisz dostęp do swoich zaszyfrowanych wiadomości."; +"sign_out_non_existing_key_backup_alert_discard_key_backup_action" = "Nie chcę moich zaszyfrowanych wiadomości"; +"sign_out_non_existing_key_backup_sign_out_confirmation_alert_title" = "Stracisz zaszyfrowane wiadomości"; +"sign_out_non_existing_key_backup_sign_out_confirmation_alert_message" = "Utracisz dostęp do zaszyfrowanych wiadomości, chyba że wykonasz kopię zapasową kluczy przed wylogowaniem."; +"sign_out_non_existing_key_backup_sign_out_confirmation_alert_sign_out_action" = "Wyloguj się"; +"sign_out_non_existing_key_backup_sign_out_confirmation_alert_backup_action" = "Utwórz kopię zapasową"; +"sign_out_key_backup_in_progress_alert_title" = "Trwa tworzenie kopii zapasowej klucza. Jeśli wylogujesz się teraz, utracisz dostęp do swoich zaszyfrowanych wiadomości."; "sign_out_key_backup_in_progress_alert_discard_key_backup_action" = "Nie chcę moich zaszyfrowanych wiadomości"; "sign_out_key_backup_in_progress_alert_cancel_action" = "Poczekam"; // MARK: - Device Verification "device_verification_title" = "Zweryfikuj urządzenie"; "device_verification_security_advice" = "Dla zapewnienia maksymalnego bezpieczeństwa zalecamy zrobienie tego samemu, bądź skorzystanie z innego, zaufanego środka komunikacji"; -"device_verification_cancelled" = "Druga strona przerwała weryfikację."; -"device_verification_cancelled_by_me" = "Weryfikacja została przerwana. Powód: %@"; -"device_verification_error_cannot_load_device" = "Nie można załadować informacji o urządzeniu."; +"device_verification_cancelled" = "Druga strona anulowała weryfikację."; +"device_verification_cancelled_by_me" = "Weryfikacja została anulowana. Powód: %@"; +"device_verification_error_cannot_load_device" = "Nie można załadować informacji dotyczących sesji."; // Mark: Incoming -"device_verification_incoming_title" = "Przyszła prośba o weryfikację"; -"device_verification_incoming_description_1" = "Zweryfikuj to urządzenie, aby oznaczyć je jako zaufane. Zaufane urządzenia dają Twoim rozmówcom dodatkowe poczucie spokoju, podczas konwersacji z włączonymi szyfrowanymi wiadomościami end-to-end."; -"device_verification_incoming_description_2" = "Weryfikacja tego urządzenia spowoduje oznaczenie go, jako zaufanego. Zostanie ono oznaczone jako zaufane również na urządzeniu Twojego rozmówcy."; +"device_verification_incoming_title" = "Przychodząca prośba o weryfikację"; +"device_verification_incoming_description_1" = "Zweryfikuj tą sesję, aby oznaczyć ją jako zaufaną. Zaufane sesje dają Twoim rozmówcom dodatkowe poczucie spokoju, podczas konwersacji w pokojach z włączonym szyfrowaniem wiadomości end-to-end."; +"device_verification_incoming_description_2" = "Weryfikacja tej sesji spowoduje oznaczenie jej, jako zaufanej. Dodatkowo zostanie ona oznaczona jako zaufana również dla Twojego rozmówcy."; // MARK: Start "device_verification_start_title" = "Zweryfikuj, porównując krótki tekst"; -"device_verification_start_wait_partner" = "Oczekiwanie na zaakceptowanie przez rozmówce..."; +"device_verification_start_wait_partner" = "Oczekiwanie na zaakceptowanie przez rozmówce…"; "device_verification_start_use_legacy" = "Nic się nie pojawiło? Nie wszystkie wersje aplikacji obsługują jeszcze interaktywną weryfikację. Prosimy o skorzystanie ze starszej wersji."; "device_verification_start_verify_button" = "Rozpocznij weryfikację"; "device_verification_start_use_legacy_action" = "Użyj starszej wersji weryfikacji"; // MARK: Verify "device_verification_verify_title_emoji" = "Sprawdź to urządzenie potwierdzając, że następujące emotikony pojawiają się na ekranie rozmówcy"; "device_verification_verify_title_number" = "Sprawdź to urządzenie potwierdzając, że następujące liczby pojawiają się na ekranie rozmówcy"; -"device_verification_verify_wait_partner" = "Oczekiwanie na rozmówce w celu potwierdzenia..."; +"device_verification_verify_wait_partner" = "Oczekiwanie na rozmówce w celu potwierdzenia…"; // MARK: Verified "device_verification_verified_title" = "Zweryfikowano!"; "device_verification_verified_description_1" = "Pomyślnie zweryfikowano to urządzenie."; "device_verification_verified_description_2" = "Bezpieczne wiadomości z tym użytkownikiem są szyfrowane metodą end-to-end i nie mogą zostać odczytane przez osoby trzecie."; -"device_verification_verified_got_it_button" = "Zrobione"; +"device_verification_verified_got_it_button" = "Rozumiem"; // MARK: Emoji "device_verification_emoji_dog" = "Pies"; "device_verification_emoji_cat" = "Kot"; @@ -692,10 +692,10 @@ "device_verification_emoji_anchor" = "Kotwica"; "device_verification_emoji_headphones" = "Słuchawki"; "device_verification_emoji_folder" = "Folder"; -"device_verification_emoji_pin" = "Przypnij"; +"device_verification_emoji_pin" = "Przypinka"; // MARK: File upload -"file_upload_error_title" = "Wyślij plik"; -"file_upload_error_unsupported_file_type_message" = "Typ pliku nie jest wspierany."; +"file_upload_error_title" = "Udostępnianie plików"; +"file_upload_error_unsupported_file_type_message" = "Nieobsługiwany typ pliku."; // MARK: Emoji picker "emoji_picker_title" = "Reakcje"; "emoji_picker_people_category" = "Emotikony i ludzie"; @@ -709,41 +709,41 @@ // MARK: Reaction history "reaction_history_title" = "Reakcje"; "auth_forgot_password_error_no_configured_identity_server" = "Brak skonfigurowanego serwera tożsamości: dodaj serwer tożsamości, aby zresetować hasło."; -"auth_softlogout_signed_out" = "Zostałeś wylogowany"; +"auth_softlogout_signed_out" = "Zostałeś(-aś) wylogowany(-a)"; "auth_softlogout_reason" = "Administrator serwera domowego (%1$@) wylogował Cię z Twojego konta %2$@ (%3$@)."; "auth_softlogout_recover_encryption_keys" = "Zaloguj się aby odzyskać klucze szyfrujące zapisane na tym urządzeniu. Potrzebujesz ich aby odczytać wszystkie swoje zaszyfrowane wiadomości na którymkolwiek z Twoich urządzeń."; "auth_softlogout_clear_data" = "Wyczyść prywatne dane"; "auth_softlogout_clear_data_message_1" = "Uwaga: Twoje prywatne dane (z uwzględnieniem kluczy szyfrujących) nadal znajdują się na tym urządzeniu."; "auth_softlogout_clear_data_message_2" = "Wyczyść je, jeżeli nie będziesz już używać tego urządzenia, lub jeśli planujesz zalogować się na inne konto."; -"auth_softlogout_clear_data_sign_out_title" = "Czy jesteś pewien?"; +"auth_softlogout_clear_data_sign_out_title" = "Czy jesteś pewien/pewna?"; "auth_softlogout_clear_data_sign_out_msg" = "Czy napewno chcesz wyczyścić wszystkie dane znajdujące się na tym urządzeniu? Zaloguj się ponownie, aby mieć dostęp do swojego konta, oraz wiadomości."; -"room_creation_error_invite_user_by_email_without_identity_server" = "Serwer tożsamości nie został skonfigurowany, więc nie możesz dodać uczestnika przy użyciu adresu email."; +"room_creation_error_invite_user_by_email_without_identity_server" = "Brak skonfigurowanego serwera tożsamości, więc nie możesz dodać uczestnika przy użyciu adresu e-mail."; "room_participants_remove_third_party_invite_msg" = "Usuwanie zaproszeń nie jest wspierane przez obecną wersję API"; "room_participants_invited_section" = "ZAPROSZENI"; -"room_participants_start_new_chat_error_using_user_email_without_identity_server" = "Serwer tożsamości nie został skonfigurowany, więc nie możesz rozpocząć rozmowy używając adresu email."; +"room_participants_start_new_chat_error_using_user_email_without_identity_server" = "Brak skonfigurowanego serwera tożsamości, więc nie możesz rozpocząć rozmowy z tą osobą używając adresu e-mail."; "room_message_unable_open_link_error_message" = "Nie udało się otworzyć adresu."; -"room_conference_call_no_power" = "Potrzebujesz uprawnień, aby zarządzać połączeniami konferencyjnymi w tym pokoju"; +"room_conference_call_no_power" = "Nie masz wystarczających uprawnień, aby zarządzać połączeniami konferencyjnymi w tym pokoju"; "room_event_action_reaction_history" = "Historia reakcji"; "room_action_camera" = "Zrób zdjęcie lub nagraj wideo"; "settings_config_no_build_info" = "Brak informacji o buildzie"; "settings_labs_message_reaction" = "Odpowiadaj na wiadomości używając emoji"; -"settings_key_backup_info" = "Zaszyfrowane wiadomości są zabezpieczone przy użyciu szyfrowania end-to-end. Tylko Ty oraz ich adresaci posiadają klucze do ich rozszyfrowania."; -"settings_key_backup_info_none" = "Twoje klucze z tego urządzenia nie posiadają kopii zapasowej."; -"settings_key_backup_info_signout_warning" = "Podłącz to urządzenie do klucza kopii zapasowej przed wylogowaniem się, aby nie utracić kluczy, które dostępne są tylko na tym urządzeniu."; -"settings_key_backup_info_valid" = "Kopia zapasowa twoich kluczy jest tworzona."; -"settings_key_backup_info_trust_signature_unknown" = "Kopia zapasowa posiada podpis urządzenia o ID: %@"; -"settings_key_backup_info_trust_signature_valid" = "Kopia zapasowa posiada prawidłowy podpis tego urządzenia"; -"settings_key_backup_info_trust_signature_valid_device_verified" = "Kopia zapasowa posiada prawidłowy podpis urządzenia %@"; -"settings_key_backup_info_trust_signature_valid_device_unverified" = "Kopia zapasowa posiada podpis urządzenia %@"; -"settings_key_backup_info_trust_signature_invalid_device_verified" = "Kopia zapasowa posiada nieprawidłowy podpis urządzenia %@"; -"settings_key_backup_info_trust_signature_invalid_device_unverified" = "Kopia zapasowa posiada nieprawidłowy podpis urządzenia %@"; -"settings_key_backup_button_connect" = "Podłącz to urządzenie do kopii zapasowej kluczy"; -"settings_key_backup_delete_confirmation_prompt_msg" = "Czy jesteś pewien? Stracisz dostęp do wszystkich swoich zaszyfrowanych wiadomości, jeżeli nie utworzyłeś poprawnej kopii zapasowej kluczy."; +"settings_key_backup_info" = "Zaszyfrowane wiadomości są zabezpieczone przy użyciu szyfrowania end-to-end. Tylko Ty oraz ich adresaci posiadają klucze do ich odszyfrowania."; +"settings_key_backup_info_none" = "Twoje klucze z bieżącej sesji nie zostały przesłane do kopii zapasowej."; +"settings_key_backup_info_signout_warning" = "Podłącz bieżącą sesję do kopii zapasowej przed wylogowaniem się, aby nie utracić kluczy, które dostępne są tylko w bieżącej sesji."; +"settings_key_backup_info_valid" = "Ta sesja tworzy kopię zapasową kluczy."; +"settings_key_backup_info_trust_signature_unknown" = "Kopia zapasowa posiada podpis sesji o ID: %@"; +"settings_key_backup_info_trust_signature_valid" = "Kopia zapasowa posiada prawidłowy podpis tej sesji"; +"settings_key_backup_info_trust_signature_valid_device_verified" = "Kopia zapasowa posiada prawidłowy podpis od %@"; +"settings_key_backup_info_trust_signature_valid_device_unverified" = "Kopia zapasowa posiada podpis od %@"; +"settings_key_backup_info_trust_signature_invalid_device_verified" = "Kopia zapasowa posiada nieprawidłowy podpis od %@"; +"settings_key_backup_info_trust_signature_invalid_device_unverified" = "Kopia zapasowa posiada nieprawidłowy podpis od %@"; +"settings_key_backup_button_connect" = "Podłącz tą sesję do kopii zapasowej kluczy"; +"settings_key_backup_delete_confirmation_prompt_msg" = "Czy jesteś pewien/pewna? Stracisz dostęp do wszystkich swoich zaszyfrowanych wiadomości, jeżeli nie utworzyłeś(-aś) poprawnej kopii zapasowej kluczy."; "room_details_access_section" = "Kto może dołączyć do pokoju?"; "room_details_history_section_prompt_msg" = "Zmiany dostępu do historii będą miały zastosowanie tylko do przyszłych wiadomości w tym pokoju. Widoczność obecnych wiadomości pozostanie bez zmian."; -"settings_key_backup_info_not_valid" = "To urządzenie nie tworzy kopii zapasowej Twoich kluczy. Ciągle jednak masz istniejącą kopię zapasową, którą możesz przywrócić i w przyszłości uzupełnić."; -"room_details_addresses_disable_main_address_prompt_title" = "Główny adres ostrzega"; -"room_details_addresses_disable_main_address_prompt_msg" = "Nie masz adresu głównego. Domyślny główny adres zostanie wybrany losowo"; +"settings_key_backup_info_not_valid" = "Ta sesja nie przesyła kluczy do kopii zapasowej, jednakże posiadasz historyczną kopie zapasową z której możesz odzyskać historyczne klucze. Tworząc w tym momencie nową kopię zapasową kluczy będziesz miał(-a) możliwość uzupełnienia jej o klucze historyczne i bieżące."; +"room_details_addresses_disable_main_address_prompt_title" = "Ostrzeżenie dotyczące głównego adresu"; +"room_details_addresses_disable_main_address_prompt_msg" = "Nie będziesz mieć określonego adresu głównego. Domyślny główny adres tego pokoju zostanie wybrany losowo"; "room_details_flair_section" = "Pokaż talent społecznościom"; "room_details_flair_invalid_id_prompt_msg" = "%@ nie jest prawidłowym identyfikatorem społeczności"; "group_participants_leave_prompt_msg" = "Czy jesteś pewien, że chcesz opuścić te grupę?"; @@ -751,7 +751,7 @@ "group_participants_invite_prompt_msg" = "Czy jesteś pewien, że chcesz zaprosić %@ do tej grupy?"; "group_participants_invite_malformed_id_title" = "Błąd. Nie udało się zaprosić"; "group_participants_invited_section" = "ZAPROSZONY"; -"receipt_status_read" = "Czytaj: "; +"receipt_status_read" = "Odczytano: "; // Media picker "media_picker_title" = "Selektor mediów"; // Image picker @@ -759,7 +759,7 @@ "image_picker_action_library" = "Wybierz z biblioteki"; "directory_server_all_native_rooms" = "Wszystkie rodzime pokoje Matrix"; // Events formatter -"event_formatter_member_updates" = "%tu zmiany członkostwa"; +"event_formatter_member_updates" = "%tu zmiany uczestnictwa"; "event_formatter_jitsi_widget_added" = "Konferencja VoIP dodana przez %@"; "event_formatter_jitsi_widget_removed" = "Konferencja VoIP usunięta przez %@"; "event_formatter_message_edited_mention" = "(edytowano)"; @@ -772,7 +772,7 @@ "camera_unavailable" = "Aparat jest niedostępny na Twoim urządzeniu"; "photo_library_access_not_granted" = "%@ nie ma uprawnień dostępu do biblioteki zdjęć, możesz to zmienić w ustawieniach prywatności"; "room_does_not_exist" = "%@ nie istnieje"; -"call_already_displayed" = "Rozmowa tutaj już trwa."; +"call_already_displayed" = "Masz już trwającą rozmowę."; "call_jitsi_error" = "Nie udało się dołączyć do rozmowy konferencyjnej."; "no_voip" = "%@ dzwoni do Ciebie ale %@ nie obsługuje jeszcze połączeń\nMożesz zignorować to powiadomienie i odebrać rozmowę na innym urządzeniu, bądź odrzucić je."; // Crash report @@ -780,38 +780,38 @@ // Key backup wrong version "e2e_key_backup_wrong_version_title" = "Nowy Klucz Kopii Zapasowej"; "room_participants_remove_third_party_invite_prompt_msg" = "Czy jesteś pewien, że chcesz odrzucić to zaproszenie?"; -"e2e_key_backup_wrong_version" = "Wykryto nową wiadomość z kluczem zabezpieczeń. \nJeżeli to nie Ty - Prosimy, abyś zmienił swoje hasło w ustawieniach."; +"e2e_key_backup_wrong_version" = "Wykryto nową kopię bezpieczeństwa. \n\nJeżeli to działanie wystąpiło bez Twojej wiedzy to zmień hasło odzyskiwania."; "e2e_key_backup_wrong_version_button_settings" = "Ustawienia"; -"e2e_key_backup_wrong_version_button_wasme" = "To ja"; -"bug_report_description" = "Proszę opisać błąd. Co zrobiłeś? Co miało się stać? Co się stało?"; -"bug_crash_report_description" = "Opisz, co zrobiłeś przed awarią:"; +"e2e_key_backup_wrong_version_button_wasme" = "To byłem ja"; +"bug_report_description" = "Proszę opisać błąd. Co zrobiłeś(-aś)? Co miało się stać? Co się stało?"; +"bug_crash_report_description" = "Opisz, co zrobiłeś(-aś) przed awarią:"; // Widget "widget_no_integrations_server_configured" = "Nie skonfigurowano serwera integracji"; -"widget_integrations_server_failed_to_connect" = "Błąd łączenia z serwerem integracji"; +"widget_integrations_server_failed_to_connect" = "Błąd połączenia z serwerem integracji"; // Share extension "share_extension_auth_prompt" = "Zaloguj się do głównej aplikacji, aby udostępniać zawartość"; "share_extension_failed_to_encrypt" = "Nie udało się wysłać. Sprawdź w głównej aplikacji ustawienia szyfrowania dla tego pokoju"; // Service terms "service_terms_modal_title" = "Warunki usługi"; -"service_terms_modal_message" = "Aby kontynuować musisz zaakceptować Warunki Usługi."; +"service_terms_modal_message" = "Aby kontynuować musisz zaakceptować warunki usługi (%@)."; "service_terms_modal_accept_button" = "Akceptuj"; "service_terms_modal_description_for_identity_server" = "Być wykrywalnym dla innych"; "service_terms_modal_description_for_integration_manager" = "Używaj Botów, mostków, widżetów i naklejek"; -"key_backup_setup_title" = "Klucz kopii zapasowej"; +"key_backup_setup_title" = "Kopia zapasowa kluczy"; "key_backup_setup_skip_alert_title" = "Jesteś pewien?"; -"key_backup_setup_skip_alert_message" = "Możesz stracić wiadomości bezpieczeństwa, jeżeli wylogujesz się z, bądź stracisz swoje urządzenie."; +"key_backup_setup_skip_alert_message" = "Możesz utracić zaszyfrowane wiadomości, jeśli wylogujesz się lub zgubisz urządzenie."; "key_backup_setup_skip_alert_skip_action" = "Pomiń"; -"key_backup_setup_intro_title" = "Nigdy nie trać zaszyfrowanych wiadomości"; -"key_backup_setup_intro_info" = "Wiadomości w szyfrowanym pokoju są zabezpieczone przez end-to-end. Tylko Ty i rozmówca macie klucze, aby odczytać te wiadomości.\nWykonaj kopię zapasową kluczy, aby ich nie zgubić."; +"key_backup_setup_intro_title" = "Nigdy nie strać zaszyfrowanych wiadomości"; +"key_backup_setup_intro_info" = "Wiadomości w zaszyfrowanych pokojach są zabezpieczone szyfrowaniem end-to-end. Tylko Ty i adresaci macie klucze do tych wiadomości.\n\nZabezpiecz swoje klucze, aby ich nie zgubić."; "user_verification_session_details_trusted_title" = "Zaufany"; "user_verification_session_details_untrusted_title" = "Niezaufany"; "user_verification_session_details_information_trusted_current_user" = "Ta sesja jest zaufana dla bezpiecznego przesyłania wiadomości, ponieważ ją zweryfikowałeś:"; "skip" = "Pomiń"; "joined" = "Dołączył"; -"switch" = "Zmień"; +"switch" = "Przełącz"; "more" = "Więcej"; // Accessibility -"accessibility_checkbox_label" = "pole wyboru"; +"accessibility_checkbox_label" = "przełącznik"; // MARK: Clients "client_desktop_name" = "Element Desktop"; "client_web_name" = "Element Web"; @@ -843,29 +843,29 @@ "media_type_accessibility_location" = "Lokalizacja"; "media_type_accessibility_file" = "Plik"; "media_type_accessibility_sticker" = "Naklejka"; -"external_link_confirmation_title" = "Sprawdź ten link"; -"external_link_confirmation_message" = "Link %@ przeniesie Cię na inną stronę: %@\n\nJesteś pewny, że chcesz kontynuować?"; -"settings_discovery_settings" = "DISCOVERY"; -"settings_identity_server_settings" = "Serwer tożsamości"; +"external_link_confirmation_title" = "Sprawdź dokładnie ten link"; +"external_link_confirmation_message" = "Link %@ przeniesie Cię na inną stronę: %@\n\nJesteś pewny(-a), że chcesz kontynuować?"; +"settings_discovery_settings" = "ODKRYWANIE"; +"settings_identity_server_settings" = "SERWER TOŻSAMOŚCI"; "settings_integrations" = "INTEGRACJE"; -"settings_three_pids_management_information_part2" = "Discovery"; +"settings_three_pids_management_information_part2" = "Odkrywanie"; "settings_three_pids_management_information_part3" = "."; "settings_security" = "BEZPIECZEŃSTWO"; "settings_calls_stun_server_fallback_button" = "Zezwalaj na rezerwowy serwer połączeń"; "settings_integrations_allow_button" = "Zarządzaj integracjami"; -"settings_add_3pid_password_title_email" = "Dodaj adres email"; +"settings_add_3pid_password_title_email" = "Dodaj adres e-mail"; "settings_add_3pid_password_title_msidsn" = "Dodaj numer telefonu"; -"settings_add_3pid_password_message" = "Aby kontynuować, podaj swoje hasło"; -"settings_add_3pid_invalid_password_message" = "Niepoprawne hasło"; -"settings_discovery_three_pids_management_information_part2" = "Ustawienia użytkownika"; +"settings_add_3pid_password_message" = "Aby kontynuować, wprowadź swoje hasło"; +"settings_add_3pid_invalid_password_message" = "Niepoprawne poświadczenia"; +"settings_discovery_three_pids_management_information_part2" = "Ustawieniach użytkownika"; "settings_discovery_three_pids_management_information_part3" = "."; "settings_discovery_error_message" = "Wystąpił błąd. Spróbuj ponownie."; -"settings_discovery_three_pid_details_title_email" = "Zarządzaj email"; +"settings_discovery_three_pid_details_title_email" = "Zarządzaj e-mail"; "settings_discovery_three_pid_details_title_phone_number" = "Zarządzaj numerem telefonu"; "settings_discovery_three_pid_details_share_action" = "Udostępnij"; "settings_discovery_three_pid_details_revoke_action" = "Unieważnij"; -"settings_discovery_three_pid_details_cancel_email_validation_action" = "Przerwij walidacje adresu email"; -"settings_discovery_three_pid_details_enter_sms_code_action" = "Podaj kod aktywacyjny SMS"; +"settings_discovery_three_pid_details_cancel_email_validation_action" = "Przerwij walidacje adresu e-mail"; +"settings_discovery_three_pid_details_enter_sms_code_action" = "Wprowadź kod aktywacyjny SMS"; "settings_identity_server_no_is" = "Nieskonfigurowany serwer tożsamości"; // Security settings "security_settings_title" = "Bezpieczeństwo"; @@ -876,8 +876,548 @@ "security_settings_secure_backup_synchronise" = "Synchronizuj"; "security_settings_secure_backup_delete" = "Usuń"; "security_settings_backup" = "BACKUP WIADOMOŚCI"; -"security_settings_crosssigning" = "PODPISYWANIE KRZYŻOWE"; -"security_settings_crosssigning_info_not_bootstrapped" = "Podpisywanie krzyżowe nie jest ustawione."; +"security_settings_crosssigning" = "CROSS-SIGNING"; +"security_settings_crosssigning_info_not_bootstrapped" = "Cross-signing nie został jeszcze skonfigurowany."; "room_info_list_section_other" = "Inne"; -"auth_phone_is_required" = "Żaden serwer tożsamości nie jest skonfigurowany, więc nie można dodać numeru telefonu w celu zresetowania hasła w przyszłości."; +"auth_phone_is_required" = "Brak skonfigurowanego serwera tożsamości, nie możesz dodać numeru telefonu na wypadek konieczności zresetowania hasła."; "less" = "Mniej"; +"room_intro_cell_information_multiple_dm_sentence2" = "Tylko Ty bierzesz udział w tej rozmowie, dopóki kogoś nie zaprosisz."; +"room_intro_cell_information_dm_sentence2" = "Tylko wy dwoje uczestniczycie w tej rozmowie, nikt inny nie może się przyłączyć."; +"room_intro_cell_information_dm_sentence1_part3" = ". "; +"room_intro_cell_information_dm_sentence1_part1" = "Rozpocząłeś rozmowę bezpośrednią z "; +"room_intro_cell_information_room_without_topic_sentence2_part2" = " aby ludzie wiedzieli na jaki temat rozmawiać w tym pokoju."; +"room_intro_cell_information_room_without_topic_sentence2_part1" = "Dodaj temat"; +"room_intro_cell_information_room_with_topic_sentence2" = "Temat: %@"; +"room_intro_cell_information_room_sentence1_part3" = ". "; +"room_intro_cell_information_room_sentence1_part1" = "To jest początek "; + +// Mark: - Room creation introduction cell + +"room_intro_cell_add_participants_action" = "Dodaj osoby"; + +// Mark: - Room avatar view + +"room_avatar_view_accessibility_label" = "awatar pokoju"; +"room_avatar_view_accessibility_hint" = "Zmień awatar pokoju"; +"invite_friends_share_text" = "Hej, porozmawiaj ze mną na %@: %@"; + +// MARK: - Invite friends + +"invite_friends_action" = "Zaproś znajomych do %@"; +"favourites_empty_view_information" = "Możesz oznaczyć pokoje rozmów jako ulubione. W tym celu naciśnij i przytrzymaj palec na ikonie wybranego pokoju na stronie startowej aplikacji, a następnie wybierz gwiazdkę."; + +// MARK: - Favourites + +"favourites_empty_view_title" = "Ulubione pokoje i osoby"; +"home_empty_view_information" = "Kompleksowa, bezpieczna aplikacja do czatu dla zespołów, przyjaciół i organizacji. Dotknij (+), aby dodać osoby i pokoje."; + +// MARK: - Home + +"home_empty_view_title" = "Witaj w %@,\n%@"; +"call_transfer_error_message" = "Przekazywanie połączeń nie powiodło się"; +"call_transfer_error_title" = "Błąd"; +"call_transfer_contacts_all" = "Wszystkie"; +"call_transfer_contacts_recent" = "Ostatnie"; +"call_transfer_dialpad" = "Klawiatura numeryczna"; +"call_transfer_users" = "Użytkownicy"; + +// MARK: - Call Transfer +"call_transfer_title" = "Przekazywanie połączeń"; + +// MARK: - Dial Pad +"dialpad_title" = "Klawiatura numeryczna"; +"room_info_list_several_members" = "%@ uczestników"; + +// MARK: - Room Info + +"room_info_list_one_member" = "1 uczestnik"; +"create_room_placeholder_address" = "#testroom:matrix.org"; +"create_room_section_header_address" = "Adres pokoju"; +"create_room_show_in_directory" = "Pokaż pokój w katalogu pokojów"; +"create_room_section_footer_type" = "Tylko zaproszone osoby mogą dołączyć do pokoju prywatnego."; +"create_room_type_public" = "Pokój publiczny"; +"create_room_type_private" = "Pokój prywatny"; +"create_room_section_header_type" = "Rodzaj pokoju"; +"create_room_section_footer_encryption" = "Szyfrowania nie można później wyłączyć."; +"create_room_enable_encryption" = "Włącz szyfrowanie"; +"create_room_section_header_encryption" = "Szyfrowanie pokoju"; +"create_room_placeholder_topic" = "Temat"; +"create_room_section_header_topic" = "Temat pokoju (opcjonalnie)"; +"create_room_placeholder_name" = "Nazwa"; +"create_room_section_header_name" = "Nazwa pokoju"; + +// MARK: - Create Room + +"create_room_title" = "Nowy pokój"; +"searchable_directory_search_placeholder" = "Nazwa lub ID"; +"searchable_directory_x_network" = "%@ Network"; + +// MARK: - Searchable Directory View Controller + +"searchable_directory_create_new_room" = "Utwórz nowy pokój"; +"biometrics_cant_unlocked_alert_message_retry" = "Spróbuj ponownie"; +"biometrics_cant_unlocked_alert_message_login" = "Zaloguj się ponownie"; +"biometrics_cant_unlocked_alert_message_x" = "Aby włączyć, użyj %@ lub zaloguj się ponownie i włącz %@ ponownie"; +"biometrics_cant_unlocked_alert_title" = "Nie można odblokować aplikacji"; +"biometrics_usage_reason" = "Aby uzyskać dostęp do aplikacji, potrzebne jest uwierzytelnienie"; +"biometrics_desetup_disable_button_title_x" = "Wyłącz %@"; +"biometrics_desetup_title_x" = "Wyłącz %@"; +"biometrics_setup_subtitle" = "Oszczędzaj czas"; +"biometrics_setup_enable_button_title_x" = "Włącz %@"; +"biometrics_setup_title_x" = "Włącz %@"; +"biometrics_settings_enable_x" = "Włącz %@"; +"biometrics_mode_face_id" = "Face ID"; + +// MARK: - Biometrics Protection + +"biometrics_mode_touch_id" = "Touch ID"; +"pin_protection_kick_user_alert_message" = "Zbyt dużo błędnych prób, zostałeś(-aś) wylogowany(-a)"; +"pin_protection_explanatory" = "Ustawienie PINu pozwala na dodatkową ochronę wiadomości i kontaktów."; +"pin_protection_not_allowed_pin" = "Ten PIN nie jest bezpieczny. Użyj inny PIN"; +"pin_protection_settings_change_pin" = "Zmień PIN"; +"pin_protection_settings_enable_pin" = "Włącz PIN"; +"pin_protection_settings_enabled_forced" = "Blokada PIN włączona"; +"pin_protection_settings_section_footer" = "Aby zresetować PIN wyloguj się i zaloguj ponownie."; +"pin_protection_settings_section_header_x" = "PIN & %@"; +"pin_protection_mismatch_too_many_times_error_message" = "Jeżeli nie pamiętasz PINu, naciśnij Zapomniałem(-am) PIN."; +"pin_protection_mismatch_error_message" = "Proszę, spróbuj ponownie"; +"pin_protection_mismatch_error_title" = "PINy się nie zgadzają"; +"pin_protection_reset_alert_action_reset" = "Zresetuj"; +"pin_protection_reset_alert_message" = "Aby zresetować PIN, będziesz musiał(-a) się ponownie zalogować i stworzyć nowy PIN"; +"pin_protection_reset_alert_title" = "Zresetuj PIN"; +"pin_protection_forgot_pin" = "Zapomniałem(-am) PIN"; +"pin_protection_enter_pin" = "Wprowadź swój PIN"; +"pin_protection_confirm_pin_to_change" = "Potwierdź PIN, aby zmień PIN"; +"pin_protection_confirm_pin_to_disable" = "Potwierdź PIN, aby wyłączyć PIN"; +"pin_protection_confirm_pin" = "Potwierdź swój PIN"; +"pin_protection_choose_pin" = "Dla poprawy bezpieczeństwa - utwórz PIN"; +"pin_protection_choose_pin_welcome_after_register" = "Witaj."; + +// MARK: - PIN Protection + +"pin_protection_choose_pin_welcome_after_login" = "Witaj ponownie."; +"major_update_done_action" = "Zrozumiałem(-am)"; +"major_update_learn_more_action" = "Dowiedz się więcej"; +"major_update_information" = "Z radością ogłaszamy, że zmieniliśmy nazwę! Twoja aplikacja jest aktualna i jesteś zalogowany na swoje konto."; + +// MARK: - Major update + +"major_update_title" = "Riot zmienił nazwę na Element"; +"cross_signing_setup_banner_subtitle" = "Uprość weryfikacje swoich innych sesji"; + +// MARK: - Cross-signing + +// Banner + +"cross_signing_setup_banner_title" = "Skonfiguruj szyfrowanie"; +"secrets_reset_authentication_message" = "Wprowadź hasło do konta, aby potwierdzić"; +"secrets_reset_reset_action" = "Zresetuj"; +"secrets_reset_warning_message" = "Po ponownym uruchomieniu nie będziesz miał(-a) historii, wiadomości, zaufanych sesji lub zaufanych użytkowników."; +"secrets_reset_warning_title" = "Jeśli wszystko zresetujesz"; +"secrets_reset_information" = "Skorzystaj z możliwości resetu tylko wtedy, gdy zgubiłeś(-aś) hasło/klucz odzyskiwania lub gdy nie masz możliwości weryfikacji tej sesji z poziomu innej sesji."; + +// MARK: - Secrets reset + +"secrets_reset_title" = "Zresetuj wszystko"; +"secrets_setup_recovery_passphrase_summary_information" = "Zapamiętaj swoje hasło odzyskiwania. Możesz go użyć do odszyfrowania zaszyfrowanych wiadomości i danych."; +"secrets_setup_recovery_passphrase_summary_title" = "Zapisz swoje hasło odzyskiwania"; +"secrets_setup_recovery_passphrase_confirm_passphrase_placeholder" = "Potwierdź hasło"; +"secrets_setup_recovery_passphrase_confirm_passphrase_title" = "Potwierdź"; +"secrets_setup_recovery_passphrase_confirm_information" = "Wprowadź ponownie swoje hasło odzyskiwania."; +"secrets_setup_recovery_passphrase_validate_action" = "Gotowe"; +"secrets_setup_recovery_passphrase_additional_information" = "Nie używaj hasła logowania do konta."; + +// Recovery passphrase + +"secrets_setup_recovery_passphrase_title" = "Ustaw hasło odzyskiwania"; +"secrets_setup_recovery_key_storage_alert_title" = "Dbaj o bezpieczeństwo"; +"secrets_setup_recovery_key_done_action" = "Gotowe"; +"secrets_setup_recovery_key_export_action" = "Zapisz"; +"secrets_setup_recovery_key_loading" = "Ładowanie…"; +"secrets_setup_recovery_key_information" = "Zapisz klucz odzyskiwania i przechowuj go w bezpiecznym miejscu. Będzie Ci on potrzebny, aby uzyskać dostęp do kopii kluczy zaszyfrowanych wiadomości przechowywanych na serwerze."; + +// MARK: - Secrets set up + +// Recovery Key + +"secrets_setup_recovery_key_title" = "Zapisz swój klucz odzyskiwania"; +"secrets_recovery_with_key_invalid_recovery_key_title" = "Nie można uzyskać dostępu do kopii bezpieczeństwa"; +"secrets_recovery_with_key_recover_action" = "Użyj klucz odzyskiwania"; +"secrets_recovery_with_key_recovery_key_placeholder" = "Wprowadź klucz odzyskiwania"; +"secrets_recovery_with_key_recovery_key_title" = "Wprowadź"; +"secrets_recovery_with_key_information_verify_device" = "Użyj klucz odzyskiwania, aby zweryfikować tę sesję."; +"secrets_recovery_with_key_information_default" = "Uzyskaj dostęp do swoich zaszyfrowanych wiadomości i swojej tożsamości cross-signing umożliwiającej weryfikację innych sesji, wprowadzając swój klucz odzyskiwania."; + +// Recover with key + +"secrets_recovery_with_key_title" = "Klucz odzyskiwania"; +"secrets_recovery_with_passphrase_invalid_passphrase_message" = "Sprawdź, czy zostało wprowadzone prawidłowe hasło odzyskiwania."; +"secrets_recovery_with_passphrase_invalid_passphrase_title" = "Nie można uzyskać dostępu do kopii bezpieczeństwa"; +"secrets_recovery_with_passphrase_lost_passphrase_action_part3" = "."; +"secrets_recovery_with_passphrase_lost_passphrase_action_part2" = "użyć klucz odzyskiwania"; +"secrets_recovery_with_passphrase_lost_passphrase_action_part1" = "Nie znasz swojego hasła odzyskiwania? Możesz "; +"secrets_recovery_with_passphrase_recover_action" = "Użyj hasło odzyskiwania"; +"secrets_recovery_with_passphrase_passphrase_placeholder" = "Wprowadź hasło odzyskiwania"; +"secrets_recovery_with_passphrase_passphrase_title" = "Wprowadź"; +"secrets_recovery_with_passphrase_information_verify_device" = "Użyj hasło odzyskiwania, aby zweryfikować tę sesję."; +"secrets_recovery_with_passphrase_information_default" = "Uzyskaj dostęp do swoich zaszyfrowanych wiadomości i swojej tożsamości umożliwiającej weryfikację innych twoich sesji, wprowadzając swoje hasło odzyskiwania."; + +// Recover with passphrase + +"secrets_recovery_with_passphrase_title" = "Hasło odzyskiwania"; +"secrets_recovery_reset_action_part_2" = "Zresetuj wszystko"; + +// MARK: - Secrets recovery + +"secrets_recovery_reset_action_part_1" = "Zapomniałeś(-aś) lub straciłeś(-aś) wszystkie opcje odzyskiwania? "; +"user_verification_session_details_verify_action_other_user" = "Manualna weryfikacja"; +"user_verification_session_details_verify_action_current_user_manually" = "Manualna weryfikacja za pomocą tekstu"; +"user_verification_session_details_verify_action_current_user" = "Interaktywna weryfikacja"; +"user_verification_session_details_additional_information_untrusted_current_user" = "Jeśli nie rozpoznajesz tej sesji to może oznaczać, że ktoś inny logował się na Twoje konto."; +"user_verification_session_details_additional_information_untrusted_other_user" = "Dopóki ten użytkownik nie zaufa tej sesji, wiadomości wysyłane do niej i z niej są oznaczane ostrzeżeniami. Alternatywnie możesz ją zweryfikować ręcznie."; +"user_verification_session_details_information_untrusted_other_user" = " zalogował się przy użyciu nowej sesji:"; +"user_verification_session_details_information_untrusted_current_user" = "Zweryfikuj tę sesję, aby oznaczyć ją jako zaufaną i przyznać jej dostęp do zaszyfrowanych wiadomości:"; +"user_verification_session_details_information_trusted_other_user_part2" = " ją zweryfikował(-a):"; +"user_verification_session_details_information_trusted_other_user_part1" = "Ta sesja umożliwia przesyłanie szyfrowanych wiadomości, ponieważ "; +"user_verification_sessions_list_session_untrusted" = "Niezaufana"; +"user_verification_sessions_list_session_trusted" = "Zaufana"; +"user_verification_sessions_list_table_title" = "Sesje"; +"user_verification_sessions_list_user_trust_level_unknown_title" = "Nieznana"; +"user_verification_sessions_list_user_trust_level_warning_title" = "Ostrzeżenie"; + +// Sessions list + +"user_verification_sessions_list_user_trust_level_trusted_title" = "Zaufana"; +"user_verification_start_additional_information" = "Zrób to osobiście lub użyj innego sposobu komunikacji."; +"user_verification_start_waiting_partner" = "Oczekiwanie na %@…"; +"user_verification_start_information_part2" = " sprawdzając jednorazowy kod na obu urządzeniach."; +"user_verification_start_information_part1" = "Aby zapewnić dodatkowe bezpieczeństwo, zweryfikuj "; + +// MARK: - User verification + +// Start + +"user_verification_start_verify_action" = "Rozpocznij weryfikację"; +"key_verification_scan_confirmation_scanned_device_information" = "Czy drugie urządzenie pokazuje tę samą tarczę?"; +"key_verification_scan_confirmation_scanned_user_information" = "Czy %@ pokazuje tę samą tarczę?"; + +// Scanned +"key_verification_scan_confirmation_scanned_title" = "Już prawie się udało!"; +"key_verification_scan_confirmation_scanning_device_waiting_other" = "Czekam na inne urządzenie…"; +"key_verification_scan_confirmation_scanning_user_waiting_other" = "Oczekiwanie na %@…"; + +// MARK: Scan confirmation + +// Scanning +"key_verification_scan_confirmation_scanning_title" = "Już prawie się udało! Oczekiwanie na potwierdzenie…"; +"key_verification_verify_qr_code_scan_other_code_success_message" = "Kod QR został pomyślnie zweryfikowany."; +"key_verification_verify_qr_code_scan_other_code_success_title" = "Kod zweryfikowany!"; +"key_verification_verify_qr_code_other_scan_my_code_title" = "Czy inny użytkownik pomyślnie zeskanował kod QR?"; +"key_verification_verify_qr_code_start_emoji_action" = "Zweryfikuj za pomocą emotikon"; +"key_verification_verify_qr_code_cannot_scan_action" = "Nie możesz zeskanować?"; +"key_verification_verify_qr_code_scan_code_action" = "Zeskanuj ich kod"; +"key_verification_verify_qr_code_emoji_information" = "Zweryfikuj, porównując unikalne emotikony."; +"key_verification_verify_qr_code_information_other_device" = "Zeskanuj poniższy kod, aby zweryfikować:"; +"key_verification_verify_qr_code_information" = "Zeskanuj kod, aby bezpiecznie wzajemnie się zweryfikować."; + +// MARK: QR code + +"key_verification_verify_qr_code_title" = "Zweryfikuj przez skanowanie"; + +// Incoming key verification request + +"key_verification_incoming_request_incoming_alert_message" = "%@ chce się zweryfikować"; +"key_verification_tile_conclusion_warning_title" = "Niezaufane logowanie"; +"key_verification_tile_conclusion_done_title" = "Zweryfikowano"; +"key_verification_tile_request_incoming_approval_decline" = "Odmów"; +"key_verification_tile_request_incoming_approval_accept" = "Zaakceptuj"; +"key_verification_tile_request_status_accepted" = "Zaakceptowałeś(-aś)"; +"key_verification_tile_request_status_cancelled" = "%@ odrzucił(-a)"; +"key_verification_tile_request_status_cancelled_by_me" = "Anulowałeś(-aś)"; +"key_verification_tile_request_status_expired" = "Wygasło"; +"key_verification_tile_request_status_waiting" = "Oczekiwanie…"; +"key_verification_tile_request_status_data_loading" = "Ładowanie danych…"; +"key_verification_tile_request_outgoing_title" = "Wysłałeś zapytanie o weryfikację"; + +// Tiles + +"key_verification_tile_request_incoming_title" = "Otrzymałeś zapytanie o weryfikację"; +"key_verification_bootstrap_not_setup_message" = "Najpierw musisz uruchomić cross-signing."; + +// MARK: - Key Verification + +"key_verification_bootstrap_not_setup_title" = "Błąd"; +"error_not_supported_on_mobile" = "Nie możesz tego zrobić z %@ mobile."; + + +// Generic errors +"error_invite_3pid_with_no_identity_server" = "Dodaj serwer tożsamości w ustawieniach, aby zapraszać przez e-mail."; + +// User + +"key_verification_verified_user_information" = "Wiadomości wysyłane do tego użytkownika są szyfrowane metodą end-to-end i nie mogą być odczytywane przez osoby trzecie."; +"key_verification_verified_this_session_information" = "Uzyskałeś(-aś) dostęp do zaszyfrowanych wiadomości w tej sesji. Dodatkowo użytkownicy będą ufać tej sesji."; +"key_verification_verified_new_session_information" = "Uzyskałeś(-aś) dostęp do zaszyfrowanych wiadomości w nowej sesji. Dodatkowo użytkownicy będą ufać tej sesji."; +"key_verification_verified_other_session_information" = "Uzyskałeś(-aś) dostęp do zaszyfrowanych wiadomości w swojej drugiej sesji. Dodatkowo użytkownicy będą ufać tej sesji."; +"key_verification_verified_new_session_title" = "Nowa sesja zweryfikowana!"; +"key_verification_manually_verify_device_validate_action" = "Zweryfikuj"; +"key_verification_manually_verify_device_additional_information" = "Jeżeli te dane się nie zgadzają to może oznaczać, że bezpieczeństwo komunikacji mogło zostać skompromitowane."; +"key_verification_manually_verify_device_key_title" = "Klucz sesji"; +"key_verification_manually_verify_device_id_title" = "Identyfikator sesji"; +"key_verification_manually_verify_device_name_title" = "Nazwa sesji"; +"key_verification_manually_verify_device_instruction" = "Potwierdź zgodność poniższych informacji porównując następujące elementy z ustawieniami użytkownika w drugiej sesji:"; + +// MARK: Manually Verify Device + +"key_verification_manually_verify_device_title" = "Manualna weryfikacja za pomocą tekstu"; +"key_verification_verify_sas_additional_information" = "Aby zapewnić maksymalne bezpieczeństwo, użyj innego zaufanego środka komunikacji lub zrób to osobiście."; +"key_verification_verify_sas_validate_action" = "Pasują"; +"key_verification_verify_sas_cancel_action" = "Nie pasują"; +"key_verification_verify_sas_title_number" = "Porównaj liczby"; + +// MARK: Verify + +"key_verification_verify_sas_title_emoji" = "Porównaj emoji"; +"device_verification_self_verify_wait_recover_secrets_checking_availability" = "Sprawdzam inne możliwości weryfikacji ..."; +"device_verification_self_verify_wait_recover_secrets_additional_information" = "Jeśli nie możesz uzyskać dostępu do żadnej istniejącej sesji"; +"device_verification_self_verify_wait_recover_secrets_with_passphrase" = "Użyj hasło lub klucz odzyskiwania"; +"device_verification_self_verify_wait_recover_secrets_without_passphrase" = "Użyj klucz odzyskiwania"; +"device_verification_self_verify_wait_additional_information" = "Działa to z klientem Element i innymi klientami Matrix obsługującymi cross-signing."; +"device_verification_self_verify_wait_information" = "Zweryfikuj tę sesję z jednej z innych sesji, przyznając jej dostęp do zaszyfrowanych wiadomości.\n\nUżyj najnowszego klienta Element na innych urządzeniach:"; +"device_verification_self_verify_wait_new_sign_in_title" = "Zweryfikuj to logowanie"; + +// MARK: Self verification wait + +"device_verification_self_verify_wait_title" = "Konfiguracja bezpieczeństwa"; +"key_verification_self_verify_unverified_sessions_alert_validate_action" = "Przejrzyj"; +"key_verification_self_verify_unverified_sessions_alert_message" = "Zweryfikuj wszystkie swoje sesje, aby upewnić się, że Twoje konto i wiadomości są bezpieczne."; + +// Unverified sessions + +"key_verification_self_verify_unverified_sessions_alert_title" = "Sprawdź, gdzie jesteś zalogowany"; +"key_verification_self_verify_current_session_alert_validate_action" = "Zweryfikuj"; +"key_verification_self_verify_current_session_alert_message" = "Inni użytkownicy mogą jej nie ufać."; + +// Current session + +"key_verification_self_verify_current_session_alert_title" = "Zweryfikuj tę sesję"; +"device_verification_self_verify_start_waiting" = "Oczekiwanie…"; +"device_verification_self_verify_start_information" = "Skorzystaj z tej sesji, aby zweryfikować nową sesję, zapewniając jej dostęp do zaszyfrowanych wiadomości."; +"device_verification_self_verify_start_verify_action" = "Rozpocznij weryfikację"; +"device_verification_self_verify_alert_validate_action" = "Zweryfikuj"; +"device_verification_self_verify_alert_message" = "Zweryfikuj nowe logowanie do swojego konta: %@"; + +// MARK: Self verification start + +// New login +"device_verification_self_verify_alert_title" = "Nowe logowanie. Czy to byłeś ty?"; +"device_verification_security_advice_number" = "Porównaj liczby, upewniając się, że pojawiają się w tej samej kolejności."; +"device_verification_security_advice_emoji" = "Porównaj unikalne emotikony, upewniając się, że pojawiają się w tej samej kolejności."; +"key_verification_user_title" = "Weryfikacja"; +"key_verification_this_session_title" = "Weryfikacja bieżącej sesji"; +"key_verification_new_session_title" = "Weryfikacja nowej sesji"; + +// MARK: - Device Verification +"key_verification_other_session_title" = "Weryfikacja istniejącej sesji"; +"sign_out_non_existing_key_backup_alert_setup_secure_backup_action" = "Zacznij korzystać z bezpiecznej kopii zapasowej"; + +// Recover from private key +"key_backup_recover_from_private_key_info" = "Przywracam kopię zapasową…"; +"secure_backup_setup_banner_subtitle" = "Pozwala ona na ochronę przed utratą dostępu do zaszyfrowanych wiadomości i danych"; + +// Banner + +"secure_backup_setup_banner_title" = "Utwórz kopię zapasową"; +"secure_key_backup_setup_cancel_alert_message" = "Jeśli anulujesz teraz, możesz utracić zaszyfrowane wiadomości i dane, jeśli utracisz dostęp do swoich danych logowania.\n\nMożesz także skonfigurować bezpieczną kopię zapasową i zarządzać kluczami w Ustawieniach."; + + +// Cancel + +"secure_key_backup_setup_cancel_alert_title" = "Jesteś pewien/pewna?"; +"secure_key_backup_setup_existing_backup_error_delete_it" = "Usuń starą kopię"; +"secure_key_backup_setup_existing_backup_error_unlock_it" = "Odblokuj starą kopię"; +"secure_key_backup_setup_existing_backup_error_info" = "Odblokuj ją, aby ponownie użyć jej w bezpiecznej kopii zapasowej lub usuń, aby utworzyć nową kopię zapasową wiadomości w bezpiecznej kopii zapasowej."; +"secure_key_backup_setup_existing_backup_error_title" = "Kopia zapasowa wiadomości już istnieje"; +"secure_key_backup_setup_intro_use_security_passphrase_title" = "Ustaw hasło odzyskiwania"; +"secure_key_backup_setup_intro_use_security_key_info" = "Wygeneruj klucz odzyskiwania. Następnie zdeponuj go w bezpiecznym miejscu."; +"secure_key_backup_setup_intro_use_security_key_title" = "Ustaw klucz odzyskiwania"; +"secure_key_backup_setup_intro_info" = "Zabezpiecz się przed utratą dostępu do zaszyfrowanych wiadomości wykonując kopię zapasową kluczy szyfrowania na Twoim serwerze domowym."; + +// MARK: Secure backup setup + +// Intro + +"secure_key_backup_setup_intro_title" = "Bezpieczna kopia zapasowa"; +"service_terms_modal_policy_checkbox_accessibility_hint" = "Zaznacz, aby zaakceptować %@"; +"service_terms_modal_message_identity_server" = "Zaakceptuj warunki serwera tożsamości (%@), aby móc wyszukiwać kontakty."; + +// Service terms - Variant for identity server when displayed out of a context +"service_terms_modal_title_identity_server" = "Odkrywanie kontaktów"; +"service_terms_modal_description_for_identity_server_1" = "Znajdź innych za pomocą numeru telefon lub adresu e-mail"; +"service_terms_modal_decline_button" = "Odmów"; +"room_widget_permission_room_id_permission" = "ID Pokoju"; +"room_widget_permission_widget_id_permission" = "Widget ID"; +"room_widget_permission_theme_permission" = "Twój motyw"; +"room_widget_permission_user_id_permission" = "Twój ID użytkownika"; +"room_widget_permission_avatar_url_permission" = "Twój avatar"; +"room_widget_permission_display_name_permission" = "Twoja wyświetlana nazwa"; +"room_widget_permission_information_title" = "Korzystanie z tego widgetu może spowodować udostępnienie poniższych danych dla %@:\n"; +"room_widget_permission_webview_information_title" = "Korzystanie z tego widgetu może ustawiać pliki cookie i udostępniać dane %@:\n"; +"room_widget_permission_creator_info_title" = "Ten widget został dodany przez:"; + +// Room widget permissions +"room_widget_permission_title" = "Uruchom widget"; +"widget_picker_manage_integrations" = "Zarządzaj integracjami…"; + +// Widget Picker +"widget_picker_title" = "Integracje"; +"widget_integration_manager_disabled" = "Musisz włączyć Menadżer Integracji w ustawieniach"; +"widget_menu_remove" = "Usuń dla wszystkich"; +"widget_menu_revoke_permission" = "Odbierz mi dostęp"; +"widget_menu_open_outside" = "Otwórz w przeglądarce"; +"widget_menu_refresh" = "Odśwież"; +"bug_report_background_mode" = "Kontynuuj w tle"; +"call_actions_unhold" = "Wznów połączenie"; +"call_no_stun_server_error_use_fallback_button" = "Spróbuj użyć %@"; +"call_no_stun_server_error_message_2" = "Alternatywnie możesz spróbować użyć serwera publicznego pod adresem %@, ale nie będzie to tak niezawodne i będzie udostępniać Twój adres IP temu serwerowi. Możesz również zarządzać tym w Ustawieniach"; +"call_no_stun_server_error_message_1" = "Poproś administratora swojego serwera domowego %@ o skonfigurowanie serwera TURN, aby połączenia działały niezawodnie."; +"call_no_stun_server_error_title" = "Połączenie nie powiodło się z powodu błędnej konfiguracji serwera"; + +// Events formatter with you +"event_formatter_widget_added_by_you" = "Dodałeś(-aś) widget: %@"; +"event_formatter_widget_removed_by_you" = "Usunąłeś(-aś) widget: %@"; +"event_formatter_jitsi_widget_removed_by_you" = "Usunąłeś(-aś) konferencję VoIP"; +"event_formatter_jitsi_widget_added_by_you" = "Dodałeś(-aś) konferencję VoIP"; +"event_formatter_call_back" = "Oddzwoń"; +"event_formatter_call_you_declined" = "Odrzuciłeś to połączenie"; +"event_formatter_call_you_currently_in" = "Bierzesz obecnie udział w tej rozmowie"; +"event_formatter_call_has_ended" = "Rozmowa zakończona"; +"event_formatter_call_video" = "Połączenie Wideo"; +"event_formatter_call_voice" = "Połączenie głosowe"; +"room_details_advanced_e2e_encryption_disabled_for_dm" = "Szyfrowanie nie jest włączone w tym pokoju."; +"room_details_advanced_e2e_encryption_enabled_for_dm" = "Szyfrowanie jest włączone w tym pokoju"; +"room_details_advanced_room_id_for_dm" = "ID:"; +"room_details_no_local_addresses_for_dm" = "Ten pokój nie ma lokalnych adresów"; +"room_details_access_section_directory_toggle_for_dm" = "Wyświetlaj ten pokój na liście pokojów"; +"room_details_access_section_anyone_for_dm" = "Każdy kto zna link pokoju, razem z gośćmi"; +"room_details_access_section_anyone_apart_from_guest_for_dm" = "Każdy kto zna link pokoju, poza gośćmi"; +"room_details_access_section_for_dm" = "Kto może dołączyć do pokoju?"; +"room_details_room_name_for_dm" = "Nazwa Pokoju"; +"room_details_photo_for_dm" = "Obraz pokoju"; +"room_details_integrations" = "Integracje"; +"room_details_search" = "Wyszukaj w pokoju"; +"room_details_title_for_dm" = "Szczegóły pokoju"; +"identity_server_settings_alert_error_invalid_identity_server" = "%@ nie jest prawidłowym serwerem tożsamości."; +"identity_server_settings_alert_error_terms_not_accepted" = "Musisz zaakceptować warunki %@, aby ustawić go jako serwer tożsamości."; +"identity_server_settings_alert_disconnect_still_sharing_3pid_button" = "Odłącz mimo wszystko"; +"identity_server_settings_alert_disconnect_still_sharing_3pid" = "Nadal udostępniasz swoje dane personalne na serwerze tożsamości %@. \n\nZalecamy usunięcie adresów e-mail i numerów telefonów z serwera tożsamości przed rozłączeniem."; +"identity_server_settings_alert_disconnect_button" = "Odłącz"; +"identity_server_settings_alert_disconnect" = "Odłączyć się od serwera tożsamości %@?"; +"identity_server_settings_alert_disconnect_title" = "Odłącz serwer tożsamości"; +"identity_server_settings_alert_change" = "Odłączyć się od serwera tożsamości %1$@ i zamiast tego połączyć się z %2$@?"; +"identity_server_settings_alert_change_title" = "Zmień serwer tożsamości"; +"identity_server_settings_alert_no_terms" = "Wybrany serwer tożsamości nie ma żadnych warunków korzystania z usługi. Kontynuuj tylko wtedy, gdy ufasz właścicielowi serwera."; +"identity_server_settings_alert_no_terms_title" = "Serwer tożsamości nie ma warunków świadczenia usług"; +"identity_server_settings_disconnect" = "Odłącz"; +"identity_server_settings_disconnect_info" = "Odłączenie się od serwera tożsamości oznacza, że nie będziesz mógł być znaleziony przez innych użytkowników oraz nie będziesz mógł zapraszać innych przez e-mail lub telefon."; +"identity_server_settings_change" = "Zmień"; +"identity_server_settings_add" = "Dodaj"; +"identity_server_settings_place_holder" = "Wprowadź serwer tożsamości"; +"identity_server_settings_no_is_description" = "Obecnie nie używasz serwera tożsamości. Dodaj go powyżej, aby móc wyszukiwać i być znajdywanym przez istniejące kontakty."; +"identity_server_settings_description" = "Obecnie używasz %@ do wyszukiwania i bycia znalezionym przez kontakty, które znasz."; + +// Identity server settings +"identity_server_settings_title" = "Serwer Tożsamości"; + +// AuthenticatedSessionViewControllerFactory +"authenticated_session_flow_not_supported" = "Ta aplikacja nie obsługuje mechanizmu uwierzytelniania na Twoim serwerze domowym."; +"manage_session_sign_out" = "Wyloguj się z tej sesji"; +"manage_session_not_trusted" = "Niezaufana"; +"manage_session_trusted" = "Zaufana przez Ciebie"; +"manage_session_name" = "Nazwa sesji"; +"manage_session_info" = "INFORMACJE O SESJI"; + +// Manage session +"manage_session_title" = "Zarządzanie sesją"; +"security_settings_user_password_description" = "Potwierdź swoją tożsamość, wprowadzając hasło do konta"; +"security_settings_coming_soon" = "Przepraszamy. Ta akcja nie jest jeszcze dostępna w aplikacji Element iOS. Użyj innego klienta Matrix, aby to skonfigurować. Element iOS będzie tego używał."; +"security_settings_complete_security_alert_message" = "Najpierw należy zapewnić bezpieczeństwo bieżącej sesji."; +"security_settings_complete_security_alert_title" = "Konfiguracja bezpieczeństwa"; +"security_settings_blacklist_unverified_devices_description" = "Sprawdź wszystkie sesje użytkowników, aby oznaczyć je jako zaufane i wysłać do nich wiadomości."; +"security_settings_blacklist_unverified_devices" = "Nigdy nie wysyłaj wiadomości do niezaufanych sesji"; +"security_settings_advanced" = "ZAAWANSOWANE"; +"security_settings_export_keys_manually" = "Eksportuj klucze ręcznie"; +"security_settings_cryptography" = "KRYPTOGRAFIA"; +"security_settings_crosssigning_complete_security" = "Konfiguracja bezpieczeństwa"; +"security_settings_crosssigning_reset" = "Zresetuj cross-signing"; +"security_settings_crosssigning_bootstrap" = "Bootstrap cross-signing"; +"security_settings_crosssigning_info_ok" = "Cross-signing jest włączony."; +"security_settings_crosssigning_info_trusted" = "Cross-signing jest włączony. Możesz ufać innym użytkownikom i innym sesjom opartym na cross-signing, ale nie możesz cross-sign z tej sesji, ponieważ nie ma ona kluczy prywatnych do cross-signing. Zapewnij bezpieczeństwo tej sesji."; +"security_settings_crosssigning_info_exists" = "Twoje konto ma tożsamość cross-signing, ale nie jest jeszcze zaufane w tej sesji. Zapewnij bezpieczeństwo tej sesji."; +"security_settings_secure_backup_description" = "Zabezpiecz się przed utratą dostępu do zaszyfrowanych wiadomości i danych wykonując kopię zapasową kluczy szyfrowania na Twoim serwerze domowym."; +"security_settings_crypto_sessions_description_2" = "Jeśli nie rozpoznajesz którejś z sesji to zmień hasło i zresetuj bezpieczną kopię zapasową."; +"settings_show_NSFW_public_rooms" = "Pokaż publiczne pokoje NSFW"; +"secrets_setup_recovery_key_storage_alert_message" = "✓ Wydrukuj klucz i przechowuj go w bezpiecznym miejscu\n✓ Zapisz klucz na pendrive lub dysku zapasowym\n✓ Skopiuj klucz na prywatnym dysku w chmurze"; +"user_verification_sessions_list_information" = "Konwersacja z tym użytkownikiem w tym pokoju jest szyfrowana end-to-end i nie może być odczytana przez osoby trzecie."; +"service_terms_modal_description_for_identity_server_2" = "Daj się znaleźć na podstawie numeru telefonu lub adresu e-mail"; +"secrets_setup_recovery_passphrase_information" = "Wprowadź unikalne hasło odzyskiwania. Będzie ono używane do zabezpieczenia Twojej kopii kluczy przechowywanej na serwerze."; +"secrets_recovery_with_key_invalid_recovery_key_message" = "Sprawdź, czy wprowadziłeś(-aś) poprawny klucz odzyskiwania."; +"secure_key_backup_setup_intro_use_security_passphrase_info" = "Wprowadź unikalne hasło i wygeneruj klucz odzyskiwania."; +"settings_identity_server_no_is_description" = "Obecnie nie używasz serwera tożsamości. Dodaj go powyżej, aby móc wyszukiwać kontakty i być znalezionym przez osoby, które znasz."; +"settings_identity_server_description" = "Korzystając z serwera tożsamości ustawionego powyżej, możesz wyszukiwać kontakty i być znalezionym przez osoby, które znasz."; +"settings_discovery_three_pid_details_information_phone_number" = "Zarządzaj preferencjami dla tego numeru telefonu, którego mogą używać inni użytkownicy, aby Cię znaleźć i zapraszać do pokojów. Dodaj lub usuń numery telefonów w ustawieniach konta."; +"settings_discovery_three_pid_details_information_email" = "Zarządzaj preferencjami dla tego adresu e-mail, którego mogą używać inni użytkownicy, aby Cię znaleźć i zapraszać do pokojów. Dodaj lub usuń adresy e-mail w ustawieniach konta."; +"settings_discovery_three_pids_management_information_part1" = "Wybierz, które adresy e-mail lub numery telefonów mogą posłużyć innym użytkownikom do znalezienia Ciebie i zaproszenia do pokoju. Dodaj lub usuń adresy e-mail lub numery telefonów z tej listy w "; +"settings_discovery_terms_not_signed" = "Zaakceptuj warunki korzystania z usługi serwera tożsamości (%@), aby umożliwić innym osobom wyszukiwanie Ciebie za pomocą adresu e-mail lub numeru telefonu."; +"settings_discovery_no_identity_server" = "Obecnie nie używasz serwera tożsamości. Pomyśl o tym, aby go dodać by móc być odnalezionym przez osoby, które znasz."; +"settings_devices_description" = "Publiczna nazwa sesji jest widoczna dla osób, z którymi się komunikujesz"; +"settings_integrations_allow_description" = "Użyj Menedżera Integracji (%@) do zarządzania botami, mostami, widżetami i pakietami naklejek.\n\nMenedżerowie integracji otrzymują dane konfiguracyjne i mogą modyfikować widżety, wysyłać zaproszenia do pokojów i ustawiać poziomy mocy w Twoim imieniu."; +"settings_calls_stun_server_fallback_description" = "Zezwalaj na użycie rezerwowego serwera połączeń %@ w przypadku, gdy Twój serwer domowy go nie oferuje (Twój adres IP będzie udostępniany podczas połączenia)."; +"settings_three_pids_management_information_part1" = "Wybierz, które adresy e-mail lub numery telefonów chcesz wykorzystać do logowania się lub odzyskiwania konta. Kontroluj, kto może Cię dzięki nim znaleźć przechodząc do "; +"room_multiple_typing_notification" = "%@ i inni"; +"room_open_dialpad" = "Klawiatura numeryczna"; +"room_place_voice_call" = "Połączenie głosowe"; +"room_accessibility_video_call" = "Połączenie Wideo"; +"room_event_action_delete_confirmation_message" = "Czy na pewno chcesz usunąć tą niewysłaną wiadomość?"; +"room_event_action_delete_confirmation_title" = "Usuń niewysłaną wiadomość"; +"room_unsent_messages_cancel_message" = "Czy na pewno chcesz usunąć wszystkie niewysłane wiadomości w tym pokoju?"; +"room_unsent_messages_cancel_title" = "Usuń niewysłane wiadomości"; +"room_message_replying_to" = "Odpisuję do %@"; +"room_message_editing" = "Edytuję"; +"room_participants_security_information_room_encrypted_for_dm" = "Wiadomości w tym pokoju są szyfrowane end-to-end.\n\nTwoje wiadomości są zabezpieczone \"zamkami\" i tylko Ty i odbiorca macie unikalne klucze do ich \"otworzenia\"."; +"room_participants_security_information_room_encrypted" = "Wiadomości w tym pokoju są szyfrowane end-to-end.\n\nTwoje wiadomości są zabezpieczone \"zamkami\" i tylko Ty i odbiorca macie unikalne klucze do ich \"otworzenia\"."; +"room_participants_security_information_room_not_encrypted_for_dm" = "Wiadomości w tym pokoju nie są szyfrowane end-to-end."; +"room_participants_filter_room_members_for_dm" = "Filtruj uczestników pokoju"; +"room_participants_leave_prompt_msg_for_dm" = "Czy napewno chcesz opuścić pokój?"; +"room_participants_leave_prompt_title_for_dm" = "Opuść pokój"; +"contacts_address_book_no_identity_server" = "Brak skonfigurowanego serwera tożsamości"; +"rooms_empty_view_information" = "Pokoje świetnie nadają się do publicznych lub prywatnych czatów grupowych. Dotknij (+), aby wyszukać istniejące pokoje lub stworzyć nowe."; +"rooms_empty_view_title" = "Pokoje"; +"people_empty_view_information" = "Dotknij (+), aby rozpocząć bezpieczną komunikację."; +"store_promotional_text" = "Zdecentralizowany komunikator internetowy dbający o Twoją prywatność. Bez dataminingu, bez backdoorów i bez dostępu osób trzecich."; +"people_empty_view_title" = "Osoby"; + +// Errors +"error_user_already_logged_in" = "Wygląda na to, że próbujesz połączyć się z innym serwerem domowym. Chcesz się wylogować?"; + +// Social login + +"social_login_list_title_continue" = "Kontynuuj używając"; +"social_login_button_title_continue" = "Kontynuuj używając %@"; +"social_login_button_title_sign_in" = "Zaloguj się używając %@"; +"social_login_button_title_sign_up" = "Zarejestruj się używając %@"; +"social_login_list_title_sign_up" = "Lub"; +"social_login_list_title_sign_in" = "Lub"; +"auth_reset_password_error_is_required" = "Brak skonfigurowanego serwera tożsamości: dodaj go w ustawieniach, aby zresetować hasło."; +"callbar_return" = "Powrót"; +"callbar_only_multiple_paused" = "%@ wstrzymanych połączeń"; +"callbar_only_single_paused" = "Wstrzymane połączenie"; +"callbar_active_and_multiple_paused" = "1 aktywne połączenie (%@) · %@ wstrzymanych połączeń"; +"callbar_active_and_single_paused" = "1 aktywne połączenie (%@) · 1 wstrzymane połączenie"; + +// Call Bar +"callbar_only_single_active" = "Aktywne połączenie (%@)"; +"auth_email_is_required" = "Brak skonfigurowanego serwera tożsamości, nie możesz dodać adresu e-mail na wypadek konieczności zresetowania hasła."; +"auth_add_email_phone_message_2" = "Ustaw adres e-mail na wypadek konieczności przywracania dostępu do konta, dodatkowo adres ten lub numer telefonu może być wykorzystany przez Twoich znajomych do znalezienia Twojego profilu."; +"auth_add_phone_message_2" = "Ustaw numer telefonu, numer ten może być wykorzystany przez Twoich znajomych do znalezienia Twojego profilu."; +"auth_add_email_message_2" = "Ustaw adres e-mail na wypadek konieczności przywracania dostępu do konta, dodatkowo adres ten może być wykorzystany przez Twoich znajomych do znalezienia Twojego profilu."; diff --git a/Riot/Assets/sq.lproj/Vector.strings b/Riot/Assets/sq.lproj/Vector.strings index b9ed2ebf3..ebf05c4a3 100644 --- a/Riot/Assets/sq.lproj/Vector.strings +++ b/Riot/Assets/sq.lproj/Vector.strings @@ -5,7 +5,7 @@ "title_groups" = "Bashkësi"; "warning" = "Sinjalizim"; // Actions -"view" = "Parje"; +"view" = "Shiheni"; "next" = "Pasuesja"; "back" = "Mbrapsht"; "continue" = "Vazhdo"; @@ -1344,3 +1344,4 @@ "room_accessibility_video_call" = "Thirrje Video"; "room_message_replying_to" = "Në përgjigje të %@"; "room_message_editing" = "Përpunim"; +"room_details_search" = "Kërkoni për dhomë"; diff --git a/Riot/Assets/tzm.lproj/InfoPlist.strings b/Riot/Assets/tzm.lproj/InfoPlist.strings index 8b1378917..2a405101f 100644 --- a/Riot/Assets/tzm.lproj/InfoPlist.strings +++ b/Riot/Assets/tzm.lproj/InfoPlist.strings @@ -1 +1,4 @@ + +"NSFaceIDUsageDescription" = "ittusmres Face ID i wekcam ɣer tsensi nnek."; +"NSPhotoLibraryUsageDescription" = "Tettusmres tasedlist n twelafin i wazan n twelafin d ibidyuten."; diff --git a/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.h b/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.h index 6bc374644..91b69a123 100644 --- a/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.h +++ b/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.h @@ -58,13 +58,6 @@ extern NSString *const kMXKRoomBubbleCellKeyVerificationIncomingRequestAcceptPre */ extern NSString *const kMXKRoomBubbleCellKeyVerificationIncomingRequestDeclinePressed; -/** - Action identifier used when the user pressed "Call back" button for a declined call. - - The `userInfo` dictionary contains an `MXEvent` object under the `kMXKRoomBubbleCellEventKey` key, representing the invite event of the declined call. - */ -extern NSString *const kMXKRoomBubbleCellCallBackButtonPressed; - /** Define a `MXKRoomBubbleTableViewCell` category at Riot level to handle bubble customisation. */ diff --git a/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m b/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m index 34bafb3de..ade4a3c1c 100644 --- a/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m +++ b/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m @@ -33,7 +33,6 @@ NSString *const kMXKRoomBubbleCellLongPressOnReactionView = @"kMXKRoomBubbleCell NSString *const kMXKRoomBubbleCellEventIdKey = @"kMXKRoomBubbleCellEventIdKey"; NSString *const kMXKRoomBubbleCellKeyVerificationIncomingRequestAcceptPressed = @"kMXKRoomBubbleCellKeyVerificationAcceptPressed"; NSString *const kMXKRoomBubbleCellKeyVerificationIncomingRequestDeclinePressed = @"kMXKRoomBubbleCellKeyVerificationDeclinePressed"; -NSString *const kMXKRoomBubbleCellCallBackButtonPressed = @"kMXKRoomBubbleCellCallBackButtonPressed"; @implementation MXKRoomBubbleTableViewCell (Riot) diff --git a/Riot/Categories/MXSession+Riot.h b/Riot/Categories/MXSession+Riot.h index f756a2ede..d5fe928de 100644 --- a/Riot/Categories/MXSession+Riot.h +++ b/Riot/Categories/MXSession+Riot.h @@ -18,6 +18,8 @@ #import +@class HomeserverConfiguration; + @interface MXSession (Riot) /** @@ -26,15 +28,9 @@ - (NSUInteger)vc_missedDiscussionsCount; /** - Check if E2E by default is welcomed on the user's HS. - The default value is YES. - - HS admins can disable it in /.well-known/matrix/client by returning: - "im.vector.riot.e2ee": { - "default": false - } - */ -- (BOOL)vc_isE2EByDefaultEnabledByHSAdmin; +Return the homeserver configuration based on HS Well-Known or BuildSettings properties according to existing values. +*/ +- (HomeserverConfiguration*)vc_homeserverConfiguration; /** Riot version of [MXSession canEnableE2EByDefaultInNewRoomWithUsers:] diff --git a/Riot/Categories/MXSession+Riot.m b/Riot/Categories/MXSession+Riot.m index fd0a53c9d..1cc498024 100644 --- a/Riot/Categories/MXSession+Riot.m +++ b/Riot/Categories/MXSession+Riot.m @@ -49,25 +49,17 @@ return missedDiscussionsCount; } -- (BOOL)vc_isE2EByDefaultEnabledByHSAdmin +- (HomeserverConfiguration*)vc_homeserverConfiguration { - BOOL isE2EByDefaultEnabledByHSAdmin = YES; - - MXWellKnown *wellKnown = self.homeserverWellknown; - - if (wellKnown.JSONDictionary[@"im.vector.riot.e2ee"][@"default"]) - { - MXJSONModelSetBoolean(isE2EByDefaultEnabledByHSAdmin, wellKnown.JSONDictionary[@"im.vector.riot.e2ee"][@"default"]); - } - - return isE2EByDefaultEnabledByHSAdmin; + HomeserverConfigurationBuilder *configurationBuilder = [HomeserverConfigurationBuilder new]; + return [configurationBuilder buildFrom:self.homeserverWellknown]; } - (MXHTTPOperation*)vc_canEnableE2EByDefaultInNewRoomWithUsers:(NSArray*)userIds success:(void (^)(BOOL canEnableE2E))success failure:(void (^)(NSError *error))failure; { - if (self.vc_isE2EByDefaultEnabledByHSAdmin) + if ([self vc_homeserverConfiguration].isE2EEByDefaultEnabled) { return [self canEnableE2EByDefaultInNewRoomWithUsers:userIds success:success failure:failure]; } diff --git a/Riot/Categories/UIView.swift b/Riot/Categories/UIView.swift index e7ede840e..9edde2c23 100644 --- a/Riot/Categories/UIView.swift +++ b/Riot/Categories/UIView.swift @@ -64,4 +64,16 @@ extension UIView { self.accessibilityTraits.insert(.notEnabled) } } + + @objc func vc_addShadow(withColor color: UIColor, offset: CGSize, radius: CGFloat, opacity: CGFloat) { + layer.shadowColor = color.cgColor + layer.shadowOpacity = Float(opacity) + layer.shadowRadius = radius + layer.shadowOffset = offset + } + + @objc func vc_removeShadow() { + layer.shadowColor = UIColor.clear.cgColor + } + } diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index 7f973231f..4565353fe 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -31,6 +31,7 @@ internal enum Asset { internal static let callChatIcon = ImageAsset(name: "call_chat_icon") internal static let callDialpadBackspaceIcon = ImageAsset(name: "call_dialpad_backspace_icon") internal static let callDialpadCallIcon = ImageAsset(name: "call_dialpad_call_icon") + internal static let callGoToChatIcon = ImageAsset(name: "call_go_to_chat_icon") internal static let callHangupLarge = ImageAsset(name: "call_hangup_large") internal static let callMoreIcon = ImageAsset(name: "call_more_icon") internal static let callPausedIcon = ImageAsset(name: "call_paused_icon") @@ -104,7 +105,9 @@ internal enum Asset { internal static let actionSticker = ImageAsset(name: "action_sticker") internal static let error = ImageAsset(name: "error") internal static let errorMessageTick = ImageAsset(name: "error_message_tick") + internal static let newClose = ImageAsset(name: "new_close") internal static let roomActivitiesRetry = ImageAsset(name: "room_activities_retry") + internal static let roomScrollUp = ImageAsset(name: "room_scroll_up") internal static let scrolldown = ImageAsset(name: "scrolldown") internal static let scrolldownDark = ImageAsset(name: "scrolldown_dark") internal static let sendingMessageTick = ImageAsset(name: "sending_message_tick") diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index ab9947434..11d0dc52d 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -502,10 +502,14 @@ internal enum VectorL10n { internal static func callbarOnlyMultiplePaused(_ p1: String) -> String { return VectorL10n.tr("Vector", "callbar_only_multiple_paused", p1) } - /// Active call (%@) + /// Tap to return to the call (%@) internal static func callbarOnlySingleActive(_ p1: String) -> String { return VectorL10n.tr("Vector", "callbar_only_single_active", p1) } + /// Tap to Join the group call (%@) + internal static func callbarOnlySingleActiveGroup(_ p1: String) -> String { + return VectorL10n.tr("Vector", "callbar_only_single_active_group", p1) + } /// Paused call internal static var callbarOnlySinglePaused: String { return VectorL10n.tr("Vector", "callbar_only_single_paused") @@ -1238,13 +1242,41 @@ internal enum VectorL10n { internal static var errorUserAlreadyLoggedIn: String { return VectorL10n.tr("Vector", "error_user_already_logged_in") } + /// Answer + internal static var eventFormatterCallAnswer: String { + return VectorL10n.tr("Vector", "event_formatter_call_answer") + } /// Call back internal static var eventFormatterCallBack: String { return VectorL10n.tr("Vector", "event_formatter_call_back") } - /// This call has ended - internal static var eventFormatterCallHasEnded: String { - return VectorL10n.tr("Vector", "event_formatter_call_has_ended") + /// Connecting… + internal static var eventFormatterCallConnecting: String { + return VectorL10n.tr("Vector", "event_formatter_call_connecting") + } + /// Connection failed + internal static var eventFormatterCallConnectionFailed: String { + return VectorL10n.tr("Vector", "event_formatter_call_connection_failed") + } + /// Decline + internal static var eventFormatterCallDecline: String { + return VectorL10n.tr("Vector", "event_formatter_call_decline") + } + /// End call + internal static var eventFormatterCallEndCall: String { + return VectorL10n.tr("Vector", "event_formatter_call_end_call") + } + /// Ended %@ + internal static func eventFormatterCallHasEnded(_ p1: String) -> String { + return VectorL10n.tr("Vector", "event_formatter_call_has_ended", p1) + } + /// Retry + internal static var eventFormatterCallRetry: String { + return VectorL10n.tr("Vector", "event_formatter_call_retry") + } + /// Ringing… + internal static var eventFormatterCallRinging: String { + return VectorL10n.tr("Vector", "event_formatter_call_ringing") } /// Video call internal static var eventFormatterCallVideo: String { @@ -1254,7 +1286,7 @@ internal enum VectorL10n { internal static var eventFormatterCallVoice: String { return VectorL10n.tr("Vector", "event_formatter_call_voice") } - /// You're currently in this call + /// Active call internal static var eventFormatterCallYouCurrentlyIn: String { return VectorL10n.tr("Vector", "event_formatter_call_you_currently_in") } @@ -1262,6 +1294,26 @@ internal enum VectorL10n { internal static var eventFormatterCallYouDeclined: String { return VectorL10n.tr("Vector", "event_formatter_call_you_declined") } + /// You missed this call + internal static var eventFormatterCallYouMissed: String { + return VectorL10n.tr("Vector", "event_formatter_call_you_missed") + } + /// Group call + internal static var eventFormatterGroupCall: String { + return VectorL10n.tr("Vector", "event_formatter_group_call") + } + /// %@ in %@ + internal static func eventFormatterGroupCallIncoming(_ p1: String, _ p2: String) -> String { + return VectorL10n.tr("Vector", "event_formatter_group_call_incoming", p1, p2) + } + /// Join + internal static var eventFormatterGroupCallJoin: String { + return VectorL10n.tr("Vector", "event_formatter_group_call_join") + } + /// Leave + internal static var eventFormatterGroupCallLeave: String { + return VectorL10n.tr("Vector", "event_formatter_group_call_leave") + } /// VoIP conference added by %@ internal static func eventFormatterJitsiWidgetAdded(_ p1: String) -> String { return VectorL10n.tr("Vector", "event_formatter_jitsi_widget_added", p1) @@ -2874,7 +2926,11 @@ internal enum VectorL10n { internal static var roomIntroCellInformationRoomWithoutTopicSentence2Part2: String { return VectorL10n.tr("Vector", "room_intro_cell_information_room_without_topic_sentence2_part2") } - /// Jump to first unread message + /// Join + internal static var roomJoinGroupCall: String { + return VectorL10n.tr("Vector", "room_join_group_call") + } + /// Jump to unread internal static var roomJumpToFirstUnread: String { return VectorL10n.tr("Vector", "room_jump_to_first_unread") } @@ -2950,6 +3006,10 @@ internal enum VectorL10n { internal static func roomNewMessagesNotification(_ p1: Int) -> String { return VectorL10n.tr("Vector", "room_new_messages_notification", p1) } + /// You need to be an admin or a moderator to start a call. + internal static var roomNoPrivilegesToCreateGroupCall: String { + return VectorL10n.tr("Vector", "room_no_privileges_to_create_group_call") + } /// Connectivity to the server has been lost. internal static var roomOfflineNotification: String { return VectorL10n.tr("Vector", "room_offline_notification") @@ -3330,6 +3390,10 @@ internal enum VectorL10n { internal static var roomResourceUsageLimitReachedMessageContact3: String { return VectorL10n.tr("Vector", "room_resource_usage_limit_reached_message_contact_3") } + /// Slide to end the call for everyone + internal static var roomSlideToEndGroupCall: String { + return VectorL10n.tr("Vector", "room_slide_to_end_group_call") + } /// Invite members internal static var roomTitleInviteMembers: String { return VectorL10n.tr("Vector", "room_title_invite_members") @@ -4230,6 +4294,10 @@ internal enum VectorL10n { internal static var settingsLabsE2eEncryptionPromptMessage: String { return VectorL10n.tr("Vector", "settings_labs_e2e_encryption_prompt_message") } + /// Ring for group calls + internal static var settingsLabsEnableRingingForGroupCalls: String { + return VectorL10n.tr("Vector", "settings_labs_enable_ringing_for_group_calls") + } /// React to messages with emoji internal static var settingsLabsMessageReaction: String { return VectorL10n.tr("Vector", "settings_labs_message_reaction") diff --git a/Riot/Managers/Call/CallPresenter.swift b/Riot/Managers/Call/CallPresenter.swift index 94d03f578..b0e72d878 100644 --- a/Riot/Managers/Call/CallPresenter.swift +++ b/Riot/Managers/Call/CallPresenter.swift @@ -17,39 +17,67 @@ import Foundation import MatrixKit +// swiftlint:disable file_length + +#if canImport(JitsiMeetSDK) +import JitsiMeetSDK +import CallKit +#endif + +/// The number of milliseconds in one second. +private let MSEC_PER_SEC: TimeInterval = 1000 + @objcMembers /// Service to manage call screens and call bar UI management. class CallPresenter: NSObject { private enum Constants { static let pipAnimationDuration: TimeInterval = 0.25 + static let groupCallInviteLifetime: TimeInterval = 30 } + /// Utilized sessions private var sessions: [MXSession] = [] + /// Call view controllers map. Keys are callIds. private var callVCs: [String: CallViewController] = [:] + /// Call background tasks map. Keys are callIds. private var callBackgroundTasks: [String: MXBackgroundTask] = [:] - private weak var presentedCallVC: CallViewController? { + /// Actively presented direct call view controller. + private weak var presentedCallVC: UIViewController? { didSet { updateOnHoldCall() } } - private weak var inBarCallVC: CallViewController? - private weak var pipCallVC: CallViewController? + private weak var inBarCallVC: UIViewController? + private weak var pipCallVC: UIViewController? + /// UI operation queue for various UI operations private var uiOperationQueue: OperationQueue = .main + /// Flag to indicate whether the presenter is active. private var isStarted: Bool = false private var callTimer: Timer? + #if canImport(JitsiMeetSDK) + private var widgetEventsListener: Any? + /// Jitsi calls map. Keys are CallKit call UUIDs, values are corresponding widgets. + private var jitsiCalls: [UUID: Widget] = [:] + /// The current Jitsi view controller being displayed or not. + private(set) var jitsiVC: JitsiViewController? { + didSet { + updateOnHoldCall() + } + } + #endif private var isCallKitEnabled: Bool { MXCallKitAdapter.callKitAvailable() && MXKAppSettings.standard()?.isCallKitEnabled == true } - private var activeCallVC: CallViewController? { + private var activeCallVC: UIViewController? { return callVCs.values.filter { (callVC) -> Bool in guard let call = callVC.mxCall else { return false } return !call.isOnHold - }.first + }.first ?? jitsiVC } private var onHoldCallVCs: [CallViewController] { @@ -101,20 +129,201 @@ class CallPresenter: NSObject { } /// Method to be called when the call status bar is tapped. - /// - Returns: If the user interaction handled or not - func callStatusBarButtonTapped() -> Bool { - if let callVC = inBarCallVC ?? activeCallVC { + func callStatusBarTapped() { + if let callVC = (inBarCallVC ?? activeCallVC) as? CallViewController { dismissCallBar(for: callVC) presentCallVC(callVC) - return true + return + } + if let jitsiVC = jitsiVC { + dismissCallBar(for: jitsiVC) + presentCallVC(jitsiVC) + } + } + + // MARK - Group Calls + + /// Open the Jitsi view controller from a widget. + /// - Parameter widget: the jitsi widget + func displayJitsiCall(withWidget widget: Widget) { + #if canImport(JitsiMeetSDK) + let createJitsiBlock = { [weak self] in + guard let self = self else { return } + self.jitsiVC = JitsiViewController() + self.jitsiVC?.openWidget(widget, withVideo: true, success: { [weak self] in + guard let self = self else { return } + if let jitsiVC = self.jitsiVC { + jitsiVC.delegate = self + self.presentCallVC(jitsiVC) + self.startJitsiCall(withWidget: widget) + } + }, failure: { [weak self] (error) in + guard let self = self else { return } + self.jitsiVC = nil + AppDelegate.theDelegate().showAlert(withTitle: nil, + message: VectorL10n.callJitsiError) + }) + } + + if let jitsiVC = jitsiVC { + if jitsiVC.widget.widgetId == widget.widgetId { + self.presentCallVC(jitsiVC) + } else { + // end previous Jitsi call first + endActiveJitsiCall() + createJitsiBlock() + } + } else { + createJitsiBlock() + } + #else + AppDelegate.theDelegate().showAlert(withTitle: nil, + message: Bundle.mxk_localizedString(forKey: "not_supported_yet")) + #endif + } + + private func startJitsiCall(withWidget widget: Widget) { + if self.jitsiCalls.first(where: { $0.value.widgetId == widget.widgetId })?.key != nil { + // this Jitsi call is already managed by this class, no need to report the call again + return + } + + guard let roomId = widget.roomId else { + return + } + + guard let session = sessions.first else { + return + } + + guard let room = session.room(withRoomId: roomId) else { + return + } + + let newUUID = UUID() + let handle = CXHandle(type: .generic, value: roomId) + let startCallAction = CXStartCallAction(call: newUUID, handle: handle) + let transaction = CXTransaction(action: startCallAction) + JMCallKitProxy.request(transaction) { (error) in + if error == nil { + JMCallKitProxy.reportCallUpdate(with: newUUID, + handle: roomId, + displayName: room.summary.displayname, + hasVideo: true) + JMCallKitProxy.reportOutgoingCall(with: newUUID, connectedAt: nil) + + self.jitsiCalls[newUUID] = widget + } + } + } + + func endActiveJitsiCall() { + guard let jitsiVC = jitsiVC else { + // there is no active Jitsi call + return + } + + if pipCallVC == jitsiVC { + // this call currently in the PiP mode, + // first present it by exiting PiP mode and then dismiss it + exitPipCallVC(jitsiVC) + } + + dismissCallVC(jitsiVC) + jitsiVC.hangup() + + self.jitsiVC = nil + + guard let widget = jitsiVC.widget else { + return + } + guard let uuid = self.jitsiCalls.first(where: { $0.value.widgetId == widget.widgetId })?.key else { + // this Jitsi call is not managed by this class + return + } + + let endCallAction = CXEndCallAction(call: uuid) + let transaction = CXTransaction(action: endCallAction) + JMCallKitProxy.request(transaction) { (error) in + if error == nil { + self.jitsiCalls.removeValue(forKey: uuid) + } + } + } + + func processWidgetEvent(_ event: MXEvent, inSession session: MXSession) { + guard JMCallKitProxy.isProviderConfigured() else { + // CallKit proxy is not configured, no benefit in parsing the event + return + } + + guard let widget = Widget(widgetEvent: event, inMatrixSession: session) else { + return + } + + if self.jitsiCalls.first(where: { $0.value.widgetId == widget.widgetId })?.key != nil { + // this Jitsi call is already managed by this class, no need to report the call again + return + } + + if widget.isActive { + guard widget.type == kWidgetTypeJitsiV1 || widget.type == kWidgetTypeJitsiV2 else { + // not a Jitsi widget, ignore + return + } + + if let jitsiVC = jitsiVC, + jitsiVC.widget.widgetId == widget.widgetId { + // this is already the Jitsi call we have atm + return + } + + if TimeInterval(event.age)/MSEC_PER_SEC > Constants.groupCallInviteLifetime { + // too late to process the event + return + } + + // an active Jitsi widget + let newUUID = UUID() + + // assume this Jitsi call will survive + self.jitsiCalls[newUUID] = widget + + if event.sender == session.myUserId { + // outgoing call + JMCallKitProxy.reportOutgoingCall(with: newUUID, connectedAt: nil) + } else { + // incoming call + guard RiotSettings.shared.enableRingingForGroupCalls else { + // do not ring for Jitsi calls + return + } + let user = session.user(withUserId: event.sender) + let displayName = NSString.localizedUserNotificationString(forKey: "GROUP_CALL_FROM_USER", + arguments: [user?.displayname as Any]) + JMCallKitProxy.reportNewIncomingCall(UUID: newUUID, + handle: widget.roomId, + displayName: displayName, + hasVideo: true) { (error) in + if error != nil { + self.jitsiCalls.removeValue(forKey: newUUID) + } + } + } + } else { + guard let uuid = self.jitsiCalls.first(where: { $0.value.widgetId == widget.widgetId })?.key else { + // this Jitsi call is not managed by this class + return + } + JMCallKitProxy.reportCall(with: uuid, endedAt: nil, reason: .remoteEnded) + self.jitsiCalls.removeValue(forKey: uuid) } - return false } // MARK: - Private private func updateOnHoldCall() { - guard let presentedCallVC = presentedCallVC else { + guard let presentedCallVC = presentedCallVC as? CallViewController else { return } @@ -131,9 +340,6 @@ class CallPresenter: NSObject { } private func shouldHandleCall(_ call: MXCall) -> Bool { - if let delegate = delegate, !delegate.callPresenter(self, shouldHandleNewCall: call) { - return false - } return callVCs.count < maximumNumberOfConcurrentCalls } @@ -181,7 +387,19 @@ class CallPresenter: NSObject { } return } - dismissCallVC(callVC, completion: completion) + if callVC.isDisplayingAlert { + completion() + } else { + dismissCallVC(callVC, completion: completion) + } + } + + private func logCallVC(_ callVC: UIViewController, log: String) { + if let callVC = callVC as? CallViewController { + NSLog("[CallPresenter] \(log): call: \(String(describing: callVC.mxCall?.callId))") + } else if let callVC = callVC as? JitsiViewController { + NSLog("[CallPresenter] \(log): call: \(callVC.widget.widgetId)") + } } // MARK: - Timer @@ -200,17 +418,18 @@ class CallPresenter: NSObject { } @objc private func callTimerFired(_ timer: Timer) { - guard let inBarCallVC = inBarCallVC else { - return + if let inBarCallVC = inBarCallVC as? CallViewController { + guard let call = inBarCallVC.mxCall else { + return + } + guard call.state != .ended else { + return + } + + updateCallBar() + } else if inBarCallVC as? JitsiViewController != nil { + updateCallBar() } - guard let call = inBarCallVC.mxCall else { - return - } - guard call.state != .ended else { - return - } - - presentCallBar(for: inBarCallVC, isUpdateOnly: true) } // MARK: - Observers @@ -232,8 +451,32 @@ class CallPresenter: NSObject { selector: #selector(callTileTapped(_:)), name: .RoomCallTileTapped, object: nil) + NotificationCenter.default.addObserver(self, + selector: #selector(groupCallTileTapped(_:)), + name: .RoomGroupCallTileTapped, + object: nil) isStarted = true + + #if canImport(JitsiMeetSDK) + JMCallKitProxy.addListener(self) + + guard let session = sessions.first else { + return + } + + widgetEventsListener = session.listenToEvents([ + MXEventType(identifier: kWidgetMatrixEventTypeString), + MXEventType(identifier: kWidgetModularEventTypeString) + ]) { (event, direction, _) in + if direction == .backwards { + // ignore backwards events + return + } + + self.processWidgetEvent(event, inSession: session) + } + #endif } private func removeCallObservers() { @@ -250,8 +493,24 @@ class CallPresenter: NSObject { NotificationCenter.default.removeObserver(self, name: .RoomCallTileTapped, object: nil) + NotificationCenter.default.removeObserver(self, + name: .RoomGroupCallTileTapped, + object: nil) isStarted = false + + #if canImport(JitsiMeetSDK) + JMCallKitProxy.removeListener(self) + + guard let session = sessions.first else { + return + } + + if let widgetEventsListener = widgetEventsListener { + session.removeListener(widgetEventsListener) + } + widgetEventsListener = nil + #endif } @objc @@ -269,6 +528,15 @@ class CallPresenter: NSObject { } newCallVC.playRingtone = !isCallKitEnabled newCallVC.delegate = self + + if !call.isIncoming { + // put other native calls on hold + callVCs.values.forEach({ $0.mxCall.hold(true) }) + + // terminate Jitsi calls + endActiveJitsiCall() + } + callVCs[call.callId] = newCallVC if UIApplication.shared.applicationState == .background && call.isIncoming { @@ -276,7 +544,7 @@ class CallPresenter: NSObject { // Without CallKit this will allow us to play vibro until the call was ended // With CallKit we'll inform the system when the call is ended to let the system terminate our app to save resources let handler = MXSDKOptions.sharedInstance().backgroundModeHandler - let callBackgroundTask = handler.startBackgroundTask(withName: "[CallService] addMatrixCallObserver", expirationHandler: nil) + let callBackgroundTask = handler.startBackgroundTask(withName: "[CallPresenter] addMatrixCallObserver", expirationHandler: nil) callBackgroundTasks[call.callId] = callBackgroundTask } @@ -296,23 +564,23 @@ class CallPresenter: NSObject { switch call.state { case .createAnswer: - NSLog("[CallService] callStateChanged: call created answer: \(call.callId)") + NSLog("[CallPresenter] callStateChanged: call created answer: \(call.callId)") if call.isIncoming, isCallKitEnabled, let callVC = callVCs[call.callId] { presentCallVC(callVC) } case .connected: - NSLog("[CallService] callStateChanged: call connected: \(call.callId)") + NSLog("[CallPresenter] callStateChanged: call connected: \(call.callId)") callTimer?.fire() case .onHold: - NSLog("[CallService] callStateChanged: call holded: \(call.callId)") + NSLog("[CallPresenter] callStateChanged: call holded: \(call.callId)") callTimer?.fire() callHolded(withCallId: call.callId) case .remotelyOnHold: - NSLog("[CallService] callStateChanged: call remotely holded: \(call.callId)") + NSLog("[CallPresenter] callStateChanged: call remotely holded: \(call.callId)") callTimer?.fire() callHolded(withCallId: call.callId) case .ended: - NSLog("[CallService] callStateChanged: call ended: \(call.callId)") + NSLog("[CallPresenter] callStateChanged: call ended: \(call.callId)") endCall(withCallId: call.callId) default: break @@ -321,7 +589,8 @@ class CallPresenter: NSObject { @objc private func callTileTapped(_ notification: Notification) { - NSLog("[CallService] callTileTapped") + NSLog("[CallPresenter] callTileTapped") + guard let bubbleData = notification.object as? RoomBubbleCellData else { return } @@ -334,7 +603,7 @@ class CallPresenter: NSObject { return } - NSLog("[CallService] callTileTapped: for call: \(callEventContent.callId)") + NSLog("[CallPresenter] callTileTapped: for call: \(callEventContent.callId)") guard let session = sessions.first else { return } @@ -350,13 +619,55 @@ class CallPresenter: NSObject { return } - presentCallVC(callVC) + if callVC == pipCallVC { + exitPipCallVC(callVC) + } else { + presentCallVC(callVC) + } + } + + @objc + private func groupCallTileTapped(_ notification: Notification) { + NSLog("[CallPresenter] groupCallTileTapped") + + guard let bubbleData = notification.object as? RoomBubbleCellData else { + return + } + + guard let randomEvent = bubbleData.allLinkedEvents().randomElement() else { + return + } + + guard randomEvent.eventType == .custom, + (randomEvent.type == kWidgetMatrixEventTypeString || + randomEvent.type == kWidgetModularEventTypeString) else { + return + } + + guard let session = sessions.first else { return } + + guard let widget = Widget(widgetEvent: randomEvent, inMatrixSession: session) else { + return + } + + NSLog("[CallPresenter] groupCallTileTapped: for call: \(widget.widgetId)") + + guard let jitsiVC = jitsiVC, + jitsiVC.widget.widgetId == widget.widgetId else { + return + } + + if jitsiVC == pipCallVC { + exitPipCallVC(jitsiVC) + } else { + presentCallVC(jitsiVC) + } } // MARK: - Call Screens - private func presentCallVC(_ callVC: CallViewController, completion: (() -> Void)? = nil) { - NSLog("[CallService] presentCallVC: call: \(String(describing: callVC.mxCall?.callId))") + private func presentCallVC(_ callVC: UIViewController, completion: (() -> Void)? = nil) { + logCallVC(callVC, log: "presentCallVC") // do not use PiP transitions here, as we really want to present the screen callVC.transitioningDelegate = nil @@ -379,8 +690,8 @@ class CallPresenter: NSObject { uiOperationQueue.addOperation(operation) } - private func dismissCallVC(_ callVC: CallViewController, completion: (() -> Void)? = nil) { - NSLog("[CallService] dismissCallVC: call: \(String(describing: callVC.mxCall?.callId))") + private func dismissCallVC(_ callVC: UIViewController, completion: (() -> Void)? = nil) { + logCallVC(callVC, log: "dismissCallVC") // do not use PiP transitions here, as we really want to dismiss the screen callVC.transitioningDelegate = nil @@ -394,8 +705,8 @@ class CallPresenter: NSObject { uiOperationQueue.addOperation(operation) } - private func enterPipCallVC(_ callVC: CallViewController, completion: (() -> Void)? = nil) { - NSLog("[CallService] enterPipCallVC: call: \(String(describing: callVC.mxCall?.callId))") + private func enterPipCallVC(_ callVC: UIViewController, completion: (() -> Void)? = nil) { + logCallVC(callVC, log: "enterPipCallVC") // assign self as transitioning delegate callVC.transitioningDelegate = self @@ -410,8 +721,8 @@ class CallPresenter: NSObject { uiOperationQueue.addOperation(operation) } - private func exitPipCallVC(_ callVC: CallViewController, completion: (() -> Void)? = nil) { - NSLog("[CallService] exitPipCallVC: call: \(String(describing: callVC.mxCall?.callId))") + private func exitPipCallVC(_ callVC: UIViewController, completion: (() -> Void)? = nil) { + logCallVC(callVC, log: "exitPipCallVC") // assign self as transitioning delegate callVC.transitioningDelegate = self @@ -428,24 +739,29 @@ class CallPresenter: NSObject { // MARK: - Call Bar - private func presentCallBar(for callVC: CallViewController?, isUpdateOnly: Bool = false, completion: (() -> Void)? = nil) { - NSLog("[CallService] presentCallBar: call: \(String(describing: callVC?.mxCall?.callId))") + private func presentCallBar(for callVC: UIViewController, completion: (() -> Void)? = nil) { + logCallVC(callVC, log: "presentCallBar") let activeCallVC = self.activeCallVC let operation = CallBarPresentOperation(presenter: self, activeCallVC: activeCallVC, numberOfPausedCalls: numberOfPausedCalls) { [weak self] in // active calls are more prior to paused ones. - // So, if user taps the bar when we have one active and one paused calls, we navigate to the active one. - if !isUpdateOnly { - self?.inBarCallVC = activeCallVC ?? callVC - } + // So, if user taps the bar when we have one active and one paused call, we navigate to the active one. + self?.inBarCallVC = activeCallVC ?? callVC completion?() } uiOperationQueue.addOperation(operation) } - private func dismissCallBar(for callVC: CallViewController, completion: (() -> Void)? = nil) { - NSLog("[CallService] dismissCallBar: call: \(String(describing: callVC.mxCall?.callId))") + private func updateCallBar() { + let activeCallVC = self.activeCallVC + + let operation = CallBarUpdateOperation(presenter: self, activeCallVC: activeCallVC, numberOfPausedCalls: numberOfPausedCalls) + uiOperationQueue.addOperation(operation) + } + + private func dismissCallBar(for callVC: UIViewController, completion: (() -> Void)? = nil) { + logCallVC(callVC, log: "dismissCallBar") let operation = CallBarDismissOperation(presenter: self) { [weak self] in if callVC == self?.inBarCallVC { @@ -474,8 +790,13 @@ extension CallPresenter: MXKCallViewControllerDelegate { // wait for the call state changes, will be handled there return } else { - dismissCallVC(callVC) - self.presentCallBar(for: callVC, completion: completion) + if callVC.mxCall.isVideoCall { + // go to pip mode here + enterPipCallVC(callVC, completion: completion) + } else { + dismissCallVC(callVC) + self.presentCallBar(for: callVC, completion: completion) + } } } @@ -497,20 +818,6 @@ extension CallPresenter: MXKCallViewControllerDelegate { presentCallVC(onHoldCallVC) } - func callViewControllerDidTapPiPButton(_ callViewController: MXKCallViewController!) { - guard let callVC = callViewController as? CallViewController else { - // this call screen is not handled by this service - return - } - - // sanity check - // do not enter PiP mode if not a video call - guard callVC.mxCall.isVideoCall else { return } - - // go to pip mode here - enterPipCallVC(callVC) - } - } // MARK: - UIViewControllerTransitioningDelegate @@ -562,3 +869,84 @@ extension OperationQueue { } } + +#if canImport(JitsiMeetSDK) +// MARK: - JMCallKitListener + +extension CallPresenter: JMCallKitListener { + + func providerDidReset() { + + } + + func performAnswerCall(UUID: UUID) { + guard let widget = jitsiCalls[UUID] else { + return + } + + displayJitsiCall(withWidget: widget) + } + + func performEndCall(UUID: UUID) { + guard let widget = jitsiCalls[UUID] else { + return + } + + if let jitsiVC = jitsiVC, jitsiVC.widget.widgetId == widget.widgetId { + // hangup an active call + dismissCallVC(jitsiVC) + endActiveJitsiCall() + } else { + // decline incoming call + JitsiService.shared.declineWidget(withId: widget.widgetId) + } + } + + func performSetMutedCall(UUID: UUID, isMuted: Bool) { + guard let widget = jitsiCalls[UUID] else { + return + } + + if let jitsiVC = jitsiVC, jitsiVC.widget.widgetId == widget.widgetId { + // mute the active Jitsi call + jitsiVC.setAudioMuted(isMuted) + } + } + + func performStartCall(UUID: UUID, isVideo: Bool) { + + } + + func providerDidActivateAudioSession(session: AVAudioSession) { + + } + + func providerDidDeactivateAudioSession(session: AVAudioSession) { + + } + + func providerTimedOutPerformingAction(action: CXAction) { + + } + +} + +// MARK: - JitsiViewControllerDelegate + +extension CallPresenter: JitsiViewControllerDelegate { + + func jitsiViewController(_ jitsiViewController: JitsiViewController!, dismissViewJitsiController completion: (() -> Void)!) { + if jitsiViewController == jitsiVC { + endActiveJitsiCall() + } + } + + func jitsiViewController(_ jitsiViewController: JitsiViewController!, goBackToApp completion: (() -> Void)!) { + if jitsiViewController == jitsiVC { + enterPipCallVC(jitsiViewController, completion: completion) + } + } + +} + +#endif diff --git a/Riot/Managers/Call/CallPresenterDelegate.swift b/Riot/Managers/Call/CallPresenterDelegate.swift index c3a47cb3a..57ee1c941 100644 --- a/Riot/Managers/Call/CallPresenterDelegate.swift +++ b/Riot/Managers/Call/CallPresenterDelegate.swift @@ -18,32 +18,31 @@ import Foundation @objc protocol CallPresenterDelegate: class { - // New call - func callPresenter(_ presenter: CallPresenter, - shouldHandleNewCall call: MXCall) -> Bool - // Call screens func callPresenter(_ presenter: CallPresenter, - presentCallViewController viewController: CallViewController, + presentCallViewController viewController: UIViewController, completion:(() -> Void)?) func callPresenter(_ presenter: CallPresenter, - dismissCallViewController viewController: CallViewController, + dismissCallViewController viewController: UIViewController, completion:(() -> Void)?) // Call Bar func callPresenter(_ presenter: CallPresenter, - presentCallBarFor activeCallViewController: CallViewController?, + presentCallBarFor activeCallViewController: UIViewController?, numberOfPausedCalls: UInt, completion:(() -> Void)?) + func callPresenter(_ presenter: CallPresenter, + updateCallBarFor activeCallViewController: UIViewController?, + numberOfPausedCalls: UInt) func callPresenter(_ presenter: CallPresenter, dismissCallBar completion:(() -> Void)?) // PiP func callPresenter(_ presenter: CallPresenter, - enterPipForCallViewController viewController: CallViewController, + enterPipForCallViewController viewController: UIViewController, completion:(() -> Void)?) func callPresenter(_ presenter: CallPresenter, - exitPipForCallViewController viewController: CallViewController, + exitPipForCallViewController viewController: UIViewController, completion:(() -> Void)?) } diff --git a/Riot/Managers/Call/Operations/CallBarPresentOperation.swift b/Riot/Managers/Call/Operations/CallBarPresentOperation.swift index 8e2eb610a..31ec542f1 100644 --- a/Riot/Managers/Call/Operations/CallBarPresentOperation.swift +++ b/Riot/Managers/Call/Operations/CallBarPresentOperation.swift @@ -19,12 +19,12 @@ import Foundation class CallBarPresentOperation: AsyncOperation { private var presenter: CallPresenter - private var activeCallVC: CallViewController? + private var activeCallVC: UIViewController? private var numberOfPausedCalls: UInt private var completion: (() -> Void)? init(presenter: CallPresenter, - activeCallVC: CallViewController?, + activeCallVC: UIViewController?, numberOfPausedCalls: UInt, completion: (() -> Void)? = nil) { self.presenter = presenter @@ -36,7 +36,10 @@ class CallBarPresentOperation: AsyncOperation { override func main() { presenter.delegate?.callPresenter(presenter, presentCallBarFor: activeCallVC, numberOfPausedCalls: numberOfPausedCalls, completion: { self.finish() - self.completion?() + // wait for the next life cycle to detect status bar layout updates + DispatchQueue.main.async { + self.completion?() + } }) } diff --git a/Riot/Managers/Call/Operations/CallBarUpdateOperation.swift b/Riot/Managers/Call/Operations/CallBarUpdateOperation.swift new file mode 100644 index 000000000..23c7a5f5d --- /dev/null +++ b/Riot/Managers/Call/Operations/CallBarUpdateOperation.swift @@ -0,0 +1,38 @@ +// +// Copyright 2020 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +class CallBarUpdateOperation: AsyncOperation { + + private var presenter: CallPresenter + private var activeCallVC: UIViewController? + private var numberOfPausedCalls: UInt + + init(presenter: CallPresenter, + activeCallVC: UIViewController?, + numberOfPausedCalls: UInt) { + self.presenter = presenter + self.activeCallVC = activeCallVC + self.numberOfPausedCalls = numberOfPausedCalls + } + + override func main() { + presenter.delegate?.callPresenter(presenter, updateCallBarFor: activeCallVC, numberOfPausedCalls: numberOfPausedCalls) + self.finish() + } + +} diff --git a/Riot/Managers/Call/Operations/CallVCDismissOperation.swift b/Riot/Managers/Call/Operations/CallVCDismissOperation.swift index 7a7073d39..facc8428c 100644 --- a/Riot/Managers/Call/Operations/CallVCDismissOperation.swift +++ b/Riot/Managers/Call/Operations/CallVCDismissOperation.swift @@ -19,11 +19,11 @@ import Foundation class CallVCDismissOperation: AsyncOperation { private var presenter: CallPresenter - private var callVC: CallViewController + private var callVC: UIViewController private var completion: (() -> Void)? init(presenter: CallPresenter, - callVC: CallViewController, + callVC: UIViewController, completion: (() -> Void)? = nil) { self.presenter = presenter self.callVC = callVC diff --git a/Riot/Managers/Call/Operations/CallVCEnterPipOperation.swift b/Riot/Managers/Call/Operations/CallVCEnterPipOperation.swift index be140e7ae..74bde2785 100644 --- a/Riot/Managers/Call/Operations/CallVCEnterPipOperation.swift +++ b/Riot/Managers/Call/Operations/CallVCEnterPipOperation.swift @@ -19,11 +19,11 @@ import Foundation class CallVCEnterPipOperation: AsyncOperation { private var presenter: CallPresenter - private var callVC: CallViewController + private var callVC: UIViewController private var completion: (() -> Void)? init(presenter: CallPresenter, - callVC: CallViewController, + callVC: UIViewController, completion: (() -> Void)? = nil) { self.presenter = presenter self.callVC = callVC diff --git a/Riot/Managers/Call/Operations/CallVCExitPipOperation.swift b/Riot/Managers/Call/Operations/CallVCExitPipOperation.swift index 0a4d17147..1e74c7ded 100644 --- a/Riot/Managers/Call/Operations/CallVCExitPipOperation.swift +++ b/Riot/Managers/Call/Operations/CallVCExitPipOperation.swift @@ -19,11 +19,11 @@ import Foundation class CallVCExitPipOperation: AsyncOperation { private var presenter: CallPresenter - private var callVC: CallViewController + private var callVC: UIViewController private var completion: (() -> Void)? init(presenter: CallPresenter, - callVC: CallViewController, + callVC: UIViewController, completion: (() -> Void)? = nil) { self.presenter = presenter self.callVC = callVC diff --git a/Riot/Managers/Call/Operations/CallVCPresentOperation.swift b/Riot/Managers/Call/Operations/CallVCPresentOperation.swift index 1a58f86cd..fae80e51b 100644 --- a/Riot/Managers/Call/Operations/CallVCPresentOperation.swift +++ b/Riot/Managers/Call/Operations/CallVCPresentOperation.swift @@ -19,11 +19,11 @@ import Foundation class CallVCPresentOperation: AsyncOperation { private var presenter: CallPresenter - private var callVC: CallViewController + private var callVC: UIViewController private var completion: (() -> Void)? init(presenter: CallPresenter, - callVC: CallViewController, + callVC: UIViewController, completion: (() -> Void)? = nil) { self.presenter = presenter self.callVC = callVC diff --git a/Riot/Managers/Call/PiPAnimator.swift b/Riot/Managers/Call/PiPAnimator.swift index 17faf256e..46171be6d 100644 --- a/Riot/Managers/Call/PiPAnimator.swift +++ b/Riot/Managers/Call/PiPAnimator.swift @@ -25,7 +25,7 @@ import Foundation class PiPAnimator: NSObject { private enum Constants { - static let pipViewScale: CGFloat = 0.3 + static let pipViewSize: CGSize = CGSize(width: 90, height: 130) } let animationDuration: TimeInterval @@ -62,15 +62,15 @@ class PiPAnimator: NSObject { pipView.delegate = pipViewDelegate keyWindow.addSubview(pipView) - let transform = CGAffineTransform(scaleX: Constants.pipViewScale, y: Constants.pipViewScale) - let targetRect = fromVC.view.bounds.applying(transform) + let scale = Constants.pipViewSize.width/pipView.frame.width + let transform = CGAffineTransform(scaleX: scale, y: scale) + let targetSize = Constants.pipViewSize let animator = UIViewPropertyAnimator(duration: animationDuration, dampingRatio: 1) { pipView.transform = transform pipView.move(in: keyWindow, - to: .bottomLeft, - targetSize: targetRect.size) + targetSize: targetSize) } animator.addCompletion { (position) in @@ -117,6 +117,7 @@ class PiPAnimator: NSObject { animator.addCompletion { (position) in toVC.additionalSafeAreaInsets = .zero + toVC.view.frame = context.finalFrame(for: toVC) toVC.view.isHidden = false snapshot.removeFromSuperview() diff --git a/Riot/Managers/Call/PiPView.swift b/Riot/Managers/Call/PiPView.swift index 2eb15b56c..8f5a0587a 100644 --- a/Riot/Managers/Call/PiPView.swift +++ b/Riot/Managers/Call/PiPView.swift @@ -17,8 +17,8 @@ import UIKit @objc enum PiPViewPosition: Int { - case bottomLeft // default value - case bottomRight + case bottomLeft + case bottomRight // default value case topRight case topLeft } @@ -32,12 +32,12 @@ import UIKit class PiPView: UIView { private enum Defaults { - static let margins: UIOffset = UIOffset(horizontal: 20, vertical: 20) + static let margins: UIEdgeInsets = UIEdgeInsets(top: 64, left: 20, bottom: 64, right: 20) static let cornerRadius: CGFloat = 8 static let animationDuration: TimeInterval = 0.25 } - var margins: UIOffset = Defaults.margins { + var margins: UIEdgeInsets = Defaults.margins { didSet { guard self.superview != nil else { return } self.move(to: self.position, animated: true) @@ -48,7 +48,7 @@ class PiPView: UIView { layer.cornerRadius = cornerRadius } } - var position: PiPViewPosition = .bottomLeft + var position: PiPViewPosition = .bottomRight weak var delegate: PiPViewDelegate? private var originalCenter: CGPoint = .zero @@ -97,7 +97,7 @@ NSLayoutConstraint.activate([ } func move(in view: UIView? = nil, - to position: PiPViewPosition = .bottomLeft, + to position: PiPViewPosition = .bottomRight, targetSize: CGSize? = nil, animated: Bool = false, completion: ((Bool) -> Void)? = nil) { @@ -148,25 +148,36 @@ NSLayoutConstraint.activate([ } let targetSize = targetSize ?? frame.size + var superviewWidth: CGFloat = 0 + var superviewHeight: CGFloat = 0 + + if UIDevice.current.orientation.isPortrait { + superviewWidth = min(view.bounds.width, view.bounds.height) + superviewHeight = max(view.bounds.width, view.bounds.height) + } else { + superviewWidth = max(view.bounds.width, view.bounds.height) + superviewHeight = min(view.bounds.width, view.bounds.height) + } + switch position { case .bottomLeft: - let origin = CGPoint(x: margins.horizontal + view.safeAreaInsets.left, - y: view.bounds.height - view.safeAreaInsets.bottom - targetSize.height - margins.vertical) + let origin = CGPoint(x: margins.left + view.safeAreaInsets.left, + y: superviewHeight - view.safeAreaInsets.bottom - targetSize.height - margins.bottom) return CGRect(origin: origin, size: targetSize) case .bottomRight: - let origin = CGPoint(x: view.bounds.width - view.safeAreaInsets.right - margins.horizontal - targetSize.width, - y: view.bounds.height - view.safeAreaInsets.bottom - targetSize.height - margins.vertical) + let origin = CGPoint(x: superviewWidth - view.safeAreaInsets.right - margins.right - targetSize.width, + y: superviewHeight - view.safeAreaInsets.bottom - targetSize.height - margins.bottom) return CGRect(origin: origin, size: targetSize) case .topRight: - let origin = CGPoint(x: view.bounds.width - view.safeAreaInsets.right - margins.horizontal - targetSize.width, - y: margins.vertical + view.safeAreaInsets.top) + let origin = CGPoint(x: superviewWidth - view.safeAreaInsets.right - margins.right - targetSize.width, + y: margins.top + view.safeAreaInsets.top) return CGRect(origin: origin, size: targetSize) case .topLeft: - let origin = CGPoint(x: margins.horizontal + view.safeAreaInsets.left, - y: margins.vertical + view.safeAreaInsets.top) + let origin = CGPoint(x: margins.left + view.safeAreaInsets.left, + y: margins.top + view.safeAreaInsets.top) return CGRect(origin: origin, size: targetSize) } diff --git a/Riot/Managers/PushNotification/PushNotificationService.m b/Riot/Managers/PushNotification/PushNotificationService.m index 44a639c9e..be6a61c79 100644 --- a/Riot/Managers/PushNotification/PushNotificationService.m +++ b/Riot/Managers/PushNotification/PushNotificationService.m @@ -323,6 +323,30 @@ Matrix session observer used to detect new opened sessions. #pragma mark - UNUserNotificationCenterDelegate +- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler +{ + NSDictionary *userInfo = notification.request.content.userInfo; + if (userInfo[Constants.userInfoKeyPresentNotificationOnForeground]) + { + if (!userInfo[Constants.userInfoKeyPresentNotificationInRoom] + && [[AppDelegate theDelegate].visibleRoomId isEqualToString:userInfo[@"room_id"]]) + { + // do not show the notification when we're in the notified room + completionHandler(UNNotificationPresentationOptionNone); + } + else + { + completionHandler(UNNotificationPresentationOptionBadge + | UNNotificationPresentationOptionSound + | UNNotificationPresentationOptionAlert); + } + } + else + { + completionHandler(UNNotificationPresentationOptionNone); + } +} + // iOS 10+, see application:handleActionWithIdentifier:forLocalNotification:withResponseInfo:completionHandler: - (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler { @@ -568,23 +592,37 @@ Matrix session observer used to detect new opened sessions. return; } - // process the call invite synchronously - [session.callManager handleCallEvent:lastCallInvite]; - MXCall *call = [session.callManager callWithCallId:lastCallInvite.content[@"call_id"]]; - if (call) + if (lastCallInvite.eventType == MXEventTypeCallInvite) { - [session.callManager.callKitAdapter reportIncomingCall:call]; - NSLog(@"[PushNotificationService] didReceiveIncomingPushWithPayload: Reporting new call in room %@ for the event: %@", roomId, eventId); - - // Wait for the sync response in cache to be processed for data integrity. - dispatch_group_notify(dispatchGroup, dispatch_get_main_queue(), ^{ - // After reporting the call, we can continue async. Launch a background sync to handle call answers/declines on other devices of the user. - [self launchBackgroundSync]; - }); + // process the call invite synchronously + [session.callManager handleCallEvent:lastCallInvite]; + MXCall *call = [session.callManager callWithCallId:lastCallInvite.content[@"call_id"]]; + if (call) + { + [session.callManager.callKitAdapter reportIncomingCall:call]; + NSLog(@"[PushNotificationService] didReceiveIncomingPushWithPayload: Reporting new call in room %@ for the event: %@", roomId, eventId); + + // Wait for the sync response in cache to be processed for data integrity. + dispatch_group_notify(dispatchGroup, dispatch_get_main_queue(), ^{ + // After reporting the call, we can continue async. Launch a background sync to handle call answers/declines on other devices of the user. + [self launchBackgroundSync]; + }); + } + else + { + NSLog(@"[PushNotificationService] didReceiveIncomingPushWithPayload: Error on call object on room %@ for the event: %@", roomId, eventId); + } + } + else if ([lastCallInvite.type isEqualToString:kWidgetMatrixEventTypeString] || + [lastCallInvite.type isEqualToString:kWidgetModularEventTypeString]) + { + [[AppDelegate theDelegate].callPresenter processWidgetEvent:lastCallInvite + inSession:session]; } else { - NSLog(@"[PushNotificationService] didReceiveIncomingPushWithPayload: Error on call object on room %@ for the event: %@", roomId, eventId); + // It's a serious error. There is nothing to avoid iOS to kill us here. + NSLog(@"[PushNotificationService] didReceiveIncomingPushWithPayload: We have an unknown type of event for %@. There is something wrong.", eventId); } } else diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift index cd39e7916..954741ccb 100644 --- a/Riot/Managers/Settings/RiotSettings.swift +++ b/Riot/Managers/Settings/RiotSettings.swift @@ -27,7 +27,6 @@ final class RiotSettings: NSObject { static let identityServerUrlString = "identityserverurl" static let enableCrashReport = "enableCrashReport" static let enableRageShake = "enableRageShake" - static let createConferenceCallsWithJitsi = "createConferenceCallsWithJitsi" static let userInterfaceTheme = "userInterfaceTheme" static let notificationsShowDecryptedContent = "showDecryptedContent" static let pinRoomsWithMissedNotifications = "pinRoomsWithMissedNotif" @@ -52,6 +51,7 @@ final class RiotSettings: NSObject { static let roomCreationScreenAllowRoomTypeConfiguration = "roomCreationScreenAllowRoomTypeConfiguration" static let roomCreationScreenRoomIsPublic = "roomCreationScreenRoomIsPublic" static let allowInviteExernalUsers = "allowInviteExernalUsers" + static let enableRingingForGroupCalls = "enableRingingForGroupCalls" static let roomSettingsScreenShowLowPriorityOption = "roomSettingsScreenShowLowPriorityOption" static let roomSettingsScreenShowDirectChatOption = "roomSettingsScreenShowDirectChatOption" static let roomSettingsScreenAllowChangingAccessSettings = "roomSettingsScreenAllowChangingAccessSettings" @@ -60,6 +60,8 @@ final class RiotSettings: NSObject { static let roomSettingsScreenShowFlairSettings = "roomSettingsScreenShowFlairSettings" static let roomSettingsScreenShowAdvancedSettings = "roomSettingsScreenShowAdvancedSettings" static let roomSettingsScreenAdvancedShowEncryptToVerifiedOption = "roomSettingsScreenAdvancedShowEncryptToVerifiedOption" + static let settingsScreenShowNotificationDecodedContentOption = "settingsScreenShowNotificationDecodedContentOption" + static let settingsScreenShowNsfwRoomsOption = "settingsScreenShowNsfwRoomsOption" static let roomsAllowToJoinPublicRooms = "roomsAllowToJoinPublicRooms" static let homeScreenShowFavouritesTab = "homeScreenShowFavouritesTab" static let homeScreenShowPeopleTab = "homeScreenShowPeopleTab" @@ -71,7 +73,11 @@ final class RiotSettings: NSObject { static let roomScreenAllowMediaLibraryAction = "roomScreenAllowMediaLibraryAction" static let roomScreenAllowStickerAction = "roomScreenAllowStickerAction" static let roomScreenAllowFilesAction = "roomScreenAllowFilesAction" + static let roomContextualMenuShowMoreOptionForMessages = "roomContextualMenuShowMoreOptionForMessages" + static let roomContextualMenuShowMoreOptionForStates = "roomContextualMenuShowMoreOptionForStates" + static let roomContextualMenuShowReportContentOption = "roomContextualMenuShowReportContentOption" static let roomInfoScreenShowIntegrations = "roomInfoScreenShowIntegrations" + static let roomMemberScreenShowIgnore = "roomMemberScreenShowIgnore" static let unifiedSearchScreenShowPublicDirectory = "unifiedSearchScreenShowPublicDirectory" } @@ -85,36 +91,6 @@ final class RiotSettings: NSObject { return userDefaults }() - // MARK: - Public - - func reset() { - defaults.removeObject(forKey: UserDefaultsKeys.settingsScreenShowChangePassword) - defaults.removeObject(forKey: UserDefaultsKeys.settingsScreenShowInviteFriends) - defaults.removeObject(forKey: UserDefaultsKeys.settingsScreenShowEnableStunServerFallback) - defaults.removeObject(forKey: UserDefaultsKeys.settingsSecurityScreenShowSessions) - defaults.removeObject(forKey: UserDefaultsKeys.settingsSecurityScreenShowSetupBackup) - defaults.removeObject(forKey: UserDefaultsKeys.settingsSecurityScreenShowRestoreBackup) - defaults.removeObject(forKey: UserDefaultsKeys.settingsSecurityScreenShowDeleteBackup) - defaults.removeObject(forKey: UserDefaultsKeys.settingsSecurityScreenShowCryptographyInfo) - defaults.removeObject(forKey: UserDefaultsKeys.settingsSecurityScreenShowCryptographyExport) - defaults.removeObject(forKey: UserDefaultsKeys.settingsSecurityScreenShowAdvancedUnverifiedDevices) - defaults.removeObject(forKey: UserDefaultsKeys.roomCreationScreenAllowEncryptionConfiguration) - defaults.removeObject(forKey: UserDefaultsKeys.roomCreationScreenRoomIsEncrypted) - defaults.removeObject(forKey: UserDefaultsKeys.roomCreationScreenAllowRoomTypeConfiguration) - defaults.removeObject(forKey: UserDefaultsKeys.roomCreationScreenRoomIsPublic) - defaults.removeObject(forKey: UserDefaultsKeys.allowInviteExernalUsers) - defaults.removeObject(forKey: UserDefaultsKeys.roomSettingsScreenShowLowPriorityOption) - defaults.removeObject(forKey: UserDefaultsKeys.roomSettingsScreenShowDirectChatOption) - defaults.removeObject(forKey: UserDefaultsKeys.roomSettingsScreenAllowChangingAccessSettings) - defaults.removeObject(forKey: UserDefaultsKeys.roomSettingsScreenAllowChangingHistorySettings) - defaults.removeObject(forKey: UserDefaultsKeys.roomSettingsScreenShowAddressSettings) - defaults.removeObject(forKey: UserDefaultsKeys.roomSettingsScreenShowFlairSettings) - defaults.removeObject(forKey: UserDefaultsKeys.roomSettingsScreenShowAdvancedSettings) - defaults.removeObject(forKey: UserDefaultsKeys.roomSettingsScreenAdvancedShowEncryptToVerifiedOption) - defaults.removeObject(forKey: UserDefaultsKeys.allowInviteExernalUsers) - defaults.removeObject(forKey: UserDefaultsKeys.roomsAllowToJoinPublicRooms) - } - // MARK: Servers var homeserverUrlString: String { @@ -228,14 +204,15 @@ final class RiotSettings: NSObject { // MARK: Labs - var createConferenceCallsWithJitsi: Bool { + /// Indicates if CallKit ringing is enabled for group calls. This setting does not disable the CallKit integration for group calls, only relates to ringing. + var enableRingingForGroupCalls: Bool { get { - return defaults.bool(forKey: UserDefaultsKeys.createConferenceCallsWithJitsi) + return defaults.bool(forKey: UserDefaultsKeys.enableRingingForGroupCalls) } set { - defaults.set(newValue, forKey: UserDefaultsKeys.createConferenceCallsWithJitsi) + defaults.set(newValue, forKey: UserDefaultsKeys.enableRingingForGroupCalls) } } - + // MARK: Calls /// Indicate if `allowStunServerFallback` settings has been set once. @@ -352,6 +329,39 @@ final class RiotSettings: NSObject { defaults.set(newValue, forKey: UserDefaultsKeys.roomScreenAllowFilesAction) } } + + // MARK: - Room Contextual Menu + + var roomContextualMenuShowMoreOptionForMessages: Bool { + get { + guard defaults.object(forKey: UserDefaultsKeys.roomContextualMenuShowMoreOptionForMessages) != nil else { + return BuildSettings.roomContextualMenuShowMoreOptionForMessages + } + return defaults.bool(forKey: UserDefaultsKeys.roomContextualMenuShowMoreOptionForMessages) + } set { + defaults.set(newValue, forKey: UserDefaultsKeys.roomContextualMenuShowMoreOptionForMessages) + } + } + var roomContextualMenuShowMoreOptionForStates: Bool { + get { + guard defaults.object(forKey: UserDefaultsKeys.roomContextualMenuShowMoreOptionForStates) != nil else { + return BuildSettings.roomContextualMenuShowMoreOptionForStates + } + return defaults.bool(forKey: UserDefaultsKeys.roomContextualMenuShowMoreOptionForStates) + } set { + defaults.set(newValue, forKey: UserDefaultsKeys.roomContextualMenuShowMoreOptionForStates) + } + } + var roomContextualMenuShowReportContentOption: Bool { + get { + guard defaults.object(forKey: UserDefaultsKeys.roomContextualMenuShowReportContentOption) != nil else { + return BuildSettings.roomContextualMenuShowReportContentOption + } + return defaults.bool(forKey: UserDefaultsKeys.roomContextualMenuShowReportContentOption) + } set { + defaults.set(newValue, forKey: UserDefaultsKeys.roomContextualMenuShowReportContentOption) + } + } // MARK: - Room Info Screen @@ -366,6 +376,19 @@ final class RiotSettings: NSObject { } } + // MARK: - Room Member Screen + + var roomMemberScreenShowIgnore: Bool { + get { + guard defaults.object(forKey: UserDefaultsKeys.roomMemberScreenShowIgnore) != nil else { + return BuildSettings.roomMemberScreenShowIgnore + } + return defaults.bool(forKey: UserDefaultsKeys.roomMemberScreenShowIgnore) + } set { + defaults.set(newValue, forKey: UserDefaultsKeys.roomMemberScreenShowIgnore) + } + } + // MARK: - Room Creation Screen var roomCreationScreenAllowEncryptionConfiguration: Bool { @@ -497,6 +520,26 @@ final class RiotSettings: NSObject { defaults.set(newValue, forKey: UserDefaultsKeys.settingsScreenShowEnableStunServerFallback) } } + var settingsScreenShowNotificationDecodedContentOption: Bool { + get { + guard defaults.object(forKey: UserDefaultsKeys.settingsScreenShowNotificationDecodedContentOption) != nil else { + return BuildSettings.settingsScreenShowNotificationDecodedContentOption + } + return defaults.bool(forKey: UserDefaultsKeys.settingsScreenShowNotificationDecodedContentOption) + } set { + defaults.set(newValue, forKey: UserDefaultsKeys.settingsScreenShowNotificationDecodedContentOption) + } + } + var settingsScreenShowNsfwRoomsOption: Bool { + get { + guard defaults.object(forKey: UserDefaultsKeys.settingsScreenShowNsfwRoomsOption) != nil else { + return BuildSettings.settingsScreenShowNsfwRoomsOption + } + return defaults.bool(forKey: UserDefaultsKeys.settingsScreenShowNsfwRoomsOption) + } set { + defaults.set(newValue, forKey: UserDefaultsKeys.settingsScreenShowNsfwRoomsOption) + } + } var settingsSecurityScreenShowSessions: Bool { get { guard defaults.object(forKey: UserDefaultsKeys.settingsSecurityScreenShowSessions) != nil else { diff --git a/Riot/Managers/Theme/Theme.swift b/Riot/Managers/Theme/Theme.swift index 54815e497..72d2d1ab8 100644 --- a/Riot/Managers/Theme/Theme.swift +++ b/Riot/Managers/Theme/Theme.swift @@ -15,10 +15,11 @@ */ import UIKit +import DesignKit /// Provide color constant values defined by the designer /// https://app.zeplin.io/project/5c122fa790c5b4241ffa6be7/screen/5c619592daff2f1241d82e75 -@objc protocol Theme { +@objc protocol Theme: ThemeV2 { var identifier: String { get } @@ -93,6 +94,9 @@ import UIKit /// Color to tint the search background image var matrixSearchBackgroundImageTintColor: UIColor { get } + /// Color to use in shadows. Should be contrast to `backgroundColor`. + var shadowColor: UIColor { get } + // MARK: - Customisation methods diff --git a/Riot/Managers/Theme/ThemeService.m b/Riot/Managers/Theme/ThemeService.m index f29dbcaab..175ab5f64 100644 --- a/Riot/Managers/Theme/ThemeService.m +++ b/Riot/Managers/Theme/ThemeService.m @@ -155,6 +155,9 @@ NSString *const kThemeServiceDidChangeThemeNotification = @"kThemeServiceDidChan // Define the UISearchBar cancel button color [[UIBarButtonItem appearanceWhenContainedInInstancesOfClasses:@[[UISearchBar class]]] setTitleTextAttributes:@{ NSForegroundColorAttributeName : self.theme.tintColor } forState: UIControlStateNormal]; + + [[UIStackView appearanceWhenContainedInInstancesOfClasses:@[[UINavigationBar class]]] setSpacing:-7]; + [[UIStackView appearanceWhenContainedInInstancesOfClasses:@[[UINavigationBar class]]] setDistribution:UIStackViewDistributionEqualCentering]; } @end diff --git a/Riot/Managers/Theme/Themes/DarkTheme.swift b/Riot/Managers/Theme/Themes/DarkTheme.swift index d3752abf4..a3bc39557 100644 --- a/Riot/Managers/Theme/Themes/DarkTheme.swift +++ b/Riot/Managers/Theme/Themes/DarkTheme.swift @@ -16,6 +16,7 @@ import Foundation import UIKit +import DesignKit /// Color constants for the dark theme @objcMembers @@ -87,6 +88,8 @@ class DarkTheme: NSObject, Theme { var matrixSearchBackgroundImageTintColor: UIColor = UIColor(rgb: 0x7E7E7E) var secondaryCircleButtonBackgroundColor: UIColor = UIColor(rgb: 0xE3E8F0) + var shadowColor: UIColor = UIColor(rgb: 0xFFFFFF) + var messageTickColor: UIColor = .white func applyStyle(onTabBar tabBar: UITabBar) { @@ -137,4 +140,10 @@ class DarkTheme: NSObject, Theme { button.tintColor = self.tintColor button.setTitleColor(self.tintColor, for: .normal) } + + /// MARK: - Theme v2 + + lazy var colors: Colors = { + return DarkColors() + }() } diff --git a/Riot/Managers/Theme/Themes/DefaultTheme.swift b/Riot/Managers/Theme/Themes/DefaultTheme.swift index eb0ed522e..7f6ae77b1 100644 --- a/Riot/Managers/Theme/Themes/DefaultTheme.swift +++ b/Riot/Managers/Theme/Themes/DefaultTheme.swift @@ -16,6 +16,7 @@ import Foundation import UIKit +import DesignKit /// Color constants for the default theme @objcMembers @@ -97,6 +98,8 @@ class DefaultTheme: NSObject, Theme { var secondaryCircleButtonBackgroundColor: UIColor = UIColor(rgb: 0xE3E8F0) + var shadowColor: UIColor = UIColor(rgb: 0x000000) + func applyStyle(onTabBar tabBar: UITabBar) { tabBar.unselectedItemTintColor = self.tabBarUnselectedItemTintColor tabBar.tintColor = self.tintColor @@ -144,4 +147,10 @@ class DefaultTheme: NSObject, Theme { button.tintColor = self.tintColor button.setTitleColor(self.tintColor, for: .normal) } + + /// MARK: - Theme v2 + + lazy var colors: Colors = { + return LightColors() + }() } diff --git a/Riot/Managers/Widgets/WidgetConstants.h b/Riot/Managers/Widgets/WidgetConstants.h new file mode 100644 index 000000000..6c6e4bfbf --- /dev/null +++ b/Riot/Managers/Widgets/WidgetConstants.h @@ -0,0 +1,42 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import + +/** + The type of matrix event used for matrix widgets. + */ +FOUNDATION_EXPORT NSString *const kWidgetMatrixEventTypeString; + +/** + The type of matrix event used for modular widgets. + TODO: It should be replaced by kWidgetMatrixEventTypeString. + */ +FOUNDATION_EXPORT NSString *const kWidgetModularEventTypeString; + +/** + Known types widgets. + */ +FOUNDATION_EXPORT NSString *const kWidgetTypeJitsiV1; +FOUNDATION_EXPORT NSString *const kWidgetTypeJitsiV2; +FOUNDATION_EXPORT NSString *const kWidgetTypeStickerPicker; + +@interface WidgetConstants : NSObject + +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)new NS_UNAVAILABLE; + +@end diff --git a/Riot/Managers/Widgets/WidgetConstants.m b/Riot/Managers/Widgets/WidgetConstants.m new file mode 100644 index 000000000..cdff6531d --- /dev/null +++ b/Riot/Managers/Widgets/WidgetConstants.m @@ -0,0 +1,27 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "WidgetConstants.h" + +NSString *const kWidgetMatrixEventTypeString = @"m.widget"; +NSString *const kWidgetModularEventTypeString = @"im.vector.modular.widgets"; +NSString *const kWidgetTypeJitsiV1 = @"jitsi"; +NSString *const kWidgetTypeJitsiV2 = @"m.jitsi"; +NSString *const kWidgetTypeStickerPicker = @"m.stickerpicker"; + +@implementation WidgetConstants + +@end diff --git a/Riot/Managers/Widgets/WidgetManager.h b/Riot/Managers/Widgets/WidgetManager.h index 99d3737ef..09c2b2588 100644 --- a/Riot/Managers/Widgets/WidgetManager.h +++ b/Riot/Managers/Widgets/WidgetManager.h @@ -20,27 +20,10 @@ #import #import "Widget.h" +#import "WidgetConstants.h" @class WidgetManagerConfig; -/** - The type of matrix event used for matrix widgets. - */ -FOUNDATION_EXPORT NSString *const kWidgetMatrixEventTypeString; - -/** - The type of matrix event used for modular widgets. - TODO: It should be replaced by kWidgetMatrixEventTypeString. - */ -FOUNDATION_EXPORT NSString *const kWidgetModularEventTypeString; - -/** - Known types widgets. - */ -FOUNDATION_EXPORT NSString *const kWidgetTypeJitsiV1; -FOUNDATION_EXPORT NSString *const kWidgetTypeJitsiV2; -FOUNDATION_EXPORT NSString *const kWidgetTypeStickerPicker; - /** Posted when a widget has been created, updated or disabled. The notification object is the `Widget` instance. diff --git a/Riot/Managers/Widgets/WidgetManager.m b/Riot/Managers/Widgets/WidgetManager.m index b9b7ba1c3..ac212da1a 100644 --- a/Riot/Managers/Widgets/WidgetManager.m +++ b/Riot/Managers/Widgets/WidgetManager.m @@ -19,17 +19,12 @@ #import "Riot-Swift.h" #import "JitsiWidgetData.h" +#import "MXSession+Riot.h" #import #pragma mark - Contants -NSString *const kWidgetMatrixEventTypeString = @"m.widget"; -NSString *const kWidgetModularEventTypeString = @"im.vector.modular.widgets"; -NSString *const kWidgetTypeJitsiV1 = @"jitsi"; -NSString *const kWidgetTypeJitsiV2 = @"m.jitsi"; -NSString *const kWidgetTypeStickerPicker = @"m.stickerpicker"; - NSString *const kWidgetManagerDidUpdateWidgetNotification = @"kWidgetManagerDidUpdateWidgetNotification"; NSString *const WidgetManagerErrorDomain = @"WidgetManagerErrorDomain"; @@ -284,7 +279,7 @@ NSString *const WidgetManagerErrorDomain = @"WidgetManagerErrorDomain"; // Riot-Web still uses V1 type NSString *widgetId = [NSString stringWithFormat:@"%@_%@_%@", kWidgetTypeJitsiV1, room.mxSession.myUser.userId, @((uint64_t)([[NSDate date] timeIntervalSince1970] * 1000))]; - NSURL *preferredJitsiServerUrl = BuildSettings.jitsiServerUrl; + NSURL *preferredJitsiServerUrl = [room.mxSession vc_homeserverConfiguration].jitsi.serverURL; JitsiService *jitsiService = JitsiService.shared; diff --git a/Riot/Model/HomeserverConfiguration/HomeserverConfiguration.swift b/Riot/Model/HomeserverConfiguration/HomeserverConfiguration.swift new file mode 100644 index 000000000..f09d8608c --- /dev/null +++ b/Riot/Model/HomeserverConfiguration/HomeserverConfiguration.swift @@ -0,0 +1,34 @@ +// +// Copyright 2020 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/// Represents the homeserver configuration (usually based on HS Well-Known or hardoced values in the project) +@objcMembers +final class HomeserverConfiguration: NSObject { + + // Note: Use an object per configuration subject when there is multiple properties related + let jitsi: HomeserverJitsiConfiguration + let isE2EEByDefaultEnabled: Bool + + init(jitsi: HomeserverJitsiConfiguration, + isE2EEByDefaultEnabled: Bool) { + self.jitsi = jitsi + self.isE2EEByDefaultEnabled = isE2EEByDefaultEnabled + + super.init() + } +} diff --git a/Riot/Model/HomeserverConfiguration/HomeserverConfigurationBuilder.swift b/Riot/Model/HomeserverConfiguration/HomeserverConfigurationBuilder.swift new file mode 100644 index 000000000..c77ff6bab --- /dev/null +++ b/Riot/Model/HomeserverConfiguration/HomeserverConfigurationBuilder.swift @@ -0,0 +1,125 @@ +// +// Copyright 2020 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/// `HomeserverConfigurationBuilder` build `HomeserverConfiguration` objects according to injected inputs +@objcMembers +final class HomeserverConfigurationBuilder: NSObject { + + // MARK: - Properties + + private let vectorWellKnownParser = VectorWellKnownParser() + + // MARK: - Public + + /// Create an `HomeserverConfiguration` from an HS Well-Known when possible otherwise it takes hardcoded values from BuildSettings by default. + func build(from wellKnown: MXWellKnown?) -> HomeserverConfiguration { + + let isE2EEByDefaultEnabled: Bool + let jitsiPreferredDomain: String + + var vectorWellKnownEncryptionConfiguration: VectorWellKnownEncryptionConfiguration? + var vectorWellKnownJitsiConfiguration: VectorWellKnownJitsiConfiguration? + + if let wellKnown = wellKnown, let vectorWellKnown = self.vectorWellKnownParser.parse(jsonDictionary: wellKnown.jsonDictionary()) { + vectorWellKnownEncryptionConfiguration = self.getEncryptionConfiguration(from: vectorWellKnown) + vectorWellKnownJitsiConfiguration = self.getJitsiConfiguration(from: vectorWellKnown) + } + + // Encryption configuration + if let vectorWellKnownEncryptionConfig = vectorWellKnownEncryptionConfiguration { + isE2EEByDefaultEnabled = vectorWellKnownEncryptionConfig.isE2EEByDefaultEnabled + } else { + // Enable E2EE by default when there is no value + isE2EEByDefaultEnabled = true + } + + // Jitsi configuration + let jitsiServerURL: URL + let hardcodedJitsiServerURL: URL = BuildSettings.jitsiServerUrl + + if let vectorWellKnownJitsiConfig = vectorWellKnownJitsiConfiguration { + jitsiPreferredDomain = vectorWellKnownJitsiConfig.preferredDomain + jitsiServerURL = self.jitsiServerURL(from: jitsiPreferredDomain) ?? hardcodedJitsiServerURL + } else { + guard let hardcodedJitsiDomain = hardcodedJitsiServerURL.host else { + fatalError("[HomeserverConfigurationBuilder] Fail to get Jitsi domain from hardcoded Jitsi URL") + } + jitsiPreferredDomain = hardcodedJitsiDomain + jitsiServerURL = hardcodedJitsiServerURL + } + + // Create HomeserverConfiguration + + let jitsiConfiguration = HomeserverJitsiConfiguration(serverDomain: jitsiPreferredDomain, + serverURL: jitsiServerURL) + + return HomeserverConfiguration(jitsi: jitsiConfiguration, isE2EEByDefaultEnabled: isE2EEByDefaultEnabled) + } + + // MARK: - Private + + private func getJitsiConfiguration(from vectorWellKnown: VectorWellKnown) -> VectorWellKnownJitsiConfiguration? { + + let jitsiConfiguration: VectorWellKnownJitsiConfiguration? + + if let lastJitsiConfiguration = vectorWellKnown.jitsi { + jitsiConfiguration = lastJitsiConfiguration + } else if let deprecatedJitsiConfiguration = vectorWellKnown.deprecatedJitsi { + NSLog("[HomeserverConfigurationBuilder] getJitsiConfiguration - Use deprecated configuration") + jitsiConfiguration = deprecatedJitsiConfiguration + } else { + NSLog("[HomeserverConfigurationBuilder] getJitsiConfiguration - No configuration found") + jitsiConfiguration = nil + } + + return jitsiConfiguration + } + + private func getEncryptionConfiguration(from vectorWellKnown: VectorWellKnown) -> VectorWellKnownEncryptionConfiguration? { + + let encryptionConfiguration: VectorWellKnownEncryptionConfiguration? + + if let lastEncryptionConfiguration = vectorWellKnown.encryption { + encryptionConfiguration = lastEncryptionConfiguration + } else if let deprecatedEncryptionConfiguration = vectorWellKnown.deprecatedEncryption { + NSLog("[HomeserverConfigurationBuilder] getEncryptionConfiguration - Use deprecated configuration") + encryptionConfiguration = deprecatedEncryptionConfiguration + } else { + NSLog("[HomeserverConfigurationBuilder] getEncryptionConfiguration - No configuration found") + encryptionConfiguration = nil + } + + return encryptionConfiguration + } + + private func jitsiServerURL(from jitsiServerDomain: String) -> URL? { + let jitsiStringURL: String + if jitsiServerDomain.starts(with: "http") { + jitsiStringURL = jitsiServerDomain + } else { + jitsiStringURL = "https://\(jitsiServerDomain)" + } + + guard let jitsiServerURL = URL(string: jitsiStringURL) else { + NSLog("[HomeserverConfigurationBuilder] Jitsi server URL is not valid") + return nil + } + + return jitsiServerURL + } +} diff --git a/Riot/Model/HomeserverConfiguration/HomeserverJitsiConfiguration.swift b/Riot/Model/HomeserverConfiguration/HomeserverJitsiConfiguration.swift new file mode 100644 index 000000000..e5e93bed0 --- /dev/null +++ b/Riot/Model/HomeserverConfiguration/HomeserverJitsiConfiguration.swift @@ -0,0 +1,31 @@ +// +// Copyright 2020 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/// `HomeserverJitsiConfiguration` gives Jitsi widget configuration used by homeserver +@objcMembers +final class HomeserverJitsiConfiguration: NSObject { + let serverDomain: String + let serverURL: URL + + init(serverDomain: String, serverURL: URL) { + self.serverDomain = serverDomain + self.serverURL = serverURL + + super.init() + } +} diff --git a/Riot/Model/WellKnown/VectorWellKnown.swift b/Riot/Model/WellKnown/VectorWellKnown.swift new file mode 100644 index 000000000..2a44c0c19 --- /dev/null +++ b/Riot/Model/WellKnown/VectorWellKnown.swift @@ -0,0 +1,60 @@ +// +// Copyright 2020 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +// MARK: - Well Known + +/// `VectorWellKnown` represents additional Well Known configuration specific to Element client +struct VectorWellKnown { + let encryption: VectorWellKnownEncryptionConfiguration? + let jitsi: VectorWellKnownJitsiConfiguration? + + // Deprecated properties + let deprecatedEncryption: VectorWellKnownEncryptionConfiguration? + let deprecatedJitsi: VectorWellKnownJitsiConfiguration? +} + +// MARK: Decodable +extension VectorWellKnown: Decodable { + /// JSON keys associated to VectorWellKnown properties + enum CodingKeys: String, CodingKey { + case encryption = "io.element.e2ee" + case jitsi = "io.element.jitsi" + // Deprecated keys + case deprecatedEncryption = "im.vector.riot.e2ee" + case deprecatedJitsi = "im.vector.riot.jitsi" + } +} + +// MARK: - Encryption + +struct VectorWellKnownEncryptionConfiguration: Decodable { + + /// Indicate if E2EE is enabled by default + let isE2EEByDefaultEnabled: Bool + + enum CodingKeys: String, CodingKey { + case isE2EEByDefaultEnabled = "default" + } +} + +// MARK: - Jitsi +struct VectorWellKnownJitsiConfiguration: Decodable { + + /// Default Jitsi server + let preferredDomain: String +} diff --git a/Riot/Model/WellKnown/VectorWellKnownParser.swift b/Riot/Model/WellKnown/VectorWellKnownParser.swift new file mode 100644 index 000000000..3fc7325c6 --- /dev/null +++ b/Riot/Model/WellKnown/VectorWellKnownParser.swift @@ -0,0 +1,34 @@ +// +// Copyright 2020 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +final class VectorWellKnownParser { + + func parse(jsonDictionary: [AnyHashable: Any]) -> VectorWellKnown? { + let serializationService = SerializationService() + let vectorWellKnown: VectorWellKnown? + + do { + vectorWellKnown = try serializationService.deserialize(jsonDictionary) + } catch { + vectorWellKnown = nil + NSLog("[VectorWellKnownParser] Fail to parse application Well Known keys with error: \(error)") + } + + return vectorWellKnown + } +} diff --git a/Riot/Modules/Application/LegacyAppDelegate.h b/Riot/Modules/Application/LegacyAppDelegate.h index ac28af8f8..1cea08e0d 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.h +++ b/Riot/Modules/Application/LegacyAppDelegate.h @@ -30,6 +30,7 @@ @protocol Configurable; @protocol LegacyAppDelegateDelegate; @class CallBar; +@class CallPresenter; #pragma mark - Notifications /** @@ -54,8 +55,8 @@ extern NSString *const AppDelegateUniversalLinkDidChangeNotification; @interface LegacyAppDelegate : UIResponder < UIApplicationDelegate, UISplitViewControllerDelegate, -UINavigationControllerDelegate, -JitsiViewControllerDelegate> +UINavigationControllerDelegate +> { // background sync management void (^_completionHandler)(UIBackgroundFetchResult); @@ -117,6 +118,11 @@ JitsiViewControllerDelegate> // Build Settings @property (nonatomic, readonly) id configuration; +/** + Call presenter instance. May be nil unless at least one session initialized. + */ +@property (nonatomic, strong, readonly) CallPresenter *callPresenter; + + (instancetype)theDelegate; #pragma mark - Push Notifications @@ -218,21 +224,6 @@ JitsiViewControllerDelegate> */ - (BOOL)handleUniversalLinkFragment:(NSString*)fragment; -#pragma mark - Jitsi call - -/** - Open the Jitsi view controller from a widget. - - @param jitsiWidget the jitsi widget. - @param video to indicate voice or video call. - */ -- (void)displayJitsiViewControllerWithWidget:(Widget*)jitsiWidget andVideo:(BOOL)video; - -/** - The current Jitsi view controller being displayed. - */ -@property (nonatomic, readonly) JitsiViewController *jitsiViewController; - #pragma mark - Call status handling /** diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index ef261c069..ece6aad63 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -224,7 +224,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni @property (nonatomic, strong) PushNotificationService *pushNotificationService; @property (nonatomic, strong) PushNotificationStore *pushNotificationStore; @property (nonatomic, strong) LocalAuthenticationService *localAuthenticationService; -@property (nonatomic, strong) CallPresenter *callPresenter; +@property (nonatomic, strong, readwrite) CallPresenter *callPresenter; @property (nonatomic, strong) MajorUpdateManager *majorUpdateManager; @@ -2136,10 +2136,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni [self logoutSendingRequestServer:YES completion:^(BOOL isLoggedOut) { if (completion) { - if (isLoggedOut) - { - [RiotSettings.shared reset]; - } completion (YES); } }]; @@ -3016,90 +3012,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni } } -#pragma mark - Jitsi call - -- (void)displayJitsiViewControllerWithWidget:(Widget*)jitsiWidget andVideo:(BOOL)video -{ -#ifdef CALL_STACK_JINGLE - if (!_jitsiViewController) - { - MXWeakify(self); - [self checkPermissionForNativeWidget:jitsiWidget fromUrl:JitsiService.shared.serverURL completion:^(BOOL granted) { - MXStrongifyAndReturnIfNil(self); - if (!granted) - { - return; - } - - self->_jitsiViewController = [JitsiViewController jitsiViewController]; - - [self->_jitsiViewController openWidget:jitsiWidget withVideo:video success:^{ - - self->_jitsiViewController.delegate = self; - [self presentJitsiViewController:nil]; - - } failure:^(NSError *error) { - - self->_jitsiViewController = nil; - - [self showAlertWithTitle:nil message:NSLocalizedStringFromTable(@"call_jitsi_error", @"Vector", nil)]; - }]; - }]; - } - else - { - [self showAlertWithTitle:nil message:NSLocalizedStringFromTable(@"call_already_displayed", @"Vector", nil)]; - } -#else - [self showAlertWithTitle:nil message:[NSBundle mxk_localizedStringForKey:@"not_supported_yet"]]; -#endif -} - -- (void)presentJitsiViewController:(void (^)(void))completion -{ - [self removeCallStatusBar]; - - if (_jitsiViewController) - { - if (@available(iOS 13.0, *)) - { - _jitsiViewController.modalPresentationStyle = UIModalPresentationFullScreen; - } - - [self presentViewController:_jitsiViewController animated:YES completion:completion]; - } -} - -- (void)jitsiViewController:(JitsiViewController *)jitsiViewController dismissViewJitsiController:(void (^)(void))completion -{ - if (jitsiViewController == _jitsiViewController) - { - [_jitsiViewController dismissViewControllerAnimated:YES completion:completion]; - _jitsiViewController = nil; - - [self removeCallStatusBar]; - } -} - -- (void)jitsiViewController:(JitsiViewController *)jitsiViewController goBackToApp:(void (^)(void))completion -{ - if (jitsiViewController == _jitsiViewController) - { - [_jitsiViewController dismissViewControllerAnimated:YES completion:^{ - - MXRoom *room = [_jitsiViewController.widget.mxSession roomWithRoomId:_jitsiViewController.widget.roomId]; - NSString *btnTitle = [NSString stringWithFormat:NSLocalizedStringFromTable(@"active_call_details", @"Vector", nil), room.summary.displayname]; - [self updateCallStatusBar:btnTitle]; - - if (completion) - { - completion(); - } - }]; - } -} - - #pragma mark - Native Widget Permission - (void)checkPermissionForNativeWidget:(Widget*)widget fromUrl:(NSURL*)url completion:(void (^)(BOOL granted))completion @@ -3232,13 +3144,8 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni return result; } -- (void)updateCallStatusBar:(NSString*)title +- (void)displayCallStatusBarWithTitle:(NSString*)title { - if (_callBar) - { - _callBar.title = title; - return; - } // Add a call status bar CGSize topBarSize = CGSizeMake([[UIScreen mainScreen] bounds].size.width, [self calculateCallStatusBarHeight]); @@ -3261,22 +3168,27 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni [_callBar.heightAnchor constraintEqualToAnchor:_callStatusBarWindow.heightAnchor].active = YES; _callStatusBarWindow.hidden = NO; - [self statusBarDidChangeFrame]; + [self deviceOrientationDidChange]; - // We need to listen to the system status bar size change events to refresh the root controller frame. + // We need to listen to the device orientation change events to refresh the root controller frame. // Else the navigation bar position will be wrong. [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(statusBarDidChangeFrame) - name:UIApplicationDidChangeStatusBarFrameNotification + selector:@selector(deviceOrientationDidChange) + name:UIDeviceOrientationDidChangeNotification object:nil]; } +- (void)updateCallStatusBarWithTitle:(NSString*)title +{ + _callBar.title = title; +} + - (void)removeCallStatusBar { if (_callStatusBarWindow) { - // No more need to listen to system status bar changes - [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidChangeStatusBarFrameNotification object:nil]; + // No more need to listen to device orientation changes + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIDeviceOrientationDidChangeNotification object:nil]; // Hide & destroy it _callStatusBarWindow.hidden = YES; @@ -3284,11 +3196,11 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni _callBar = nil; _callStatusBarWindow = nil; - [self statusBarDidChangeFrame]; + [self deviceOrientationDidChange]; } } -- (void)statusBarDidChangeFrame +- (void)deviceOrientationDidChange { UIApplication *app = [UIApplication sharedApplication]; UIViewController *rootController = app.keyWindow.rootViewController; @@ -3300,30 +3212,20 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni { CGFloat callStatusBarHeight = [self calculateCallStatusBarHeight]; - UIInterfaceOrientation statusBarOrientation = [UIApplication sharedApplication].statusBarOrientation; + UIDeviceOrientation deviceOrientation = [UIDevice currentDevice].orientation; + CGFloat width; - switch (statusBarOrientation) + if (UIDeviceOrientationIsPortrait(deviceOrientation)) { - case UIInterfaceOrientationLandscapeLeft: - { - _callStatusBarWindow.frame = CGRectMake(-rootControllerFrame.size.width / 2, -callStatusBarHeight / 2, rootControllerFrame.size.width, callStatusBarHeight); - _callStatusBarWindow.transform = CGAffineTransformMake(0, -1, 1, 0, callStatusBarHeight / 2, rootControllerFrame.size.width / 2); - break; - } - case UIInterfaceOrientationLandscapeRight: - { - _callStatusBarWindow.frame = CGRectMake(-rootControllerFrame.size.width / 2, -callStatusBarHeight / 2, rootControllerFrame.size.width, callStatusBarHeight); - _callStatusBarWindow.transform = CGAffineTransformMake(0, 1, -1, 0, rootControllerFrame.size.height - callStatusBarHeight / 2, rootControllerFrame.size.width / 2); - break; - } - default: - { - _callStatusBarWindow.transform = CGAffineTransformIdentity; - _callStatusBarWindow.frame = CGRectMake(0, 0, rootControllerFrame.size.width, callStatusBarHeight); - break; - } + width = MIN(rootControllerFrame.size.width, rootControllerFrame.size.height); } - + else + { + width = MAX(rootControllerFrame.size.width, rootControllerFrame.size.height); + } + + _callStatusBarWindow.frame = CGRectMake(0, 0, width, callStatusBarHeight); + // Apply the vertical offset due to call status bar rootControllerFrame.origin.y = callStatusBarHeight; rootControllerFrame.size.height -= callStatusBarHeight; @@ -4485,12 +4387,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni #pragma mark - CallPresenterDelegate -- (BOOL)callPresenter:(CallPresenter *)presenter shouldHandleNewCall:(MXCall *)call -{ - // Ignore the call if a call is already in progress - return _jitsiViewController == nil; -} - - (void)callPresenter:(CallPresenter *)presenter presentCallViewController:(CallViewController *)viewController completion:(void (^)(void))completion { if (@available(iOS 13.0, *)) @@ -4498,19 +4394,23 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni viewController.modalPresentationStyle = UIModalPresentationFullScreen; } - [self presentViewController:viewController animated:YES completion:completion]; + [self presentViewController:viewController animated:NO completion:completion]; } -- (void)callPresenter:(CallPresenter *)presenter dismissCallViewController:(CallViewController *)viewController completion:(void (^)(void))completion +- (void)callPresenter:(CallPresenter *)presenter dismissCallViewController:(UIViewController *)viewController completion:(void (^)(void))completion { // Check whether the call view controller is actually presented if (viewController.presentingViewController) { - [viewController dismissViewControllerAnimated:YES completion:^{ + [viewController.presentingViewController dismissViewControllerAnimated:NO completion:^{ - if (viewController.shouldPromptForStunServerFallback) + if ([viewController isKindOfClass:CallViewController.class]) { - [self promptForStunServerFallback]; + CallViewController *callVC = (CallViewController *)viewController; + if (callVC.shouldPromptForStunServerFallback) + { + [self promptForStunServerFallback]; + } } if (completion) @@ -4529,26 +4429,69 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni } } -- (void)callPresenter:(CallPresenter *)presenter presentCallBarFor:(CallViewController *)activeCallViewController numberOfPausedCalls:(NSUInteger)numberOfPausedCalls completion:(void (^)(void))completion +- (void)callPresenter:(CallPresenter *)presenter presentCallBarFor:(UIViewController *)activeCallViewController numberOfPausedCalls:(NSUInteger)numberOfPausedCalls completion:(void (^)(void))completion +{ + [self displayCallStatusBarWithTitle:nil]; + [self callPresenter:presenter updateCallBarFor:activeCallViewController numberOfPausedCalls:numberOfPausedCalls]; + + if (completion) + { + completion(); + } +} + +- (void)callPresenter:(CallPresenter *)presenter updateCallBarFor:(UIViewController *)activeCallViewController numberOfPausedCalls:(NSUInteger)numberOfPausedCalls { NSString *btnTitle; if (activeCallViewController) { + NSString *callStatus = @""; + BOOL isGroupCall = NO; + if ([activeCallViewController isKindOfClass:[CallViewController class]]) + { + CallViewController *activeCallVC = (CallViewController *)activeCallViewController; + callStatus = activeCallVC.callStatusLabel.text; + } + else if ([activeCallViewController isKindOfClass:[JitsiViewController class]]) + { + JitsiViewController *jitsiVC = (JitsiViewController *)activeCallViewController; + NSUInteger duration = jitsiVC.callDuration / 1000; + NSUInteger secs = duration % 60; + NSUInteger mins = (duration / 60) % 60; + NSUInteger hours = duration / 3600; + if (hours > 0) + { + callStatus = [NSString stringWithFormat:@"%02tu:%02tu:%02tu", hours, mins, secs]; + } + else + { + callStatus = [NSString stringWithFormat:@"%02tu:%02tu", mins, secs]; + } + isGroupCall = YES; + } + if (numberOfPausedCalls == 0) { // only one active - btnTitle = [NSString stringWithFormat:NSLocalizedStringFromTable(@"callbar_only_single_active", @"Vector", nil), activeCallViewController.callStatusLabel.text]; + if (isGroupCall) + { + btnTitle = [NSString stringWithFormat:NSLocalizedStringFromTable(@"callbar_only_single_active_group", @"Vector", nil), callStatus]; + } + else + { + btnTitle = [NSString stringWithFormat:NSLocalizedStringFromTable(@"callbar_only_single_active", @"Vector", nil), callStatus]; + } } else if (numberOfPausedCalls == 1) { // one active and one paused - btnTitle = [NSString stringWithFormat:NSLocalizedStringFromTable(@"callbar_active_and_single_paused", @"Vector", nil), activeCallViewController.callStatusLabel.text]; + btnTitle = [NSString stringWithFormat:NSLocalizedStringFromTable(@"callbar_active_and_single_paused", @"Vector", nil), callStatus]; } else { // one active and multiple paused - btnTitle = [NSString stringWithFormat:NSLocalizedStringFromTable(@"callbar_active_and_multiple_paused", @"Vector", nil), activeCallViewController.callStatusLabel.text, @(numberOfPausedCalls)]; + btnTitle = [NSString stringWithFormat:NSLocalizedStringFromTable(@"callbar_active_and_multiple_paused", @"Vector", nil), callStatus, @(numberOfPausedCalls)]; } } else @@ -4564,12 +4507,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni } } - [self updateCallStatusBar:btnTitle]; - - if (completion) - { - completion(); - } + [self updateCallStatusBarWithTitle:btnTitle]; } - (void)callPresenter:(CallPresenter *)presenter dismissCallBar:(void (^)(void))completion @@ -4582,7 +4520,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni } } -- (void)callPresenter:(CallPresenter *)presenter enterPipForCallViewController:(CallViewController *)viewController completion:(void (^)(void))completion +- (void)callPresenter:(CallPresenter *)presenter enterPipForCallViewController:(UIViewController *)viewController completion:(void (^)(void))completion { // Check whether the call view controller is actually presented if (viewController.presentingViewController) @@ -4598,7 +4536,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni } } -- (void)callPresenter:(CallPresenter *)presenter exitPipForCallViewController:(CallViewController *)viewController completion:(void (^)(void))completion +- (void)callPresenter:(CallPresenter *)presenter exitPipForCallViewController:(UIViewController *)viewController completion:(void (^)(void))completion { if (@available(iOS 13.0, *)) { @@ -4610,18 +4548,11 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni #pragma mark - CallBarDelegate -- (void)callBarDidTapReturnButton:(CallBar *)callBar +- (void)callBarDidTap:(CallBar *)callBar { - if ([_callPresenter callStatusBarButtonTapped]) - { - return; - } - else if (_jitsiViewController) - { - [self presentJitsiViewController:nil]; - } + [_callPresenter callStatusBarTapped]; } - + #pragma mark - Authentication - (BOOL)continueSSOLoginWithToken:(NSString*)loginToken txnId:(NSString*)txnId diff --git a/Riot/Modules/Authentication/AuthenticationViewController.m b/Riot/Modules/Authentication/AuthenticationViewController.m index 0a738a370..48e73eda0 100644 --- a/Riot/Modules/Authentication/AuthenticationViewController.m +++ b/Riot/Modules/Authentication/AuthenticationViewController.m @@ -1430,7 +1430,7 @@ static const CGFloat kAuthInputContainerViewMinHeightConstraintConstant = 150.0; // TODO: This is still not sure we want to disable the automatic cross-signing bootstrap // if the admin disabled e2e by default. // Do like riot-web for the moment - if (session.vc_isE2EByDefaultEnabledByHSAdmin) + if ([session vc_homeserverConfiguration].isE2EEByDefaultEnabled) { // Bootstrap cross-signing on user's account // We do it for both registration and new login as long as cross-signing does not exist yet diff --git a/Riot/Modules/Call/CallViewController.h b/Riot/Modules/Call/CallViewController.h index 54ef33aa6..98bedce60 100644 --- a/Riot/Modules/Call/CallViewController.h +++ b/Riot/Modules/Call/CallViewController.h @@ -21,13 +21,11 @@ */ @interface CallViewController : MXKCallViewController -@property (weak, nonatomic) IBOutlet UIView *gradientMaskContainerView; @property (weak, nonatomic) IBOutlet UIButton *chatButton; @property (weak, nonatomic) IBOutlet UIView *callControlsBackgroundView; @property (unsafe_unretained, nonatomic) IBOutlet NSLayoutConstraint *callerImageViewWidthConstraint; -@property (weak, nonatomic) IBOutlet NSLayoutConstraint *moreButtonLeadingConstraint; // Effect views @property (weak, nonatomic) IBOutlet MXKImageView *blurredCallerImageView; diff --git a/Riot/Modules/Call/CallViewController.m b/Riot/Modules/Call/CallViewController.m index 92dc8438d..bfcf9d46c 100644 --- a/Riot/Modules/Call/CallViewController.m +++ b/Riot/Modules/Call/CallViewController.m @@ -107,6 +107,12 @@ [self.endCallButton setImage:hangUpButtonImage forState:UIControlStateNormal]; [self.endCallButton setImage:hangUpButtonImage forState:UIControlStateHighlighted]; + // force orientation to portrait if phone + if ([UIDevice currentDevice].isPhone) + { + [[UIDevice currentDevice] setValue:[NSNumber numberWithInteger: UIInterfaceOrientationPortrait] forKey:@"orientation"]; + } + [self updateLocalPreviewLayout]; [self configureUserInterface]; @@ -134,6 +140,8 @@ self.callStatusLabel.textColor = self.overriddenTheme.baseTextPrimaryColor; [self.resumeButton setTitleColor:self.overriddenTheme.tintColor forState:UIControlStateNormal]; + [self.transferButton setTitleColor:self.overriddenTheme.tintColor + forState:UIControlStateNormal]; self.localPreviewContainerView.layer.borderColor = self.overriddenTheme.tintColor.CGColor; self.localPreviewContainerView.layer.borderWidth = 2; @@ -152,6 +160,30 @@ [super viewWillDisappear:animated]; } +- (UIInterfaceOrientationMask)supportedInterfaceOrientations +{ + // limit orientation to portrait only for phone + if ([UIDevice currentDevice].isPhone) + { + return UIInterfaceOrientationMaskPortrait; + } + return [super supportedInterfaceOrientations]; +} + +- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation +{ + if ([UIDevice currentDevice].isPhone) + { + return UIInterfaceOrientationPortrait; + } + return [super preferredInterfaceOrientationForPresentation]; +} + +- (BOOL)shouldAutorotate +{ + return NO; +} + #pragma mark - override MXKViewController - (UIView *)createIncomingCallView @@ -347,18 +379,6 @@ return _overriddenTheme; } -- (void)setMxCall:(MXCall *)mxCall -{ - [super setMxCall:mxCall]; - - if (self.videoMuteButton.isHidden) - { - // shift more button to left - self.moreButtonLeadingConstraint.constant = 8.0; - [self.view layoutIfNeeded]; - } -} - - (UIImage*)picturePlaceholder { CGFloat fontSize = floor(self.callerImageViewWidthConstraint.constant * 0.7); @@ -385,26 +405,19 @@ - (void)updatePeerInfoDisplay { - NSString *peerDisplayName; - NSString *peerAvatarURL; + [super updatePeerInfoDisplay]; + NSString *peerAvatarURL; + if (self.peer) { - peerDisplayName = [self.peer displayname]; - if (!peerDisplayName.length) - { - peerDisplayName = self.peer.userId; - } peerAvatarURL = self.peer.avatarUrl; } else if (self.mxCall.isConferenceCall) { - peerDisplayName = self.mxCall.room.summary.displayname; peerAvatarURL = self.mxCall.room.summary.avatar; } - - self.callerNameLabel.text = peerDisplayName; - + self.blurredCallerImageView.contentMode = UIViewContentModeScaleAspectFill; self.callerImageView.contentMode = UIViewContentModeScaleAspectFill; if (peerAvatarURL) @@ -415,7 +428,7 @@ andImageOrientation:UIImageOrientationUp previewImage:self.picturePlaceholder mediaManager:self.mainSession.mediaManager]; - + // Retrieve the avatar in full resolution [self.callerImageView setImageURI:peerAvatarURL withType:nil @@ -521,7 +534,8 @@ showsBackspaceButton:NO showsCallButton:NO formattingEnabled:NO - editingEnabled:NO]; + editingEnabled:NO + playTones:YES]; DialpadViewController *controller = [DialpadViewController instantiateWithConfiguration:config]; controller.delegate = self; self.customSizedPresentationController = [[CustomSizedPresentationController alloc] initWithPresentedViewController:controller presentingViewController:self]; diff --git a/Riot/Modules/Call/CallViewController.xib b/Riot/Modules/Call/CallViewController.xib index 821dfd614..d5cf47f3c 100644 --- a/Riot/Modules/Call/CallViewController.xib +++ b/Riot/Modules/Call/CallViewController.xib @@ -1,9 +1,9 @@ - - + + - + @@ -20,24 +20,25 @@ - + + - + + - - + @@ -77,21 +78,54 @@ - - - - - - - - - + + + + + + @@ -321,7 +357,7 @@ - + @@ -425,7 +461,7 @@ - + @@ -433,7 +469,7 @@ - + @@ -445,14 +481,14 @@ - + + - + - - - + + diff --git a/Riot/Modules/Call/Dialpad/DialpadConfiguration.swift b/Riot/Modules/Call/Dialpad/DialpadConfiguration.swift index a1d91f3d2..04d617ea9 100644 --- a/Riot/Modules/Call/Dialpad/DialpadConfiguration.swift +++ b/Riot/Modules/Call/Dialpad/DialpadConfiguration.swift @@ -38,6 +38,9 @@ class DialpadConfiguration: NSObject { /// Option for a dial pad to enable editing on typed text or not. var editingEnabled: Bool + /// Option for a dial pad to play tones when digits tapped or not. + var playTones: Bool + /// Default configuration object. All options are enabled by default. static let `default`: DialpadConfiguration = DialpadConfiguration() @@ -46,13 +49,15 @@ class DialpadConfiguration: NSObject { showsBackspaceButton: Bool = true, showsCallButton: Bool = true, formattingEnabled: Bool = true, - editingEnabled: Bool = true) { + editingEnabled: Bool = true, + playTones: Bool = true) { self.showsTitle = showsTitle self.showsCloseButton = showsCloseButton self.showsBackspaceButton = showsBackspaceButton self.showsCallButton = showsCallButton self.formattingEnabled = formattingEnabled self.editingEnabled = editingEnabled + self.playTones = playTones super.init() } diff --git a/Riot/Modules/Call/Dialpad/DialpadViewController.storyboard b/Riot/Modules/Call/Dialpad/DialpadViewController.storyboard index 8f81e6b7f..33d53a158 100644 --- a/Riot/Modules/Call/Dialpad/DialpadViewController.storyboard +++ b/Riot/Modules/Call/Dialpad/DialpadViewController.storyboard @@ -1,9 +1,9 @@ - + - + @@ -171,7 +171,7 @@ - - - @@ -285,15 +286,27 @@ - + - + + + + + + + + + + + + + diff --git a/Riot/Modules/Call/Dialpad/DialpadViewController.swift b/Riot/Modules/Call/Dialpad/DialpadViewController.swift index 743b39471..23e2c21ce 100644 --- a/Riot/Modules/Call/Dialpad/DialpadViewController.swift +++ b/Riot/Modules/Call/Dialpad/DialpadViewController.swift @@ -60,6 +60,7 @@ class DialpadViewController: UIViewController { } @IBOutlet private weak var lineView: UIView! @IBOutlet private weak var digitsStackView: UIStackView! + @IBOutlet private var digitButtons: [DialpadButton]! @IBOutlet private weak var backspaceButton: DialpadActionButton! { didSet { backspaceButton.type = .backspace @@ -83,6 +84,20 @@ class DialpadViewController: UIViewController { private enum Constants { static let sizeOniPad: CGSize = CGSize(width: 375, height: 667) static let additionalTopInset: CGFloat = 20 + static let digitButtonViewDatas: [Int: DialpadButton.ViewData] = [ + -2: .init(title: "#", tone: 1211), + -1: .init(title: "*", tone: 1210), + 0: .init(title: "0", tone: 1200, subtitle: "+"), + 1: .init(title: "1", tone: 1201, showsSubtitleSpace: true), + 2: .init(title: "2", tone: 1202, subtitle: "ABC"), + 3: .init(title: "3", tone: 1203, subtitle: "DEF"), + 4: .init(title: "4", tone: 1204, subtitle: "GHI"), + 5: .init(title: "5", tone: 1205, subtitle: "JKL"), + 6: .init(title: "6", tone: 1206, subtitle: "MNO"), + 7: .init(title: "7", tone: 1207, subtitle: "PQRS"), + 8: .init(title: "8", tone: 1208, subtitle: "TUV"), + 9: .init(title: "9", tone: 1209, subtitle: "WXYZ") + ] } private var wasCursorAtTheEnd: Bool = true @@ -135,6 +150,12 @@ class DialpadViewController: UIViewController { if UIDevice.current.isPhone { UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation") } + + for button in digitButtons { + if let viewData = Constants.digitButtonViewDatas[button.tag] { + button.render(withViewData: viewData) + } + } } override var preferredStatusBarStyle: UIStatusBarStyle { @@ -212,10 +233,15 @@ class DialpadViewController: UIViewController { theme.applyStyle(onNavigationBar: navigationBar) } - titleLabel.textColor = theme.noticeSecondaryColor + if theme.identifier == ThemeIdentifier.light.rawValue { + titleLabel.textColor = theme.noticeSecondaryColor + closeButton.setBackgroundImage(Asset.Images.closeButton.image.vc_tintedImage(usingColor: theme.tabBarUnselectedItemTintColor), for: .normal) + } else { + titleLabel.textColor = theme.baseTextSecondaryColor + closeButton.setBackgroundImage(Asset.Images.closeButton.image.vc_tintedImage(usingColor: theme.baseTextSecondaryColor), for: .normal) + } phoneNumberTextField.textColor = theme.textPrimaryColor lineView.backgroundColor = theme.lineBreakColor - closeButton.setBackgroundImage(Asset.Images.closeButton.image.vc_tintedImage(usingColor: theme.tabBarUnselectedItemTintColor), for: .normal) updateThemesOfAllButtons(in: digitsStackView, with: theme) } @@ -253,12 +279,19 @@ class DialpadViewController: UIViewController { } @IBAction private func digitButtonAction(_ sender: DialpadButton) { - let digit = sender.title(for: .normal) ?? "" + guard let digitViewData = Constants.digitButtonViewDatas[sender.tag] else { + return + } + let digit = digitViewData.title defer { delegate?.dialpadViewControllerDidTapDigit?(self, digit: digit) } + if configuration.playTones { + AudioServicesPlaySystemSound(digitViewData.tone) + } + if !configuration.editingEnabled { phoneNumber += digit return diff --git a/Riot/Modules/Call/Dialpad/Views/DialpadActionButton.swift b/Riot/Modules/Call/Dialpad/Views/DialpadActionButton.swift index 3426269d5..86db7ba40 100644 --- a/Riot/Modules/Call/Dialpad/Views/DialpadActionButton.swift +++ b/Riot/Modules/Call/Dialpad/Views/DialpadActionButton.swift @@ -30,9 +30,11 @@ class DialpadActionButton: DialpadButton { override func update(theme: Theme) { switch type { case .backspace: - backgroundColor = theme.warningColor + backgroundColor = .clear + tintColor = theme.colors.tertiaryContent case .call: - backgroundColor = theme.tintColor + backgroundColor = theme.colors.accent + tintColor = .white } } diff --git a/Riot/Modules/Call/Dialpad/Views/DialpadButton.swift b/Riot/Modules/Call/Dialpad/Views/DialpadButton.swift index c8e55b5ea..f39394e19 100644 --- a/Riot/Modules/Call/Dialpad/Views/DialpadButton.swift +++ b/Riot/Modules/Call/Dialpad/Views/DialpadButton.swift @@ -19,8 +19,27 @@ import UIKit /// Digit button class for Dialpad screen class DialpadButton: UIButton { + struct ViewData { + let title: String + let tone: SystemSoundID + let subtitle: String? + let showsSubtitleSpace: Bool + + init(title: String, tone: SystemSoundID, subtitle: String? = nil, showsSubtitleSpace: Bool = false) { + self.title = title + self.tone = tone + self.subtitle = subtitle + self.showsSubtitleSpace = showsSubtitleSpace + } + } + + private var viewData: ViewData? + private var theme: Theme = ThemeService.shared().theme + private enum Constants { static let size: CGSize = CGSize(width: 68, height: 68) + static let titleFont: UIFont = .boldSystemFont(ofSize: 32) + static let subtitleFont: UIFont = .boldSystemFont(ofSize: 12) } init() { @@ -40,6 +59,31 @@ class DialpadButton: UIButton { private func setup() { clipsToBounds = true layer.cornerRadius = Constants.size.width/2 + vc_enableMultiLinesTitle() + } + + func render(withViewData viewData: ViewData) { + self.viewData = viewData + + let totalAttributedString = NSMutableAttributedString(string: viewData.title, + attributes: [ + .font: Constants.titleFont, + .foregroundColor: theme.textPrimaryColor + ]) + + if let subtitle = viewData.subtitle { + totalAttributedString.append(NSAttributedString(string: "\n" + subtitle, attributes: [ + .font: Constants.subtitleFont, + .foregroundColor: theme.textPrimaryColor + ])) + } else if viewData.showsSubtitleSpace { + totalAttributedString.append(NSAttributedString(string: "\n ", attributes: [ + .font: Constants.subtitleFont, + .foregroundColor: theme.textPrimaryColor + ])) + } + + setAttributedTitle(totalAttributedString, for: .normal) } } @@ -49,8 +93,14 @@ class DialpadButton: UIButton { extension DialpadButton: Themable { func update(theme: Theme) { - setTitleColor(theme.textPrimaryColor, for: .normal) + self.theme = theme + backgroundColor = theme.headerBackgroundColor + + // re-render view data if set + if let viewData = self.viewData { + render(withViewData: viewData) + } } } diff --git a/Riot/Modules/Call/Views/CallBar/CallBar.swift b/Riot/Modules/Call/Views/CallBar/CallBar.swift index d79531e32..941e22b0a 100644 --- a/Riot/Modules/Call/Views/CallBar/CallBar.swift +++ b/Riot/Modules/Call/Views/CallBar/CallBar.swift @@ -18,24 +18,13 @@ import Foundation import Reusable @objc protocol CallBarDelegate: class { - func callBarDidTapReturnButton(_ callBar: CallBar) + func callBarDidTap(_ callBar: CallBar) } @objcMembers class CallBar: UIView, NibLoadable { - - @IBOutlet private weak var callIcon: UIImageView! { - didSet { - callIcon.image = Asset.Images.voiceCallHangonIcon.image.vc_tintedImage(usingColor: .white) - } - } + @IBOutlet private weak var titleLabel: UILabel! - @IBOutlet private weak var returnLabel: UILabel! { - didSet { - returnLabel.text = VectorL10n.callbarReturn - } - } - @IBOutlet private weak var returnButton: UIButton! weak var delegate: CallBarDelegate? @@ -43,6 +32,13 @@ class CallBar: UIView, NibLoadable { return CallBar.loadFromNib() } + override func awakeFromNib() { + super.awakeFromNib() + + let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapGestureRecognized(_:))) + addGestureRecognizer(tapRecognizer) + } + var title: String? { get { return titleLabel.text @@ -51,7 +47,8 @@ class CallBar: UIView, NibLoadable { } } - @IBAction private func returnButtonTapped(_ sender: UIButton) { - delegate?.callBarDidTapReturnButton(self) + @objc + private func tapGestureRecognized(_ sender: UITapGestureRecognizer) { + delegate?.callBarDidTap(self) } } diff --git a/Riot/Modules/Call/Views/CallBar/CallBar.xib b/Riot/Modules/Call/Views/CallBar/CallBar.xib index 93de09976..79873a89c 100644 --- a/Riot/Modules/Call/Views/CallBar/CallBar.xib +++ b/Riot/Modules/Call/Views/CallBar/CallBar.xib @@ -1,9 +1,9 @@ - + - + @@ -17,53 +17,20 @@ - - - - - - - - - - - - - - - - + - - + - - - @@ -76,15 +43,9 @@ - - - - - - diff --git a/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewModel.swift b/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewModel.swift index a9a48bdde..4967b5161 100644 --- a/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewModel.swift +++ b/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewModel.swift @@ -39,7 +39,7 @@ final class EnterNewRoomDetailsViewModel: EnterNewRoomDetailsViewModelType { init(session: MXSession) { self.session = session - roomCreationParameters.isEncrypted = session.vc_isE2EByDefaultEnabledByHSAdmin() && RiotSettings.shared.roomCreationScreenRoomIsEncrypted + roomCreationParameters.isEncrypted = session.vc_homeserverConfiguration().isE2EEByDefaultEnabled && RiotSettings.shared.roomCreationScreenRoomIsEncrypted roomCreationParameters.isPublic = RiotSettings.shared.roomCreationScreenRoomIsPublic } diff --git a/Riot/Modules/Integrations/WidgetPicker/WidgetPickerViewController.m b/Riot/Modules/Integrations/WidgetPicker/WidgetPickerViewController.m index 014cb45e1..ad4e35e82 100644 --- a/Riot/Modules/Integrations/WidgetPicker/WidgetPickerViewController.m +++ b/Riot/Modules/Integrations/WidgetPicker/WidgetPickerViewController.m @@ -78,18 +78,21 @@ } // Link to the integration manager - alertAction = [UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"widget_picker_manage_integrations", @"Vector", nil) - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * _Nonnull action) - { - IntegrationManagerViewController *modularVC = [[IntegrationManagerViewController alloc] initForMXSession:self->mxSession - inRoom:self->roomId - screen:kIntegrationManagerMainScreen - widgetId:nil]; + if (RiotSettings.shared.roomInfoScreenShowIntegrations) + { + alertAction = [UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"widget_picker_manage_integrations", @"Vector", nil) + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * _Nonnull action) + { + IntegrationManagerViewController *modularVC = [[IntegrationManagerViewController alloc] initForMXSession:self->mxSession + inRoom:self->roomId + screen:kIntegrationManagerMainScreen + widgetId:nil]; - [mxkViewController presentViewController:modularVC animated:NO completion:nil]; - }]; - [self.alertController addAction:alertAction]; + [mxkViewController presentViewController:modularVC animated:NO completion:nil]; + }]; + [self.alertController addAction:alertAction]; + } // Cancel alertAction = [UIAlertAction actionWithTitle:[NSBundle mxk_localizedStringForKey:@"cancel"] diff --git a/Riot/Modules/Integrations/Widgets/Jitsi/JitsiService.swift b/Riot/Modules/Integrations/Widgets/Jitsi/JitsiService.swift index aeade5370..2d5a3a7b3 100644 --- a/Riot/Modules/Integrations/Widgets/Jitsi/JitsiService.swift +++ b/Riot/Modules/Integrations/Widgets/Jitsi/JitsiService.swift @@ -22,9 +22,14 @@ import JitsiMeetSDK enum JitsiServiceError: Error { case widgetContentCreationFailed case emptyResponse + case noWellKnown case unknown } +private enum HTTPStatusCodes { + static let notFound: Int = 404 +} + /// JitsiService enables to abstract and configure Jitsi Meet SDK @objcMembers final class JitsiService: NSObject { @@ -61,6 +66,10 @@ final class JitsiService: NSObject { private var httpClients: [String: MXHTTPClient] = [:] + /// Holds widgetIds for declined group calls. Made a map to speed up lookups. + /// Values are useless, not used with false values. + private var declinedJitsiWidgets: [String: Bool] = [:] + // MARK: - Setup private override init() { @@ -69,6 +78,18 @@ final class JitsiService: NSObject { // MARK: - Public + func declineWidget(withId widgetId: String) { + declinedJitsiWidgets[widgetId] = true + } + + func resetDeclineForWidget(withId widgetId: String) { + declinedJitsiWidgets.removeValue(forKey: widgetId) + } + + func isWidgetDeclined(withId widgetId: String) -> Bool { + return declinedJitsiWidgets[widgetId] == true + } + // MARK: Configuration func configureDefaultConferenceOptions(with serverURL: URL) { @@ -104,6 +125,11 @@ final class JitsiService: NSObject { completion(.failure(error)) } }, failure: { (error) in + if let urlResponse = MXHTTPOperation.urlResponse(fromError: error), + urlResponse.statusCode == HTTPStatusCodes.notFound { + completion(.failure(JitsiServiceError.noWellKnown)) + return + } completion(.failure(error ?? JitsiServiceError.unknown)) }) } @@ -111,20 +137,37 @@ final class JitsiService: NSObject { /// Create Jitsi widget content @discardableResult func createJitsiWidgetContent(jitsiServerURL: URL, roomID: String, isAudioOnly: Bool, success: @escaping ([String: Any]) -> Void, failure: @escaping ((Error) -> Void)) -> MXHTTPOperation? { + guard let serverDomain = jitsiServerURL.host else { + failure(JitsiServiceError.widgetContentCreationFailed) + return nil + } + return self.getWellKnown(for: jitsiServerURL) { (result) in + var continueOperation: Bool = false + var authType: JitsiAuthenticationType? + switch result { case .success(let jitsiWellKnown): - if let serverDomain = jitsiServerURL.host, let widgetContent = self.createJitsiWidgetContent(serverDomain: serverDomain, - authenticationType: jitsiWellKnown.authenticationType, - roomID: roomID, - isAudioOnly: isAudioOnly) { - success(widgetContent) - } else { - failure(JitsiServiceError.widgetContentCreationFailed) - } + authType = jitsiWellKnown.authenticationType + continueOperation = true case .failure(let error): NSLog("[JitsiService] Fail to get Jitsi Well Known with error: \(error)") - failure(error) + if let error = error as? JitsiServiceError, error == .noWellKnown { + // no well-known, continue with no auth + continueOperation = true + } else { + failure(error) + } + } + + if continueOperation, + let widgetContent = self.createJitsiWidgetContent(serverDomain: serverDomain, + authenticationType: authType, + roomID: roomID, + isAudioOnly: isAudioOnly) { + success(widgetContent) + } else { + failure(JitsiServiceError.widgetContentCreationFailed) } } } diff --git a/Riot/Modules/Integrations/Widgets/Jitsi/JitsiViewController.h b/Riot/Modules/Integrations/Widgets/Jitsi/JitsiViewController.h index 93419ebb3..653608c47 100644 --- a/Riot/Modules/Integrations/Widgets/Jitsi/JitsiViewController.h +++ b/Riot/Modules/Integrations/Widgets/Jitsi/JitsiViewController.h @@ -58,6 +58,11 @@ success:(void (^)(void))success failure:(void (^)(NSError *error))failure; +/** + Set audio muted for the Jitsi call. + */ +- (void)setAudioMuted:(BOOL)muted; + /** Hang up the jitsi conference call in progress. */ @@ -68,6 +73,11 @@ */ @property (nonatomic, readonly) Widget *widget; +/** + Total duration of the call. In milliseconds. + */ +@property (nonatomic, readonly) NSUInteger callDuration; + /** The delegate for the view controller. */ diff --git a/Riot/Modules/Integrations/Widgets/Jitsi/JitsiViewController.m b/Riot/Modules/Integrations/Widgets/Jitsi/JitsiViewController.m index f93bcea75..29d528350 100644 --- a/Riot/Modules/Integrations/Widgets/Jitsi/JitsiViewController.m +++ b/Riot/Modules/Integrations/Widgets/Jitsi/JitsiViewController.m @@ -22,8 +22,17 @@ @import JitsiMeetSDK; static const NSString *kJitsiDataErrorKey = @"error"; +/** + Class name for RCTSafeAreaView. It's in the React Native SDK, so we cannot import its header. + */ +static NSString * _Nonnull kRCTSafeAreaViewClassName = @"RCTSafeAreaView"; -@interface JitsiViewController () +/* + Some feature flags defined in https://github.com/jitsi/jitsi-meet/blob/master/react/features/base/flags/constants.js + */ +static NSString * _Nonnull kJitsiFeatureFlagChatEnabled = @"chat.enabled"; + +@interface JitsiViewController () // The jitsi-meet SDK view @property (nonatomic, weak) IBOutlet JitsiMeetView *jitsiMeetView; @@ -33,6 +42,11 @@ static const NSString *kJitsiDataErrorKey = @"error"; @property (nonatomic, strong) NSString *jwtToken; @property (nonatomic) BOOL startWithVideo; +/** + Overlay views in self.jitsiMeetView. Only provided if the screen is in the PiP mode. + */ +@property (nonatomic, strong) NSArray *overlayViews; + @end @implementation JitsiViewController @@ -157,11 +171,33 @@ static const NSString *kJitsiDataErrorKey = @"error"; }]; } +- (void)setAudioMuted:(BOOL)muted +{ + [self.jitsiMeetView setAudioMuted:muted]; +} + - (void)hangup { [self.jitsiMeetView leave]; } +- (NSUInteger)callDuration +{ + MXEvent *widgetEvent = self.widget.widgetEvent; + if (widgetEvent) + { + if (widgetEvent.originServerTs == kMXUndefinedTimestamp) + { + return 0; + } + else + { + return (uint64_t)[NSDate date].timeIntervalSince1970*1000 - widgetEvent.originServerTs; + } + } + return 0; +} + #pragma mark - Private // Fill Jitsi data based on Matrix Widget V2 widget data @@ -237,12 +273,33 @@ static const NSString *kJitsiDataErrorKey = @"error"; andEmail:nil andAvatar:avatarUrl]; builder.token = self.jwtToken; + [builder setFeatureFlag:kJitsiFeatureFlagChatEnabled withBoolean:NO]; }]; [self.jitsiMeetView join:jitsiMeetConferenceOptions]; } } +/** + Finds all the views in self.jitsiMeetView recursively those kind of class with the name `kRCTSafeAreaViewClassName`. + */ +- (NSArray*)overlayViewsIn:(UIView *)view +{ + Class class = NSClassFromString(kRCTSafeAreaViewClassName); + if ([view isKindOfClass:class]) + { + return @[view]; + } + + NSMutableArray *result = [NSMutableArray arrayWithCapacity:2]; + + [view.subviews enumerateObjectsUsingBlock:^(__kindof UIView * _Nonnull subview, NSUInteger idx, BOOL * _Nonnull stop) { + [result addObjectsFromArray:[self overlayViewsIn:subview]]; + }]; + + return result; +} + #pragma mark - JitsiMeetViewDelegate - (void)conferenceWillJoin:(NSDictionary *)data @@ -287,6 +344,26 @@ static const NSString *kJitsiDataErrorKey = @"error"; } } +#pragma mark - PictureInPicturable + +- (void)enterPiP +{ + self.overlayViews = [self overlayViewsIn:self.view]; + for (UIView *view in self.overlayViews) + { + view.alpha = 0; + } +} + +- (void)exitPiP +{ + for (UIView *view in self.overlayViews) + { + view.alpha = 1.0; + } + self.overlayViews = nil; +} + @end #endif diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.h b/Riot/Modules/Room/CellData/RoomBubbleCellData.h index 7637153f0..af8c19090 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.h +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.h @@ -28,6 +28,7 @@ typedef NS_ENUM(NSInteger, RoomBubbleCellDataTag) RoomBubbleCellDataTagKeyVerificationRequest, RoomBubbleCellDataTagKeyVerificationConclusion, RoomBubbleCellDataTagCall, + RoomBubbleCellDataTagGroupCall, RoomBubbleCellDataTagRoomCreationIntro }; diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.m b/Riot/Modules/Room/CellData/RoomBubbleCellData.m index 2f00e796b..28987ffba 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.m +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.m @@ -64,6 +64,8 @@ static NSAttributedString *timestampVerticalWhitespace = nil; if (self) { + self.displayTimestampForSelectedComponentOnLeftWhenPossible = YES; + switch (event.eventType) { case MXEventTypeRoomMember: @@ -131,6 +133,8 @@ static NSAttributedString *timestampVerticalWhitespace = nil; } break; case MXEventTypeCallInvite: + case MXEventTypeCallAnswer: + case MXEventTypeCallHangup: case MXEventTypeCallReject: { self.tag = RoomBubbleCellDataTagCall; @@ -140,6 +144,25 @@ static NSAttributedString *timestampVerticalWhitespace = nil; // Collapse them by default self.collapsed = YES; + + // Show timestamps always on right + self.displayTimestampForSelectedComponentOnLeftWhenPossible = NO; + } + case MXEventTypeCustom: + { + if ([event.type isEqualToString:kWidgetMatrixEventTypeString] + || [event.type isEqualToString:kWidgetModularEventTypeString]) + { + Widget *widget = [[Widget alloc] initWithWidgetEvent:event inMatrixSession:roomDataSource.mxSession]; + if ([widget.type isEqualToString:kWidgetTypeJitsiV1] || + [widget.type isEqualToString:kWidgetTypeJitsiV2]) + { + self.tag = RoomBubbleCellDataTagGroupCall; + + // Show timestamps always on right + self.displayTimestampForSelectedComponentOnLeftWhenPossible = NO; + } + } } break; default: @@ -153,8 +176,6 @@ static NSAttributedString *timestampVerticalWhitespace = nil; // Reset attributedTextMessage to force reset MXKRoomCellData parameters self.attributedTextMessage = nil; - - self.displayTimestampForSelectedComponentOnLeftWhenPossible = YES; } return self; @@ -743,9 +764,15 @@ static NSAttributedString *timestampVerticalWhitespace = nil; case RoomBubbleCellDataTagCall: shouldAddEvent = NO; break; + case RoomBubbleCellDataTagGroupCall: + shouldAddEvent = NO; + break; case RoomBubbleCellDataTagRoomCreateConfiguration: shouldAddEvent = NO; break; + case RoomBubbleCellDataTagRoomCreationIntro: + shouldAddEvent = NO; + break; default: break; } @@ -788,9 +815,25 @@ static NSAttributedString *timestampVerticalWhitespace = nil; shouldAddEvent = NO; break; case MXEventTypeCallInvite: + case MXEventTypeCallAnswer: + case MXEventTypeCallHangup: case MXEventTypeCallReject: shouldAddEvent = NO; break; + case MXEventTypeCustom: + { + if ([event.type isEqualToString:kWidgetMatrixEventTypeString] + || [event.type isEqualToString:kWidgetModularEventTypeString]) + { + Widget *widget = [[Widget alloc] initWithWidgetEvent:event inMatrixSession:roomDataSource.mxSession]; + if ([widget.type isEqualToString:kWidgetTypeJitsiV1] || + [widget.type isEqualToString:kWidgetTypeJitsiV2]) + { + shouldAddEvent = NO; + } + } + break; + } default: break; } diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.m b/Riot/Modules/Room/DataSources/RoomDataSource.m index 5edf5ba24..0a639ebc2 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.m +++ b/Riot/Modules/Room/DataSources/RoomDataSource.m @@ -856,7 +856,9 @@ const CGFloat kTypingCellHeight = 24; cellData.showTimestampForSelectedComponent = self.showBubbleDateTimeOnSelection; - if (cellData.collapsed && cellData.nextCollapsableCellData) + if (cellData.collapsed + && cellData.nextCollapsableCellData + && cellData.tag != RoomBubbleCellDataTagCall) { // Select nothing for a collased cell but open it [self collapseRoomBubble:cellData collapsed:NO]; diff --git a/Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m b/Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m index 368d5993d..82a621795 100644 --- a/Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m +++ b/Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m @@ -628,7 +628,7 @@ } // Check whether the option Ignore may be presented - if (self.mxRoomMember.membership == MXMembershipJoin) + if (RiotSettings.shared.roomMemberScreenShowIgnore && self.mxRoomMember.membership == MXMembershipJoin) { // is he already ignored ? if (![self.mainSession isUserIgnored:self.mxRoomMember.userId]) diff --git a/Riot/Modules/Room/RoomViewController.h b/Riot/Modules/Room/RoomViewController.h index 50e86e70b..dac5cbb05 100644 --- a/Riot/Modules/Room/RoomViewController.h +++ b/Riot/Modules/Room/RoomViewController.h @@ -34,6 +34,11 @@ */ extern NSNotificationName const RoomCallTileTappedNotification; +/** + Notification string used to indicate group call tile tapped in a room. Notification object will be the `RoomBubbleCellData` object. + */ +extern NSNotificationName const RoomGroupCallTileTappedNotification; + @interface RoomViewController : MXKRoomViewController // The preview header @@ -42,16 +47,18 @@ extern NSNotificationName const RoomCallTileTappedNotification; // The jump to last unread banner @property (weak, nonatomic) IBOutlet UIView *jumpToLastUnreadBannerContainer; -@property (weak, nonatomic) IBOutlet NSLayoutConstraint *jumpToLastUnreadBannerContainerTopConstraint; +@property (weak, nonatomic) IBOutlet UIView *jumpToLastUnreadBanner; @property (weak, nonatomic) IBOutlet UIImageView *jumpToLastUnreadImageView; @property (weak, nonatomic) IBOutlet UIButton *jumpToLastUnreadButton; @property (weak, nonatomic) IBOutlet UILabel *jumpToLastUnreadLabel; @property (weak, nonatomic) IBOutlet UIButton *resetReadMarkerButton; -@property (weak, nonatomic) IBOutlet UIView *jumpToLastUnreadBannerSeparatorView; @property (weak, nonatomic) IBOutlet UIView *inputBackgroundView; @property (weak, nonatomic) IBOutlet UIButton *scrollToBottomButton; @property (weak, nonatomic) IBOutlet BadgeLabel *scrollToBottomBadgeLabel; +// Remove Jitsi widget container +@property (weak, nonatomic) IBOutlet UIView *removeJitsiWidgetContainer; + /** Preview data for a room invitation received by email, or a link to a room. */ diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 8b3a14c21..d08af5d10 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -129,12 +129,13 @@ #import "Riot-Swift.h" NSNotificationName const RoomCallTileTappedNotification = @"RoomCallTileTappedNotification"; +NSNotificationName const RoomGroupCallTileTappedNotification = @"RoomGroupCallTileTappedNotification"; const NSTimeInterval kResizeComposerAnimationDuration = .05; @interface RoomViewController () + RoomDataSourceDelegate, RoomCreationModalCoordinatorBridgePresenterDelegate, RoomInfoCoordinatorBridgePresenterDelegate, DialpadViewControllerDelegate, RemoveJitsiWidgetViewDelegate> { // The preview header @@ -195,6 +196,12 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; // Tell whether the view controller is appeared or not. BOOL isAppeared; + + // Tell whether the room has a Jitsi call or not. + BOOL hasJitsiCall; + + // The right bar button items back up. + NSArray *rightBarButtonItems; // Observe kThemeServiceDidChangeThemeNotification to handle user interface theme change. id kThemeServiceDidChangeThemeNotificationObserver; @@ -213,6 +220,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; } @property (nonatomic, weak) IBOutlet UIView *overlayContainerView; +@property (nonatomic, strong) RemoveJitsiWidgetView *removeJitsiWidgetView; @property (nonatomic, strong) RoomContextualMenuViewController *roomContextualMenuViewController; @@ -372,6 +380,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; // call cells [self.bubblesTableView registerClass:RoomDirectCallStatusBubbleCell.class forCellReuseIdentifier:RoomDirectCallStatusBubbleCell.defaultReuseIdentifier]; + [self.bubblesTableView registerClass:RoomGroupCallStatusBubbleCell.class forCellReuseIdentifier:RoomGroupCallStatusBubbleCell.defaultReuseIdentifier]; [self.bubblesTableView registerClass:RoomCreationIntroCell.class forCellReuseIdentifier:RoomCreationIntroCell.defaultReuseIdentifier]; @@ -379,6 +388,8 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [self vc_removeBackTitle]; + [self setupRemoveJitsiWidgetRemoveView]; + // Replace the default input toolbar view. // Note: this operation will force the layout of subviews. That is why cell view classes must be registered before. [self updateRoomInputToolbarViewClassIfNeeded]; @@ -447,11 +458,11 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; self.activityIndicator.backgroundColor = ThemeService.shared.theme.overlayBackgroundColor; + [self.removeJitsiWidgetView updateWithTheme:ThemeService.shared.theme]; + // Prepare jump to last unread banner - self.jumpToLastUnreadBannerContainer.backgroundColor = ThemeService.shared.theme.backgroundColor; - self.jumpToLastUnreadImageView.tintColor = ThemeService.shared.theme.textPrimaryColor; + self.jumpToLastUnreadImageView.tintColor = ThemeService.shared.theme.tintColor; self.jumpToLastUnreadLabel.textColor = ThemeService.shared.theme.textPrimaryColor; - self.jumpToLastUnreadBannerSeparatorView.backgroundColor = ThemeService.shared.theme.lineBreakColor; self.previewHeaderContainer.backgroundColor = ThemeService.shared.theme.headerBackgroundColor; @@ -465,23 +476,31 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [self.bubblesTableView reloadData]; } - self.scrollToBottomButton.layer.shadowColor = [UIColor blackColor].CGColor; - self.scrollToBottomButton.layer.shadowOpacity = 0.2; - self.scrollToBottomButton.layer.shadowRadius = 6; - self.scrollToBottomButton.layer.shadowOffset = CGSizeMake(0, 4); + [self.scrollToBottomButton vc_addShadowWithColor:ThemeService.shared.theme.shadowColor + offset:CGSizeMake(0, 4) + radius:6 + opacity:0.2]; self.inputBackgroundView.backgroundColor = [ThemeService.shared.theme.backgroundColor colorWithAlphaComponent:0.98]; - if ([ThemeService.shared.themeId isEqualToString:@"light"]) + if (ThemeService.shared.isCurrentThemeDark) + { + [self.scrollToBottomButton setImage:[UIImage imageNamed:@"scrolldown_dark"] forState:UIControlStateNormal]; + + self.jumpToLastUnreadBanner.backgroundColor = ThemeService.shared.theme.colors.navigation; + [self.jumpToLastUnreadBanner vc_removeShadow]; + self.resetReadMarkerButton.tintColor = ThemeService.shared.theme.colors.quarterlyContent; + } + else { [self.scrollToBottomButton setImage:[UIImage imageNamed:@"scrolldown"] forState:UIControlStateNormal]; - } - else if ([ThemeService.shared.themeId isEqualToString:@"dark"] || [ThemeService.shared.themeId isEqualToString:@"black"]) - { - [self.scrollToBottomButton setImage:[UIImage imageNamed:@"scrolldown_dark"] forState:UIControlStateNormal]; - } - else if (@available(iOS 12.0, *) && ThemeService.shared.theme.userInterfaceStyle == UIUserInterfaceStyleDark) { - [self.scrollToBottomButton setImage:[UIImage imageNamed:@"scrolldown_dark"] forState:UIControlStateNormal]; + + self.jumpToLastUnreadBanner.backgroundColor = ThemeService.shared.theme.colors.background; + [self.jumpToLastUnreadBanner vc_addShadowWithColor:ThemeService.shared.theme.shadowColor + offset:CGSizeMake(0, 4) + radius:8 + opacity:0.1]; + self.resetReadMarkerButton.tintColor = ThemeService.shared.theme.colors.tertiaryContent; } self.scrollToBottomBadgeLabel.badgeColor = ThemeService.shared.theme.tintColor; @@ -510,6 +529,9 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; // Refresh the room title view [self refreshRoomTitle]; + // refresh remove Jitsi widget view + [self refreshRemoveJitsiWidgetView]; + // Refresh tool bar if the room data source is set. if (self.roomDataSource) { @@ -621,6 +643,14 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; }]; [self refreshMissedDiscussionsCount:YES]; self.keyboardHeight = MAX(self.keyboardHeight, 0); + + if (hasJitsiCall && + ![[AppDelegate theDelegate].callPresenter.jitsiVC.widget.roomId isEqualToString:self.roomDataSource.roomId]) + { + // the room had a Jitsi call before, but not now + hasJitsiCall = NO; + [self reloadBubblesTable:YES]; + } } - (void)viewDidDisappear:(BOOL)animated @@ -650,6 +680,13 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [[NSNotificationCenter defaultCenter] removeObserver:mxEventDidDecryptNotificationObserver]; mxEventDidDecryptNotificationObserver = nil; } + + JitsiViewController *jitsiVC = [AppDelegate theDelegate].callPresenter.jitsiVC; + if ([jitsiVC.widget.roomId isEqualToString:self.roomDataSource.roomId]) + { + hasJitsiCall = YES; + [self reloadBubblesTable:YES]; + } } - (void)viewDidLayoutSubviews @@ -657,7 +694,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [super viewDidLayoutSubviews]; UIEdgeInsets contentInset = self.bubblesTableView.contentInset; - contentInset.bottom = self.bottomLayoutGuide.length; + contentInset.bottom = self.view.safeAreaInsets.bottom; self.bubblesTableView.contentInset = contentInset; // Check here whether a subview has been added or removed @@ -709,15 +746,18 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; self.previewHeaderContainerHeightConstraint.constant = frame.origin.y + frame.size.height; self.bubblesTableViewTopConstraint.constant = self.previewHeaderContainerHeightConstraint.constant - self.bubblesTableView.mxk_adjustedContentInset.top; - self.jumpToLastUnreadBannerContainerTopConstraint.constant = self.previewHeaderContainerHeightConstraint.constant; } else { // In non expanded header mode, the navigation bar is opaque // The table view must not display behind it self.edgesForExtendedLayout = UIRectEdgeLeft | UIRectEdgeBottom | UIRectEdgeRight; - - self.jumpToLastUnreadBannerContainerTopConstraint.constant = self.bubblesTableView.mxk_adjustedContentInset.top; // no expanded + } + + // stay at the bottom if already was + if (self.isBubblesTableScrollViewAtTheBottom) + { + [self scrollBubblesTableViewToBottomAnimated:NO]; } [self refreshMissedDiscussionsCount:YES]; @@ -932,14 +972,14 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; self.eventsAcknowledgementEnabled = YES; - // Set room title view - [self refreshRoomTitle]; - // Store ref on customized room data source if ([dataSource isKindOfClass:RoomDataSource.class]) { customizedRoomDataSource = (RoomDataSource*)dataSource; } + + // Set room title view + [self refreshRoomTitle]; } else { @@ -1355,8 +1395,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; if (roomDataSource.currentTypingUsers && !roomDataSource.currentTypingUsers.count) { [roomDataSource resetTypingNotification]; - NSInteger count = [self.bubblesTableView numberOfRowsInSection:0]; - [self.bubblesTableView deleteRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:count - 1 inSection:0]] withRowAnimation:UITableViewRowAnimationNone]; + [self.bubblesTableView reloadData]; } } @@ -1368,6 +1407,29 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; #pragma mark - Internals +- (UIBarButtonItem *)videoCallBarButtonItem +{ + UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"video_call"] + style:UIBarButtonItemStylePlain + target:self + action:@selector(onVideoCallPressed:)]; + item.accessibilityLabel = NSLocalizedStringFromTable(@"room_accessibility_video_call", @"Vector", nil); + + return item; +} + +- (void)setupRemoveJitsiWidgetRemoveView +{ + self.removeJitsiWidgetView = [RemoveJitsiWidgetView instantiate]; + self.removeJitsiWidgetView.delegate = self; + + [self.removeJitsiWidgetContainer vc_addSubViewMatchingParent:self.removeJitsiWidgetView]; + + self.removeJitsiWidgetContainer.hidden = YES; + + [self refreshRemoveJitsiWidgetView]; +} + - (void)forceLayoutRefresh { // Sanity check: check whether the table view data source is set. @@ -1409,7 +1471,18 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; MXCall *callInRoom = [self.roomDataSource.mxSession.callManager callInRoom:self.roomDataSource.roomId]; return (callInRoom && callInRoom.state != MXCallStateEnded) - || [[AppDelegate theDelegate].jitsiViewController.widget.roomId isEqualToString:self.roomDataSource.roomId]; + || customizedRoomDataSource.jitsiWidget; +} + +/** + Returns a flag for the current user whether it's privileged to add/remove Jitsi widgets to this room. + */ +- (BOOL)canEditJitsiWidget +{ + MXRoomPowerLevels *powerLevels = [self.roomDataSource.roomState powerLevels]; + NSInteger requiredPower = [powerLevels minimumPowerLevelForSendingEventAsStateEvent:kWidgetModularEventTypeString]; + NSInteger myPower = [powerLevels powerLevelOfUserWithUserID:self.roomDataSource.mxSession.myUserId]; + return myPower >= requiredPower; } - (void)refreshRoomTitle @@ -1428,27 +1501,84 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; if (self.roomDataSource.isLive) { rightBarButtonItems = [NSMutableArray new]; + BOOL hasCustomJoinButton = NO; - UIEdgeInsets itemInsets = UIEdgeInsetsMake(0, -5, 0, 5); if (self.supportCallOption) { - UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"voice_call_hangon_icon"] style:UIBarButtonItemStylePlain target:self action:@selector(onVoiceCallPressed:)]; - item.accessibilityLabel = NSLocalizedStringFromTable(@"room_accessibility_call", @"Vector", nil); - item.imageInsets = UIEdgeInsetsMake(0, -5, 0, 5); - [rightBarButtonItems addObject:item]; - - item = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"video_call"] style:UIBarButtonItemStylePlain target:self action:@selector(onVideoCallPressed:)]; - item.imageInsets = UIEdgeInsetsMake(0, 10, 0, -10); - item.accessibilityLabel = NSLocalizedStringFromTable(@"room_accessibility_video_call", @"Vector", nil); - [rightBarButtonItems addObject:item]; - itemInsets = UIEdgeInsetsMake(0, 20, 0, -20); + if (self.roomDataSource.room.summary.membersCount.joined == 2 && self.roomDataSource.room.isDirect) + { + // voice call button for Matrix call + UIBarButtonItem *itemVoice = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"voice_call_hangon_icon"] + style:UIBarButtonItemStylePlain + target:self + action:@selector(onVoiceCallPressed:)]; + itemVoice.accessibilityLabel = NSLocalizedStringFromTable(@"room_accessibility_call", @"Vector", nil); + itemVoice.enabled = !self.isCallActive; + [rightBarButtonItems addObject:itemVoice]; + + // video call button for Matrix call + UIBarButtonItem *itemVideo = [self videoCallBarButtonItem]; + itemVideo.enabled = !self.isCallActive; + [rightBarButtonItems addObject:itemVideo]; + } + else + { + // video call button for Jitsi call + if (self.isCallActive) + { + JitsiViewController *jitsiVC = [AppDelegate theDelegate].callPresenter.jitsiVC; + if ([jitsiVC.widget.roomId isEqualToString:self.roomDataSource.roomId]) + { + // show a disabled call button + UIBarButtonItem *item = [self videoCallBarButtonItem]; + item.enabled = NO; + [rightBarButtonItems addObject:item]; + } + else + { + // show Join button + CallTileActionButton *button = [CallTileActionButton new]; + [button setImage:[UIImage imageNamed:@"call_video_icon"] + forState:UIControlStateNormal]; + [button setTitle:NSLocalizedStringFromTable(@"room_join_group_call", @"Vector", nil) + forState:UIControlStateNormal]; + [button addTarget:self + action:@selector(onVideoCallPressed:) + forControlEvents:UIControlEventTouchUpInside]; + button.contentEdgeInsets = UIEdgeInsetsMake(4, 12, 4, 12); + UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithCustomView:button]; + item.accessibilityLabel = NSLocalizedStringFromTable(@"room_accessibility_video_call", @"Vector", nil); + [rightBarButtonItems addObject:item]; + + hasCustomJoinButton = YES; + } + } + else + { + // show a video call button + // item will still be enabled, and when tapped an alert will be displayed to the user + UIBarButtonItem *item = [self videoCallBarButtonItem]; + if (!self.canEditJitsiWidget) + { + item.image = [[UIImage imageNamed:@"video_call"] vc_withAlpha:0.3]; + } + [rightBarButtonItems addObject:item]; + } + } } if ([self widgetsCount:NO]) { - UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"integrations_icon"] style:UIBarButtonItemStylePlain target:self action:@selector(onIntegrationsPressed:)]; - item.imageInsets = itemInsets; + UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"integrations_icon"] + style:UIBarButtonItemStylePlain + target:self + action:@selector(onIntegrationsPressed:)]; item.accessibilityLabel = NSLocalizedStringFromTable(@"room_accessibility_integrations", @"Vector", nil); + if (hasCustomJoinButton) + { + item.imageInsets = UIEdgeInsetsMake(0, -5, 0, -5); + item.landscapeImagePhoneInsets = UIEdgeInsetsMake(0, -5, 0, -5); + } [rightBarButtonItems addObject:item]; } @@ -1993,7 +2123,6 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; animations:^{ self.bubblesTableViewTopConstraint.constant = 0; - self.jumpToLastUnreadBannerContainerTopConstraint.constant = self.bubblesTableView.mxk_adjustedContentInset.top; // Force to render the view [self forceLayoutRefresh]; @@ -2112,7 +2241,6 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; animations:^{ self.bubblesTableViewTopConstraint.constant = self.previewHeaderContainerHeightConstraint.constant - self.bubblesTableView.mxk_adjustedContentInset.top; - self.jumpToLastUnreadBannerContainerTopConstraint.constant = self.previewHeaderContainerHeightConstraint.constant; previewHeader.roomAvatar.alpha = 1; @@ -2227,6 +2355,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; { cellViewClass = RoomDirectCallStatusBubbleCell.class; } + else if (bubbleData.tag == RoomBubbleCellDataTagGroupCall) + { + cellViewClass = RoomGroupCallStatusBubbleCell.class; + } else if (bubbleData.isIncoming) { if (bubbleData.isAttachmentWithThumbnail) @@ -2403,6 +2535,20 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; { // post notification `RoomCallTileTapped` [[NSNotificationCenter defaultCenter] postNotificationName:RoomCallTileTappedNotification object:bubbleData]; + + preventBubblesTableViewScroll = YES; + [self selectEventWithId:tappedEvent.eventId]; + } + } + else if (bubbleData.tag == RoomBubbleCellDataTagGroupCall) + { + if ([bubbleData isKindOfClass:[RoomBubbleCellData class]]) + { + // post notification `RoomGroupCallTileTapped` + [[NSNotificationCenter defaultCenter] postNotificationName:RoomGroupCallTileTappedNotification object:bubbleData]; + + preventBubblesTableViewScroll = YES; + [self selectEventWithId:tappedEvent.eventId]; } } else @@ -2537,13 +2683,83 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [self showReactionHistoryForEventId:tappedEventId animated:YES]; } } - else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellCallBackButtonPressed]) + else if ([actionIdentifier isEqualToString:RoomDirectCallStatusBubbleCell.callBackAction]) { MXEvent *callInviteEvent = userInfo[kMXKRoomBubbleCellEventKey]; MXCallInviteEventContent *eventContent = [MXCallInviteEventContent modelFromJSON:callInviteEvent.content]; [self placeCallWithVideo2:eventContent.isVideoCall]; } + else if ([actionIdentifier isEqualToString:RoomDirectCallStatusBubbleCell.declineAction]) + { + MXEvent *callInviteEvent = userInfo[kMXKRoomBubbleCellEventKey]; + MXCallInviteEventContent *eventContent = [MXCallInviteEventContent modelFromJSON:callInviteEvent.content]; + + MXCall *call = [self.mainSession.callManager callWithCallId:eventContent.callId]; + [call hangup]; + } + else if ([actionIdentifier isEqualToString:RoomDirectCallStatusBubbleCell.answerAction]) + { + MXEvent *callInviteEvent = userInfo[kMXKRoomBubbleCellEventKey]; + MXCallInviteEventContent *eventContent = [MXCallInviteEventContent modelFromJSON:callInviteEvent.content]; + + MXCall *call = [self.mainSession.callManager callWithCallId:eventContent.callId]; + [call answer]; + } + else if ([actionIdentifier isEqualToString:RoomDirectCallStatusBubbleCell.endCallAction]) + { + MXEvent *callInviteEvent = userInfo[kMXKRoomBubbleCellEventKey]; + MXCallInviteEventContent *eventContent = [MXCallInviteEventContent modelFromJSON:callInviteEvent.content]; + + MXCall *call = [self.mainSession.callManager callWithCallId:eventContent.callId]; + [call hangup]; + } + else if ([actionIdentifier isEqualToString:RoomGroupCallStatusBubbleCell.joinAction] || + [actionIdentifier isEqualToString:RoomGroupCallStatusBubbleCell.answerAction]) + { + MXWeakify(self); + NSString *appDisplayName = [[NSBundle mainBundle] infoDictionary][@"CFBundleDisplayName"]; + + // Check app permissions first + [MXKTools checkAccessForCall:YES + manualChangeMessageForAudio:[NSString stringWithFormat:[NSBundle mxk_localizedStringForKey:@"microphone_access_not_granted_for_call"], appDisplayName] + manualChangeMessageForVideo:[NSString stringWithFormat:[NSBundle mxk_localizedStringForKey:@"camera_access_not_granted_for_call"], appDisplayName] + showPopUpInViewController:self completionHandler:^(BOOL granted) { + + MXStrongifyAndReturnIfNil(self); + if (granted) + { + // Present the Jitsi view controller + Widget *jitsiWidget = [self->customizedRoomDataSource jitsiWidget]; + if (jitsiWidget) + { + [[AppDelegate theDelegate].callPresenter displayJitsiCallWithWidget:jitsiWidget]; + } + } + else + { + NSLog(@"[RoomVC] didRecognizeAction:inCell:userInfo Warning: The application does not have the permission to join/answer the group call"); + } + }]; + + MXEvent *widgetEvent = userInfo[kMXKRoomBubbleCellEventKey]; + Widget *widget = [[Widget alloc] initWithWidgetEvent:widgetEvent + inMatrixSession:customizedRoomDataSource.mxSession]; + [[JitsiService shared] resetDeclineForWidgetWithId:widget.widgetId]; + } + else if ([actionIdentifier isEqualToString:RoomGroupCallStatusBubbleCell.leaveAction]) + { + [[AppDelegate theDelegate].callPresenter endActiveJitsiCall]; + [self reloadBubblesTable:YES]; + } + else if ([actionIdentifier isEqualToString:RoomGroupCallStatusBubbleCell.declineAction]) + { + MXEvent *widgetEvent = userInfo[kMXKRoomBubbleCellEventKey]; + Widget *widget = [[Widget alloc] initWithWidgetEvent:widgetEvent + inMatrixSession:customizedRoomDataSource.mxSession]; + [[JitsiService shared] declineWidgetWithId:widget.widgetId]; + [self reloadBubblesTable:YES]; + } else if ([actionIdentifier isEqualToString:RoomCreationIntroCell.tapOnAvatarView]) { [self showRoomAvatarChange]; @@ -2575,6 +2791,23 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell *)cell; MXKAttachment *attachment = roomBubbleTableViewCell.bubbleData.attachment; + BOOL isJitsiCallEvent = NO; + switch (selectedEvent.eventType) { + case MXEventTypeCustom: + if ([selectedEvent.type isEqualToString:kWidgetMatrixEventTypeString] + || [selectedEvent.type isEqualToString:kWidgetModularEventTypeString]) + { + Widget *widget = [[Widget alloc] initWithWidgetEvent:selectedEvent inMatrixSession:self.roomDataSource.mxSession]; + if ([widget.type isEqualToString:kWidgetTypeJitsiV1] || + [widget.type isEqualToString:kWidgetTypeJitsiV2]) + { + isJitsiCallEvent = YES; + } + } + default: + break; + } + if (currentAlert) { [currentAlert dismissViewControllerAnimated:NO completion:nil]; @@ -2660,26 +2893,29 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; }]]; } - [currentAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"room_event_action_quote", @"Vector", nil) - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { - - if (weakSelf) - { - typeof(self) self = weakSelf; + if (!isJitsiCallEvent) + { + [currentAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"room_event_action_quote", @"Vector", nil) + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { - [self cancelEventSelection]; + if (weakSelf) + { + typeof(self) self = weakSelf; + + [self cancelEventSelection]; + + // Quote the message a la Markdown into the input toolbar composer + self.inputToolbarView.textMessage = [NSString stringWithFormat:@"%@\n>%@\n\n", self.inputToolbarView.textMessage, selectedComponent.textMessage]; + + // And display the keyboard + [self.inputToolbarView becomeFirstResponder]; + } - // Quote the message a la Markdown into the input toolbar composer - self.inputToolbarView.textMessage = [NSString stringWithFormat:@"%@\n>%@\n\n", self.inputToolbarView.textMessage, selectedComponent.textMessage]; - - // And display the keyboard - [self.inputToolbarView becomeFirstResponder]; - } - - }]]; + }]]; + } - if (BuildSettings.messageDetailsAllowShare) + if (!isJitsiCallEvent && BuildSettings.messageDetailsAllowShare) { [currentAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"room_event_action_share", @"Vector", nil) style:UIAlertActionStyleDefault @@ -2988,7 +3224,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; } } - if (![selectedEvent.sender isEqualToString:self.mainSession.myUser.userId]) + if (![selectedEvent.sender isEqualToString:self.mainSession.myUser.userId] && RiotSettings.shared.roomContextualMenuShowReportContentOption) { [currentAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"room_event_action_report", @"Vector", nil) style:UIAlertActionStyleDefault @@ -3098,7 +3334,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; }]]; } - if (self.roomDataSource.room.summary.isEncrypted) + if (!isJitsiCallEvent && self.roomDataSource.room.summary.isEncrypted) { [currentAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"room_event_action_view_encryption", @"Vector", nil) style:UIAlertActionStyleDefault @@ -3572,94 +3808,69 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; - (void)placeCallWithVideo2:(BOOL)video { - __weak __typeof(self) weakSelf = self; - - // If there is already a jitsi widget, join it Widget *jitsiWidget = [customizedRoomDataSource jitsiWidget]; if (jitsiWidget) { - [[AppDelegate theDelegate] displayJitsiViewControllerWithWidget:jitsiWidget andVideo:video]; + // If there is already a Jitsi call, join it + [[AppDelegate theDelegate].callPresenter displayJitsiCallWithWidget:jitsiWidget]; } - - // If enabled, create the conf using jitsi widget and open it directly - else if (RiotSettings.shared.createConferenceCallsWithJitsi - && self.roomDataSource.room.summary.membersCount.joined > 2) - { - [self startActivityIndicator]; - - [[WidgetManager sharedManager] createJitsiWidgetInRoom:self.roomDataSource.room - withVideo:video - success:^(Widget *jitsiWidget) - { - if (weakSelf) - { - typeof(self) self = weakSelf; - [self stopActivityIndicator]; - - [[AppDelegate theDelegate] displayJitsiViewControllerWithWidget:jitsiWidget andVideo:video]; - } - } - failure:^(NSError *error) - { - if (weakSelf) - { - typeof(self) self = weakSelf; - [self stopActivityIndicator]; - - [self showJitsiErrorAsAlert:error]; - } - }]; - } - // Classic conference call is not supported in encrypted rooms - else if (self.roomDataSource.room.summary.isEncrypted && self.roomDataSource.room.summary.membersCount.joined > 2) - { - [currentAlert dismissViewControllerAnimated:NO completion:nil]; - - currentAlert = [UIAlertController alertControllerWithTitle:[NSBundle mxk_localizedStringForKey:@"room_no_conference_call_in_encrypted_rooms"] message:nil preferredStyle:UIAlertControllerStyleAlert]; - - [currentAlert addAction:[UIAlertAction actionWithTitle:[NSBundle mxk_localizedStringForKey:@"ok"] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) - { - if (weakSelf) - { - typeof(self) self = weakSelf; - self->currentAlert = nil; - } - - }]]; - - [currentAlert mxk_setAccessibilityIdentifier:@"RoomVCCallAlert"]; - [self presentViewController:currentAlert animated:YES completion:nil]; - } - - // In case of conference call, check that the user has enough power level - else if (self.roomDataSource.room.summary.membersCount.joined > 2 && - ![MXCallManager canPlaceConferenceCallInRoom:self.roomDataSource.room roomState:self.roomDataSource.roomState]) - { - [currentAlert dismissViewControllerAnimated:NO completion:nil]; - - currentAlert = [UIAlertController alertControllerWithTitle:[NSBundle mxk_localizedStringForKey:@"room_no_power_to_create_conference_call"] message:nil preferredStyle:UIAlertControllerStyleAlert]; - - [currentAlert addAction:[UIAlertAction actionWithTitle:[NSBundle mxk_localizedStringForKey:@"ok"] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) - { - if (weakSelf) - { - typeof(self) self = weakSelf; - self->currentAlert = nil; - } - }]]; - - [currentAlert mxk_setAccessibilityIdentifier:@"RoomVCCallAlert"]; - [self presentViewController:currentAlert animated:YES completion:nil]; - } - - // Classic 1:1 or group call can be done else { - [self.roomDataSource.room placeCallWithVideo:video success:nil failure:nil]; + if (self.roomDataSource.room.summary.membersCount.joined == 2 && self.roomDataSource.room.isDirect) + { + // Matrix call + [self.roomDataSource.room placeCallWithVideo:video success:nil failure:nil]; + } + else + { + // Jitsi call + if (self.canEditJitsiWidget) + { + // User has right to add a Jitsi widget + // Create the Jitsi widget and open it directly + [self startActivityIndicator]; + + MXWeakify(self); + + [[WidgetManager sharedManager] createJitsiWidgetInRoom:self.roomDataSource.room + withVideo:video + success:^(Widget *jitsiWidget) + { + MXStrongifyAndReturnIfNil(self); + [self stopActivityIndicator]; + + [[AppDelegate theDelegate].callPresenter displayJitsiCallWithWidget:jitsiWidget]; + } + failure:^(NSError *error) + { + MXStrongifyAndReturnIfNil(self); + [self stopActivityIndicator]; + + [self showJitsiErrorAsAlert:error]; + }]; + } + else + { + // Insufficient privileges to add a Jitsi widget + MXWeakify(self); + [currentAlert dismissViewControllerAnimated:NO completion:nil]; + + currentAlert = [UIAlertController alertControllerWithTitle:[NSBundle mxk_localizedStringForKey:@"room_no_privileges_to_create_group_call"] + message:nil + preferredStyle:UIAlertControllerStyleAlert]; + + [currentAlert addAction:[UIAlertAction actionWithTitle:[NSBundle mxk_localizedStringForKey:@"ok"] + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) + { + MXStrongifyAndReturnIfNil(self); + self->currentAlert = nil; + }]]; + + [currentAlert mxk_setAccessibilityIdentifier:@"RoomVCCallAlert"]; + [self presentViewController:currentAlert animated:YES completion:nil]; + } + } } } @@ -3670,9 +3881,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; { [callInRoom hangup]; } - else if ([[AppDelegate theDelegate].jitsiViewController.widget.roomId isEqualToString:self.roomDataSource.roomId]) + else if ([[AppDelegate theDelegate].callPresenter.jitsiVC.widget.roomId isEqualToString:self.roomDataSource.roomId]) { - [[AppDelegate theDelegate].jitsiViewController hangup]; + [[AppDelegate theDelegate].callPresenter endActiveJitsiCall]; + [self reloadBubblesTable:YES]; } [self refreshActivitiesViewDisplay]; @@ -4265,17 +4477,20 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; - (void)listenWidgetNotifications { + MXWeakify(self); + kMXKWidgetManagerDidUpdateWidgetObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kWidgetManagerDidUpdateWidgetNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { + MXStrongifyAndReturnIfNil(self); + Widget *widget = notif.object; if (widget.mxSession == self.roomDataSource.mxSession - && [widget.roomId isEqualToString:customizedRoomDataSource.roomId]) + && [widget.roomId isEqualToString:self->customizedRoomDataSource.roomId]) { - // Jitsi conference widget existence is shown in the bottom bar - // Update the bar - [self refreshActivitiesViewDisplay]; - [self refreshRoomInputToolbar]; + // Call button update [self refreshRoomTitle]; + // Remove Jitsi widget view update + [self refreshRemoveJitsiWidgetView]; } }]; } @@ -4323,8 +4538,6 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [roomActivitiesView removeGestureRecognizer:roomActivitiesView.gestureRecognizers[0]]; } - Widget *jitsiWidget = [customizedRoomDataSource jitsiWidget]; - if ([self.roomDataSource.mxSession.syncError.errcode isEqualToString:kMXErrCodeStringResourceLimitExceeded]) { self.activitiesViewExpanded = YES; @@ -4384,102 +4597,6 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; } }]; } - else if (customizedRoomDataSource.roomState.isOngoingConferenceCall) - { - // Show the "Ongoing conference call" banner only if the user is not in the conference - MXCall *callInRoom = [self.roomDataSource.mxSession.callManager callInRoom:self.roomDataSource.roomId]; - if (callInRoom && callInRoom.state != MXCallStateEnded) - { - if ([self checkUnsentMessages] == NO) - { - [self refreshTypingNotification]; - } - } - else - { - self.activitiesViewExpanded = YES; - [roomActivitiesView displayOngoingConferenceCall:^(BOOL video) { - - NSLog(@"[RoomVC] onOngoingConferenceCallPressed"); - - // Make sure there is not yet a call - if (![customizedRoomDataSource.mxSession.callManager callInRoom:customizedRoomDataSource.roomId]) - { - [customizedRoomDataSource.room placeCallWithVideo:video success:nil failure:nil]; - } - } onClosePressed:nil]; - } - } - else if (jitsiWidget) - { - // The room has an active jitsi widget - // Show it in the banner if the user is not already in - LegacyAppDelegate *appDelegate = [AppDelegate theDelegate]; - if ([appDelegate.jitsiViewController.widget.widgetId isEqualToString:jitsiWidget.widgetId]) - { - if ([self checkUnsentMessages] == NO) - { - [self refreshTypingNotification]; - } - } - else - { - self.activitiesViewExpanded = YES; - [roomActivitiesView displayOngoingConferenceCall:^(BOOL video) { - - NSLog(@"[RoomVC] onOngoingConferenceCallPressed (jitsi)"); - - __weak __typeof(self) weakSelf = self; - NSString *appDisplayName = [[NSBundle mainBundle] infoDictionary][@"CFBundleDisplayName"]; - - // Check app permissions first - [MXKTools checkAccessForCall:video - manualChangeMessageForAudio:[NSString stringWithFormat:[NSBundle mxk_localizedStringForKey:@"microphone_access_not_granted_for_call"], appDisplayName] - manualChangeMessageForVideo:[NSString stringWithFormat:[NSBundle mxk_localizedStringForKey:@"camera_access_not_granted_for_call"], appDisplayName] - showPopUpInViewController:self completionHandler:^(BOOL granted) { - - if (weakSelf) - { - if (granted) - { - // Present the Jitsi view controller - [appDelegate displayJitsiViewControllerWithWidget:jitsiWidget andVideo:video]; - } - else - { - NSLog(@"[RoomVC] onOngoingConferenceCallPressed: Warning: The application does not have the perssion to join the call"); - } - } - }]; - - } onClosePressed:^{ - - [self startActivityIndicator]; - - // Close the widget - __weak __typeof(self) weakSelf = self; - [[WidgetManager sharedManager] closeWidget:jitsiWidget.widgetId inRoom:self.roomDataSource.room success:^{ - - if (weakSelf) - { - typeof(self) self = weakSelf; - [self stopActivityIndicator]; - - // The banner will automatically leave thanks to kWidgetManagerDidUpdateWidgetNotification - } - - } failure:^(NSError *error) { - if (weakSelf) - { - typeof(self) self = weakSelf; - - [self showJitsiErrorAsAlert:error]; - [self stopActivityIndicator]; - } - }]; - }]; - } - } else if ([self checkUnsentMessages] == NO) { // Show "scroll to bottom" icon when the most recent message is not visible, @@ -5022,6 +5139,32 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; } } +- (void)refreshRemoveJitsiWidgetView +{ + if (self.roomDataSource.isLive && !self.roomDataSource.isPeeking) + { + Widget *jitsiWidget = [customizedRoomDataSource jitsiWidget]; + + if (jitsiWidget && self.canEditJitsiWidget) + { + [self.removeJitsiWidgetView reset]; + self.removeJitsiWidgetContainer.hidden = NO; + self.removeJitsiWidgetView.delegate = self; + } + else + { + self.removeJitsiWidgetContainer.hidden = YES; + self.removeJitsiWidgetView.delegate = nil; + } + } + else + { + [self.removeJitsiWidgetView reset]; + self.removeJitsiWidgetContainer.hidden = YES; + self.removeJitsiWidgetView.delegate = self; + } +} + - (void)refreshJumpToLastUnreadBannerDisplay { // This banner is only displayed when the room timeline is in live (and no peeking). @@ -5369,12 +5512,25 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; ]; } - return @[ - [self copyMenuItemWithEvent:event andCell:cell], - [self replyMenuItemWithEvent:event], - [self editMenuItemWithEvent:event], - [self moreMenuItemWithEvent:event andCell:cell] - ]; + BOOL showMoreOption = (event.isState && RiotSettings.shared.roomContextualMenuShowMoreOptionForStates) || (!event.isState && RiotSettings.shared.roomContextualMenuShowMoreOptionForMessages); + + if (showMoreOption) + { + return @[ + [self copyMenuItemWithEvent:event andCell:cell], + [self replyMenuItemWithEvent:event], + [self editMenuItemWithEvent:event], + [self moreMenuItemWithEvent:event andCell:cell] + ]; + } + else + { + return @[ + [self copyMenuItemWithEvent:event andCell:cell], + [self replyMenuItemWithEvent:event], + [self editMenuItemWithEvent:event] + ]; + } } - (void)showContextualMenuForEvent:(MXEvent*)event fromSingleTapGesture:(BOOL)usedSingleTapGesture cell:(id)cell animated:(BOOL)animated @@ -5580,6 +5736,17 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; case MXEventTypeKeyVerificationCancel: isCopyActionEnabled = NO; break; + case MXEventTypeCustom: + if ([event.type isEqualToString:kWidgetMatrixEventTypeString] + || [event.type isEqualToString:kWidgetModularEventTypeString]) + { + Widget *widget = [[Widget alloc] initWithWidgetEvent:event inMatrixSession:self.roomDataSource.mxSession]; + if ([widget.type isEqualToString:kWidgetTypeJitsiV1] || + [widget.type isEqualToString:kWidgetTypeJitsiV2]) + { + isCopyActionEnabled = NO; + } + } default: break; } @@ -5956,4 +6123,37 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; self.roomInfoCoordinatorBridgePresenter = nil; } +#pragma mark - RemoveJitsiWidgetViewDelegate + +- (void)removeJitsiWidgetViewDidCompleteSliding:(RemoveJitsiWidgetView *)view +{ + view.delegate = nil; + Widget *jitsiWidget = [customizedRoomDataSource jitsiWidget]; + + [self startActivityIndicator]; + + // close the widget + MXWeakify(self); + + [[WidgetManager sharedManager] closeWidget:jitsiWidget.widgetId + inRoom:self.roomDataSource.room + success:^{ + MXStrongifyAndReturnIfNil(self); + [self stopActivityIndicator]; + // we can wait for kWidgetManagerDidUpdateWidgetNotification, but we want to be faster + self.removeJitsiWidgetContainer.hidden = YES; + self.removeJitsiWidgetView.delegate = nil; + + // end active call if exists + if ([[AppDelegate theDelegate].callPresenter.jitsiVC.widget.widgetId isEqualToString:jitsiWidget.widgetId]) + { + [[AppDelegate theDelegate].callPresenter endActiveJitsiCall]; + } + } failure:^(NSError *error) { + MXStrongifyAndReturnIfNil(self); + [self showJitsiErrorAsAlert:error]; + [self stopActivityIndicator]; + }]; +} + @end diff --git a/Riot/Modules/Room/RoomViewController.xib b/Riot/Modules/Room/RoomViewController.xib index 2371d6a0e..5b1a0a909 100644 --- a/Riot/Modules/Room/RoomViewController.xib +++ b/Riot/Modules/Room/RoomViewController.xib @@ -15,15 +15,15 @@ - - - - - + + + + + @@ -55,76 +55,86 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/Room/Views/BubbleCells/Call/Direct/RoomDirectCallStatusBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/Call/Direct/RoomDirectCallStatusBubbleCell.swift index 8a5391c8b..a3dbb63dc 100644 --- a/Riot/Modules/Room/Views/BubbleCells/Call/Direct/RoomDirectCallStatusBubbleCell.swift +++ b/Riot/Modules/Room/Views/BubbleCells/Call/Direct/RoomDirectCallStatusBubbleCell.swift @@ -16,68 +16,187 @@ import UIKit +/// The number of milliseconds in one second. +private let MSEC_PER_SEC: TimeInterval = 1000 + +@objcMembers class RoomDirectCallStatusBubbleCell: RoomBaseCallBubbleCell { - private enum Constants { - static let statusTextFontSize: CGFloat = 14 - static let statusTextInsets: UIEdgeInsets = UIEdgeInsets(top: 8, left: 8, bottom: 12, right: 8) - // swiftlint:disable force_unwrapping - static let statusCallBackURL: URL = URL(string: "element://call")! - // swiftlint:enable force_unwrapping + private static var className: String { + return String(describing: self) } - private lazy var statusTextView: UITextView = { - let textView = UITextView() - textView.font = .systemFont(ofSize: Constants.statusTextFontSize) - textView.backgroundColor = .clear - textView.textColor = ThemeService.shared().theme.noticeSecondaryColor - textView.linkTextAttributes = [ - .font: UIFont.systemFont(ofSize: Constants.statusTextFontSize), - .foregroundColor: ThemeService.shared().theme.tintColor - ] - textView.textAlignment = .center - textView.contentInset = .zero - textView.isEditable = false - textView.isSelectable = false - textView.isScrollEnabled = false - textView.scrollsToTop = false - textView.textContainerInset = Constants.statusTextInsets - textView.textContainer.lineFragmentPadding = 0 - textView.delegate = self - return textView - }() - - override var bottomContentView: UIView? { - return statusTextView + /// Action identifier used when the user pressed "Call back" button for a declined call. + /// The `userInfo` dictionary contains an `MXEvent` object under the `kMXKRoomBubbleCellEventKey` key, representing the invite event of the declined call. + static var callBackAction: String { + return self.className + ".callBack" } - override func update(theme: Theme) { - super.update(theme: theme) - statusTextView.textColor = theme.noticeSecondaryColor - statusTextView.linkTextAttributes = [ - .font: UIFont.systemFont(ofSize: Constants.statusTextFontSize), - .foregroundColor: theme.tintColor - ] + /// Action identifier used when the user pressed "Answer" button for an incoming call. + /// The `userInfo` dictionary contains an `MXEvent` object under the `kMXKRoomBubbleCellEventKey` key, representing the invite event of the call. + static var answerAction: String { + return self.className + ".answer" + } + + /// Action identifier used when the user pressed "Decline" button for an incoming call. + /// The `userInfo` dictionary contains an `MXEvent` object under the `kMXKRoomBubbleCellEventKey` key, representing the invite event of the call. + static var declineAction: String { + return self.className + ".decline" + } + + /// Action identifier used when the user pressed "End call" button for an incoming call. + /// The `userInfo` dictionary contains an `MXEvent` object under the `kMXKRoomBubbleCellEventKey` key, representing the invite event of the call. + static var endCallAction: String { + return self.className + ".endCall" + } + + private var callDurationString: String = "" + private var isVideoCall: Bool = false + private var isIncoming: Bool = false + private var callInviteEvent: MXEvent? + private var viewState: ViewState = .unknown { + didSet { + updateBottomContentView() + } + } + + private enum ViewState { + case unknown + case ringing + case active + case declined + case missed + case ended + case failed + } + + private static var callDurationFormatter: DateComponentsFormatter { + let formatter = DateComponentsFormatter() + formatter.zeroFormattingBehavior = .dropAll + formatter.allowedUnits = [.hour, .minute, .second] + formatter.unitsStyle = .abbreviated + return formatter + } + + private func updateBottomContentView() { + bottomContentView = bottomView(for: viewState) + } + + private var callTypeIcon: UIImage { + if isVideoCall { + return Asset.Images.callVideoIcon.image + } else { + return Asset.Images.voiceCallHangonIcon.image + } + } + + private var actionUserInfo: [AnyHashable: Any]? { + if let event = callInviteEvent { + return [kMXKRoomBubbleCellEventKey: event] + } + return nil + } + + private func bottomView(for state: ViewState) -> UIView? { + switch state { + case .unknown: + return nil + case .ringing: + let view = HorizontalButtonsContainerView.loadFromNib() + + view.firstButton.style = .negative + view.firstButton.setTitle(VectorL10n.eventFormatterCallDecline, for: .normal) + view.firstButton.setImage(Asset.Images.voiceCallHangupIcon.image, for: .normal) + view.firstButton.removeTarget(nil, action: nil, for: .touchUpInside) + view.firstButton.addTarget(self, action: #selector(declineCallAction(_:)), for: .touchUpInside) + + view.secondButton.style = .positive + view.secondButton.setTitle(VectorL10n.eventFormatterCallAnswer, for: .normal) + view.secondButton.setImage(callTypeIcon, for: .normal) + view.secondButton.removeTarget(nil, action: nil, for: .touchUpInside) + view.secondButton.addTarget(self, action: #selector(answerCallAction(_:)), for: .touchUpInside) + + return view + case .active: + let view = HorizontalButtonsContainerView.loadFromNib() + view.secondButton.isHidden = true + + view.firstButton.style = .negative + view.firstButton.setTitle(VectorL10n.eventFormatterCallEndCall, for: .normal) + view.firstButton.setImage(Asset.Images.voiceCallHangupIcon.image, for: .normal) + view.firstButton.removeTarget(nil, action: nil, for: .touchUpInside) + view.firstButton.addTarget(self, action: #selector(endCallAction(_:)), for: .touchUpInside) + + return view + case .declined: + let view = HorizontalButtonsContainerView.loadFromNib() + view.secondButton.isHidden = true + + view.firstButton.style = .positive + view.firstButton.setTitle(VectorL10n.eventFormatterCallBack, for: .normal) + view.firstButton.setImage(callTypeIcon, for: .normal) + view.firstButton.removeTarget(nil, action: nil, for: .touchUpInside) + view.firstButton.addTarget(self, action: #selector(callBackAction(_:)), for: .touchUpInside) + + return view + case .missed: + let view = HorizontalButtonsContainerView.loadFromNib() + view.secondButton.isHidden = true + + view.firstButton.style = .positive + view.firstButton.setTitle(VectorL10n.eventFormatterCallBack, for: .normal) + view.firstButton.setImage(callTypeIcon, for: .normal) + view.firstButton.removeTarget(nil, action: nil, for: .touchUpInside) + view.firstButton.addTarget(self, action: #selector(callBackAction(_:)), for: .touchUpInside) + + return view + case .ended: + return nil + case .failed: + let view = HorizontalButtonsContainerView.loadFromNib() + view.secondButton.isHidden = true + + view.firstButton.style = .positive + view.firstButton.setTitle(VectorL10n.eventFormatterCallRetry, for: .normal) + view.firstButton.setImage(callTypeIcon, for: .normal) + view.firstButton.removeTarget(nil, action: nil, for: .touchUpInside) + view.firstButton.addTarget(self, action: #selector(callBackAction(_:)), for: .touchUpInside) + + return view + } } private func configure(withCall call: MXCall) { switch call.state { - case .connected, - .fledgling, - .waitLocalMedia, - .createOffer, - .inviteSent, - .createAnswer, - .connecting, - .onHold, - .remotelyOnHold: - statusTextView.text = VectorL10n.eventFormatterCallYouCurrentlyIn + case .fledgling, + .waitLocalMedia, + .createOffer, + .connecting: + viewState = .active + if call.isIncoming { + statusText = VectorL10n.eventFormatterCallYouCurrentlyIn + } else { + statusText = VectorL10n.eventFormatterCallConnecting + } + case .inviteSent: + if call.isIncoming { + statusText = VectorL10n.eventFormatterCallYouCurrentlyIn + } else { + statusText = VectorL10n.eventFormatterCallRinging + } + case .createAnswer, + .connected, + .onHold, + .remotelyOnHold: + viewState = .active + statusText = VectorL10n.eventFormatterCallYouCurrentlyIn case .ringing: if call.isIncoming { - // should not be here - statusTextView.text = nil + viewState = .ringing + statusText = nil } else { - statusTextView.text = VectorL10n.eventFormatterCallYouCurrentlyIn + viewState = .active + statusText = VectorL10n.eventFormatterCallYouCurrentlyIn } case .ended: switch call.endReason { @@ -85,19 +204,29 @@ class RoomDirectCallStatusBubbleCell: RoomBaseCallBubbleCell { .hangup, .hangupElsewhere, .remoteHangup, - .missed, .answeredElseWhere: - statusTextView.text = VectorL10n.eventFormatterCallHasEnded + viewState = .ended + statusText = VectorL10n.eventFormatterCallHasEnded(callDurationString) + case .missed: + if call.isIncoming { + viewState = .missed + statusText = VectorL10n.eventFormatterCallYouMissed + } else { + statusText = VectorL10n.eventFormatterCallHasEnded(callDurationString) + } case .busy: configureForRejectedCall(call: call) @unknown default: - statusTextView.text = VectorL10n.eventFormatterCallHasEnded + viewState = .ended + statusText = VectorL10n.eventFormatterCallHasEnded(callDurationString) } case .inviteExpired, .answeredElseWhere: - statusTextView.text = VectorL10n.eventFormatterCallHasEnded + viewState = .ended + statusText = VectorL10n.eventFormatterCallHasEnded(callDurationString) @unknown default: - statusTextView.text = VectorL10n.eventFormatterCallHasEnded + viewState = .ended + statusText = VectorL10n.eventFormatterCallHasEnded(callDurationString) } } @@ -114,46 +243,135 @@ class RoomDirectCallStatusBubbleCell: RoomBaseCallBubbleCell { } if isMyReject { - - let centerParagraphStyle = NSMutableParagraphStyle() - centerParagraphStyle.alignment = .center - - let mutableAttrString = NSMutableAttributedString(string: VectorL10n.eventFormatterCallYouDeclined + " " + VectorL10n.eventFormatterCallBack, attributes: [ - .font: UIFont.systemFont(ofSize: Constants.statusTextFontSize), - .foregroundColor: ThemeService.shared().theme.noticeSecondaryColor, - .paragraphStyle: centerParagraphStyle - ]) - - let range = mutableAttrString.mutableString.range(of: VectorL10n.eventFormatterCallBack) - if range.location != NSNotFound { - mutableAttrString.addAttribute(.link, value: Constants.statusCallBackURL, range: range) - } - - statusTextView.attributedText = mutableAttrString - statusTextView.isSelectable = true + viewState = .declined + statusText = VectorL10n.eventFormatterCallYouDeclined } else { - statusTextView.text = VectorL10n.eventFormatterCallHasEnded + viewState = .ended + statusText = VectorL10n.eventFormatterCallHasEnded(callDurationString) } } + private func configureForHangupCall(withEvent event: MXEvent) { + guard let hangupEventContent = MXCallHangupEventContent(fromJSON: event.content) else { + viewState = .ended + statusText = VectorL10n.eventFormatterCallHasEnded(callDurationString) + return + } + + switch hangupEventContent.reasonType { + case .userHangup: + viewState = .ended + statusText = VectorL10n.eventFormatterCallHasEnded(callDurationString) + default: + viewState = .failed + statusText = VectorL10n.eventFormatterCallConnectionFailed + } + } + + private func configureForUnansweredCall() { + if isIncoming { + // missed call + viewState = .missed + statusText = VectorL10n.eventFormatterCallYouMissed + } else { + // outgoing unanswered call + viewState = .ended + statusText = VectorL10n.eventFormatterCallHasEnded(callDurationString) + } + } + + // MARK: - Actions + + @objc + private func callBackAction(_ sender: CallTileActionButton) { + self.delegate?.cell(self, + didRecognizeAction: Self.callBackAction, + userInfo: actionUserInfo) + } + + @objc + private func declineCallAction(_ sender: CallTileActionButton) { + self.delegate?.cell(self, + didRecognizeAction: Self.declineAction, + userInfo: actionUserInfo) + } + + @objc + private func answerCallAction(_ sender: CallTileActionButton) { + self.delegate?.cell(self, + didRecognizeAction: Self.answerAction, + userInfo: actionUserInfo) + } + + @objc + private func endCallAction(_ sender: CallTileActionButton) { + self.delegate?.cell(self, + didRecognizeAction: Self.endCallAction, + userInfo: actionUserInfo) + } + // MARK: - MXKCellRendering override func render(_ cellData: MXKCellData!) { super.render(cellData) + viewState = .unknown + guard let bubbleCellData = cellData as? RoomBubbleCellData else { return } let events = bubbleCellData.allLinkedEvents() - // getting a random event for call id is enough - guard let randomEvent = bubbleCellData.events.randomElement() else { + guard let inviteEvent = events.first(where: { $0.eventType == .callInvite }) else { return } - guard let callEventContent = MXCallEventContent(fromJSON: randomEvent.content) else { return } - let callId = callEventContent.callId + if bubbleCellData.senderId == bubbleCellData.mxSession.myUserId { + // event sent by my user, no means in displaying our own avatar and display name + if let directUserId = bubbleCellData.mxSession.directUserId(inRoom: bubbleCellData.roomId) { + let user = bubbleCellData.mxSession.user(withUserId: directUserId) + + let placeholder = AvatarGenerator.generateAvatar(forMatrixItem: directUserId, + withDisplayName: user?.displayname) + + innerContentView.avatarImageView.setImageURI(user?.avatarUrl, + withType: nil, + andImageOrientation: .up, + toFitViewSize: innerContentView.avatarImageView.frame.size, + with: MXThumbnailingMethodCrop, + previewImage: placeholder, + mediaManager: bubbleCellData.mxSession.mediaManager) + innerContentView.avatarImageView.defaultBackgroundColor = .clear + + innerContentView.callerNameLabel.text = user?.displayname + } + } else { + innerContentView.avatarImageView.setImageURI(bubbleCellData.senderAvatarUrl, + withType: nil, + andImageOrientation: .up, + toFitViewSize: innerContentView.avatarImageView.frame.size, + with: MXThumbnailingMethodCrop, + previewImage: bubbleCellData.senderAvatarPlaceholder, + mediaManager: bubbleCellData.mxSession.mediaManager) + innerContentView.avatarImageView.defaultBackgroundColor = .clear + + innerContentView.callerNameLabel.text = bubbleCellData.senderDisplayName + } + + guard let callInviteEventContent = MXCallInviteEventContent(fromJSON: inviteEvent.content) else { + return + } + isVideoCall = callInviteEventContent.isVideoCall() + callDurationString = readableCallDuration(from: events) + isIncoming = inviteEvent.sender != bubbleCellData.mxSession.myUserId + callInviteEvent = inviteEvent + innerContentView.callIconView.image = self.callTypeIcon + innerContentView.callTypeLabel.text = isVideoCall ? + VectorL10n.eventFormatterCallVideo : + VectorL10n.eventFormatterCallVoice + + let callId = callInviteEventContent.callId guard let call = bubbleCellData.mxSession.callManager.call(withCallId: callId) else { // check events include a reject event @@ -162,46 +380,50 @@ class RoomDirectCallStatusBubbleCell: RoomBaseCallBubbleCell { return } - // there is no reject event, we can just say this call has ended - statusTextView.text = VectorL10n.eventFormatterCallHasEnded + // check events include an answer event + if !events.contains(where: { $0.eventType == .callAnswer }) { + configureForUnansweredCall() + return + } + + // check events include a hangup event + if let hangupEvent = events.first(where: { $0.eventType == .callHangup }) { + configureForHangupCall(withEvent: hangupEvent) + return + } + + // there is no reject or hangup event, we can just say this call has ended + viewState = .ended + statusText = VectorL10n.eventFormatterCallHasEnded(callDurationString) return } configure(withCall: call) } - override func prepareForReuse() { - statusTextView.isSelectable = false - statusTextView.text = nil - statusTextView.attributedText = nil - - super.prepareForReuse() - } - -} - -// MARK: - UITextViewDelegate - -extension RoomDirectCallStatusBubbleCell { - - override func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { - if URL == Constants.statusCallBackURL && interaction == .invokeDefaultAction { - let userInfo: [AnyHashable: Any]? - - guard let bubbleCellData = bubbleData as? RoomBubbleCellData else { - return false - } - let events = bubbleCellData.allLinkedEvents() - if let callInviteEvent = events.first(where: { $0.eventType == .callInvite }) { - userInfo = [kMXKRoomBubbleCellEventKey: callInviteEvent] - } else { - userInfo = nil - } - - self.delegate?.cell(self, didRecognizeAction: kMXKRoomBubbleCellCallBackButtonPressed, userInfo: userInfo) - return true + private func callDuration(from events: [MXEvent]) -> TimeInterval { + guard let startDate = events.first(where: { $0.eventType == .callAnswer })?.originServerTs else { + // never started + return 0 } - return false + guard let endDate = events.first(where: { $0.eventType == .callHangup })?.originServerTs + ?? events.first(where: { $0.eventType == .callReject })?.originServerTs else { + // not ended yet, compute the diff from now + return (NSTimeIntervalSince1970 - TimeInterval(startDate))/MSEC_PER_SEC + } + + // ended, compute the diff between two dates + return TimeInterval(endDate - startDate)/MSEC_PER_SEC + } + + private func readableCallDuration(from events: [MXEvent]) -> String { + let duration = callDuration(from: events) + + if duration <= 0 { + return "" + } + + return RoomDirectCallStatusBubbleCell.callDurationFormatter.string(from: duration) ?? "" } } diff --git a/Riot/Modules/Room/Views/BubbleCells/Call/Group/RoomGroupCallStatusBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/Call/Group/RoomGroupCallStatusBubbleCell.swift new file mode 100644 index 000000000..15d7a9cd6 --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/Call/Group/RoomGroupCallStatusBubbleCell.swift @@ -0,0 +1,334 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit + +/// The number of milliseconds in one second. +private let MSEC_PER_SEC: TimeInterval = 1000 + +@objcMembers +class RoomGroupCallStatusBubbleCell: RoomBaseCallBubbleCell { + + private static var className: String { + return String(describing: self) + } + + /// Action identifier used when the user pressed "Join" button for an active call. + /// The `userInfo` dictionary contains an `MXEvent` object under the `kMXKRoomBubbleCellEventKey` key, representing the widget event of the call. + static var joinAction: String { + return self.className + ".join" + } + + /// Action identifier used when the user pressed "Leave" button for an active call. + /// The `userInfo` dictionary contains an `MXEvent` object under the `kMXKRoomBubbleCellEventKey` key, representing the widget event of the call. + static var leaveAction: String { + return self.className + ".leave" + } + + /// Action identifier used when the user pressed "Answer" button for an incoming call. + /// The `userInfo` dictionary contains an `MXEvent` object under the `kMXKRoomBubbleCellEventKey` key, representing the widget event of the call. + static var answerAction: String { + return self.className + ".answer" + } + + /// Action identifier used when the user pressed "Decline" button for an incoming call. + /// The `userInfo` dictionary contains an `MXEvent` object under the `kMXKRoomBubbleCellEventKey` key, representing the widget event of the call. + static var declineAction: String { + return self.className + ".decline" + } + + private var callDurationString: String = "" + private var isIncoming: Bool = false + private var widgetEvent: MXEvent! + private var widgetId: String! + private var viewState: ViewState = .unknown { + didSet { + updateBottomContentView() + } + } + + private enum Constants { + static let secondsToDisplayAnswerDeclineOptions: TimeInterval = 30 + } + + private enum ViewState { + case unknown + case ringing + case active + case declined + case ended + } + + private static var callDurationFormatter: DateComponentsFormatter { + let formatter = DateComponentsFormatter() + formatter.zeroFormattingBehavior = .dropAll + formatter.allowedUnits = [.hour, .minute, .second] + formatter.unitsStyle = .abbreviated + return formatter + } + + private func updateBottomContentView() { + bottomContentView = bottomView(for: viewState) + } + + private var callTypeIcon: UIImage { + // always return a video call icon + return Asset.Images.callVideoIcon.image + } + + private var isJoined: Bool { + return widgetId != nil && + AppDelegate.theDelegate().callPresenter.jitsiVC?.widget.widgetId == widgetId + } + + private var actionUserInfo: [AnyHashable: Any]? { + if let event = widgetEvent { + return [kMXKRoomBubbleCellEventKey: event] + } + return nil + } + + private func bottomView(for state: ViewState) -> UIView? { + switch state { + case .unknown: + return nil + case .ringing: + let view = HorizontalButtonsContainerView.loadFromNib() + + view.firstButton.style = .negative + view.firstButton.setTitle(VectorL10n.eventFormatterCallDecline, for: .normal) + view.firstButton.setImage(Asset.Images.voiceCallHangupIcon.image, for: .normal) + view.firstButton.removeTarget(nil, action: nil, for: .touchUpInside) + view.firstButton.addTarget(self, action: #selector(declineCallAction(_:)), for: .touchUpInside) + + view.secondButton.style = .positive + view.secondButton.setTitle(VectorL10n.eventFormatterCallAnswer, for: .normal) + view.secondButton.setImage(callTypeIcon, for: .normal) + view.secondButton.removeTarget(nil, action: nil, for: .touchUpInside) + view.secondButton.addTarget(self, action: #selector(answerCallAction(_:)), for: .touchUpInside) + + return view + case .active: + let view = HorizontalButtonsContainerView.loadFromNib() + view.secondButton.isHidden = true + + if isJoined { + // show a "Leave" button + view.firstButton.style = .negative + view.firstButton.setTitle(VectorL10n.eventFormatterGroupCallLeave, for: .normal) + view.firstButton.setImage(nil, for: .normal) + view.firstButton.removeTarget(nil, action: nil, for: .touchUpInside) + view.firstButton.addTarget(self, action: #selector(leaveAction(_:)), for: .touchUpInside) + } else { + // show a "Join" button + view.firstButton.style = .positive + view.firstButton.setTitle(VectorL10n.eventFormatterGroupCallJoin, for: .normal) + view.firstButton.setImage(callTypeIcon, for: .normal) + view.firstButton.removeTarget(nil, action: nil, for: .touchUpInside) + view.firstButton.addTarget(self, action: #selector(joinAction(_:)), for: .touchUpInside) + } + + return view + case .declined: + let view = HorizontalButtonsContainerView.loadFromNib() + view.secondButton.isHidden = true + + view.firstButton.style = .positive + view.firstButton.setTitle(VectorL10n.eventFormatterGroupCallJoin, for: .normal) + view.firstButton.setImage(callTypeIcon, for: .normal) + view.firstButton.removeTarget(nil, action: nil, for: .touchUpInside) + view.firstButton.addTarget(self, action: #selector(joinAction(_:)), for: .touchUpInside) + + return view + case .ended: + return nil + } + } + + // MARK: - Actions + + @objc + private func joinAction(_ sender: CallTileActionButton) { + self.delegate?.cell(self, + didRecognizeAction: Self.joinAction, + userInfo: actionUserInfo) + } + + @objc + private func leaveAction(_ sender: CallTileActionButton) { + self.delegate?.cell(self, + didRecognizeAction: Self.leaveAction, + userInfo: actionUserInfo) + } + + @objc + private func declineCallAction(_ sender: CallTileActionButton) { + self.delegate?.cell(self, + didRecognizeAction: Self.declineAction, + userInfo: actionUserInfo) + } + + @objc + private func answerCallAction(_ sender: CallTileActionButton) { + self.delegate?.cell(self, + didRecognizeAction: Self.answerAction, + userInfo: actionUserInfo) + } + + // MARK: - MXKCellRendering + + override func render(_ cellData: MXKCellData!) { + super.render(cellData) + + viewState = .unknown + + guard let bubbleCellData = cellData as? RoomBubbleCellData else { + return + } + + let events = bubbleCellData.allLinkedEvents() + + NSLog("[RoomGroupCallStatusBubbleCell] render: \(events.count) events: \(events)") + + guard let widgetEvent = events + .first(where: { + $0.eventType == .custom && + ($0.type == kWidgetMatrixEventTypeString || $0.type == kWidgetModularEventTypeString) + }) else { + return + } + + guard let widgetId = widgetEvent.stateKey else { + return + } + + guard let room = bubbleCellData.mxSession.room(withRoomId: widgetEvent.roomId) else { + return + } + + callDurationString = readableCallDuration(from: widgetEvent, endEvent: nil) + isIncoming = widgetEvent.sender != bubbleCellData.mxSession.myUserId + self.widgetEvent = widgetEvent + self.widgetId = widgetId + innerContentView.callIconView.image = Asset.Images.callVideoIcon.image + innerContentView.callTypeLabel.text = VectorL10n.eventFormatterCallVideo + + if isIncoming && !isJoined && + TimeInterval(widgetEvent.age)/MSEC_PER_SEC < Constants.secondsToDisplayAnswerDeclineOptions { + + if JitsiService.shared.isWidgetDeclined(withId: widgetId) { + innerContentView.callerNameLabel.text = room.summary.displayname + room.summary.setRoomAvatarImageIn(innerContentView.avatarImageView) + + viewState = .declined + statusText = VectorL10n.eventFormatterCallYouDeclined + } else { + innerContentView.callerNameLabel.text = VectorL10n.eventFormatterGroupCallIncoming(bubbleCellData.senderDisplayName, room.summary.displayname) + + innerContentView.avatarImageView.setImageURI(bubbleCellData.senderAvatarUrl, + withType: nil, + andImageOrientation: .up, + toFitViewSize: innerContentView.avatarImageView.frame.size, + with: MXThumbnailingMethodCrop, + previewImage: bubbleCellData.senderAvatarPlaceholder, + mediaManager: bubbleCellData.mxSession.mediaManager) + + viewState = .ringing + statusText = nil + } + } else { + innerContentView.callerNameLabel.text = room.summary.displayname + + room.summary.setRoomAvatarImageIn(innerContentView.avatarImageView) + } + + innerContentView.avatarImageView.defaultBackgroundColor = .clear + + room.state { [weak self] (roomState) in + guard let self = self else { return } + guard let widgets = WidgetManager.shared()?.widgets(ofTypes: [ + kWidgetTypeJitsiV1, + kWidgetTypeJitsiV2 + ], + in: room, + with: roomState) else { + self.viewState = .ended + self.statusText = VectorL10n.eventFormatterCallHasEnded(self.callDurationString) + return + } + + let removeWidgetEvent = roomState?.stateEvents + .filter({ $0.stateKey == widgetId }) + .first(where: { $0.content.isEmpty }) + self.callDurationString = self.readableCallDuration(from: widgetEvent, + endEvent: removeWidgetEvent) + + guard let widget = widgets.first(where: { $0.widgetId == widgetId }) else { + self.viewState = .ended + self.statusText = VectorL10n.eventFormatterCallHasEnded(self.callDurationString) + return + } + + if widget.isActive { + if !self.isIncoming { + self.viewState = .active + self.statusText = VectorL10n.eventFormatterCallYouCurrentlyIn + } else if !self.isJoined && + TimeInterval(widgetEvent.age)/MSEC_PER_SEC < Constants.secondsToDisplayAnswerDeclineOptions { + + if JitsiService.shared.isWidgetDeclined(withId: widgetId) { + self.viewState = .declined + self.statusText = VectorL10n.eventFormatterCallYouDeclined + } else { + self.viewState = .ringing + self.statusText = nil + } + } else { + self.viewState = .active + self.statusText = VectorL10n.eventFormatterCallYouCurrentlyIn + } + } else { + self.viewState = .ended + self.statusText = VectorL10n.eventFormatterCallHasEnded(self.callDurationString) + } + } + } + + private func callDuration(from startEvent: MXEvent?, endEvent: MXEvent?) -> TimeInterval { + guard let startDate = startEvent?.originServerTs else { + // never started + return 0 + } + guard let endDate = endEvent?.originServerTs else { + // not ended yet, compute the diff from now + return (NSTimeIntervalSince1970 - TimeInterval(startDate))/MSEC_PER_SEC + } + + // ended, compute the diff between two dates + return TimeInterval(max(0, Double(endDate) - Double(startDate)))/MSEC_PER_SEC + } + + private func readableCallDuration(from startEvent: MXEvent?, endEvent: MXEvent?) -> String { + let duration = callDuration(from: startEvent, endEvent: endEvent) + + if duration <= 0 { + return "" + } + + return RoomGroupCallStatusBubbleCell.callDurationFormatter.string(from: duration) ?? "" + } + +} diff --git a/Riot/Modules/Room/Views/BubbleCells/Call/RoomBaseCallBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/Call/RoomBaseCallBubbleCell.swift index fed709feb..d760d83bd 100644 --- a/Riot/Modules/Room/Views/BubbleCells/Call/RoomBaseCallBubbleCell.swift +++ b/Riot/Modules/Room/Views/BubbleCells/Call/RoomBaseCallBubbleCell.swift @@ -19,7 +19,7 @@ import Reusable class RoomBaseCallBubbleCell: MXKRoomBubbleTableViewCell { - fileprivate lazy var innerContentView: CallBubbleCellBaseContentView = { + lazy var innerContentView: CallBubbleCellBaseContentView = { return CallBubbleCellBaseContentView.loadFromNib() }() @@ -36,16 +36,29 @@ class RoomBaseCallBubbleCell: MXKRoomBubbleTableViewCell { override func setupViews() { super.setupViews() - self.contentView.vc_removeAllSubviews() self.contentView.vc_addSubViewMatchingParent(innerContentView) - - updateBottomContentView() } - // Properties to override - private(set) var bottomContentView: UIView? + // Bottom content view. Will be spanned in bottomContainerView + var bottomContentView: UIView? { + didSet { + updateBottomContentView() + } + } - func updateBottomContentView() { + var statusText: String? { + get { + return innerContentView.statusText + } set { + innerContentView.statusText = newValue + } + } + + private func updateBottomContentView() { + defer { + innerContentView.relayoutCallSummary() + } + innerContentView.bottomContainerView.vc_removeAllSubviews() guard let bottomContentView = bottomContentView else { return } @@ -58,15 +71,37 @@ class RoomBaseCallBubbleCell: MXKRoomBubbleTableViewCell { // MARK: - Overrides + override var bubbleInfoContainer: UIView! { + get { + guard let infoContainer = innerContentView.bubbleInfoContainer else { + fatalError("[RoomBaseCallBubbleCell] bubbleInfoContainer should not be used before set") + } + return infoContainer + } set { + super.bubbleInfoContainer = newValue + } + } + override var bubbleOverlayContainer: UIView! { get { guard let overlayContainer = innerContentView.bubbleOverlayContainer else { fatalError("[RoomBaseCallBubbleCell] bubbleOverlayContainer should not be used before set") } return overlayContainer + } set { + super.bubbleOverlayContainer = newValue + } + } + + override var bubbleInfoContainerTopConstraint: NSLayoutConstraint! { + get { + guard let infoContainerTopConstraint = innerContentView.bubbleInfoContainerTopConstraint else { + fatalError("[RoomBaseCallBubbleCell] bubbleInfoContainerTopConstraint should not be used before set") + } + return infoContainerTopConstraint } set { - super.bubbleOverlayContainer = newValue + super.bubbleInfoContainerTopConstraint = newValue } } @@ -95,7 +130,28 @@ class RoomBaseCallBubbleCell: MXKRoomBubbleTableViewCell { } cell.render(cellData) - return cell.contentView.systemLayoutSizeFitting(fittingSize).height + // we need to add suitable height manually for read receipts view, as adding of them is not handled in the render method + var readReceiptsHeight: CGFloat = 0 + if let bubbleCellData = cellData as? RoomBubbleCellData, + bubbleCellData.showBubbleReceipts, + bubbleCellData.readReceipts.count > 0 { + readReceiptsHeight = cell.innerContentView.readReceiptsContainerView.systemLayoutSizeFitting(fittingSize).height + + cell.innerContentView.interItemSpacing + } + + return cell.contentView.systemLayoutSizeFitting(fittingSize).height + readReceiptsHeight + } + +} + +extension RoomBaseCallBubbleCell: BubbleCellReadReceiptsDisplayable { + + func addReadReceiptsView(_ readReceiptsView: UIView) { + innerContentView.addReadReceiptsView(readReceiptsView) + } + + func removeReadReceiptsView() { + innerContentView.removeReadReceiptsView() } } @@ -104,10 +160,13 @@ extension RoomBaseCallBubbleCell: Themable { func update(theme: Theme) { innerContentView.update(theme: theme) + if let themable = bottomContentView as? Themable { + themable.update(theme: theme) + } } } -extension RoomBaseCallBubbleCell: NibLoadable, Reusable { +extension RoomBaseCallBubbleCell: NibReusable { } diff --git a/Riot/Modules/Room/Views/BubbleCells/Call/RoomBaseCallBubbleCell.xib b/Riot/Modules/Room/Views/BubbleCells/Call/RoomBaseCallBubbleCell.xib index b7b585fca..052188b08 100644 --- a/Riot/Modules/Room/Views/BubbleCells/Call/RoomBaseCallBubbleCell.xib +++ b/Riot/Modules/Room/Views/BubbleCells/Call/RoomBaseCallBubbleCell.xib @@ -1,19 +1,18 @@ - + - + - + - - + diff --git a/Riot/Modules/Room/Views/RemoveJitsiWidget/ArrowsAnimationView.swift b/Riot/Modules/Room/Views/RemoveJitsiWidget/ArrowsAnimationView.swift new file mode 100644 index 000000000..1c4c0c9d2 --- /dev/null +++ b/Riot/Modules/Room/Views/RemoveJitsiWidget/ArrowsAnimationView.swift @@ -0,0 +1,114 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +class ArrowsAnimationView: UIView { + + private enum Constants { + static let numberOfArrows: Int = 3 + static let arrowSize: CGSize = CGSize(width: 14, height: 14) + static let gradientAnimationKey: String = "gradient" + static let gradientRatios: [CGFloat] = [1.0, 0.3, 0.2] + } + + private var gradientLayer: CAGradientLayer! + private lazy var gradientAnimation: CABasicAnimation = { + let animation = CABasicAnimation(keyPath: "locations") + animation.fromValue = [0.0, 0.0, 0.25] + animation.toValue = [0.75, 1.0, 1.0] + animation.repeatCount = .infinity + animation.duration = 1 + return animation + }() + private var theme: Theme = ThemeService.shared().theme + private var arrowImageViews: [UIImageView] = [] + + override init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setup() + } + + private func setup() { + let arrowImage = Asset.Images.disclosureIcon.image + for i in 0.. Bool { + switch (lhs, rhs) { + case (.notStarted, .notStarted): + return true + case (let .sliding(percentage1), let .sliding(percentage2)): + return percentage1 == percentage2 + case (.completed, .completed): + return true + default: + return false + } + } + } + + @IBOutlet private weak var slidingViewLeadingConstraint: NSLayoutConstraint! + @IBOutlet private weak var slidingView: UIView! + @IBOutlet private weak var slidingViewLabel: UILabel! { + didSet { + slidingViewLabel.text = VectorL10n.roomSlideToEndGroupCall + } + } + @IBOutlet private weak var arrowsView: ArrowsAnimationView! + @IBOutlet private weak var hangupView: UIView! + @IBOutlet private weak var hangupImage: UIImageView! + @IBOutlet private weak var topSeparatorView: UIView! + @IBOutlet private weak var bottomSeparatorView: UIView! + + private var state: State = .notStarted + private var theme: Theme = ThemeService.shared().theme + + // MARK - Private + + private func configure(withState state: State) { + switch state { + case .notStarted: + arrowsView.isAnimating = false + hangupView.backgroundColor = .clear + hangupImage.tintColor = theme.noticeColor + slidingViewLeadingConstraint.constant = 0 + case .sliding(let percentage): + arrowsView.isAnimating = true + if percentage < Constants.activationThreshold { + hangupView.backgroundColor = .clear + hangupImage.tintColor = theme.noticeColor + } else { + hangupView.backgroundColor = theme.noticeColor + hangupImage.tintColor = theme.callScreenButtonTintColor + } + slidingViewLeadingConstraint.constant = percentage * slidingView.frame.width + case .completed: + arrowsView.isAnimating = false + hangupView.backgroundColor = theme.noticeColor + hangupImage.tintColor = theme.callScreenButtonTintColor + } + } + + private func updateState(to newState: State) { + guard newState != state else { + return + } + configure(withState: newState) + state = newState + + if state == .completed { + delegate?.removeJitsiWidgetViewDidCompleteSliding(self) + } + } + + // MARK: - Touch Handling + + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + super.touchesBegan(touches, with: event) + + if state == .notStarted { + updateState(to: .sliding(percentage: 0)) + } + } + + override func touchesMoved(_ touches: Set, with event: UIEvent?) { + super.touchesMoved(touches, with: event) + + switch state { + case .sliding: + if let touch = touches.first { + let touchLocation = touch.location(in: self) + if frame.contains(touchLocation) { + let percentage = touchLocation.x / frame.width + updateState(to: .sliding(percentage: percentage)) + } + } + default: + break + } + } + + override func touchesEnded(_ touches: Set, with event: UIEvent?) { + super.touchesEnded(touches, with: event) + + switch state { + case .sliding(let percentage): + if percentage < Constants.activationThreshold { + updateState(to: .notStarted) + } else { + updateState(to: .completed) + } + default: + break + } + } + + override func touchesCancelled(_ touches: Set, with event: UIEvent?) { + super.touchesCancelled(touches, with: event) + + switch state { + case .sliding(let percentage): + if percentage < Constants.activationThreshold { + updateState(to: .notStarted) + } else { + updateState(to: .completed) + } + default: + break + } + } + + // MARK: - API + + weak var delegate: RemoveJitsiWidgetViewDelegate? + + static func instantiate() -> RemoveJitsiWidgetView { + let view = RemoveJitsiWidgetView.loadFromNib() + view.update(theme: ThemeService.shared().theme) + return view + } + + func reset() { + updateState(to: .notStarted) + } + +} + +// MARK: - NibLoadable + +extension RemoveJitsiWidgetView: NibLoadable { } + +// MARK: - Themable + +extension RemoveJitsiWidgetView: Themable { + + func update(theme: Theme) { + self.theme = theme + + self.backgroundColor = theme.headerBackgroundColor + + slidingViewLabel.textColor = theme.textPrimaryColor + arrowsView.update(theme: theme) + topSeparatorView.backgroundColor = theme.lineBreakColor + bottomSeparatorView.backgroundColor = theme.lineBreakColor + + configure(withState: state) + } + +} diff --git a/Riot/Modules/Room/Views/RemoveJitsiWidget/RemoveJitsiWidgetView.xib b/Riot/Modules/Room/Views/RemoveJitsiWidget/RemoveJitsiWidgetView.xib new file mode 100644 index 000000000..6d2d62b0b --- /dev/null +++ b/Riot/Modules/Room/Views/RemoveJitsiWidget/RemoveJitsiWidgetView.xib @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/Room/Views/RemoveJitsiWidget/RemoveJitsiWidgetViewDelegate.swift b/Riot/Modules/Room/Views/RemoveJitsiWidget/RemoveJitsiWidgetViewDelegate.swift new file mode 100644 index 000000000..f48af79a1 --- /dev/null +++ b/Riot/Modules/Room/Views/RemoveJitsiWidget/RemoveJitsiWidgetViewDelegate.swift @@ -0,0 +1,25 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +@objc +protocol RemoveJitsiWidgetViewDelegate: class { + + /// Tells the delegate that the user complete sliding on the view + /// - Parameter view: The view instance + func removeJitsiWidgetViewDidCompleteSliding(_ view: RemoveJitsiWidgetView) +} diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 12adb0364..0ae7fb7c2 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -142,7 +142,7 @@ enum enum { - LABS_USE_JITSI_WIDGET_INDEX = 0, + LABS_ENABLE_RINGING_FOR_GROUP_CALLS_INDEX = 0 }; enum @@ -350,7 +350,10 @@ TableViewSectionsDelegate> Section *sectionNotificationSettings = [Section sectionWithTag:SECTION_TAG_NOTIFICATIONS]; [sectionNotificationSettings addRowWithTag:NOTIFICATION_SETTINGS_ENABLE_PUSH_INDEX]; - [sectionNotificationSettings addRowWithTag:NOTIFICATION_SETTINGS_SHOW_DECODED_CONTENT]; + if (RiotSettings.shared.settingsScreenShowNotificationDecodedContentOption) + { + [sectionNotificationSettings addRowWithTag:NOTIFICATION_SETTINGS_SHOW_DECODED_CONTENT]; + } [sectionNotificationSettings addRowWithTag:NOTIFICATION_SETTINGS_GLOBAL_SETTINGS_INDEX]; [sectionNotificationSettings addRowWithTag:NOTIFICATION_SETTINGS_PIN_MISSED_NOTIFICATIONS_INDEX]; [sectionNotificationSettings addRowWithTag:NOTIFICATION_SETTINGS_PIN_UNREAD_INDEX]; @@ -458,7 +461,10 @@ TableViewSectionsDelegate> [sectionOther addRowWithTag:OTHER_PRIVACY_INDEX]; } [sectionOther addRowWithTag:OTHER_THIRD_PARTY_INDEX]; - [sectionOther addRowWithTag:OTHER_SHOW_NSFW_ROOMS_INDEX]; + if (RiotSettings.shared.settingsScreenShowNsfwRoomsOption) + { + [sectionOther addRowWithTag:OTHER_SHOW_NSFW_ROOMS_INDEX]; + } if (BuildSettings.settingsScreenAllowChangingCrashUsageDataSettings) { @@ -480,9 +486,12 @@ TableViewSectionsDelegate> if (BuildSettings.settingsScreenShowLabSettings) { Section *sectionLabs = [Section sectionWithTag:SECTION_TAG_LABS]; - [sectionLabs addRowWithTag:LABS_USE_JITSI_WIDGET_INDEX]; + [sectionLabs addRowWithTag:LABS_ENABLE_RINGING_FOR_GROUP_CALLS_INDEX]; sectionLabs.headerTitle = NSLocalizedStringFromTable(@"settings_labs", @"Vector", nil); - [tmpSections addObject:sectionLabs]; + if (sectionLabs.hasAnyRows) + { + [tmpSections addObject:sectionLabs]; + } } if ([groupsDataSource numberOfSectionsInTableView:self.tableView] && groupsDataSource.joinedGroupsSection != -1) @@ -2244,16 +2253,16 @@ TableViewSectionsDelegate> } else if (section == SECTION_TAG_LABS) { - if (row == LABS_USE_JITSI_WIDGET_INDEX) + if (row == LABS_ENABLE_RINGING_FOR_GROUP_CALLS_INDEX) { - MXKTableViewCellWithLabelAndSwitch* labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; - - labelAndSwitchCell.mxkLabel.text = NSLocalizedStringFromTable(@"settings_labs_create_conference_with_jitsi", @"Vector", nil); - labelAndSwitchCell.mxkSwitch.on = RiotSettings.shared.createConferenceCallsWithJitsi; - labelAndSwitchCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor; - - [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleJitsiForConference:) forControlEvents:UIControlEventTouchUpInside]; - + MXKTableViewCellWithLabelAndSwitch *labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; + + labelAndSwitchCell.mxkLabel.text = NSLocalizedStringFromTable(@"settings_labs_enable_ringing_for_group_calls", @"Vector", nil); + labelAndSwitchCell.mxkSwitch.on = RiotSettings.shared.enableRingingForGroupCalls; + labelAndSwitchCell.mxkSwitch.tintColor = ThemeService.shared.theme.tintColor; + + [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleEnableRingingForGroupCalls:) forControlEvents:UIControlEventValueChanged]; + cell = labelAndSwitchCell; } } @@ -2944,14 +2953,12 @@ TableViewSectionsDelegate> } } -- (void)toggleJitsiForConference:(id)sender +- (void)toggleEnableRingingForGroupCalls:(UISwitch *)sender { - if (sender && [sender isKindOfClass:UISwitch.class]) + if (sender) { - UISwitch *switchButton = (UISwitch*)sender; + RiotSettings.shared.enableRingingForGroupCalls = sender.isOn; - RiotSettings.shared.createConferenceCallsWithJitsi = switchButton.isOn; - [self.tableView reloadData]; } } diff --git a/Riot/Modules/SplitView/SplitViewCoordinator.swift b/Riot/Modules/SplitView/SplitViewCoordinator.swift index 3f78605f7..59e8b6a2a 100644 --- a/Riot/Modules/SplitView/SplitViewCoordinator.swift +++ b/Riot/Modules/SplitView/SplitViewCoordinator.swift @@ -46,7 +46,7 @@ final class SplitViewCoordinator: NSObject, SplitViewCoordinatorType { self.rootRouter = router self.session = session - let splitViewController = UISplitViewController() + let splitViewController = RiotSplitViewController() splitViewController.preferredDisplayMode = .allVisible self.splitViewController = splitViewController } diff --git a/Riot/Modules/TabBar/MasterTabBarController.m b/Riot/Modules/TabBar/MasterTabBarController.m index dd7a3e9f2..81583532d 100644 --- a/Riot/Modules/TabBar/MasterTabBarController.m +++ b/Riot/Modules/TabBar/MasterTabBarController.m @@ -64,6 +64,9 @@ // The groups data source GroupsDataSource *groupsDataSource; + + // All tabs deinfed in the storyboard + NSArray *initalTabs; } @property(nonatomic,getter=isHidden) BOOL hidden; @@ -131,7 +134,7 @@ }]; [self userInterfaceThemeDidChange]; - [self updateTabs]; + initalTabs = [NSArray arrayWithArray:self.viewControllers]; } - (void)userInterfaceThemeDidChange @@ -161,6 +164,8 @@ // Show the tab bar view controller content only when a user is logged in. self.hidden = ([MXKAccountManager sharedManager].accounts.count == 0); + + [self updateTabs]; } - (void)viewDidAppear:(BOOL)animated @@ -884,13 +889,7 @@ - (void)updateTabs { - if (RiotSettings.shared.homeScreenShowCommunitiesTab && RiotSettings.shared.homeScreenShowRoomsTab - && RiotSettings.shared.homeScreenShowPeopleTab && RiotSettings.shared.homeScreenShowFavouritesTab) - { - return; - } - - NSMutableArray *newTabs = [NSMutableArray arrayWithArray:self.viewControllers]; + NSMutableArray *newTabs = [NSMutableArray arrayWithArray:initalTabs]; if (!RiotSettings.shared.homeScreenShowCommunitiesTab) { [newTabs removeObjectAtIndex:TABBAR_GROUPS_INDEX]; diff --git a/Riot/SupportingFiles/Riot-Bridging-Header.h b/Riot/SupportingFiles/Riot-Bridging-Header.h index 7daa40759..2a6d6a754 100644 --- a/Riot/SupportingFiles/Riot-Bridging-Header.h +++ b/Riot/SupportingFiles/Riot-Bridging-Header.h @@ -6,6 +6,7 @@ @import MatrixKit; #import "WebViewViewController.h" +#import "RiotSplitViewController.h" #import "RiotNavigationController.h" #import "ThemeService.h" #import "TableViewCellWithCheckBoxAndLabel.h" diff --git a/Riot/Utils/Constants.swift b/Riot/Utils/Constants.swift index 886615072..fcf886256 100644 --- a/Riot/Utils/Constants.swift +++ b/Riot/Utils/Constants.swift @@ -16,9 +16,16 @@ import Foundation -enum Constants { +@objcMembers +class Constants: NSObject { - static let toBeRemovedNotificationCategoryIdentifier = "TO_BE_REMOVED" - static let callInviteNotificationCategoryIdentifier = "CALL_INVITE" + static let toBeRemovedNotificationCategoryIdentifier: String = "TO_BE_REMOVED" + static let callInviteNotificationCategoryIdentifier: String = "CALL_INVITE" + + /// Notification userInfo key to present a notification when the app is on foreground. Value should be set as a Bool for this key. + static let userInfoKeyPresentNotificationOnForeground: String = "ALWAYS_PRESENT_NOTIFICATION" + + /// Notification userInfo key to present a notification even if the app is on foreground and in the notification's room screen. Value should be set as a Bool for this key. + static let userInfoKeyPresentNotificationInRoom: String = "IN_ROOM_PRESENT_NOTIFICATION" } diff --git a/Riot/Utils/EventFormatter.m b/Riot/Utils/EventFormatter.m index 532d26162..f0038508c 100644 --- a/Riot/Utils/EventFormatter.m +++ b/Riot/Utils/EventFormatter.m @@ -127,14 +127,7 @@ static NSString *const kEventFormatterTimeFormat = @"HH:mm"; || [activeWidget.type isEqualToString:kWidgetTypeJitsiV2]) { // This was a jitsi widget - if (isEventSenderMyUser) - { - displayText = NSLocalizedStringFromTable(@"event_formatter_jitsi_widget_removed_by_you", @"Vector", nil); - } - else - { - displayText = [NSString stringWithFormat:NSLocalizedStringFromTable(@"event_formatter_jitsi_widget_removed", @"Vector", nil), senderDisplayName]; - } + return nil; } else { @@ -191,9 +184,7 @@ static NSString *const kEventFormatterTimeFormat = @"HH:mm"; } break; case MXEventTypeCallCandidates: - case MXEventTypeCallAnswer: case MXEventTypeCallSelectAnswer: - case MXEventTypeCallHangup: case MXEventTypeCallNegotiate: case MXEventTypeCallReplaces: case MXEventTypeCallRejectReplacement: diff --git a/Riot/target.yml b/Riot/target.yml index ccd717311..7a0d46b75 100644 --- a/Riot/target.yml +++ b/Riot/target.yml @@ -34,6 +34,7 @@ targets: - target: RiotShareExtension - target: SiriIntents - target: RiotNSE + - target: DesignKit configFiles: Debug: Debug.xcconfig diff --git a/RiotNSE/Common.xcconfig b/RiotNSE/Common.xcconfig index ee4f8b558..7479a1ec8 100644 --- a/RiotNSE/Common.xcconfig +++ b/RiotNSE/Common.xcconfig @@ -27,3 +27,4 @@ INFOPLIST_FILE = RiotNSE/Info.plist CODE_SIGN_ENTITLEMENTS = RiotNSE/RiotNSE.entitlements SKIP_INSTALL = YES +SWIFT_OBJC_BRIDGING_HEADER = $(SRCROOT)/$(PRODUCT_NAME)/SupportingFiles/RiotNSE-Bridging-Header.h diff --git a/RiotNSE/NotificationService.swift b/RiotNSE/NotificationService.swift index a7def37e1..ec2230cb9 100644 --- a/RiotNSE/NotificationService.swift +++ b/RiotNSE/NotificationService.swift @@ -269,6 +269,7 @@ class NotificationService: UNNotificationServiceExtension { case .success(let (roomState, eventSenderName)): var notificationTitle: String? var notificationBody: String? + var additionalUserInfo: [AnyHashable: Any]? var threadIdentifier: String? = roomId let currentUserId = account.mxCredentials.userId @@ -372,6 +373,22 @@ class NotificationService: UNNotificationServiceExtension { } notificationBody = NSString.localizedUserNotificationString(forKey: "STICKER_FROM_USER", arguments: [eventSenderName as Any]) + case .custom: + if (event.type == kWidgetMatrixEventTypeString || event.type == kWidgetModularEventTypeString), + let type = event.content?["type"] as? String, + (type == kWidgetTypeJitsiV1 || type == kWidgetTypeJitsiV2) { + notificationBody = NSString.localizedUserNotificationString(forKey: "GROUP_CALL_STARTED", arguments: nil) + notificationTitle = roomDisplayName + + // call notifications should stand out from normal messages, so we don't stack them + threadIdentifier = nil + // only send VoIP pushes if ringing is enabled for group calls + if RiotSettings.shared.enableRingingForGroupCalls { + self.sendVoipPush(forEvent: event) + } else { + additionalUserInfo = [Constants.userInfoKeyPresentNotificationOnForeground: true] + } + } default: break } @@ -393,7 +410,8 @@ class NotificationService: UNNotificationServiceExtension { threadIdentifier: threadIdentifier, userId: currentUserId, event: event, - pushRule: pushRule) + pushRule: pushRule, + additionalInfo: additionalUserInfo) NSLog("[NotificationService] notificationContentForEvent: Calling onComplete.") onComplete(notificationContent) @@ -451,7 +469,8 @@ class NotificationService: UNNotificationServiceExtension { threadIdentifier: String?, userId: String?, event: MXEvent, - pushRule: MXPushRule?) -> UNNotificationContent { + pushRule: MXPushRule?, + additionalInfo: [AnyHashable: Any]? = nil) -> UNNotificationContent { let notificationContent = UNMutableNotificationContent() if let title = title { @@ -469,12 +488,16 @@ class NotificationService: UNNotificationServiceExtension { if let soundName = notificationSoundName(fromPushRule: pushRule) { notificationContent.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: soundName)) } - notificationContent.userInfo = notificationUserInfo(forEvent: event, andUserId: userId) + notificationContent.userInfo = notificationUserInfo(forEvent: event, + andUserId: userId, + additionalInfo: additionalInfo) return notificationContent } - private func notificationUserInfo(forEvent event: MXEvent, andUserId userId: String?) -> [AnyHashable: Any] { + private func notificationUserInfo(forEvent event: MXEvent, + andUserId userId: String?, + additionalInfo: [AnyHashable: Any]? = nil) -> [AnyHashable: Any] { var notificationUserInfo: [AnyHashable: Any] = [ "type": "full", "room_id": event.roomId as Any, @@ -483,6 +506,11 @@ class NotificationService: UNNotificationServiceExtension { if let userId = userId { notificationUserInfo["user_id"] = userId } + if let additionalInfo = additionalInfo { + for (key, value) in additionalInfo { + notificationUserInfo[key] = value + } + } return notificationUserInfo } diff --git a/RiotNSE/SupportingFiles/RiotNSE-Bridging-Header.h b/RiotNSE/SupportingFiles/RiotNSE-Bridging-Header.h new file mode 100644 index 000000000..aa38f8734 --- /dev/null +++ b/RiotNSE/SupportingFiles/RiotNSE-Bridging-Header.h @@ -0,0 +1,22 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#ifndef RiotNSE_Bridging_Header_h +#define RiotNSE_Bridging_Header_h + +#import "WidgetConstants.h" + +#endif /* RiotNSE_Bridging_Header_h */ diff --git a/RiotNSE/target.yml b/RiotNSE/target.yml index de68fdcd7..edfbb540a 100644 --- a/RiotNSE/target.yml +++ b/RiotNSE/target.yml @@ -58,3 +58,4 @@ targets: - path: ../Riot/Utils/Constants.swift - path: ../Riot/Categories/String.swift - path: ../Riot/Categories/Character.swift + - path: ../Riot/Managers/Widgets/WidgetConstants.m diff --git a/RiotTests/HomeserverConfigurationTests.swift b/RiotTests/HomeserverConfigurationTests.swift new file mode 100644 index 000000000..d13ef9211 --- /dev/null +++ b/RiotTests/HomeserverConfigurationTests.swift @@ -0,0 +1,71 @@ +// +// Copyright 2020 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +@testable import Riot + +class HomeserverConfigurationTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + // MARK: - Tests + + func testHomeserverConfigurationBuilder() { + + let expectedJitsiServer = "your.jitsi.example.org" + let expectedJitsiServerStringURL = "https://\(expectedJitsiServer)" + let expectedDeprecatedJitsiServer = "your.deprecated.jitsi.example.org" + let expectedE2EEEByDefaultEnabled = true + let expectedDeprecatedE2EEEByDefaultEnabled = false + + let wellKnownDictionary: [String: Any] = [ + "m.homeserver": [ + "base_url": "https://your.homeserver.org" + ], + "m.identity_server": [ + "base_url": "https://your.identity-server.org" + ], + "im.vector.riot.e2ee" : [ + "default" : expectedDeprecatedE2EEEByDefaultEnabled + ], + "im.vector.riot.jitsi" : [ + "preferredDomain" : expectedDeprecatedJitsiServer + ], + "io.element.e2ee" : [ + "default" : expectedE2EEEByDefaultEnabled + ], + "io.element.jitsi" : [ + "preferredDomain" : expectedJitsiServer + ] + ] + + let wellKnown = MXWellKnown(fromJSON: wellKnownDictionary) + + let homeserverConfigurationBuilder = HomeserverConfigurationBuilder() + let homeserverConfiguration = homeserverConfigurationBuilder.build(from: wellKnown) + + XCTAssertEqual(homeserverConfiguration.jitsi.serverDomain, expectedJitsiServer) + XCTAssertEqual(homeserverConfiguration.jitsi.serverURL.absoluteString, expectedJitsiServerStringURL) + XCTAssertEqual(homeserverConfiguration.isE2EEByDefaultEnabled, expectedE2EEEByDefaultEnabled) + } +} diff --git a/RiotTests/VectorWellKnownTests.swift b/RiotTests/VectorWellKnownTests.swift new file mode 100644 index 000000000..d27ebf020 --- /dev/null +++ b/RiotTests/VectorWellKnownTests.swift @@ -0,0 +1,104 @@ +// +// Copyright 2020 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +@testable import Riot + +class VectorWellKnownTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + // MARK: - Tests + + func testVectorWellKnownParsing() { + + let expectedJitsiServer = "your.jitsi.example.org" + let expectedE2EEEByDefaultEnabled = false + + let wellKnownDictionary: [String: Any] = [ + "im.vector.riot.e2ee" : [ + "default" : expectedE2EEEByDefaultEnabled + ], + "im.vector.riot.jitsi" : [ + "preferredDomain" : expectedJitsiServer + ], + "io.element.e2ee" : [ + "default" : expectedE2EEEByDefaultEnabled + ], + "io.element.jitsi" : [ + "preferredDomain" : expectedJitsiServer + ] + ] + + let serializationService = SerializationService() + + do { + let vectorWellKnown: VectorWellKnown = try serializationService.deserialize(wellKnownDictionary) + + let jistiConfiguration = vectorWellKnown.jitsi + let encryptionConfiguration = vectorWellKnown.encryption + + XCTAssertNotNil(jistiConfiguration) + XCTAssertNotNil(encryptionConfiguration) + + XCTAssertEqual(jistiConfiguration?.preferredDomain, expectedJitsiServer) + XCTAssertEqual(encryptionConfiguration?.isE2EEByDefaultEnabled, expectedE2EEEByDefaultEnabled) + + let deprecatedJistiConfiguration = vectorWellKnown.deprecatedJitsi + let deprecatedEncryptionConfiguration = vectorWellKnown.deprecatedEncryption + + XCTAssertNotNil(deprecatedJistiConfiguration) + XCTAssertNotNil(deprecatedEncryptionConfiguration) + + XCTAssertEqual(deprecatedJistiConfiguration?.preferredDomain, expectedJitsiServer) + XCTAssertEqual(deprecatedEncryptionConfiguration?.isE2EEByDefaultEnabled, expectedE2EEEByDefaultEnabled) + + } catch { + XCTFail("Fail with error: \(error)") + } + } + + func testVectorWellKnownParsingMissingKey() { + + let expectedE2EEEByDefaultEnabled = false + + let wellKnownDictionary: [String: Any] = [ + "io.element.e2ee" : [ + "default" : expectedE2EEEByDefaultEnabled + ] + ] + + let serializationService = SerializationService() + + do { + let vectorWellKnown: VectorWellKnown = try serializationService.deserialize(wellKnownDictionary) + + XCTAssertNil(vectorWellKnown.jitsi) + XCTAssertNotNil(vectorWellKnown.encryption) + + XCTAssertEqual(vectorWellKnown.encryption?.isE2EEByDefaultEnabled, expectedE2EEEByDefaultEnabled) + } catch { + XCTFail("Fail with error: \(error)") + } + } +} diff --git a/project.yml b/project.yml index 729329e43..b2384fd09 100644 --- a/project.yml +++ b/project.yml @@ -30,4 +30,5 @@ include: - path: RiotTests/target.yml - path: RiotShareExtension/target.yml - path: SiriIntents/target.yml - - path: RiotNSE/target.yml \ No newline at end of file + - path: RiotNSE/target.yml + - path: DesignKit/target.yml