Merge branch 'spaces_beta' into spaces_feature_unavailable

# Conflicts:
#	Riot/Modules/Application/LegacyAppDelegate.h
This commit is contained in:
SBiOSoftWhare 2021-05-07 15:08:45 +02:00
commit 8f437e9fbe
141 changed files with 4727 additions and 1196 deletions

View file

@ -5,6 +5,15 @@ Changes to be released in next version
*
🙌 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.
* Spaces: Hide spaces from room list and home but keep space invites (#4252).
* Spaces: Show space invites and advertise that they are not available (#4277).
* Advertise that spaces are not available when tapping on a space link or a space invite (#4279).
@ -14,6 +23,7 @@ Changes to be released in next version
* 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
*

View file

@ -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

27
DesignKit/Common.xcconfig Normal file
View file

@ -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

20
DesignKit/Debug.xcconfig Normal file
View file

@ -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"

27
DesignKit/DesignKit.h Normal file
View file

@ -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 <Foundation/Foundation.h>
//! 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 <DesignKit/PublicHeader.h>

22
DesignKit/Info.plist Normal file
View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
</plist>

View file

@ -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"

View file

@ -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 }
}

View file

@ -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.
}

View file

@ -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() {}
}

View file

@ -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() {}
}

33
DesignKit/target.yml Normal file
View file

@ -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

View file

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -19,5 +19,8 @@
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

View file

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 794 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -19,5 +19,8 @@
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -19,8 +19,5 @@
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 336 B

After

Width:  |  Height:  |  Size: 370 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 B

After

Width:  |  Height:  |  Size: 647 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 888 B

After

Width:  |  Height:  |  Size: 686 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 772 B

View file

@ -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"
}
}

View file

@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 609 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,015 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 588 B

After

Width:  |  Height:  |  Size: 735 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 776 B

After

Width:  |  Height:  |  Size: 958 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -20,8 +20,6 @@
<false/>
<key>syncLocalContacts</key>
<false/>
<key>createConferenceCallsWithJitsi</key>
<true/>
<key>enableRageShake</key>
<true/>
<key>maxAllowedMediaCacheSize</key>
@ -34,5 +32,7 @@
<integer>15020851</integer>
<key>enableBotCreation</key>
<false/>
<key>enableRingingForGroupCalls</key>
<false/>
</dict>
</plist>

View file

@ -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"
}
}
}

View file

@ -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";

View file

@ -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: %@";

View file

@ -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.
*/

View file

@ -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)

View file

@ -18,6 +18,8 @@
#import <MatrixSDK/MXSession.h>
@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:]

View file

@ -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<NSString*>*)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];
}

View file

@ -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
}
}

View file

@ -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")

View file

@ -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")
@ -4498,7 +4566,7 @@ internal enum VectorL10n {
internal static var spaceBetaAnnounceBadge: String {
return VectorL10n.tr("Vector", "space_beta_announce_badge")
}
/// Spaces are a new way to group rooms and people. Theyre not on iOS yet, but you can use them now on Web and Desktop.
/// Spaces are a new way to group rooms and people. Theyre not on iOS yet, but you can use them now on Web and Desktop.
internal static var spaceBetaAnnounceInformation: String {
return VectorL10n.tr("Vector", "space_beta_announce_information")
}

View file

@ -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

View file

@ -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)?)
}

View file

@ -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?()
}
})
}

View file

@ -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()
}
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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()

View file

@ -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)
}

View file

@ -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

View file

@ -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"
static let hideSpaceBetaAnnounce = "hideSpaceBetaAnnounce"
}
@ -199,14 +205,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.
@ -323,6 +330,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
@ -337,6 +377,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 {
@ -468,6 +521,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 {

View file

@ -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

View file

@ -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

View file

@ -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()
}()
}

View file

@ -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()
}()
}

View file

@ -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 <Foundation/Foundation.h>
/**
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

View file

@ -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

View file

@ -20,27 +20,10 @@
#import <MatrixSDK/MatrixSDK.h>
#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.

View file

@ -19,17 +19,12 @@
#import "Riot-Swift.h"
#import "JitsiWidgetData.h"
#import "MXSession+Riot.h"
#import <MatrixKit/MatrixKit.h>
#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;

View file

@ -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()
}
}

View file

@ -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
}
}

View file

@ -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()
}
}

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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<Configurable> 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

View file

@ -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;
@ -3061,90 +3061,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
@ -3277,13 +3193,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]);
@ -3306,22 +3217,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;
@ -3329,11 +3245,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;
@ -3345,30 +3261,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;
@ -4530,12 +4436,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, *))
@ -4543,19 +4443,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)
@ -4574,26 +4478,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
@ -4609,12 +4556,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
}
}
[self updateCallStatusBar:btnTitle];
if (completion)
{
completion();
}
[self updateCallStatusBarWithTitle:btnTitle];
}
- (void)callPresenter:(CallPresenter *)presenter dismissCallBar:(void (^)(void))completion
@ -4627,7 +4569,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)
@ -4643,7 +4585,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, *))
{
@ -4655,18 +4597,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

View file

@ -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

View file

@ -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;

View file

@ -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];

View file

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17506" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_5" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17505"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17703"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@ -20,24 +20,25 @@
<outlet property="callerImageView" destination="v1I-LH-wvv" id="xjv-GV-1ST"/>
<outlet property="callerImageViewWidthConstraint" destination="wqp-O7-7Yc" id="Yx8-2L-lzA"/>
<outlet property="callerNameLabel" destination="IW8-8P-mS3" id="eaa-oo-l01"/>
<outlet property="cameraSwitchButton" destination="Iiz-W1-oNW" id="bsM-mH-ti8"/>
<outlet property="cameraSwitchButton" destination="QPM-9v-fDU" id="CbP-Nw-sX7"/>
<outlet property="chatButton" destination="Iiz-W1-oNW" id="ion-1n-TOr"/>
<outlet property="endCallButton" destination="lVK-d8-Dqf" id="zgE-Go-bo6"/>
<outlet property="localPreviewActivityView" destination="Bhz-hS-5B6" id="2yA-xn-ITf"/>
<outlet property="localPreviewContainerView" destination="6gQ-zo-2Zw" id="6Ba-YZ-OmX"/>
<outlet property="localPreviewContainerView" destination="6gQ-zo-2Zw" id="2ch-8A-wzL"/>
<outlet property="localPreviewContainerViewHeightConstraint" destination="dZW-ZL-5rV" id="apO-M6-y8C"/>
<outlet property="localPreviewContainerViewLeadingConstraint" destination="Qvg-FG-sBr" id="gtd-fs-rQh"/>
<outlet property="localPreviewContainerViewTopConstraint" destination="6gi-ec-ZnO" id="a0K-Ix-oIo"/>
<outlet property="localPreviewContainerViewWidthConstraint" destination="gyu-kv-SLy" id="urn-uo-hnt"/>
<outlet property="localPreviewVideoView" destination="5XG-md-r93" id="Zdf-Mj-rWp"/>
<outlet property="moreButton" destination="xCi-hD-FBs" id="2Hn-T9-jZY"/>
<outlet property="moreButtonLeadingConstraint" destination="Erd-Js-vLN" id="fpN-9m-rOc"/>
<outlet property="onHoldCallContainerView" destination="4TX-46-pAi" id="SsP-eX-aHP"/>
<outlet property="onHoldCallerImageView" destination="uFS-C2-TxV" id="SfP-iP-jgd"/>
<outlet property="overlayContainerView" destination="JAR-tn-sGN" id="09u-3G-1UA"/>
<outlet property="pausedIcon" destination="rjn-DE-i10" id="CHR-mM-Dae"/>
<outlet property="pipButton" destination="Yq6-d2-Ea2" id="scW-S6-fpr"/>
<outlet property="rejectCallButton" destination="H4g-11-Y8d" id="8aM-2q-Lv7"/>
<outlet property="remotePreviewContainerView" destination="Tjb-57-yB1" id="MaR-IC-ZKw"/>
<outlet property="resumeButton" destination="OaQ-Ki-dbe" id="23i-sp-y3f"/>
<outlet property="transferButton" destination="wqv-Uf-iNe" id="jua-io-h9q"/>
<outlet property="videoMuteButton" destination="UHM-u9-ODN" id="bfQ-lc-WMB"/>
<outlet property="view" destination="iN0-l3-epB" id="JDF-cz-roW"/>
</connections>
@ -77,21 +78,54 @@
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<accessibility key="accessibilityConfiguration" identifier="CallVCRemotePreviewContainerView"/>
</view>
<view hidden="YES" opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleAspectFill" translatesAutoresizingMaskIntoConstraints="NO" id="6gQ-zo-2Zw" userLabel="Local Preview Container">
<rect key="frame" x="20" y="464" width="79" height="106"/>
<view opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleAspectFill" translatesAutoresizingMaskIntoConstraints="NO" id="6gQ-zo-2Zw" userLabel="Local Preview Container">
<rect key="frame" x="20" y="464" width="90" height="130"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="5XG-md-r93" userLabel="Local Preview Video View">
<rect key="frame" x="0.0" y="0.0" width="90" height="130"/>
<color key="backgroundColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="DsK-QY-Lz0">
<rect key="frame" x="0.0" y="106" width="90" height="24"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="QPM-9v-fDU">
<rect key="frame" x="60" y="-6" width="30" height="30"/>
<constraints>
<constraint firstAttribute="height" constant="30" id="A1c-u0-AXO"/>
<constraint firstAttribute="width" constant="30" id="pzQ-Xn-q0G"/>
</constraints>
<state key="normal" image="camera_switch"/>
<connections>
<action selector="onButtonPressed:" destination="-1" eventType="touchUpInside" id="GQO-Fz-RBU"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="height" constant="24" id="LuF-W3-nmg"/>
<constraint firstAttribute="bottom" secondItem="QPM-9v-fDU" secondAttribute="bottom" id="Pvs-4X-IjN"/>
<constraint firstAttribute="trailing" secondItem="QPM-9v-fDU" secondAttribute="trailing" id="lFs-G2-cuc"/>
</constraints>
</view>
<activityIndicatorView hidden="YES" opaque="NO" contentMode="scaleToFill" hidesWhenStopped="YES" style="whiteLarge" translatesAutoresizingMaskIntoConstraints="NO" id="Bhz-hS-5B6">
<rect key="frame" x="21" y="34.5" width="37" height="37"/>
<rect key="frame" x="26.666666666666664" y="46.666666666666686" width="37" height="37"/>
</activityIndicatorView>
</subviews>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<color key="backgroundColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<accessibility key="accessibilityConfiguration" identifier="CallVCLocalPreviewContainerView">
<accessibilityTraits key="traits" notEnabled="YES"/>
</accessibility>
<constraints>
<constraint firstAttribute="height" constant="106" id="dZW-ZL-5rV"/>
<constraint firstAttribute="width" constant="79" id="gyu-kv-SLy"/>
<constraint firstItem="DsK-QY-Lz0" firstAttribute="leading" secondItem="6gQ-zo-2Zw" secondAttribute="leading" id="7Hz-rj-fhw"/>
<constraint firstAttribute="bottom" secondItem="DsK-QY-Lz0" secondAttribute="bottom" id="C9P-5h-UAT"/>
<constraint firstAttribute="trailing" secondItem="DsK-QY-Lz0" secondAttribute="trailing" id="ZbL-0k-X3Z"/>
<constraint firstItem="5XG-md-r93" firstAttribute="leading" secondItem="6gQ-zo-2Zw" secondAttribute="leading" id="aQX-E8-wvE"/>
<constraint firstItem="5XG-md-r93" firstAttribute="top" secondItem="6gQ-zo-2Zw" secondAttribute="top" id="aWP-oA-jDd"/>
<constraint firstAttribute="height" constant="130" id="dZW-ZL-5rV"/>
<constraint firstAttribute="width" constant="90" id="gyu-kv-SLy"/>
<constraint firstItem="Bhz-hS-5B6" firstAttribute="centerY" secondItem="6gQ-zo-2Zw" secondAttribute="centerY" id="jTh-Rj-Eew"/>
<constraint firstAttribute="bottom" secondItem="5XG-md-r93" secondAttribute="bottom" id="phV-bf-EXl"/>
<constraint firstAttribute="trailing" secondItem="5XG-md-r93" secondAttribute="trailing" id="t7q-e1-042"/>
<constraint firstItem="Bhz-hS-5B6" firstAttribute="centerX" secondItem="6gQ-zo-2Zw" secondAttribute="centerX" id="w3I-wQ-iiT"/>
</constraints>
</view>
@ -110,7 +144,6 @@
<constraint firstAttribute="width" constant="44" id="yAh-Gb-GtB"/>
</constraints>
<fontDescription key="fontDescription" name="Helvetica-Bold" family="Helvetica" pointSize="13"/>
<inset key="titleEdgeInsets" minX="-69" minY="61" maxX="0.0" maxY="0.0"/>
<state key="normal" image="back_icon">
<color key="titleColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
@ -122,17 +155,43 @@
<action selector="onButtonPressed:" destination="-1" eventType="touchUpInside" id="yKw-Aj-XkE"/>
</connections>
</button>
<button opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" adjustsImageWhenHighlighted="NO" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Iiz-W1-oNW" userLabel="Switch Camera Btn">
<rect key="frame" x="360" y="5" width="44" height="44"/>
<label opaque="NO" userInteractionEnabled="NO" contentMode="scaleToFill" text="Name" textAlignment="center" lineBreakMode="middleTruncation" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="IW8-8P-mS3">
<rect key="frame" x="54" y="4" width="306" height="20.333333333333332"/>
<accessibility key="accessibilityConfiguration" identifier="CallVCCallerNameLabel"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="scaleToFill" text="Duration" textAlignment="center" lineBreakMode="tailTruncation" minimumFontSize="12" translatesAutoresizingMaskIntoConstraints="NO" id="29y-MK-OWH">
<rect key="frame" x="182" y="32.333333333333329" width="50" height="14.333333333333336"/>
<accessibility key="accessibilityConfiguration" identifier="CallVCCallStatusLabel"/>
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="12"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="IW8-8P-mS3" secondAttribute="trailing" constant="54" id="KKK-Wz-1w3"/>
<constraint firstItem="IW8-8P-mS3" firstAttribute="top" secondItem="sOu-ER-kOe" secondAttribute="top" constant="4" id="Wwm-db-dYV"/>
<constraint firstItem="nff-fB-sTq" firstAttribute="leading" secondItem="sOu-ER-kOe" secondAttribute="leading" constant="10" id="guD-oo-Tl5"/>
<constraint firstItem="29y-MK-OWH" firstAttribute="top" secondItem="IW8-8P-mS3" secondAttribute="bottom" constant="8" id="iXL-M7-wEB"/>
<constraint firstItem="nff-fB-sTq" firstAttribute="centerY" secondItem="sOu-ER-kOe" secondAttribute="centerY" id="vP7-sN-t5g"/>
<constraint firstItem="29y-MK-OWH" firstAttribute="centerX" secondItem="sOu-ER-kOe" secondAttribute="centerX" id="yrO-M6-IFX"/>
<constraint firstItem="IW8-8P-mS3" firstAttribute="leading" secondItem="nff-fB-sTq" secondAttribute="trailing" id="zfx-8Q-WF1"/>
<constraint firstAttribute="height" constant="54" id="zsh-GQ-fv2"/>
</constraints>
</view>
<view clearsContextBeforeDrawing="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="nk9-Un-LVP" userLabel="Call Control Container View">
<rect key="frame" x="0.0" y="774" width="414" height="68"/>
<subviews>
<button opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" adjustsImageWhenHighlighted="NO" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Iiz-W1-oNW" userLabel="Chat Button">
<rect key="frame" x="12" y="7" width="48" height="48"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<accessibility key="accessibilityConfiguration" identifier="CallVCCameraSwitchButton"/>
<constraints>
<constraint firstAttribute="height" constant="44" id="2ci-nt-cgP"/>
<constraint firstAttribute="width" constant="44" id="owR-ds-wTp"/>
</constraints>
<fontDescription key="fontDescription" name="Helvetica-Bold" family="Helvetica" pointSize="13"/>
<inset key="titleEdgeInsets" minX="-69" minY="61" maxX="0.0" maxY="0.0"/>
<state key="normal" image="camera_switch">
<inset key="contentEdgeInsets" minX="12" minY="12" maxX="12" maxY="12"/>
<state key="normal" image="call_go_to_chat_icon">
<color key="titleColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
@ -143,84 +202,70 @@
<action selector="onButtonPressed:" destination="-1" eventType="touchUpInside" id="UaC-FO-rmW"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="Iiz-W1-oNW" secondAttribute="trailing" constant="10" id="YYf-d4-U5q"/>
<constraint firstItem="nff-fB-sTq" firstAttribute="leading" secondItem="sOu-ER-kOe" secondAttribute="leading" constant="10" id="guD-oo-Tl5"/>
<constraint firstItem="Iiz-W1-oNW" firstAttribute="centerY" secondItem="sOu-ER-kOe" secondAttribute="centerY" id="ns9-vB-ywU"/>
<constraint firstItem="nff-fB-sTq" firstAttribute="centerY" secondItem="sOu-ER-kOe" secondAttribute="centerY" id="vP7-sN-t5g"/>
<constraint firstAttribute="height" constant="54" id="zsh-GQ-fv2"/>
</constraints>
</view>
<view clearsContextBeforeDrawing="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="nk9-Un-LVP" userLabel="Call Control Container View">
<rect key="frame" x="0.0" y="798" width="414" height="64"/>
<subviews>
<button opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" adjustsImageWhenHighlighted="NO" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Yq6-d2-Ea2">
<rect key="frame" x="71" y="8" width="48" height="48"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<accessibility key="accessibilityConfiguration" identifier="CallVCSpeakerButton"/>
<fontDescription key="fontDescription" name="Helvetica-Bold" family="Helvetica" pointSize="13"/>
<inset key="contentEdgeInsets" minX="12" minY="12" maxX="12" maxY="12"/>
<state key="normal" image="call_pip_icon">
<color key="titleColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<state key="highlighted">
<color key="titleColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<connections>
<action selector="onButtonPressed:" destination="-1" eventType="touchUpInside" id="kfq-w6-EXx"/>
</connections>
</button>
<button opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" adjustsImageWhenHighlighted="NO" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="fxP-zM-kfT">
<rect key="frame" x="123" y="12" width="48" height="48"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<accessibility key="accessibilityConfiguration" identifier="CallVCAudioMuteButton"/>
<fontDescription key="fontDescription" name="Helvetica-Bold" family="Helvetica" pointSize="13"/>
<state key="normal" image="call_audio_mute_off_icon">
<color key="titleColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<state key="highlighted">
<color key="titleColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<connections>
<action selector="onButtonPressed:" destination="-1" eventType="touchUpInside" id="CgE-f8-nPS"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="lVK-d8-Dqf" userLabel="End Call Button">
<rect key="frame" x="183" y="8" width="48" height="48"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<accessibility key="accessibilityConfiguration" identifier="CallVCEndCallButton"/>
<inset key="contentEdgeInsets" minX="4" minY="4" maxX="4" maxY="4"/>
<state key="normal" image="call_hangup_large">
<color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<state key="selected" image="call_hangup_icon"/>
<state key="highlighted" image="call_hangup_icon"/>
<connections>
<action selector="onButtonPressed:" destination="-1" eventType="touchUpInside" id="ZTw-gz-mwM"/>
</connections>
</button>
<button opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" adjustsImageWhenHighlighted="NO" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="UHM-u9-ODN">
<rect key="frame" x="243" y="12" width="48" height="48"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<accessibility key="accessibilityConfiguration" identifier="CallVCVideoMuteButton"/>
<fontDescription key="fontDescription" name="Helvetica-Bold" family="Helvetica" pointSize="13"/>
<state key="normal" image="call_video_mute_off_icon">
<color key="titleColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<state key="highlighted">
<color key="titleColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<connections>
<action selector="onButtonPressed:" destination="-1" eventType="touchUpInside" id="9Jd-Wv-foD"/>
</connections>
</button>
<button opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" adjustsImageWhenHighlighted="NO" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="xCi-hD-FBs">
<rect key="frame" x="295" y="8" width="48" height="48"/>
<stackView opaque="NO" contentMode="scaleToFill" spacing="18" translatesAutoresizingMaskIntoConstraints="NO" id="K0z-Tt-rHv">
<rect key="frame" x="105" y="6" width="204" height="56"/>
<subviews>
<button opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" adjustsImageWhenHighlighted="NO" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="fxP-zM-kfT" userLabel="Audio Mute Button">
<rect key="frame" x="0.0" y="0.0" width="56" height="56"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<accessibility key="accessibilityConfiguration" identifier="CallVCAudioMuteButton"/>
<constraints>
<constraint firstAttribute="width" constant="56" id="2za-pq-LeK"/>
<constraint firstAttribute="height" constant="56" id="WzU-Gl-l7E"/>
</constraints>
<fontDescription key="fontDescription" name="Helvetica-Bold" family="Helvetica" pointSize="13"/>
<state key="normal" image="call_audio_mute_off_icon">
<color key="titleColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<state key="highlighted">
<color key="titleColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<connections>
<action selector="onButtonPressed:" destination="-1" eventType="touchUpInside" id="CgE-f8-nPS"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="lVK-d8-Dqf" userLabel="End Call Button">
<rect key="frame" x="74" y="0.0" width="56" height="56"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<accessibility key="accessibilityConfiguration" identifier="CallVCEndCallButton"/>
<constraints>
<constraint firstAttribute="width" constant="56" id="dOZ-Rv-ioc"/>
<constraint firstAttribute="height" constant="56" id="ry3-Rb-qxA"/>
</constraints>
<state key="normal" image="call_hangup_large">
<color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<state key="selected" image="call_hangup_icon"/>
<state key="highlighted" image="call_hangup_icon"/>
<connections>
<action selector="onButtonPressed:" destination="-1" eventType="touchUpInside" id="ZTw-gz-mwM"/>
</connections>
</button>
<button opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" adjustsImageWhenHighlighted="NO" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="UHM-u9-ODN" userLabel="Video Mute Button">
<rect key="frame" x="148" y="0.0" width="56" height="56"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<accessibility key="accessibilityConfiguration" identifier="CallVCVideoMuteButton"/>
<constraints>
<constraint firstAttribute="width" constant="56" id="jA5-Bg-GZa"/>
<constraint firstAttribute="height" constant="56" id="tJ7-KN-uzF"/>
</constraints>
<fontDescription key="fontDescription" name="Helvetica-Bold" family="Helvetica" pointSize="13"/>
<state key="normal" image="call_video_mute_off_icon">
<color key="titleColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<state key="highlighted">
<color key="titleColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<connections>
<action selector="onButtonPressed:" destination="-1" eventType="touchUpInside" id="9Jd-Wv-foD"/>
</connections>
</button>
</subviews>
</stackView>
<button opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" adjustsImageWhenHighlighted="NO" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="xCi-hD-FBs" userLabel="More Button">
<rect key="frame" x="354" y="6" width="48" height="48"/>
<accessibility key="accessibilityConfiguration" identifier="CallVCChatButton"/>
<fontDescription key="fontDescription" name="Helvetica-Bold" family="Helvetica" pointSize="13"/>
<inset key="contentEdgeInsets" minX="12" minY="12" maxX="12" maxY="12"/>
@ -239,21 +284,17 @@
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<accessibility key="accessibilityConfiguration" identifier="CallVCCallControlContainerView"/>
<constraints>
<constraint firstItem="UHM-u9-ODN" firstAttribute="leading" secondItem="lVK-d8-Dqf" secondAttribute="trailing" constant="12" id="4l8-GS-LMC"/>
<constraint firstItem="lVK-d8-Dqf" firstAttribute="centerX" secondItem="nk9-Un-LVP" secondAttribute="centerX" id="C77-dh-aGp"/>
<constraint firstItem="Yq6-d2-Ea2" firstAttribute="centerY" secondItem="nk9-Un-LVP" secondAttribute="centerY" id="CgC-oj-Epe"/>
<constraint firstItem="xCi-hD-FBs" firstAttribute="leading" secondItem="lVK-d8-Dqf" secondAttribute="trailing" constant="64" id="Erd-Js-vLN"/>
<constraint firstItem="fxP-zM-kfT" firstAttribute="centerY" secondItem="nk9-Un-LVP" secondAttribute="centerY" constant="4" id="FXE-lL-Q91"/>
<constraint firstItem="UHM-u9-ODN" firstAttribute="centerY" secondItem="nk9-Un-LVP" secondAttribute="centerY" constant="4" id="aOF-7c-8gC"/>
<constraint firstItem="fxP-zM-kfT" firstAttribute="leading" secondItem="Yq6-d2-Ea2" secondAttribute="trailing" constant="4" id="aR8-up-zPg"/>
<constraint firstAttribute="height" constant="64" id="eS5-qj-HTc"/>
<constraint firstItem="lVK-d8-Dqf" firstAttribute="centerY" secondItem="nk9-Un-LVP" secondAttribute="centerY" id="hY9-ut-lPv"/>
<constraint firstItem="lVK-d8-Dqf" firstAttribute="leading" secondItem="fxP-zM-kfT" secondAttribute="trailing" constant="12" id="jZa-SH-3LK"/>
<constraint firstItem="xCi-hD-FBs" firstAttribute="centerY" secondItem="nk9-Un-LVP" secondAttribute="centerY" id="orn-eU-ogl"/>
<constraint firstItem="K0z-Tt-rHv" firstAttribute="centerY" secondItem="nk9-Un-LVP" secondAttribute="centerY" id="42s-hp-QOL"/>
<constraint firstItem="Iiz-W1-oNW" firstAttribute="leading" secondItem="nk9-Un-LVP" secondAttribute="leading" constant="12" id="6Ac-bF-DKJ"/>
<constraint firstAttribute="trailing" secondItem="xCi-hD-FBs" secondAttribute="trailing" constant="12" id="PIj-uS-7Wf"/>
<constraint firstItem="Iiz-W1-oNW" firstAttribute="centerY" secondItem="nk9-Un-LVP" secondAttribute="centerY" constant="-3" id="WGS-jR-0O2"/>
<constraint firstAttribute="height" constant="68" id="eS5-qj-HTc"/>
<constraint firstItem="xCi-hD-FBs" firstAttribute="centerY" secondItem="nk9-Un-LVP" secondAttribute="centerY" constant="-4" id="orn-eU-ogl"/>
<constraint firstItem="K0z-Tt-rHv" firstAttribute="centerX" secondItem="nk9-Un-LVP" secondAttribute="centerX" id="qPd-Nd-Opc"/>
</constraints>
</view>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="equalSpacing" alignment="center" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="Zc0-eq-2kY">
<rect key="frame" x="167" y="364" width="80" height="168"/>
<rect key="frame" x="167" y="311.66666666666669" width="80" height="117"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="FNb-tG-f7m">
<rect key="frame" x="0.0" y="0.0" width="80" height="80"/>
@ -288,22 +329,8 @@
<constraint firstAttribute="width" constant="80" id="wqp-O7-7Yc"/>
</constraints>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="scaleToFill" text="Name" textAlignment="center" lineBreakMode="tailTruncation" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="IW8-8P-mS3">
<rect key="frame" x="17" y="88" width="46.5" height="20.5"/>
<accessibility key="accessibilityConfiguration" identifier="CallVCCallerNameLabel"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="scaleToFill" text="Duration" textAlignment="center" lineBreakMode="tailTruncation" minimumFontSize="12" translatesAutoresizingMaskIntoConstraints="NO" id="29y-MK-OWH">
<rect key="frame" x="15" y="116.5" width="50" height="14.5"/>
<accessibility key="accessibilityConfiguration" identifier="CallVCCallStatusLabel"/>
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="12"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="OaQ-Ki-dbe">
<rect key="frame" x="13" y="139" width="54" height="29"/>
<rect key="frame" x="13" y="88" width="54" height="29"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="14"/>
<color key="tintColor" systemColor="systemGreenColor"/>
<state key="normal" title="Resume"/>
@ -311,6 +338,15 @@
<action selector="onButtonPressed:" destination="-1" eventType="touchUpInside" id="pJG-7Q-UTa"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="wqv-Uf-iNe" userLabel="Transfer">
<rect key="frame" x="11.5" y="176" width="57" height="29"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="14"/>
<color key="tintColor" systemColor="systemGreenColor"/>
<state key="normal" title="Transfer"/>
<connections>
<action selector="onButtonPressed:" destination="-1" eventType="touchUpInside" id="KUu-CC-dnC"/>
</connections>
</button>
</subviews>
</stackView>
</subviews>
@ -321,7 +357,7 @@
<constraint firstItem="nk9-Un-LVP" firstAttribute="leading" secondItem="JAR-tn-sGN" secondAttribute="leading" id="9wq-Y2-kZa"/>
<constraint firstItem="sOu-ER-kOe" firstAttribute="leading" secondItem="JAR-tn-sGN" secondAttribute="leading" id="INI-a4-QCn"/>
<constraint firstAttribute="trailing" secondItem="sOu-ER-kOe" secondAttribute="trailing" id="LEA-qC-Gko"/>
<constraint firstItem="Zc0-eq-2kY" firstAttribute="centerY" secondItem="JAR-tn-sGN" secondAttribute="centerY" id="ju3-Ix-AGc"/>
<constraint firstItem="Zc0-eq-2kY" firstAttribute="centerY" secondItem="JAR-tn-sGN" secondAttribute="centerY" constant="-78" id="ju3-Ix-AGc"/>
<constraint firstAttribute="trailing" secondItem="nk9-Un-LVP" secondAttribute="trailing" id="yES-yI-i3m"/>
</constraints>
</view>
@ -425,7 +461,7 @@
<constraint firstItem="6gQ-zo-2Zw" firstAttribute="leading" secondItem="r1a-fi-tZ0" secondAttribute="leading" priority="750" constant="20" id="Qvg-FG-sBr"/>
<constraint firstItem="VWv-s0-46r" firstAttribute="leading" secondItem="r1a-fi-tZ0" secondAttribute="leading" id="SNc-WF-jXg"/>
<constraint firstItem="4TX-46-pAi" firstAttribute="top" secondItem="r1a-fi-tZ0" secondAttribute="top" constant="56" id="TQG-q5-eS7"/>
<constraint firstItem="r1a-fi-tZ0" firstAttribute="bottom" secondItem="nk9-Un-LVP" secondAttribute="bottom" id="VpW-QU-xiw"/>
<constraint firstItem="r1a-fi-tZ0" firstAttribute="bottom" secondItem="nk9-Un-LVP" secondAttribute="bottom" constant="20" id="VpW-QU-xiw"/>
<constraint firstItem="r1a-fi-tZ0" firstAttribute="trailing" secondItem="JAR-tn-sGN" secondAttribute="trailing" id="Xcc-jT-zbd"/>
<constraint firstItem="UGa-hI-iqx" firstAttribute="top" secondItem="r1a-fi-tZ0" secondAttribute="top" constant="120" id="YlJ-XZ-rCM"/>
<constraint firstItem="Tjb-57-yB1" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" id="aKy-Ol-bhc"/>
@ -433,7 +469,7 @@
<constraint firstItem="JAR-tn-sGN" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" id="ck8-BX-Uyq"/>
<constraint firstItem="UGa-hI-iqx" firstAttribute="centerX" secondItem="iN0-l3-epB" secondAttribute="centerX" id="dzn-mw-OrJ"/>
<constraint firstItem="sOu-ER-kOe" firstAttribute="top" secondItem="r1a-fi-tZ0" secondAttribute="top" id="iV6-LV-kcn"/>
<constraint firstItem="r1a-fi-tZ0" firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="6gQ-zo-2Zw" secondAttribute="bottom" constant="74" id="jCm-3K-6ah"/>
<constraint firstItem="r1a-fi-tZ0" firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="6gQ-zo-2Zw" secondAttribute="bottom" constant="90" id="jCm-3K-6ah"/>
<constraint firstItem="Tjb-57-yB1" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="mW3-RA-hx2"/>
<constraint firstItem="VWv-s0-46r" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" id="pzW-oL-TS7"/>
<constraint firstAttribute="trailing" secondItem="Tjb-57-yB1" secondAttribute="trailing" id="rXn-dm-69F"/>
@ -445,14 +481,14 @@
</objects>
<resources>
<image name="back_icon" width="14" height="23"/>
<image name="call_audio_mute_off_icon" width="48" height="48"/>
<image name="call_audio_mute_off_icon" width="68" height="68"/>
<image name="call_go_to_chat_icon" width="24" height="24"/>
<image name="call_hangup_icon" width="24.5" height="26"/>
<image name="call_hangup_large" width="40" height="40"/>
<image name="call_hangup_large" width="68" height="68"/>
<image name="call_more_icon" width="24" height="24"/>
<image name="call_paused_white_icon" width="20" height="20"/>
<image name="call_pip_icon" width="24" height="24"/>
<image name="call_video_mute_off_icon" width="48" height="48"/>
<image name="camera_switch" width="24" height="24"/>
<image name="call_video_mute_off_icon" width="68" height="68"/>
<image name="camera_switch" width="13.333333015441895" height="13.333333015441895"/>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>

View file

@ -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()
}

View file

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17506" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="dBQ-CG-VDL">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="dBQ-CG-VDL">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17505"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17703"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="Stack View standard spacing" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
@ -171,7 +171,7 @@
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" alignment="center" spacing="32" translatesAutoresizingMaskIntoConstraints="NO" id="dvZ-XT-zLP">
<rect key="frame" x="53.5" y="299" width="268" height="93.5"/>
<subviews>
<button opaque="NO" tag="-99" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="hh8-U6-dJT" customClass="DialpadButton" customModule="Riot" customModuleProvider="target">
<button opaque="NO" tag="-1" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="hh8-U6-dJT" customClass="DialpadButton" customModule="Riot" customModuleProvider="target">
<rect key="frame" x="0.0" y="12.5" width="68" height="68"/>
<constraints>
<constraint firstAttribute="height" constant="68" id="4mU-dy-vEa"/>
@ -196,7 +196,7 @@
<action selector="digitButtonAction:" destination="dBQ-CG-VDL" eventType="touchUpInside" id="Irt-qu-feP"/>
</connections>
</button>
<button opaque="NO" tag="-1" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Izh-vM-Fao" customClass="DialpadButton" customModule="Riot" customModuleProvider="target">
<button opaque="NO" tag="-2" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Izh-vM-Fao" customClass="DialpadButton" customModule="Riot" customModuleProvider="target">
<rect key="frame" x="200" y="12.5" width="68" height="68"/>
<constraints>
<constraint firstAttribute="width" constant="68" id="4J4-8u-YKL"/>
@ -213,18 +213,13 @@
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" alignment="center" spacing="32" translatesAutoresizingMaskIntoConstraints="NO" id="xzj-PF-Bxu">
<rect key="frame" x="53.5" y="398.5" width="268" height="93.5"/>
<subviews>
<button opaque="NO" tag="-99" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="SVW-fH-Ey6" customClass="DialpadActionButton" customModule="Riot" customModuleProvider="target">
<button opaque="NO" userInteractionEnabled="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="SVW-fH-Ey6">
<rect key="frame" x="0.0" y="12.5" width="68" height="68"/>
<constraints>
<constraint firstAttribute="height" constant="68" id="L2l-Qz-R8A"/>
<constraint firstAttribute="width" constant="68" id="nEw-2X-e3a"/>
</constraints>
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<inset key="contentEdgeInsets" minX="0.0" minY="0.0" maxX="4" maxY="0.0"/>
<state key="normal" image="call_dialpad_backspace_icon"/>
<connections>
<action selector="backspaceButtonAction:" destination="dBQ-CG-VDL" eventType="touchUpInside" id="3MX-35-8Jg"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="B17-BJ-SiW" customClass="DialpadActionButton" customModule="Riot" customModuleProvider="target">
<rect key="frame" x="100" y="12.5" width="68" height="68"/>
@ -238,13 +233,19 @@
<action selector="callButtonAction:" destination="dBQ-CG-VDL" eventType="touchUpInside" id="TOV-1D-XQj"/>
</connections>
</button>
<button opaque="NO" userInteractionEnabled="NO" tag="-1" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Xig-ln-gBC">
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Xig-ln-gBC" customClass="DialpadActionButton" customModule="Riot" customModuleProvider="target">
<rect key="frame" x="200" y="12.5" width="68" height="68"/>
<constraints>
<constraint firstAttribute="height" constant="68" id="WLJ-xL-89j"/>
<constraint firstAttribute="width" constant="68" id="afw-C4-e8i"/>
</constraints>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="28"/>
<color key="tintColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<inset key="contentEdgeInsets" minX="0.0" minY="0.0" maxX="4" maxY="0.0"/>
<state key="normal" image="call_dialpad_backspace_icon"/>
<connections>
<action selector="backspaceButtonAction:" destination="dBQ-CG-VDL" eventType="touchUpInside" id="ONz-mt-4D3"/>
</connections>
</button>
</subviews>
</stackView>
@ -285,15 +286,27 @@
</constraints>
</view>
<connections>
<outlet property="backspaceButton" destination="SVW-fH-Ey6" id="s2W-oS-a90"/>
<outlet property="backspaceButton" destination="Xig-ln-gBC" id="kKQ-7z-fVM"/>
<outlet property="callButton" destination="B17-BJ-SiW" id="rmo-gK-pkv"/>
<outlet property="closeButton" destination="6Yp-ue-lX3" id="xc1-ue-iUo"/>
<outlet property="digitsStackView" destination="fnQ-jm-HT2" id="ltK-Bg-hwY"/>
<outlet property="lineView" destination="iak-30-PMd" id="T6L-kl-Dsy"/>
<outlet property="phoneNumberTextField" destination="iWR-Bv-qzs" id="ezn-FP-ihl"/>
<outlet property="phoneNumberTextFieldTopConstraint" destination="nkS-49-CiR" id="Anh-q0-26b"/>
<outlet property="spaceButton" destination="Xig-ln-gBC" id="tJi-AD-WOu"/>
<outlet property="spaceButton" destination="SVW-fH-Ey6" id="WJd-w7-YxB"/>
<outlet property="titleLabel" destination="ObS-Bw-Z12" id="0ZH-xI-zhw"/>
<outletCollection property="digitButtons" destination="Dum-9Z-dKh" collectionClass="NSMutableArray" id="mnM-oA-G7N"/>
<outletCollection property="digitButtons" destination="TIL-aL-5ij" collectionClass="NSMutableArray" id="4Ge-8g-ooV"/>
<outletCollection property="digitButtons" destination="Yrk-zC-XL5" collectionClass="NSMutableArray" id="123-Qp-rzG"/>
<outletCollection property="digitButtons" destination="JfX-9z-x4y" collectionClass="NSMutableArray" id="cPo-i1-bbI"/>
<outletCollection property="digitButtons" destination="hI1-sM-NT5" collectionClass="NSMutableArray" id="b8d-5A-i6e"/>
<outletCollection property="digitButtons" destination="cMg-az-Zgx" collectionClass="NSMutableArray" id="J0I-Qb-Zj6"/>
<outletCollection property="digitButtons" destination="EF6-7Y-DhC" collectionClass="NSMutableArray" id="r8I-vD-jSM"/>
<outletCollection property="digitButtons" destination="4Od-gu-C4x" collectionClass="NSMutableArray" id="XFt-db-l5O"/>
<outletCollection property="digitButtons" destination="Rgd-Tt-xlI" collectionClass="NSMutableArray" id="aGQ-Ef-eBC"/>
<outletCollection property="digitButtons" destination="hh8-U6-dJT" collectionClass="NSMutableArray" id="EaB-Ug-qsx"/>
<outletCollection property="digitButtons" destination="4Rc-ls-1wr" collectionClass="NSMutableArray" id="cUp-yg-U48"/>
<outletCollection property="digitButtons" destination="Izh-vM-Fao" collectionClass="NSMutableArray" id="Gfp-Fl-LMb"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="bLY-II-iJ3" userLabel="First Responder" sceneMemberID="firstResponder"/>

View file

@ -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

View file

@ -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
}
}

View file

@ -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)
}
}
}

View file

@ -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)
}
}

Some files were not shown because too many files have changed in this diff Show more