Initial commit

This commit is contained in:
Tha_14 2024-02-22 21:43:11 +02:00
commit 1b96a031d2
1108 changed files with 157706 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,634 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import UIKit
protocol ActiveSessionCoordinatorDelegate: class {
func activeSessionCoordinatorDidLogout(_ coordinator: ActiveSessionCoordinator, importToxProfileFromURL: URL?)
func activeSessionCoordinatorDeleteProfile(_ coordinator: ActiveSessionCoordinator)
func activeSessionCoordinatorRecreateCoordinatorsStack(_ coordinator: ActiveSessionCoordinator, options: CoordinatorOptions)
func activeSessionCoordinatorDidStartCall(_ coordinator: ActiveSessionCoordinator)
func activeSessionCoordinatorDidFinishCall(_ coordinator: ActiveSessionCoordinator)
}
private struct Options {
static let ToShowKey = "ToShowKey"
static let StoredOptions = "StoredOptions"
enum Coordinator {
case none
case settings
}
}
private struct IpadObjects {
let splitController: UISplitViewController
let primaryController: PrimaryIpadController
let keyboardObserver = KeyboardObserver()
}
private struct IphoneObjects {
enum TabCoordinator: Int {
case friends = 0
case chats = 1
case settings = 2
case profile = 3
static func allValues() -> [TabCoordinator]{
return [friends, chats, settings, profile]
}
}
let chatsCoordinator: ChatsTabCoordinator
let tabBarController: TabBarController
let friendsTabBarItem: TabBarBadgeItem
let chatsTabBarItem: TabBarBadgeItem
let profileTabBarItem: TabBarProfileItem
}
class ActiveSessionCoordinator: NSObject {
weak var delegate: ActiveSessionCoordinatorDelegate?
fileprivate let theme: Theme
fileprivate let window: UIWindow
// Tox manager is stored here
var toxManager: OCTManager!
fileprivate let friendsCoordinator: FriendsTabCoordinator
fileprivate let settingsCoordinator: SettingsTabCoordinator
fileprivate let profileCoordinator: ProfileTabCoordinator
fileprivate let notificationCoordinator: NotificationCoordinator
fileprivate let automationCoordinator: AutomationCoordinator
var callCoordinator: CallCoordinator!
/**
One of following properties will be non-empty, depending on running device.
*/
fileprivate var iPhone: IphoneObjects!
fileprivate var iPad: IpadObjects!
init(theme: Theme, window: UIWindow, toxManager: OCTManager) {
self.theme = theme
self.window = window
self.toxManager = toxManager
self.friendsCoordinator = FriendsTabCoordinator(theme: theme, toxManager: toxManager)
self.settingsCoordinator = SettingsTabCoordinator(theme: theme)
self.profileCoordinator = ProfileTabCoordinator(theme: theme, toxManager: toxManager)
self.notificationCoordinator = NotificationCoordinator(theme: theme, submanagerObjects: toxManager.objects)
self.automationCoordinator = AutomationCoordinator(submanagerObjects: toxManager.objects, submanagerFiles: toxManager.files)
super.init()
// order matters
createDeviceSpecificObjects()
createCallCoordinator()
toxManager.user.delegate = self
friendsCoordinator.delegate = self
settingsCoordinator.delegate = self
profileCoordinator.delegate = self
notificationCoordinator.delegate = self
NotificationCenter.default.addObserver(self, selector: #selector(ActiveSessionCoordinator.applicationWillTerminate), name: NSNotification.Name.UIApplicationWillTerminate, object: nil)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
@objc func applicationWillTerminate() {
toxManager = nil
// Giving tox some time to close all connections.
let until = Date(timeIntervalSinceNow:1.0)
RunLoop.current.run(until: until)
}
}
extension ActiveSessionCoordinator: TopCoordinatorProtocol {
func startWithOptions(_ options: CoordinatorOptions?) {
switch InterfaceIdiom.current() {
case .iPhone:
iPhone.tabBarController.selectedIndex = IphoneObjects.TabCoordinator.chats.rawValue
iPhone.chatsCoordinator.startWithOptions(nil)
window.rootViewController = iPhone.tabBarController
case .iPad:
primaryIpadControllerShowFriends(iPad.primaryController)
window.rootViewController = iPad.splitController
}
var settingsOptions: CoordinatorOptions?
let toShow = options?[Options.ToShowKey] as? Options.Coordinator ?? .none
switch toShow {
case .none:
break
case .settings:
settingsOptions = options?[Options.StoredOptions] as? CoordinatorOptions
}
friendsCoordinator.startWithOptions(nil)
settingsCoordinator.startWithOptions(settingsOptions)
profileCoordinator.startWithOptions(nil)
notificationCoordinator.startWithOptions(nil)
automationCoordinator.startWithOptions(nil)
callCoordinator.startWithOptions(nil)
toxManager.bootstrap.addPredefinedNodes()
toxManager.bootstrap.bootstrap()
updateUserAvatar()
updateUserName()
switch toShow {
case .none:
break
case .settings:
showSettings()
}
}
func handleLocalNotification(_ notification: UILocalNotification) {
notificationCoordinator.handleLocalNotification(notification)
}
func handleInboxURL(_ url: URL) {
let fileName = url.lastPathComponent
let filePath = url.path
let isToxFile = url.isToxURL()
let style: UIAlertControllerStyle
switch InterfaceIdiom.current() {
case .iPhone:
style = .actionSheet
case .iPad:
style = .alert
}
let alert = UIAlertController(title: nil, message: fileName, preferredStyle: style)
if isToxFile {
alert.addAction(UIAlertAction(title: String(localized: "create_profile"), style: .default) { [unowned self] _ -> Void in
self.logout(importToxProfileFromURL: url)
})
}
alert.addAction(UIAlertAction(title: String(localized: "file_send_to_contact"), style: .default) { [unowned self] _ -> Void in
self.sendFileToChats(filePath, fileName: fileName)
})
alert.addAction(UIAlertAction(title: String(localized: "alert_cancel"), style: .cancel, handler: nil))
switch InterfaceIdiom.current() {
case .iPhone:
iPhone.tabBarController.present(alert, animated: true, completion: nil)
case .iPad:
iPad.splitController.present(alert, animated: true, completion: nil)
}
}
}
extension ActiveSessionCoordinator: OCTSubmanagerUserDelegate {
func submanagerUser(_ submanager: OCTSubmanagerUser, connectionStatusUpdate connectionStatus: OCTToxConnectionStatus) {
updateUserStatusView()
let show = (connectionStatus == .none)
notificationCoordinator.toggleConnectingView(show: show, animated: true)
}
}
extension ActiveSessionCoordinator: NotificationCoordinatorDelegate {
func notificationCoordinator(_ coordinator: NotificationCoordinator, showChat chat: OCTChat) {
showChat(chat)
}
func notificationCoordinatorShowFriendRequest(_ coordinator: NotificationCoordinator, showRequest request: OCTFriendRequest) {
showFriendRequest(request)
}
func notificationCoordinatorAnswerIncomingCall(_ coordinator: NotificationCoordinator, userInfo: String) {
callCoordinator.answerIncomingCallWithUserInfo(userInfo)
}
func notificationCoordinator(_ coordinator: NotificationCoordinator, updateFriendsBadge badge: Int) {
let text: String? = (badge > 0) ? "\(badge)" : nil
switch InterfaceIdiom.current() {
case .iPhone:
iPhone.friendsTabBarItem.badgeText = text
case .iPad:
iPad.primaryController.friendsBadgeText = text
break
}
}
func notificationCoordinator(_ coordinator: NotificationCoordinator, updateChatsBadge badge: Int) {
switch InterfaceIdiom.current() {
case .iPhone:
iPhone.chatsTabBarItem.badgeText = (badge > 0) ? "\(badge)" : nil
case .iPad:
// none
break
}
}
}
extension ActiveSessionCoordinator: CallCoordinatorDelegate {
func callCoordinator(_ coordinator: CallCoordinator, notifyAboutBackgroundCallFrom caller: String, userInfo: String) {
notificationCoordinator.showCallNotificationWithCaller(caller, userInfo: userInfo)
}
func callCoordinatorDidStartCall(_ coordinator: CallCoordinator) {
delegate?.activeSessionCoordinatorDidStartCall(self)
}
func callCoordinatorDidFinishCall(_ coordinator: CallCoordinator) {
delegate?.activeSessionCoordinatorDidFinishCall(self)
}
}
extension ActiveSessionCoordinator: FriendsTabCoordinatorDelegate {
func friendsTabCoordinatorOpenChat(_ coordinator: FriendsTabCoordinator, forFriend friend: OCTFriend) {
let chat = toxManager.chats.getOrCreateChat(with: friend)
showChat(chat!)
}
func friendsTabCoordinatorCall(_ coordinator: FriendsTabCoordinator, toFriend friend: OCTFriend) {
let chat = toxManager.chats.getOrCreateChat(with: friend)!
callCoordinator.callToChat(chat, enableVideo: false)
}
func friendsTabCoordinatorVideoCall(_ coordinator: FriendsTabCoordinator, toFriend friend: OCTFriend) {
let chat = toxManager.chats.getOrCreateChat(with: friend)!
callCoordinator.callToChat(chat, enableVideo: true)
}
}
extension ActiveSessionCoordinator: ChatsTabCoordinatorDelegate {
func chatsTabCoordinator(_ coordinator: ChatsTabCoordinator, chatWillAppear chat: OCTChat) {
notificationCoordinator.banNotificationsForChat(chat)
}
func chatsTabCoordinator(_ coordinator: ChatsTabCoordinator, chatWillDisapper chat: OCTChat) {
notificationCoordinator.unbanNotificationsForChat(chat)
}
func chatsTabCoordinator(_ coordinator: ChatsTabCoordinator, callToChat chat: OCTChat, enableVideo: Bool) {
callCoordinator.callToChat(chat, enableVideo: enableVideo)
}
}
extension ActiveSessionCoordinator: SettingsTabCoordinatorDelegate {
func settingsTabCoordinatorRecreateCoordinatorsStack(_ coordinator: SettingsTabCoordinator, options settingsOptions: CoordinatorOptions) {
delegate?.activeSessionCoordinatorRecreateCoordinatorsStack(self, options: [
Options.ToShowKey: Options.Coordinator.settings,
Options.StoredOptions: settingsOptions,
])
}
}
extension ActiveSessionCoordinator: ProfileTabCoordinatorDelegate {
func profileTabCoordinatorDelegateLogout(_ coordinator: ProfileTabCoordinator) {
logout()
}
func profileTabCoordinatorDelegateDeleteProfile(_ coordinator: ProfileTabCoordinator) {
delegate?.activeSessionCoordinatorDeleteProfile(self)
}
func profileTabCoordinatorDelegateDidChangeUserStatus(_ coordinator: ProfileTabCoordinator) {
updateUserStatusView()
}
func profileTabCoordinatorDelegateDidChangeAvatar(_ coordinator: ProfileTabCoordinator) {
updateUserAvatar()
}
func profileTabCoordinatorDelegateDidChangeUserName(_ coordinator: ProfileTabCoordinator) {
updateUserName()
}
}
extension ActiveSessionCoordinator: PrimaryIpadControllerDelegate {
func primaryIpadController(_ controller: PrimaryIpadController, didSelectChat chat: OCTChat) {
showChat(chat)
}
func primaryIpadControllerShowFriends(_ controller: PrimaryIpadController) {
iPad.splitController.showDetailViewController(friendsCoordinator.navigationController, sender: nil)
}
func primaryIpadControllerShowSettings(_ controller: PrimaryIpadController) {
iPad.splitController.showDetailViewController(settingsCoordinator.navigationController, sender: nil)
}
func primaryIpadControllerShowProfile(_ controller: PrimaryIpadController) {
iPad.splitController.showDetailViewController(profileCoordinator.navigationController, sender: nil)
}
}
extension ActiveSessionCoordinator: ChatPrivateControllerDelegate {
func chatPrivateControllerWillAppear(_ controller: ChatPrivateController) {
notificationCoordinator.banNotificationsForChat(controller.chat)
}
func chatPrivateControllerWillDisappear(_ controller: ChatPrivateController) {
notificationCoordinator.unbanNotificationsForChat(controller.chat)
}
func chatPrivateControllerCallToChat(_ controller: ChatPrivateController, enableVideo: Bool) {
callCoordinator.callToChat(controller.chat, enableVideo: enableVideo)
}
func chatPrivateControllerShowQuickLookController(
_ controller: ChatPrivateController,
dataSource: QuickLookPreviewControllerDataSource,
selectedIndex: Int)
{
let controller = QuickLookPreviewController()
controller.dataSource = dataSource
controller.dataSourceStorage = dataSource
controller.currentPreviewItemIndex = selectedIndex
iPad.splitController.present(controller, animated: true, completion: nil)
}
}
extension ActiveSessionCoordinator: FriendSelectControllerDelegate {
func friendSelectController(_ controller: FriendSelectController, didSelectFriend friend: OCTFriend) {
rootViewController().dismiss(animated: true) { [unowned self] in
guard let filePath = controller.userInfo as? String else {
return
}
let chat = self.toxManager.chats.getOrCreateChat(with: friend)
self.sendFile(filePath, toChat: chat!)
}
}
func friendSelectControllerCancel(_ controller: FriendSelectController) {
rootViewController().dismiss(animated: true, completion: nil)
guard let filePath = controller.userInfo as? String else {
return
}
_ = try? FileManager.default.removeItem(atPath: filePath)
}
}
private extension ActiveSessionCoordinator {
func createDeviceSpecificObjects() {
switch InterfaceIdiom.current() {
case .iPhone:
let chatsCoordinator = ChatsTabCoordinator(theme: theme, submanagerObjects: toxManager.objects, submanagerChats: toxManager.chats, submanagerFiles: toxManager.files)
chatsCoordinator.delegate = self
let tabBarControllers = IphoneObjects.TabCoordinator.allValues().map { object -> UINavigationController in
switch object {
case .friends:
return friendsCoordinator.navigationController
case .chats:
return chatsCoordinator.navigationController
case .settings:
return settingsCoordinator.navigationController
case .profile:
return profileCoordinator.navigationController
}
}
let tabBarItems = createTabBarItems()
let friendsTabBarItem = tabBarItems[IphoneObjects.TabCoordinator.friends.rawValue] as! TabBarBadgeItem
let chatsTabBarItem = tabBarItems[IphoneObjects.TabCoordinator.chats.rawValue] as! TabBarBadgeItem
let profileTabBarItem = tabBarItems[IphoneObjects.TabCoordinator.profile.rawValue] as! TabBarProfileItem
let tabBarController = TabBarController(theme: theme, controllers: tabBarControllers, tabBarItems: tabBarItems)
iPhone = IphoneObjects(
chatsCoordinator: chatsCoordinator,
tabBarController: tabBarController,
friendsTabBarItem: friendsTabBarItem,
chatsTabBarItem: chatsTabBarItem,
profileTabBarItem: profileTabBarItem)
case .iPad:
let splitController = UISplitViewController()
splitController.preferredDisplayMode = .allVisible
let primaryController = PrimaryIpadController(theme: theme, submanagerChats: toxManager.chats, submanagerObjects: toxManager.objects)
primaryController.delegate = self
splitController.viewControllers = [UINavigationController(rootViewController: primaryController)]
iPad = IpadObjects(splitController: splitController, primaryController: primaryController)
}
}
func createCallCoordinator() {
let presentingController: UIViewController
switch InterfaceIdiom.current() {
case .iPhone:
presentingController = iPhone.tabBarController
case .iPad:
presentingController = iPad.splitController
}
self.callCoordinator = CallCoordinator(
theme: theme,
presentingController: presentingController,
submanagerCalls: toxManager.calls,
submanagerObjects: toxManager.objects)
callCoordinator.delegate = self
}
func createTabBarItems() -> [TabBarAbstractItem] {
return IphoneObjects.TabCoordinator.allValues().map {
switch $0 {
case .friends:
let item = TabBarBadgeItem(theme: theme)
item.image = UIImage(named: "tab-bar-friends")
item.text = String(localized: "contacts_title")
item.badgeAccessibilityEnding = String(localized: "contact_requests_section")
return item
case .chats:
let item = TabBarBadgeItem(theme: theme)
item.image = UIImage(named: "tab-bar-chats")
item.text = String(localized: "chats_title")
item.badgeAccessibilityEnding = String(localized: "accessibility_chats_ending")
return item
case .settings:
let item = TabBarBadgeItem(theme: theme)
item.image = UIImage(named: "tab-bar-settings")
item.text = String(localized: "settings_title")
return item
case .profile:
return TabBarProfileItem(theme: theme)
}
}
}
func showFriendRequest(_ request: OCTFriendRequest) {
switch InterfaceIdiom.current() {
case .iPhone:
iPhone.tabBarController.selectedIndex = IphoneObjects.TabCoordinator.friends.rawValue
case .iPad:
primaryIpadControllerShowFriends(iPad.primaryController)
}
friendsCoordinator.showRequest(request, animated: false)
}
/**
Returns active chat controller if it is visible, nil otherwise.
*/
func activeChatController() -> ChatPrivateController? {
switch InterfaceIdiom.current() {
case .iPhone:
if iPhone.tabBarController.selectedIndex != IphoneObjects.TabCoordinator.chats.rawValue {
return nil
}
return iPhone.chatsCoordinator.activeChatController()
case .iPad:
return iPadDetailController() as? ChatPrivateController
}
}
func showChat(_ chat: OCTChat) {
switch InterfaceIdiom.current() {
case .iPhone:
if iPhone.tabBarController.selectedIndex != IphoneObjects.TabCoordinator.chats.rawValue {
iPhone.tabBarController.selectedIndex = IphoneObjects.TabCoordinator.chats.rawValue
}
iPhone.chatsCoordinator.showChat(chat, animated: false)
case .iPad:
if let chatVC = iPadDetailController() as? ChatPrivateController {
if chatVC.chat == chat {
// controller is already visible
return
}
}
let controller = ChatPrivateController(
theme: theme,
chat: chat,
submanagerChats: toxManager.chats,
submanagerObjects: toxManager.objects,
submanagerFiles: toxManager.files,
delegate: self,
showKeyboardOnAppear: iPad.keyboardObserver.keyboardVisible)
let navigation = UINavigationController(rootViewController: controller)
iPad.splitController.showDetailViewController(navigation, sender: nil)
}
}
func showSettings() {
switch InterfaceIdiom.current() {
case .iPhone:
iPhone.tabBarController.selectedIndex = IphoneObjects.TabCoordinator.settings.rawValue
case .iPad:
primaryIpadControllerShowFriends(iPad.primaryController)
}
}
func updateUserStatusView() {
let status = UserStatus(connectionStatus: toxManager.user.connectionStatus, userStatus: toxManager.user.userStatus)
let connectionstatus = ConnectionStatus(connectionStatus: toxManager.user.connectionStatus)
switch InterfaceIdiom.current() {
case .iPhone:
iPhone.profileTabBarItem.userStatus = status
iPhone.profileTabBarItem.connectionStatus = connectionstatus
case .iPad:
iPad.primaryController.userStatus = status
}
}
func updateUserAvatar() {
var avatar: UIImage?
if let avatarData = toxManager.user.userAvatar() {
avatar = UIImage(data: avatarData)
}
switch InterfaceIdiom.current() {
case .iPhone:
iPhone.profileTabBarItem.userImage = avatar
case .iPad:
iPad.primaryController.userAvatar = avatar
}
}
func updateUserName() {
switch InterfaceIdiom.current() {
case .iPhone:
// nop
break
case .iPad:
iPad.primaryController.userName = toxManager.user.userName()
}
}
func iPadDetailController() -> UIViewController? {
guard iPad.splitController.viewControllers.count == 2 else {
return nil
}
let controller = iPad.splitController.viewControllers[1]
if let navigation = controller as? UINavigationController {
return navigation.topViewController
}
return controller
}
func logout(importToxProfileFromURL profileURL: URL? = nil) {
delegate?.activeSessionCoordinatorDidLogout(self, importToxProfileFromURL: profileURL)
}
func rootViewController() -> UIViewController {
switch InterfaceIdiom.current() {
case .iPhone:
return iPhone.tabBarController
case .iPad:
return iPad.splitController
}
}
func sendFileToChats(_ filePath: String, fileName: String) {
let controller = FriendSelectController(theme: theme, submanagerObjects: toxManager.objects)
controller.delegate = self
controller.title = String(localized: "file_send_to_contact")
controller.userInfo = filePath as AnyObject?
let navigation = UINavigationController(rootViewController: controller)
rootViewController().present(navigation, animated: true, completion: nil)
}
func sendFile(_ filePath: String, toChat chat: OCTChat) {
showChat(chat)
toxManager.files.sendFile(atPath: filePath, moveToUploads: true, to: chat, failureBlock: { (error: Error) in
handleErrorWithType(.sendFileToFriend, error: error as NSError)
})
}
}

View File

@ -0,0 +1,24 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import UIKit
class ActiveSessionNavigationCoordinator {
let theme: Theme
let navigationController: UINavigationController
init(theme: Theme) {
self.theme = theme
self.navigationController = UINavigationController()
}
init(theme: Theme, navigationController: UINavigationController) {
self.theme = theme
self.navigationController = navigationController
}
func startWithOptions(_ options: CoordinatorOptions?) {
preconditionFailure("This method must be overridden")
}
}

View File

@ -0,0 +1,245 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import UIKit
import SnapKit
private struct Constants {
static let TextViewTopOffset = 5.0
static let TextViewXOffset = 5.0
static let QrCodeBottomSpacerDeltaHeight = 70.0
static let SendAlertTextViewBottomOffset = -10.0
static let SendAlertTextViewXOffset = 5.0
static let SendAlertTextViewHeight = 70.0
}
protocol AddFriendControllerDelegate: class {
func addFriendControllerScanQRCode(
_ controller: AddFriendController,
validateCodeHandler: @escaping (String) -> Bool,
didScanHander: @escaping (String) -> Void)
func addFriendControllerDidFinish(_ controller: AddFriendController)
}
class AddFriendController: UIViewController {
weak var delegate: AddFriendControllerDelegate?
fileprivate let theme: Theme
fileprivate weak var submanagerFriends: OCTSubmanagerFriends!
fileprivate var textView: UITextView!
fileprivate var orTopSpacer: UIView!
fileprivate var qrCodeBottomSpacer: UIView!
fileprivate var orLabel: UILabel!
fileprivate var qrCodeButton: UIButton!
fileprivate var cachedMessage: String?
init(theme: Theme, submanagerFriends: OCTSubmanagerFriends) {
self.theme = theme
self.submanagerFriends = submanagerFriends
super.init(nibName: nil, bundle: nil)
addNavigationButtons()
edgesForExtendedLayout = UIRectEdge()
title = String(localized: "add_contact_title")
}
required convenience init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
loadViewWithBackgroundColor(theme.colorForType(.NormalBackground))
createViews()
installConstraints()
updateSendButton()
}
}
extension AddFriendController {
@objc func qrCodeButtonPressed() {
func prepareString(_ string: String) -> String {
var string = string
string = string.uppercased().trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
if string.hasPrefix("TOX:") {
return String(string[string.index(string.startIndex, offsetBy: 4) ..< string.endIndex])
}
return string
}
delegate?.addFriendControllerScanQRCode(self, validateCodeHandler: {
return isAddressString(prepareString($0))
}, didScanHander: { [unowned self] in
self.textView.text = prepareString($0)
self.updateSendButton()
})
}
@objc func sendButtonPressed() {
textView.resignFirstResponder()
let messageView = UITextView()
messageView.text = cachedMessage
let placeholderstring = NSAttributedString.init(string: String(localized: "add_contact_default_message_text"))
//messageView.attributedPlaceholder = placeholderstring
messageView.font = UIFont.systemFont(ofSize: 17.0)
messageView.layer.cornerRadius = 5.0
messageView.layer.masksToBounds = true
let alert = SDCAlertController(
title: String(localized: "add_contact_default_message_title"),
message: nil,
preferredStyle: .alert)!
alert.contentView.addSubview(messageView)
messageView.snp.makeConstraints {
$0.top.equalTo(alert.contentView)
$0.bottom.equalTo(alert.contentView).offset(Constants.SendAlertTextViewBottomOffset);
$0.leading.equalTo(alert.contentView).offset(Constants.SendAlertTextViewXOffset);
$0.trailing.equalTo(alert.contentView).offset(-Constants.SendAlertTextViewXOffset);
$0.height.equalTo(Constants.SendAlertTextViewHeight);
}
alert.addAction(SDCAlertAction(title: String(localized: "alert_cancel"), style: .default, handler: nil))
alert.addAction(SDCAlertAction(title: String(localized: "add_contact_send"), style: .recommended) { [unowned self] action in
self.cachedMessage = messageView.text
let message = messageView.text.isEmpty ? "Antidote is Tox" : messageView.text
do {
try self.submanagerFriends.sendFriendRequest(toAddress: self.textView.text, message: message)
}
catch let error as NSError {
handleErrorWithType(.toxAddFriend, error: error)
return
}
self.delegate?.addFriendControllerDidFinish(self)
})
alert.present(completion: nil)
}
}
extension AddFriendController: UITextViewDelegate {
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if text == "\n" {
updateSendButton()
textView.resignFirstResponder()
return false
}
let resultText = (textView.text! as NSString).replacingCharacters(in: range, with: text)
let maxLength = Int(kOCTToxAddressLength)
if resultText.lengthOfBytes(using: String.Encoding.utf8) > maxLength {
textView.text = resultText.substringToByteLength(maxLength, encoding: String.Encoding.utf8)
updateSendButton()
return false
}
updateSendButton()
return true
}
func textViewDidChange(_ textView: UITextView) {
updateSendButton()
}
}
private extension AddFriendController {
func addNavigationButtons() {
navigationItem.rightBarButtonItem = UIBarButtonItem(
title: String(localized: "add_contact_send"),
style: .done,
target: self,
action: #selector(AddFriendController.sendButtonPressed))
}
func createViews() {
textView = UITextView()
let placeholderstring = NSAttributedString.init(string: String(localized: "add_contact_tox_id_placeholder"))
//textView.attributedPlaceholder = (placeholderstring)
textView.delegate = self
textView.isScrollEnabled = false
textView.font = UIFont.systemFont(ofSize: 17)
textView.textColor = theme.colorForType(.NormalText)
textView.backgroundColor = .clear
textView.returnKeyType = .done
textView.layer.cornerRadius = 5.0
textView.layer.borderWidth = 0.5
textView.layer.borderColor = theme.colorForType(.SeparatorsAndBorders).cgColor
textView.layer.masksToBounds = true
view.addSubview(textView)
orTopSpacer = createSpacer()
qrCodeBottomSpacer = createSpacer()
orLabel = UILabel()
orLabel.text = String(localized: "add_contact_or_label")
orLabel.textColor = theme.colorForType(.NormalText)
orLabel.backgroundColor = .clear
view.addSubview(orLabel)
qrCodeButton = UIButton(type: .system)
qrCodeButton.setTitle(String(localized: "add_contact_use_qr"), for: UIControlState())
qrCodeButton.titleLabel!.font = UIFont.antidoteFontWithSize(16.0, weight: .bold)
qrCodeButton.addTarget(self, action: #selector(AddFriendController.qrCodeButtonPressed), for: .touchUpInside)
view.addSubview(qrCodeButton)
}
func createSpacer() -> UIView {
let spacer = UIView()
spacer.backgroundColor = .clear
view.addSubview(spacer)
return spacer
}
func installConstraints() {
textView.snp.makeConstraints {
$0.top.equalTo(view).offset(Constants.TextViewTopOffset)
$0.leading.equalTo(view).offset(Constants.TextViewXOffset)
$0.trailing.equalTo(view).offset(-Constants.TextViewXOffset)
$0.bottom.equalTo(view.snp.centerY)
}
orTopSpacer.snp.makeConstraints {
$0.top.equalTo(textView.snp.bottom)
}
orLabel.snp.makeConstraints {
$0.top.equalTo(orTopSpacer.snp.bottom)
$0.centerX.equalTo(view)
}
qrCodeButton.snp.makeConstraints {
$0.top.equalTo(orLabel.snp.bottom)
$0.centerX.equalTo(view)
}
qrCodeBottomSpacer.snp.makeConstraints {
$0.top.equalTo(qrCodeButton.snp.bottom)
$0.bottom.equalTo(view)
$0.height.equalTo(orTopSpacer)
}
}
func updateSendButton() {
navigationItem.rightBarButtonItem!.isEnabled = isAddressString(textView.text)
}
}

View File

@ -0,0 +1,50 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import Foundation
import AVFoundation
class AlertAudioPlayer {
enum Sound: String {
case NewMessage = "isotoxin_NewMessage"
}
var playOnlyIfApplicationIsActive = true
fileprivate var sounds: [Sound: SystemSoundID]!
init() {
sounds = [
.NewMessage: createSystemSoundForSound(.NewMessage),
]
}
deinit {
for (_, systemSound) in sounds {
AudioServicesDisposeSystemSoundID(systemSound)
}
}
func playSound(_ sound: Sound) {
if playOnlyIfApplicationIsActive && !UIApplication.isActive {
return
}
guard let systemSound = sounds[sound] else {
return
}
AudioServicesPlayAlertSound(systemSound)
}
}
private extension AlertAudioPlayer {
func createSystemSoundForSound(_ sound: Sound) -> SystemSoundID {
let url = Bundle.main.url(forResource: sound.rawValue, withExtension: "aac")!
var sound: SystemSoundID = 0
AudioServicesCreateSystemSoundID(url as CFURL, &sound)
return sound
}
}

View File

@ -0,0 +1,43 @@
//
// Use this file to import your target's public headers that you would like to expose to Swift.
//
#import <objcTox/OCTCall.h>
#import <objcTox/OCTChat.h>
#import <objcTox/OCTDefaultFileStorage.h>
#import <objcTox/OCTFriend.h>
#import <objcTox/OCTFriendRequest.h>
#import <objcTox/OCTManager.h>
#import <objcTox/OCTManagerFactory.h>
#import <objcTox/OCTManagerConfiguration.h>
#import <objcTox/OCTManagerConstants.h>
#import <objcTox/OCTMessageAbstract.h>
#import <objcTox/OCTMessageCall.h>
#import <objcTox/OCTMessageFile.h>
#import <objcTox/OCTMessageText.h>
#import <objcTox/OCTSubmanagerBootstrap.h>
#import <objcTox/OCTSubmanagerCalls.h>
#import <objcTox/OCTSubmanagerCallsDelegate.h>
#import <objcTox/OCTSubmanagerChats.h>
#import <objcTox/OCTSubmanagerFiles.h>
#import <objcTox/OCTSubmanagerFilesProgressSubscriber.h>
#import <objcTox/OCTSubmanagerFriends.h>
#import <objcTox/OCTSubmanagerObjects.h>
#import <objcTox/OCTSubmanagerUser.h>
#import <objcTox/OCTTox.h>
#import <objcTox/OCTToxEncryptSave.h>
#import <objcTox/OCTToxConstants.h>
#import <objcTox/OCTView.h>
#undef LOG_INFO
#undef LOG_DEBUG
#import "DDLog.h"
#import "DDASLLogger.h"
#import "DDTTYLogger.h"
#import <LNNotificationsUI/LNNotificationsUI.h>
#import <SDCAlertView/SDCAlertController.h>
#import <UITextView+Placeholder/UITextView+Placeholder.h>
#import <JGProgressHUD/JGProgressHUD.h>
#import "ExceptionHandling.h"

View File

@ -0,0 +1,89 @@
<?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>NSLocationWhenInUseUsageDescription</key>
<string>Used to share your location with your contacts</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>${PRODUCT_NAME}</string>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeIconFiles</key>
<array/>
<key>CFBundleTypeName</key>
<string>Data</string>
<key>LSHandlerRank</key>
<string>Default</string>
<key>LSItemContentTypes</key>
<array>
<string>public.data</string>
</array>
</dict>
</array>
<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>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSApplicationCategoryType</key>
<string></string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<false/>
<key>NSCameraUsageDescription</key>
<string>You can use video calls, send photos and videos, scan QR codes.</string>
<key>NSMicrophoneUsageDescription</key>
<string>You can use audio and video calls.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>You can send photos and videos.</string>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<string>fetch</string>
<string>location</string>
<string>remote-notification</string>
<string>voip</string>
</array>
<key>UILaunchStoryboardName</key>
<string>Launch Screen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
</array>
<key>UISupportsDocumentBrowser</key>
<false/>
<key>UIUserInterfaceStyle</key>
<string>Light</string>
</dict>
</plist>

View File

@ -0,0 +1,12 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
#ifndef Antidote_Prefix_pch
#define Antidote_Prefix_pch
// Include any system framework and library headers here that should be included in all compilation units.
// You will also need to set the Prefix Header build setting of one or more of your targets to reference this file.
#endif /* Antidote_Prefix_pch */

View File

@ -0,0 +1,12 @@
<?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>aps-environment</key>
<string>development</string>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)org.zoxcore.Antidote</string>
</array>
</dict>
</plist>

View File

@ -0,0 +1,163 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import UIKit
class AppCoordinator {
fileprivate let window: UIWindow
var activeCoordinator: TopCoordinatorProtocol!
fileprivate var theme: Theme
init(window: UIWindow) {
self.window = window
let filepath = Bundle.main.path(forResource: "default-theme", ofType: "yaml")!
let yamlString = try! NSString(contentsOfFile:filepath, encoding:String.Encoding.utf8.rawValue) as String
theme = try! Theme(yamlString: yamlString)
applyTheme(theme)
}
}
// MARK: CoordinatorProtocol
extension AppCoordinator: TopCoordinatorProtocol {
func startWithOptions(_ options: CoordinatorOptions?) {
let storyboard = UIStoryboard(name: "LaunchPlaceholderBoard", bundle: Bundle.main)
window.rootViewController = storyboard.instantiateViewController(withIdentifier: "LaunchPlaceholderController")
recreateActiveCoordinator(options: options)
}
func handleLocalNotification(_ notification: UILocalNotification) {
activeCoordinator.handleLocalNotification(notification)
}
func handleInboxURL(_ url: URL) {
activeCoordinator.handleInboxURL(url)
}
}
extension AppCoordinator: RunningCoordinatorDelegate {
func runningCoordinatorDidLogout(_ coordinator: RunningCoordinator, importToxProfileFromURL: URL?) {
KeychainManager().deleteActiveAccountData()
recreateActiveCoordinator()
if let url = importToxProfileFromURL,
let coordinator = activeCoordinator as? LoginCoordinator {
coordinator.handleInboxURL(url)
}
}
func runningCoordinatorDeleteProfile(_ coordinator: RunningCoordinator) {
let userDefaults = UserDefaultsManager()
let profileManager = ProfileManager()
let name = userDefaults.lastActiveProfile!
do {
try profileManager.deleteProfileWithName(name)
KeychainManager().deleteActiveAccountData()
userDefaults.lastActiveProfile = nil
recreateActiveCoordinator()
}
catch let error as NSError {
handleErrorWithType(.deleteProfile, error: error)
}
}
func runningCoordinatorRecreateCoordinatorsStack(_ coordinator: RunningCoordinator, options: CoordinatorOptions) {
recreateActiveCoordinator(options: options, skipAuthorizationChallenge: true)
}
}
extension AppCoordinator: LoginCoordinatorDelegate {
func loginCoordinatorDidLogin(_ coordinator: LoginCoordinator, manager: OCTManager, password: String) {
KeychainManager().toxPasswordForActiveAccount = password
recreateActiveCoordinator(manager: manager, skipAuthorizationChallenge: true)
}
}
// MARK: Private
private extension AppCoordinator {
func applyTheme(_ theme: Theme) {
let linkTextColor = theme.colorForType(.LinkText)
UIButton.appearance().tintColor = linkTextColor
UISwitch.appearance().onTintColor = linkTextColor
UINavigationBar.appearance().tintColor = linkTextColor
}
func recreateActiveCoordinator(options: CoordinatorOptions? = nil,
manager: OCTManager? = nil,
skipAuthorizationChallenge: Bool = false) {
if let password = KeychainManager().toxPasswordForActiveAccount {
let successBlock: (OCTManager) -> Void = { [unowned self] manager -> Void in
self.activeCoordinator = self.createRunningCoordinatorWithManager(manager,
options: options,
skipAuthorizationChallenge: skipAuthorizationChallenge)
}
if let manager = manager {
successBlock(manager)
}
else {
let deleteActiveAccountAndRetry: () -> Void = { [unowned self] in
KeychainManager().deleteActiveAccountData()
self.recreateActiveCoordinator(options: options,
manager: manager,
skipAuthorizationChallenge: skipAuthorizationChallenge)
}
guard let profileName = UserDefaultsManager().lastActiveProfile else {
deleteActiveAccountAndRetry()
return
}
let path = ProfileManager().pathForProfileWithName(profileName)
guard let configuration = OCTManagerConfiguration.configurationWithBaseDirectory(path) else {
deleteActiveAccountAndRetry()
return
}
ToxFactory.createToxWithConfiguration(configuration,
encryptPassword: password,
successBlock: successBlock,
failureBlock: { _ in
log("Cannot create tox with configuration \(configuration)")
deleteActiveAccountAndRetry()
})
}
}
else {
activeCoordinator = createLoginCoordinator(options)
}
}
func createRunningCoordinatorWithManager(_ manager: OCTManager,
options: CoordinatorOptions?,
skipAuthorizationChallenge: Bool) -> RunningCoordinator {
let coordinator = RunningCoordinator(theme: theme,
window: window,
toxManager: manager,
skipAuthorizationChallenge: skipAuthorizationChallenge)
coordinator.delegate = self
coordinator.startWithOptions(options)
return coordinator
}
func createLoginCoordinator(_ options: CoordinatorOptions?) -> LoginCoordinator {
let coordinator = LoginCoordinator(theme: theme, window: window)
coordinator.delegate = self
coordinator.startWithOptions(options)
return coordinator
}
}

321
Antidote/AppDelegate.swift Normal file
View File

@ -0,0 +1,321 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import UIKit
import Firebase
import os
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
let gcmMessageIDKey = "gcm.message_id"
var coordinator: AppCoordinator!
let callManager = CallManager()
lazy var providerDelegate: ProviderDelegate = ProviderDelegate(callManager: self.callManager)
var backgroundTask: UIBackgroundTaskIdentifier = UIBackgroundTaskInvalid
var gps_was_stopped_by_forground: Bool = false
static var lastStartGpsTS: Int64 = 0
static var location_sharing_contact_pubkey: String = "-1"
class var shared: AppDelegate {
return UIApplication.shared.delegate as! AppDelegate
}
func displayIncomingCall(uuid: UUID, handle: String, hasVideo: Bool = false, completion: ((NSError?) -> Void)?) {
providerDelegate.reportIncomingCall(uuid: uuid, handle: handle, hasVideo: hasVideo, completion: completion)
}
func endIncomingCalls() {
providerDelegate.endIncomingCalls()
}
func applicationWillEnterForeground(_ application: UIApplication) {
os_log("AppDelegate:applicationWillEnterForeground")
UIApplication.shared.endBackgroundTask(self.backgroundTask)
self.backgroundTask = UIBackgroundTaskInvalid
gps_was_stopped_by_forground = true
let gps = LocationManager.shared
if !gps.isHasAccess() {
os_log("AppDelegate:applicationWillEnterForeground:gps:no_access")
} else if gps.state == .Monitoring {
os_log("AppDelegate:applicationWillEnterForeground:gps:STOP")
gps.stopMonitoring()
}
os_log("AppDelegate:applicationWillEnterForeground:DidEnterBackground:2:END")
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
window = UIWindow(frame:UIScreen.main.bounds)
print("didFinishLaunchingWithOptions")
os_log("AppDelegate:didFinishLaunchingWithOptions:start")
if ProcessInfo.processInfo.arguments.contains("UI_TESTING") {
// Speeding up animations for UI tests.
window!.layer.speed = 1000
}
configureLoggingStuff()
coordinator = AppCoordinator(window: window!)
coordinator.startWithOptions(nil)
if let notification = launchOptions?[UIApplicationLaunchOptionsKey.localNotification] as? UILocalNotification {
coordinator.handleLocalNotification(notification)
}
window?.backgroundColor = UIColor.white
window?.makeKeyAndVisible()
FirebaseApp.configure()
Messaging.messaging().delegate = self
if #available(iOS 10.0, *) {
UNUserNotificationCenter.current().delegate = self
let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
UNUserNotificationCenter.current().requestAuthorization(
options: authOptions,
completionHandler: { _, _ in }
)
} else {
let settings: UIUserNotificationSettings =
UIUserNotificationSettings(types: [.alert, .badge, .sound], categories: nil)
application.registerUserNotificationSettings(settings)
}
application.registerForRemoteNotifications()
// HINT: try to go online every 47 minutes
let bgfetchInterval: TimeInterval = 47 * 60
application.setMinimumBackgroundFetchInterval(bgfetchInterval);
os_log("AppDelegate:didFinishLaunchingWithOptions:end")
return true
}
func applicationWillTerminate(_ application: UIApplication) {
print("WillTerminate")
os_log("AppDelegate:applicationWillTerminate")
}
func applicationDidReceiveMemoryWarning(_ application: UIApplication) {
print("DidReceiveMemoryWarning")
os_log("AppDelegate:applicationDidReceiveMemoryWarning")
}
func applicationDidEnterBackground(_ application: UIApplication) {
print("DidEnterBackground")
os_log("AppDelegate:applicationDidEnterBackground:start")
backgroundTask = UIApplication.shared.beginBackgroundTask (expirationHandler: { [unowned self] in
UIApplication.shared.endBackgroundTask(self.backgroundTask)
self.backgroundTask = UIBackgroundTaskInvalid
os_log("AppDelegate:applicationDidEnterBackground:3:expirationHandler:END")
})
DispatchQueue.main.asyncAfter(wallDeadline: DispatchWallTime.now() + 15) {
if (UserDefaultsManager().LongerbgMode == true) {
os_log("AppDelegate:applicationDidEnterBackground:PushSelf:start")
let coord = self.coordinator.activeCoordinator
let runcoord = coord as! RunningCoordinator
runcoord.activeSessionCoordinator?.toxManager.chats.sendOwnPush()
} else {
os_log("AppDelegate:applicationDidEnterBackground:PushSelf:longer-bg-mode not active in settings")
}
}
gps_was_stopped_by_forground = false
let gps = LocationManager.shared
if gps.isHasAccess() {
AppDelegate.lastStartGpsTS = Date().millisecondsSince1970
gps.startMonitoring()
os_log("AppDelegate:applicationDidEnterBackground:gps:START")
DispatchQueue.main.asyncAfter(wallDeadline: DispatchWallTime.now() + (3 * 60)) {
os_log("AppDelegate:applicationDidEnterBackground:4:gps:finishing")
let gps = LocationManager.shared
if !gps.isHasAccess() {
os_log("AppDelegate:applicationDidEnterBackground:4:gps:no_access")
} else if gps.state == .Monitoring {
if (self.gps_was_stopped_by_forground == false) {
let diffTime = Date().millisecondsSince1970 - AppDelegate.lastStartGpsTS
os_log("AppDelegate:applicationDidEnterBackground:4:gps:Tlast=%ld", AppDelegate.lastStartGpsTS)
os_log("AppDelegate:applicationDidEnterBackground:4:gps:Tnow=%ld", Date().millisecondsSince1970)
os_log("AppDelegate:applicationDidEnterBackground:4:gps:Tdiff=%ld", diffTime)
if (diffTime > (((3 * 60) - 4) * 1000))
{
os_log("AppDelegate:applicationDidEnterBackground:4:gps:STOP")
gps.stopMonitoring()
} else {
os_log("AppDelegate:applicationDidEnterBackground:4:gps:STOP skipped, must be an old timer")
}
} else {
os_log("AppDelegate:applicationDidEnterBackground:4:gps:was stopped by forground, skipping")
}
} else {
os_log("AppDelegate:applicationDidEnterBackground:4:gps:STOP skipped, gps was stopped already")
}
}
} else {
os_log("AppDelegate:applicationDidEnterBackground:gps:no_access")
}
DispatchQueue.main.asyncAfter(wallDeadline: DispatchWallTime.now() + 25) {
UIApplication.shared.endBackgroundTask(self.backgroundTask)
self.backgroundTask = UIBackgroundTaskInvalid
os_log("AppDelegate:applicationDidEnterBackground:1:END")
}
}
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
print("performFetchWithCompletionHandler:start")
os_log("AppDelegate:performFetchWithCompletionHandler:start")
// HINT: we have 30 seconds here. use 25 of those 30 seconds to be on the safe side
DispatchQueue.main.asyncAfter(deadline: .now() + 25) { [weak self] in
completionHandler(UIBackgroundFetchResult.newData)
print("performFetchWithCompletionHandler:end")
os_log("AppDelegate:performFetchWithCompletionHandler:end")
}
}
func application(_ application: UIApplication, didReceive notification: UILocalNotification) {
coordinator.handleLocalNotification(notification)
}
func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any]) -> Bool {
coordinator.handleInboxURL(url)
return true
}
func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool {
coordinator.handleInboxURL(url)
return true
}
// Device received notification (legacy callback)
//
func application(_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any]) {
if let messageID = userInfo[gcmMessageIDKey] {
print("Message ID: \(messageID)")
}
}
// tells the app that a remote notification arrived that indicates there is data to be fetched.
// ios 7+
//
func application(_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult)
-> Void) {
if let messageID = userInfo[gcmMessageIDKey] {
print("Message ID: \(messageID)")
}
os_log("AppDelegate:didReceiveRemoteNotification:start")
// HINT: we have 30 seconds here. use 25 of those 30 seconds to be on the safe side
DispatchQueue.main.asyncAfter(deadline: .now() + 25) { [weak self] in
completionHandler(UIBackgroundFetchResult.newData)
os_log("AppDelegate:didReceiveRemoteNotification:start")
}
}
// APNs failed to register the device for push notifications
//
func application(_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error) {
print("Unable to register for remote notifications: \(error.localizedDescription)")
}
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
print("APNs token retrieved: \(deviceToken)")
os_log("AppDelegate:didRegisterForRemoteNotificationsWithDeviceToken")
}
}
@available(iOS 10, *)
extension AppDelegate: UNUserNotificationCenterDelegate {
// determine what to do if app is in foreground when a notification is coming
// ios 10+ UNUserNotificationCenterDelegate method
//
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions)
-> Void) {
let userInfo = notification.request.content.userInfo
if let messageID = userInfo[gcmMessageIDKey] {
print("Message ID: \(messageID)")
}
completionHandler([[.alert, .sound]])
}
// Process and handle the user's response to a delivered notification.
// ios 10+ UNUserNotificationCenterDelegate method
//
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void) {
let userInfo = response.notification.request.content.userInfo
if let messageID = userInfo[gcmMessageIDKey] {
print("Message ID: \(messageID)")
}
completionHandler()
}
}
private extension AppDelegate {
func configureLoggingStuff() {
DDLog.add(DDASLLogger.sharedInstance())
// DDLog.add(DDTTYLogger.sharedInstance())
}
}
extension AppDelegate: MessagingDelegate {
func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
print("Firebase registration token: \(String(describing: fcmToken))")
let dataDict: [String: String] = ["token": fcmToken ?? ""]
NotificationCenter.default.post(
name: Notification.Name("FCMToken"),
object: nil,
userInfo: dataDict
)
}
}
// Convenience AppWide Simple Alert
extension AppDelegate {
func alert(_ title: String, _ msg: String? = nil) {
os_log("AppDelegate:alert")
let cnt = UIAlertController(title: title, message: msg, preferredStyle: .alert)
cnt.addAction(UIAlertAction(title: "Ok", style: .default, handler: { [weak cnt] act in
cnt?.dismiss(animated: true, completion: nil)
}))
// guard let vc = AppDelegate.topViewController() else { return }
// vc.present(cnt, animated: true, completion: nil)
}
}
extension Date {
var millisecondsSince1970: Int64 {
Int64((self.timeIntervalSince1970 * 1000.0).rounded())
}
init(milliseconds: Int64) {
self = Date(timeIntervalSince1970: TimeInterval(milliseconds) / 1000)
}
}

View File

@ -0,0 +1,81 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import Foundation
import AVFoundation
class AudioPlayer {
enum Sound: String {
case Calltone = "isotoxin_Calltone"
case Hangup = "isotoxin_Hangup"
case Ringtone = "isotoxin_Ringtone"
case RingtoneWhileCall = "isotoxin_RingtoneWhileCall"
}
var playOnlyIfApplicationIsActive = true
fileprivate var players = [Sound: AVAudioPlayer]()
func playSound(_ sound: Sound, loop: Bool) {
if playOnlyIfApplicationIsActive && !UIApplication.isActive {
return
}
guard let player = playerForSound(sound) else {
return
}
player.numberOfLoops = loop ? -1 : 1
player.currentTime = 0.0
player.play()
}
func isPlayingSound(_ sound: Sound) -> Bool {
guard let player = playerForSound(sound) else {
return false
}
return player.isPlaying
}
func isPlaying() -> Bool {
let pl = players.filter {
$0.1.isPlaying
}
return !pl.isEmpty
}
func stopSound(_ sound: Sound) {
guard let player = playerForSound(sound) else {
return
}
player.stop()
}
func stopAll() {
for (_, player) in players {
player.stop()
}
}
}
private extension AudioPlayer {
func playerForSound(_ sound: Sound) -> AVAudioPlayer? {
if let player = players[sound] {
return player
}
guard let path = Bundle.main.path(forResource: sound.rawValue, ofType: "aac") else {
return nil
}
guard let player = try? AVAudioPlayer(contentsOf: URL(fileURLWithPath: path)) else {
return nil
}
players[sound] = player
return player
}
}

View File

@ -0,0 +1,110 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import Foundation
import MobileCoreServices
import os
private struct Constants {
static let MaxFileSizeWiFi: OCTToxFileSize = 100 * 1024 * 1024
static let MaxFileSizeWWAN: OCTToxFileSize = 20 * 1024 * 1024
}
class AutomationCoordinator: NSObject {
fileprivate weak var submanagerFiles: OCTSubmanagerFiles!
fileprivate var fileMessagesToken: RLMNotificationToken?
fileprivate let userDefaults = UserDefaultsManager()
fileprivate let reachability = Reach()
init(submanagerObjects: OCTSubmanagerObjects, submanagerFiles: OCTSubmanagerFiles) {
self.submanagerFiles = submanagerFiles
super.init()
let predicate = NSPredicate(format: "senderUniqueIdentifier != nil AND messageFile != nil")
let results = submanagerObjects.messages(predicate: predicate)
fileMessagesToken = results.addNotificationBlock { [unowned self] change in
switch change {
case .initial:
break
case .update(let results, _, let insertions, _):
guard let results = results else {
break
}
for index in insertions {
let message = results[index]
self.proceedNewFileMessage(message)
}
case .error(let error):
fatalError("\(error)")
}
}
}
}
extension AutomationCoordinator: CoordinatorProtocol {
func startWithOptions(_ options: CoordinatorOptions?) {
// nop
}
}
private extension AutomationCoordinator {
func proceedNewFileMessage(_ message: OCTMessageAbstract) {
let usingWiFi = self.usingWiFi()
os_log("AutomationCoordinator:usingWiFi=%d", usingWiFi)
switch userDefaults.autodownloadImages {
case .Never:
return
case .UsingWiFi:
if !usingWiFi {
return
}
case .Always:
break
}
// HINT: now we apply autodownload to all files, not only images
// if !UTTypeConformsTo(message.messageFile!.fileUTI as CFString? ?? "" as CFString, kUTTypeImage) {
// // download images only
// return
// }
// skip too large files
if usingWiFi {
if message.messageFile!.fileSize > Constants.MaxFileSizeWiFi {
return
}
}
else {
if message.messageFile!.fileSize > Constants.MaxFileSizeWWAN {
return
}
}
// workaround for deadlock in objcTox https://github.com/Antidote-for-Tox/objcTox/issues/51
let delayTime = DispatchTime.now() + Double(Int64(0.0 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)
DispatchQueue.main.asyncAfter(deadline: delayTime) { [weak self] in
self?.submanagerFiles.acceptFileTransfer(message, failureBlock: nil)
}
}
func usingWiFi() -> Bool
{
switch reachability.connectionStatus() {
case .offline:
return false
case .unknown:
return false
case .online(let type):
switch type {
case .wwan:
return false
case .wiFi:
return true
}
}
}
}

View File

@ -0,0 +1,131 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import Foundation
class AvatarManager {
enum AvatarType: String {
case Normal
case Call
}
fileprivate let theme: Theme
fileprivate let cache: NSCache<AnyObject, AnyObject>
init(theme: Theme) {
self.theme = theme
self.cache = NSCache()
}
/**
Returns round avatar created from string with a given diameter. Searches for an avatar in cache first,
if not found creates it.
- Parameters:
- string: String to create avatar from. In case of empty string avatar will be set to "?".
- diameter: Diameter of circle with avatar.
- Returns: Avatar from given string with given size.
*/
func avatarFromString(_ string: String, diameter: CGFloat, type: AvatarType = .Normal) -> UIImage {
var string = string
if string.isEmpty {
string = "?"
}
let key = keyFromString(string, diameter: diameter, type: type)
if let avatar = cache.object(forKey: key as AnyObject) as? UIImage {
return avatar
}
let avatar = createAvatarFromString(string, diameter: diameter, type: type)
cache.setObject(avatar, forKey: key as AnyObject)
return avatar
}
}
private extension AvatarManager {
func keyFromString(_ string: String, diameter: CGFloat, type: AvatarType) -> String {
return "\(string)-\(diameter)-\(type.rawValue)"
}
func createAvatarFromString(_ string: String, diameter: CGFloat, type: AvatarType) -> UIImage {
let avatarString = avatarStringFromString(string)
let label = UILabel()
label.layer.borderWidth = 1.0
label.layer.masksToBounds = true
label.textAlignment = .center
label.text = avatarString
switch type {
case .Normal:
label.backgroundColor = theme.colorForType(.NormalBackground)
label.layer.borderColor = theme.colorForType(.LinkText).cgColor
label.textColor = theme.colorForType(.LinkText)
case .Call:
label.backgroundColor = .clear
label.layer.borderColor = theme.colorForType(.CallButtonIconColor).cgColor
label.textColor = theme.colorForType(.CallButtonIconColor)
}
var size: CGSize
var fontSize = diameter
repeat {
fontSize -= 1
let font = UIFont.antidoteFontWithSize(fontSize, weight: .light)
size = avatarString.stringSizeWithFont(font)
}
while (max(size.width, size.height) > diameter)
let frame = CGRect(x: 0, y: 0, width: diameter, height: diameter)
label.font = UIFont.antidoteFontWithSize(fontSize * 0.6, weight: .light)
label.layer.cornerRadius = frame.size.width / 2
label.frame = frame
return imageWithLabel(label)
}
func avatarStringFromString(_ string: String) -> String {
guard !string.isEmpty else {
return ""
}
// Avatar can have alphanumeric symbols and ? sign.
let badSymbols = (CharacterSet.alphanumerics.inverted as NSCharacterSet).mutableCopy() as! NSMutableCharacterSet
badSymbols.removeCharacters(in: "?")
let words = string.components(separatedBy: CharacterSet.whitespaces).map {
$0.components(separatedBy: badSymbols as CharacterSet).joined(separator: "")
}.filter {
!$0.isEmpty
}
var result = words.map {
$0.isEmpty ? "" : $0[$0.startIndex ..< $0.index($0.startIndex, offsetBy: 1)]
}.joined(separator: "")
let numberOfLetters = min(2, result.count)
result = result.uppercased()
return String(result[result.startIndex ..< result.index(result.startIndex, offsetBy: numberOfLetters)])
}
func imageWithLabel(_ label: UILabel) -> UIImage {
UIGraphicsBeginImageContextWithOptions(label.bounds.size, false, 0.0)
label.layer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image!
}
}

39
Antidote/BaseCell.swift Normal file
View File

@ -0,0 +1,39 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import UIKit
class BaseCell: UITableViewCell {
static var staticReuseIdentifier: String {
get {
return NSStringFromClass(self)
}
}
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
createViews()
installConstraints()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/**
Override this method in subclass.
*/
func setupWithTheme(_ theme: Theme, model: BaseCellModel) {}
/**
Override this method in subclass.
*/
func createViews() {}
/**
Override this method in subclass.
*/
func installConstraints() {}
}

View File

@ -0,0 +1,9 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import Foundation
class BaseCellModel {
}

104
Antidote/BubbleView.swift Normal file
View File

@ -0,0 +1,104 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import UIKit
import SnapKit
private struct Constants {
static let TextViewMinWidth = 5.0
static let TextViewMaxWidth = 260.0
static let TextViewMinHeight = 10.0
static let TextViewVerticalOffset = 1.0
static let TextViewHorizontalOffset = 5.0
}
class BubbleView: UIView {
fileprivate var textView: UITextView!
var text: String? {
get {
return textView.text
}
set {
textView.text = newValue
}
}
var attributedText: NSAttributedString? {
get {
return textView.attributedText
}
set {
textView.attributedText = newValue
}
}
var textColor: UIColor {
get {
return textView.textColor!
}
set {
textView.textColor = newValue
}
}
var font: UIFont? {
get {
return textView.font
}
set {
textView.font = newValue
}
}
override var tintColor: UIColor! {
didSet {
textView.linkTextAttributes = [
NSAttributedStringKey.foregroundColor.rawValue: tintColor,
NSAttributedStringKey.underlineStyle.rawValue: NSUnderlineStyle.styleSingle.rawValue,
]
}
}
var selectable: Bool {
get {
return textView.isSelectable
}
set {
textView.isSelectable = newValue
}
}
override init(frame: CGRect) {
super.init(frame: frame)
layer.cornerRadius = 12.0
layer.masksToBounds = true
textView = UITextView()
textView.backgroundColor = .clear
textView.isEditable = false
textView.isScrollEnabled = false
textView.dataDetectorTypes = .all
textView.font = UIFont.systemFont(ofSize: 16.0)
addSubview(textView)
textView.snp.makeConstraints {
$0.top.equalTo(self).offset(Constants.TextViewVerticalOffset)
$0.bottom.equalTo(self).offset(-Constants.TextViewVerticalOffset)
$0.leading.equalTo(self).offset(Constants.TextViewHorizontalOffset)
$0.trailing.equalTo(self).offset(-Constants.TextViewHorizontalOffset)
$0.width.greaterThanOrEqualTo(Constants.TextViewMinWidth)
$0.width.lessThanOrEqualTo(Constants.TextViewMaxWidth)
$0.height.greaterThanOrEqualTo(Constants.TextViewMinHeight)
}
}
required convenience init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

86
Antidote/Bus.swift Normal file
View File

@ -0,0 +1,86 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Bus.swift
// CLBackgroundAccess
//
// Created by Samer Murad on 10.04.21.
//
import Foundation
import UIKit
// Convenience wrapper around NotificationCenter
// MARK: - Decleration
class Bus {
typealias Unsubscriber = () -> Void
static let shared = Bus()
private init() {}
}
// MARK: - Subscribe / Post
extension Bus {
/// Subscribe to an event, return an Unsubscriber method (call to remove sub)
func on(event: Events, object: Any? = nil, queue: OperationQueue? = nil, cb: @escaping (Notification) -> Void) -> Unsubscriber {
let center = NotificationCenter.default
let notificationName = event.notifciationName()
let observer = center.addObserver(forName: notificationName, object: object, queue: queue, using: cb)
return {
if object != nil {
center.removeObserver(observer, name: notificationName, object: object)
} else {
center.removeObserver(observer)
}
}
}
/// Post event
func post(event: Events, object: Any? = nil, userInfo: [AnyHashable: Any]? = nil) {
guard event.isManualPostSupported() else { return }
let center = NotificationCenter.default
print("Event dispatch:", event)
center.post(name: event.notifciationName(), object: object, userInfo: userInfo)
}
}
// MARK: - Events Enum
extension Bus {
enum Events: String {
// Location Events
case LocationUpdate
case LocationAuthUpdate
case LocationManagerStateChange
// Builtin Events
case AppEnteredBackground
case AppEnteredForeground
}
}
// MARK: - Events enum Notification.Name support and system events guard
extension Bus.Events {
func notifciationName() -> Notification.Name {
//switch self {
//case .AppEnteredBackground:
// return UIApplication.NSNotification.Name.UIApplicationDidEnterBackground
//case .AppEnteredForeground:
// return UIApplication.NSNotification.Name.UIApplicationWillEnterForeground
//default:
return Notification.Name(self.rawValue)
//}
}
func isManualPostSupported() -> Bool {
let name = Notification.Name(self.rawValue)
let actualNotificationName = self.notifciationName()
let isSupported = name == actualNotificationName
if !isSupported {
print("WARN: Event \"", self, "\" Wrapps the System Event \"", actualNotificationName.rawValue, "\" And should not be posted manually")
}
return isSupported
}
}

View File

@ -0,0 +1,403 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import UIKit
import SnapKit
protocol CallActiveControllerDelegate: class {
func callActiveController(_ controller: CallActiveController, mute: Bool)
func callActiveController(_ controller: CallActiveController, speaker: Bool)
func callActiveController(_ controller: CallActiveController, outgoingVideo: Bool)
func callActiveControllerDecline(_ controller: CallActiveController)
func callActiveControllerSwitchCamera(_ controller: CallActiveController)
}
private struct Constants {
static let BigCenterContainerTopOffset = 50.0
static let BigButtonOffset = 30.0
static let SmallButtonOffset = 20.0
static let SmallBottomOffset: CGFloat = -20.0
static let VideoPreviewOffset = -20.0
static let VideoPreviewSize = CGSize(width: 150.0, height: 110)
static let SwitchCameraOffset = 5.0
static let ControlsAnimationDuration = 0.3
}
class CallActiveController: CallBaseController {
enum State {
case none
case reaching
case active(duration: TimeInterval)
}
weak var delegate: CallActiveControllerDelegate?
var state: State = .none {
didSet {
// load view
_ = view
switch state {
case .none:
infoLabel.text = nil
case .reaching:
infoLabel.text = String(localized: "call_reaching")
bigVideoButton?.isEnabled = false
smallVideoButton?.isEnabled = false
case .active(let duration):
infoLabel.text = String(timeInterval: duration)
bigVideoButton?.isEnabled = true
smallVideoButton?.isEnabled = true
}
}
}
var mute: Bool = false {
didSet {
bigMuteButton?.isSelected = mute
smallMuteButton?.isSelected = mute
}
}
var speaker: Bool = false {
didSet {
bigSpeakerButton?.isSelected = speaker
smallSpeakerButton?.isSelected = speaker
}
}
var outgoingVideo: Bool = false {
didSet {
bigVideoButton?.isSelected = outgoingVideo
smallVideoButton?.isSelected = outgoingVideo
}
}
var videoFeed: UIView? {
didSet {
if oldValue === videoFeed {
return
}
if let old = oldValue {
old.removeFromSuperview()
}
if let feed = videoFeed {
view.insertSubview(feed, belowSubview: videoPreviewView)
feed.bounds.size = view.bounds.size
feed.snp.makeConstraints {
$0.edges.equalTo(view)
}
updateViewsWithTraitCollection(self.traitCollection)
}
}
}
var videoPreviewLayer: CALayer? {
didSet {
if oldValue === videoPreviewLayer {
return
}
if let old = oldValue {
old.removeFromSuperlayer()
videoPreviewView.isHidden = true
}
if let layer = videoPreviewLayer {
videoPreviewView.layer.addSublayer(layer)
videoPreviewView.bringSubview(toFront: switchCameraButton)
videoPreviewView.isHidden = false
view.layoutIfNeeded()
}
updateViewsWithTraitCollection(self.traitCollection)
}
}
fileprivate var showControls = true {
didSet {
let offset = showControls ? Constants.SmallBottomOffset : smallContainerView.frame.size.height
smallContainerViewBottomConstraint.update(offset: offset)
toggleTopContainer(hidden: !showControls)
UIView.animate(withDuration: Constants.ControlsAnimationDuration, animations: { [unowned self] in
self.view.layoutIfNeeded()
})
}
}
fileprivate var videoPreviewView: UIView!
fileprivate var switchCameraButton: UIButton!
fileprivate var bigContainerView: UIView!
fileprivate var bigCenterContainer: UIView!
fileprivate var bigMuteButton: CallButton?
fileprivate var bigSpeakerButton: CallButton?
fileprivate var bigVideoButton: CallButton?
fileprivate var bigDeclineButton: CallButton?
fileprivate var smallContainerViewBottomConstraint: Constraint!
fileprivate var smallContainerView: UIView!
fileprivate var smallMuteButton: CallButton?
fileprivate var smallSpeakerButton: CallButton?
fileprivate var smallVideoButton: CallButton?
fileprivate var smallDeclineButton: CallButton?
override init(theme: Theme, callerName: String) {
super.init(theme: theme, callerName: callerName)
}
required convenience init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
super.loadView()
createGestureRecognizers()
createVideoPreviewView()
createBigViews()
createSmallViews()
installConstraints()
view.bringSubview(toFront: topContainer)
setButtonsInitValues()
updateViewsWithTraitCollection(self.traitCollection)
}
override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
updateViewsWithTraitCollection(newCollection)
showControls = true
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if let layer = videoPreviewLayer {
layer.frame.size = videoPreviewView.frame.size
}
}
override func prepareForRemoval() {
super.prepareForRemoval()
bigMuteButton?.isEnabled = false
bigSpeakerButton?.isEnabled = false
bigVideoButton?.isEnabled = false
bigDeclineButton?.isEnabled = false
smallMuteButton?.isEnabled = false
smallSpeakerButton?.isEnabled = false
smallVideoButton?.isEnabled = false
smallDeclineButton?.isEnabled = false
}
}
// MARK: Actions
extension CallActiveController {
@objc func tapOnView() {
guard !smallContainerView.isHidden else {
return
}
showControls = !showControls
}
@objc func muteButtonPressed(_ button: CallButton) {
mute = !button.isSelected
delegate?.callActiveController(self, mute: mute)
}
@objc func speakerButtonPressed(_ button: CallButton) {
speaker = !button.isSelected
delegate?.callActiveController(self, speaker: speaker)
}
@objc func videoButtonPressed(_ button: CallButton) {
outgoingVideo = !button.isSelected
delegate?.callActiveController(self, outgoingVideo: outgoingVideo)
}
@objc func declineButtonPressed() {
delegate?.callActiveControllerDecline(self)
}
@objc func switchCameraButtonPressed() {
delegate?.callActiveControllerSwitchCamera(self)
}
}
private extension CallActiveController {
func createGestureRecognizers() {
let tapGR = UITapGestureRecognizer(target: self, action: #selector(CallActiveController.tapOnView))
view.addGestureRecognizer(tapGR)
}
func createVideoPreviewView() {
videoPreviewView = UIView()
videoPreviewView.backgroundColor = theme.colorForType(.CallVideoPreviewBackground)
view.addSubview(videoPreviewView)
videoPreviewView.isHidden = !outgoingVideo
let image = UIImage.templateNamed("switch-camera")
switchCameraButton = UIButton()
switchCameraButton.tintColor = theme.colorForType(.CallButtonIconColor)
switchCameraButton.setImage(image, for: UIControlState())
switchCameraButton.addTarget(self, action: #selector(CallActiveController.switchCameraButtonPressed), for: .touchUpInside)
videoPreviewView.addSubview(switchCameraButton)
}
func createBigViews() {
bigContainerView = UIView()
bigContainerView.backgroundColor = .clear
view.addSubview(bigContainerView)
bigCenterContainer = UIView()
bigCenterContainer.backgroundColor = .clear
bigContainerView.addSubview(bigCenterContainer)
bigMuteButton = addButtonWithType(.mute, buttonSize: .big, action: #selector(CallActiveController.muteButtonPressed(_:)), container: bigCenterContainer)
bigSpeakerButton = addButtonWithType(.speaker, buttonSize: .big, action: #selector(CallActiveController.speakerButtonPressed(_:)), container: bigCenterContainer)
bigVideoButton = addButtonWithType(.video, buttonSize: .big, action: #selector(CallActiveController.videoButtonPressed(_:)), container: bigCenterContainer)
bigDeclineButton = addButtonWithType(.decline, buttonSize: .small, action: #selector(CallActiveController.declineButtonPressed), container: bigContainerView)
}
func createSmallViews() {
smallContainerView = UIView()
smallContainerView.backgroundColor = .clear
view.addSubview(smallContainerView)
smallMuteButton = addButtonWithType(.mute, buttonSize: .small, action: #selector(CallActiveController.muteButtonPressed(_:)), container: smallContainerView)
smallSpeakerButton = addButtonWithType(.speaker, buttonSize: .small, action: #selector(CallActiveController.speakerButtonPressed(_:)), container: smallContainerView)
smallVideoButton = addButtonWithType(.video, buttonSize: .small, action: #selector(CallActiveController.videoButtonPressed(_:)), container: smallContainerView)
smallDeclineButton = addButtonWithType(.decline, buttonSize: .small, action: #selector(CallActiveController.declineButtonPressed), container: smallContainerView)
}
func addButtonWithType(_ type: CallButton.ButtonType, buttonSize: CallButton.ButtonSize, action: Selector, container: UIView) -> CallButton {
let button = CallButton(theme: theme, type: type, buttonSize: buttonSize)
button.addTarget(self, action: action, for: .touchUpInside)
container.addSubview(button)
return button
}
func installConstraints() {
videoPreviewView.snp.makeConstraints {
$0.trailing.equalTo(view).offset(Constants.VideoPreviewOffset)
$0.bottom.equalTo(smallContainerView.snp.top).offset(Constants.VideoPreviewOffset)
$0.width.equalTo(Constants.VideoPreviewSize.width)
$0.height.equalTo(Constants.VideoPreviewSize.height)
}
switchCameraButton.snp.makeConstraints {
$0.top.equalTo(videoPreviewView).offset(Constants.SwitchCameraOffset)
$0.trailing.equalTo(videoPreviewView).offset(-Constants.SwitchCameraOffset)
}
bigContainerView.snp.makeConstraints {
$0.top.equalTo(topContainer.snp.bottom)
$0.leading.trailing.bottom.equalTo(view)
}
bigCenterContainer.snp.makeConstraints {
$0.centerX.equalTo(bigContainerView)
$0.centerY.equalTo(view)
}
bigMuteButton!.snp.makeConstraints {
$0.top.equalTo(bigCenterContainer)
$0.leading.equalTo(bigCenterContainer)
}
bigSpeakerButton!.snp.makeConstraints {
$0.top.equalTo(bigCenterContainer)
$0.trailing.equalTo(bigCenterContainer)
$0.leading.equalTo(bigMuteButton!.snp.trailing).offset(Constants.BigButtonOffset)
}
bigVideoButton!.snp.makeConstraints {
$0.top.equalTo(bigMuteButton!.snp.bottom).offset(Constants.BigButtonOffset)
$0.leading.equalTo(bigCenterContainer)
$0.bottom.equalTo(bigCenterContainer)
}
bigDeclineButton!.snp.makeConstraints {
$0.centerX.equalTo(bigContainerView)
$0.top.greaterThanOrEqualTo(bigCenterContainer).offset(Constants.BigButtonOffset)
$0.bottom.equalTo(bigContainerView).offset(-Constants.BigButtonOffset)
}
smallContainerView.snp.makeConstraints {
smallContainerViewBottomConstraint = $0.bottom.equalTo(view).offset(Constants.SmallBottomOffset).constraint
$0.centerX.equalTo(view)
}
smallMuteButton!.snp.makeConstraints {
$0.top.bottom.equalTo(smallContainerView)
$0.leading.equalTo(smallContainerView)
}
smallSpeakerButton!.snp.makeConstraints {
$0.top.bottom.equalTo(smallContainerView)
$0.leading.equalTo(smallMuteButton!.snp.trailing).offset(Constants.SmallButtonOffset)
}
smallVideoButton!.snp.makeConstraints {
$0.top.bottom.equalTo(smallContainerView)
$0.leading.equalTo(smallSpeakerButton!.snp.trailing).offset(Constants.SmallButtonOffset)
}
smallDeclineButton!.snp.makeConstraints {
$0.top.bottom.equalTo(smallContainerView)
$0.leading.equalTo(smallVideoButton!.snp.trailing).offset(Constants.SmallButtonOffset)
$0.trailing.equalTo(smallContainerView)
}
}
func setButtonsInitValues() {
bigMuteButton?.isSelected = mute
smallMuteButton?.isSelected = mute
bigSpeakerButton?.isSelected = speaker
smallSpeakerButton?.isSelected = speaker
bigVideoButton?.isSelected = outgoingVideo
smallVideoButton?.isSelected = outgoingVideo
}
func updateViewsWithTraitCollection(_ traitCollection: UITraitCollection) {
if videoFeed != nil || videoPreviewLayer != nil {
bigContainerView.isHidden = true
smallContainerView.isHidden = false
return
}
switch traitCollection.verticalSizeClass {
case .regular:
bigContainerView.isHidden = false
smallContainerView.isHidden = true
case .unspecified:
fallthrough
case .compact:
bigContainerView.isHidden = true
smallContainerView.isHidden = false
}
}
}

View File

@ -0,0 +1,117 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import UIKit
import SnapKit
private struct Constants {
static let TopContainerHeight = 80.0
static let CallerLabelTopOffset = 20.0
static let InfoLabelBottomOffset = -5.0
static let LabelHorizontalOffset = 20.0
}
class CallBaseController: UIViewController {
let theme: Theme
let callerName: String
var topContainer: UIView!
var callerLabel: UILabel!
var infoLabel: UILabel!
fileprivate var topContainerTopConstraint: Constraint!
init(theme: Theme, callerName: String) {
self.theme = theme
self.callerName = callerName
super.init(nibName: nil, bundle: nil)
}
required convenience init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
// HINT: turn on ProximitySensor to blank screen when near the ear
UIDevice.current.isProximityMonitoringEnabled = true
}
deinit {
// HINT: reset ProximitySensor when calling screen is closed
UIDevice.current.isProximityMonitoringEnabled = false
}
override func loadView() {
loadViewWithBackgroundColor(.clear)
addBlurredBackground()
createTopViews()
installConstraints()
}
/**
Prepare for removal by disabling all active views.
*/
func prepareForRemoval() {
infoLabel.text = String(localized: "call_ended")
}
func toggleTopContainer(hidden: Bool) {
let offset = hidden ? -topContainer.frame.size.height : 0.0
topContainerTopConstraint.update(offset: offset)
}
}
private extension CallBaseController {
func addBlurredBackground() {
let effectView = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
effectView.frame = view.bounds
view.insertSubview(effectView, at: 0)
effectView.snp.makeConstraints {
$0.edges.equalTo(view)
}
}
func createTopViews() {
topContainer = UIView() // UIVisualEffectView(effect: UIBlurEffect(style: .dark))
view.addSubview(topContainer)
callerLabel = UILabel()
callerLabel.text = callerName
callerLabel.textColor = theme.colorForType(.CallTextColor)
callerLabel.textAlignment = .center
callerLabel.font = UIFont.systemFont(ofSize: 20.0)
topContainer.addSubview(callerLabel)
infoLabel = UILabel()
infoLabel.textColor = theme.colorForType(.CallTextColor)
infoLabel.textAlignment = .center
infoLabel.font = UIFont.antidoteFontWithSize(18.0, weight: .light)
topContainer.addSubview(infoLabel)
}
func installConstraints() {
topContainer.snp.makeConstraints {
topContainerTopConstraint = $0.top.equalTo(view).constraint
$0.top.leading.trailing.equalTo(view)
$0.height.equalTo(Constants.TopContainerHeight)
}
callerLabel.snp.makeConstraints {
$0.top.equalTo(topContainer).offset(Constants.CallerLabelTopOffset)
$0.leading.equalTo(topContainer).offset(Constants.LabelHorizontalOffset)
$0.trailing.equalTo(topContainer).offset(-Constants.LabelHorizontalOffset)
}
infoLabel.snp.makeConstraints {
$0.bottom.equalTo(topContainer).offset(Constants.InfoLabelBottomOffset)
$0.leading.equalTo(topContainer).offset(Constants.LabelHorizontalOffset)
$0.trailing.equalTo(topContainer).offset(-Constants.LabelHorizontalOffset)
}
}
}

132
Antidote/CallButton.swift Normal file
View File

@ -0,0 +1,132 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import UIKit
private struct Constants {
static let SmallSize: CGFloat = 60.0
static let BigSize: CGFloat = 80.0
static let ImageSize: CGFloat = 30.0
}
class CallButton: UIButton {
enum ButtonSize {
case small
case big
}
enum ButtonType {
case decline
case answerAudio
case answerVideo
case mute
case speaker
case video
}
override var isSelected: Bool {
didSet {
if let selectedTintColor = selectedTintColor {
tintColor = isSelected ? selectedTintColor : normalTintColor
}
}
}
override var isHighlighted: Bool {
didSet {
if isHighlighted {
tintColor = normalTintColor
}
else {
if let selectedTintColor = selectedTintColor {
tintColor = isSelected ? selectedTintColor : normalTintColor
}
}
}
}
fileprivate let buttonSize: ButtonSize
fileprivate let normalTintColor: UIColor
fileprivate var selectedTintColor: UIColor?
init(theme: Theme, type: ButtonType, buttonSize: ButtonSize) {
self.buttonSize = buttonSize
self.normalTintColor = theme.colorForType(.CallButtonIconColor)
super.init(frame: CGRect.zero)
switch buttonSize {
case .small:
layer.cornerRadius = Constants.SmallSize / 2
case .big:
layer.cornerRadius = Constants.BigSize / 2
}
layer.masksToBounds = true
let imageName: String
let backgroundColor: UIColor
var selectedBackgroundColor: UIColor? = nil
switch type {
case .decline:
imageName = "end-call"
backgroundColor = theme.colorForType(.CallDeclineButtonBackground)
case .answerAudio:
imageName = "start-call-30"
backgroundColor = theme.colorForType(.CallAnswerButtonBackground)
case .answerVideo:
imageName = "video-call-30"
backgroundColor = theme.colorForType(.CallAnswerButtonBackground)
case .mute:
imageName = "mute"
backgroundColor = theme.colorForType(.CallControlBackground)
selectedTintColor = theme.colorForType(.CallButtonSelectedIconColor)
selectedBackgroundColor = theme.colorForType(.CallControlSelectedBackground)
case .speaker:
imageName = "speaker"
backgroundColor = theme.colorForType(.CallControlBackground)
selectedTintColor = theme.colorForType(.CallButtonSelectedIconColor)
selectedBackgroundColor = theme.colorForType(.CallControlSelectedBackground)
case .video:
imageName = "video-call-30"
backgroundColor = theme.colorForType(.CallControlBackground)
selectedTintColor = theme.colorForType(.CallButtonSelectedIconColor)
selectedBackgroundColor = theme.colorForType(.CallControlSelectedBackground)
}
tintColor = normalTintColor
switch type {
case .mute:
let image = UIImage.templateNamed("mute")
setImage(image, for: .normal)
let image2 = UIImage.templateNamed("mute-selected")
setImage(image2, for: .selected)
default:
let image = UIImage.templateNamed(imageName)
setImage(image, for: UIControlState())
}
let backgroundImage = UIImage.imageWithColor(backgroundColor, size: CGSize(width: 1.0, height: 1.0))
setBackgroundImage(backgroundImage, for:UIControlState())
if let selected = selectedBackgroundColor {
let backgroundImage = UIImage.imageWithColor(selected, size: CGSize(width: 1.0, height: 1.0))
setBackgroundImage(backgroundImage, for:UIControlState.selected)
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var intrinsicContentSize : CGSize {
switch buttonSize {
case .small:
return CGSize(width: Constants.SmallSize, height: Constants.SmallSize)
case .big:
return CGSize(width: Constants.BigSize, height: Constants.BigSize)
}
}
}

View File

@ -0,0 +1,396 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import Foundation
import CallKit
protocol CallCoordinatorDelegate: class {
func callCoordinator(_ coordinator: CallCoordinator, notifyAboutBackgroundCallFrom caller: String, userInfo: String)
func callCoordinatorDidStartCall(_ coordinator: CallCoordinator)
func callCoordinatorDidFinishCall(_ coordinator: CallCoordinator)
}
private struct Constants {
static let DeclineAfterInterval = 1.5
}
private class ActiveCall {
var callToken: RLMNotificationToken?
fileprivate let call: OCTCall
fileprivate let navigation: UINavigationController
fileprivate var usingFrontCamera: Bool = true
init(call: OCTCall, navigation: UINavigationController) {
self.call = call
self.navigation = navigation
}
deinit {
callToken?.invalidate()
}
}
class CallCoordinator: NSObject {
weak var delegate: CallCoordinatorDelegate?
fileprivate let theme: Theme
fileprivate weak var presentingController: UIViewController!
fileprivate weak var submanagerCalls: OCTSubmanagerCalls!
fileprivate weak var submanagerObjects: OCTSubmanagerObjects!
fileprivate var providerdelegate: ProviderDelegate!
fileprivate let audioPlayer = AudioPlayer()
fileprivate var activeCall: ActiveCall? {
didSet {
switch (oldValue, activeCall) {
case (.none, .some):
delegate?.callCoordinatorDidStartCall(self)
case (.some, .none):
delegate?.callCoordinatorDidFinishCall(self)
default:
break
}
}
}
init(theme: Theme, presentingController: UIViewController, submanagerCalls: OCTSubmanagerCalls, submanagerObjects: OCTSubmanagerObjects) {
self.theme = theme
self.presentingController = presentingController
self.submanagerCalls = submanagerCalls
self.submanagerObjects = submanagerObjects
super.init()
// CALL:
print("cc:controler:init:01")
submanagerCalls.delegate = self
}
func callToChat(_ chat: OCTChat, enableVideo: Bool) {
// CALL:
print("cc:controler:callToChat:01")
do {
let call = try submanagerCalls.call(to: chat, enableAudio: true, enableVideo: enableVideo)
var nickname = String(localized: "contact_deleted")
if let friend = chat.friends.lastObject() as? OCTFriend {
nickname = friend.nickname
}
let controller = CallActiveController(theme: theme, callerName: nickname)
controller.delegate = self
// CALL:
print("cc:controler:callToChat:02")
startActiveCallWithCall(call, controller: controller)
}
catch let error as NSError {
handleErrorWithType(.callToChat, error: error)
}
}
func answerIncomingCallWithUserInfo(_ userInfo: String) {
// CALL:
print("cc:controler:answerIncomingCallWithUserInfo:01")
guard let activeCall = activeCall else { return }
guard activeCall.call.uniqueIdentifier == userInfo else { return }
guard activeCall.call.status == .ringing else { return }
answerCall(enableVideo: false)
}
}
extension CallCoordinator: CoordinatorProtocol {
func startWithOptions(_ options: CoordinatorOptions?) {
}
}
extension CallCoordinator: OCTSubmanagerCallDelegate {
func callSubmanager(_ callSubmanager: OCTSubmanagerCalls!, receive call: OCTCall!, audioEnabled: Bool, videoEnabled: Bool) {
guard activeCall == nil else {
// Currently we support only one call at a time
_ = try? submanagerCalls.send(.cancel, to: call)
return
}
let nickname = call.caller?.nickname ?? ""
// CALL: start incoming call
print("cc:controler:incoming_call:01")
if !UIApplication.isActive {
delegate?.callCoordinator(self, notifyAboutBackgroundCallFrom: nickname, userInfo: call.uniqueIdentifier)
// CALL: start incoming call
print("cc:controler:incoming_call:BG")
let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(expirationHandler: nil)
DispatchQueue.main.asyncAfter(wallDeadline: DispatchWallTime.now() + 0.1) {
AppDelegate.shared.displayIncomingCall(uuid: UUID(), handle: nickname, hasVideo: false) { _ in
UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
}
}
}
let controller = CallIncomingController(theme: theme, callerName: nickname)
controller.delegate = self
startActiveCallWithCall(call, controller: controller)
print("cc:controler:incoming_call:99")
}
}
extension CallCoordinator: CallIncomingControllerDelegate {
func callIncomingControllerDecline(_ controller: CallIncomingController) {
// CALL:
print("cc:controler:callIncomingControllerDecline:01")
declineCall(callWasRemoved: false)
}
func callIncomingControllerAnswerAudio(_ controller: CallIncomingController) {
// CALL:
print("cc:controler:callIncomingControllerAnswerAudio:01")
answerCall(enableVideo: false)
}
func callIncomingControllerAnswerVideo(_ controller: CallIncomingController) {
// CALL:
print("cc:controler:callIncomingControllerAnswerVideo:01")
answerCall(enableVideo: true)
}
}
extension CallCoordinator: CallActiveControllerDelegate {
func callActiveController(_ controller: CallActiveController, mute: Bool) {
submanagerCalls.enableMicrophone = !mute
}
func callActiveController(_ controller: CallActiveController, speaker: Bool) {
do {
try submanagerCalls.routeAudio(toSpeaker: speaker)
}
catch {
handleErrorWithType(.routeAudioToSpeaker)
controller.speaker = !speaker
}
}
func callActiveController(_ controller: CallActiveController, outgoingVideo: Bool) {
guard let activeCall = activeCall else {
assert(false, "This method should be called only if active call is non-nil")
return
}
do {
try submanagerCalls.enableVideoSending(outgoingVideo, for: activeCall.call)
}
catch {
handleErrorWithType(.enableVideoSending)
controller.outgoingVideo = !outgoingVideo
}
}
func callActiveControllerDecline(_ controller: CallActiveController) {
// CALL:
print("cc:controler:callActiveControllerDecline:02")
declineCall(callWasRemoved: false)
}
func callActiveControllerSwitchCamera(_ controller: CallActiveController) {
guard let activeCall = activeCall else {
assert(false, "This method should be called only if active call is non-nil")
return
}
do {
let front = !activeCall.usingFrontCamera
try submanagerCalls.switch(toCameraFront: front)
self.activeCall?.usingFrontCamera = front
}
catch {
handleErrorWithType(.callSwitchCamera)
}
}
}
extension CallCoordinator {
func declineCall(callWasRemoved wasRemoved: Bool) {
// CALL:
print("cc:controler:declineCall:01")
guard let activeCall = activeCall else {
// assert(false, "This method should be called only if active call is non-nil")
return
}
if !wasRemoved {
_ = try? submanagerCalls.send(.cancel, to: activeCall.call)
}
audioPlayer.stopAll()
if let controller = activeCall.navigation.topViewController as? CallBaseController {
controller.prepareForRemoval()
}
let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(expirationHandler: nil)
DispatchQueue.main.asyncAfter(wallDeadline: DispatchWallTime.now() + 0.1) {
AppDelegate.shared.endIncomingCalls()
UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
}
// self.providerdelegate.endIncomingCall()
let delayTime = DispatchTime.now() + Double(Int64(Constants.DeclineAfterInterval * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)
DispatchQueue.main.asyncAfter(deadline: delayTime) { [weak self] in
self?.presentingController.dismiss(animated: true, completion: nil)
self?.activeCall = nil
}
}
func startActiveCallWithCall(_ call: OCTCall, controller: CallBaseController) {
guard activeCall == nil else {
assert(false, "This method should be called only if there is no active call")
return
}
// CALL:
print("cc:controler:startActiveCallWithCall:01")
let navigation = UINavigationController(rootViewController: controller)
navigation.modalPresentationStyle = .overCurrentContext
navigation.isNavigationBarHidden = true
navigation.modalTransitionStyle = .crossDissolve
activeCall = ActiveCall(call: call, navigation: navigation)
let predicate = NSPredicate(format: "uniqueIdentifier == %@", call.uniqueIdentifier)
let results = submanagerObjects.calls(predicate: predicate)
activeCall!.callToken = results.addNotificationBlock { [unowned self] change in
switch change {
case .initial:
break
case .update(_, let deletions, _, let modifications):
if deletions.count > 0 {
self.declineCall(callWasRemoved: true)
}
else if modifications.count > 0 {
self.activeCallWasUpdated()
}
case .error(let error):
fatalError("\(error)")
}
}
presentingController.present(navigation, animated: true, completion: nil)
activeCallWasUpdated()
}
func answerCall(enableVideo: Bool) {
// CALL:
print("cc:controler:answerCall:01")
guard let activeCall = activeCall else {
// assert(false, "This method should be called only if active call is non-nil")
return
}
guard activeCall.call.status == .ringing else {
// assert(false, "Call status should be .Ringing")
return
}
do {
try submanagerCalls.answer(activeCall.call, enableAudio: true, enableVideo: enableVideo)
}
catch let error as NSError {
handleErrorWithType(.answerCall, error: error)
declineCall(callWasRemoved: false)
}
}
func activeCallWasUpdated() {
// CALL:
print("cc:controler:activeCallWasUpdated:01")
guard let activeCall = activeCall else {
assert(false, "This method should be called only if active call is non-nil")
return
}
switch activeCall.call.status {
case .ringing:
if !audioPlayer.isPlayingSound(.Ringtone) {
audioPlayer.playSound(.Ringtone, loop: true)
}
// no update for ringing status
return
case .dialing:
if !audioPlayer.isPlayingSound(.Calltone) {
audioPlayer.playSound(.Calltone, loop: true)
}
case .active:
if audioPlayer.isPlaying() {
audioPlayer.stopAll()
}
}
var activeController = activeCall.navigation.topViewController as? CallActiveController
if (activeController == nil) {
let nickname = activeCall.call.caller?.nickname ?? ""
activeController = CallActiveController(theme: theme, callerName: nickname)
activeController!.delegate = self
activeCall.navigation.setViewControllers([activeController!], animated: false)
}
switch activeCall.call.status {
case .ringing:
break
case .dialing:
activeController!.state = .reaching
case .active:
activeController!.state = .active(duration: activeCall.call.callDuration)
}
activeController!.outgoingVideo = activeCall.call.videoIsEnabled
if activeCall.call.videoIsEnabled {
if activeController!.videoPreviewLayer == nil {
submanagerCalls.getVideoCallPreview { [weak activeController] layer in
activeController?.videoPreviewLayer = layer
}
}
}
else {
if activeController!.videoPreviewLayer != nil {
activeController!.videoPreviewLayer = nil
}
}
if activeCall.call.friendSendingVideo {
if activeController!.videoFeed == nil {
activeController!.videoFeed = submanagerCalls.videoFeed()
}
}
else {
if activeController!.videoFeed != nil {
activeController!.videoFeed = nil
}
}
}
}

View File

@ -0,0 +1,125 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import UIKit
import SnapKit
private struct Constants {
static let AvatarSize: CGFloat = 140.0
static let ButtonContainerTopMinOffset = 10.0
static let ButtonContainerBottomOffset = -50.0
static let ButtonHorizontalOffset = 20.0
}
protocol CallIncomingControllerDelegate: class {
func callIncomingControllerDecline(_ controller: CallIncomingController)
func callIncomingControllerAnswerAudio(_ controller: CallIncomingController)
func callIncomingControllerAnswerVideo(_ controller: CallIncomingController)
}
class CallIncomingController: CallBaseController {
weak var delegate: CallIncomingControllerDelegate?
fileprivate var avatarView: UIImageView!
fileprivate var buttonContainer: UIView!
fileprivate var declineButton: CallButton!
fileprivate var audioButton: CallButton!
fileprivate var videoButton: CallButton!
fileprivate var uuid_call: UUID!
override func loadView() {
super.loadView()
createViews()
installConstraints()
infoLabel.text = String(localized: "call_incoming")
}
override func viewDidLoad() {
super.viewDidLoad()
}
override func prepareForRemoval() {
super.prepareForRemoval()
declineButton.isEnabled = false
audioButton.isEnabled = false
videoButton.isEnabled = false
}
}
// MARK: Actions
extension CallIncomingController {
@objc func declineButtonPressed() {
// CALL: end incoming call
delegate?.callIncomingControllerDecline(self)
}
@objc func audioButtonPressed() {
delegate?.callIncomingControllerAnswerAudio(self)
}
@objc func videoButtonPressed() {
delegate?.callIncomingControllerAnswerVideo(self)
}
}
private extension CallIncomingController {
func createViews() {
let avatarManager = AvatarManager(theme: theme)
avatarView = UIImageView()
avatarView.image = avatarManager.avatarFromString(callerName, diameter: Constants.AvatarSize, type: .Call)
view.addSubview(avatarView)
buttonContainer = UIView()
buttonContainer.backgroundColor = .clear
view.addSubview(buttonContainer)
declineButton = CallButton(theme: theme, type: .decline, buttonSize: .small)
declineButton.addTarget(self, action: #selector(CallIncomingController.declineButtonPressed), for: .touchUpInside)
buttonContainer.addSubview(declineButton)
audioButton = CallButton(theme: theme, type: .answerAudio, buttonSize: .small)
audioButton.addTarget(self, action: #selector(CallIncomingController.audioButtonPressed), for: .touchUpInside)
buttonContainer.addSubview(audioButton)
videoButton = CallButton(theme: theme, type: .answerVideo, buttonSize: .small)
videoButton.addTarget(self, action: #selector(CallIncomingController.videoButtonPressed), for: .touchUpInside)
buttonContainer.addSubview(videoButton)
}
func installConstraints() {
avatarView.snp.makeConstraints {
$0.center.equalTo(view)
}
buttonContainer.snp.makeConstraints {
$0.centerX.equalTo(view)
$0.top.greaterThanOrEqualTo(avatarView.snp.bottom).offset(Constants.ButtonContainerTopMinOffset)
$0.bottom.equalTo(view).offset(Constants.ButtonContainerBottomOffset).priority(250)
}
declineButton.snp.makeConstraints {
$0.top.bottom.equalTo(buttonContainer)
$0.leading.equalTo(buttonContainer)
}
audioButton.snp.makeConstraints {
$0.top.bottom.equalTo(buttonContainer)
$0.leading.equalTo(declineButton.snp.trailing).offset(Constants.ButtonHorizontalOffset)
}
videoButton.snp.makeConstraints {
$0.top.bottom.equalTo(buttonContainer)
$0.leading.equalTo(audioButton.snp.trailing).offset(Constants.ButtonHorizontalOffset)
$0.trailing.equalTo(buttonContainer)
}
}
}

View File

@ -0,0 +1,93 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
/**
* Copyright (c) 2017 Razeware LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import Foundation
enum CallState {
case connecting
case active
case held
case ended
}
enum ConnectedState {
case pending
case complete
}
/// represents a phone call
class Call {
let uuid: UUID
let outgoing: Bool
let handle: String
var state: CallState = .ended {
// didSet is a property observer
// https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Properties.html
didSet {
stateChanged?()
}
}
var connectedState: ConnectedState = .pending {
didSet {
connectedStateChanged?()
}
}
var stateChanged: (() -> Void)?
var connectedStateChanged: (() -> Void)?
init(uuid: UUID, outgoing: Bool = false, handle: String) {
self.uuid = uuid
self.outgoing = outgoing
self.handle = handle
}
func start(completion: ((_ success: Bool) -> Void)?) {
completion?(true)
DispatchQueue.main.asyncAfter(wallDeadline: DispatchWallTime.now() + 3) {
self.state = .connecting
self.connectedState = .pending
DispatchQueue.main.asyncAfter(wallDeadline: DispatchWallTime.now() + 1.5) {
self.state = .active
self.connectedState = .complete
}
}
}
func answer() {
state = .active
}
func end() {
state = .ended
}
}

View File

@ -0,0 +1,101 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
/**
* Copyright (c) 2017 Razeware LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import Foundation
import CallKit
class CallManager {
var callsChangedHandler: (() -> Void)?
private(set) var calls = [Call]()
private let callController = CXCallController()
func callWithUUID(uuid: UUID) -> Call? {
guard let index = calls.index(where: { $0.uuid == uuid }) else {
return nil
}
return calls[index]
}
func add(call: Call) {
calls.append(call)
call.stateChanged = { [weak self] in
guard let strongSelf = self else { return }
strongSelf.callsChangedHandler?()
}
callsChangedHandler?()
}
func startCall(handle: String, videoEnabled: Bool) {
let handle = CXHandle(type: .phoneNumber, value: handle)
// generate a new UUID, use it to instantiate startCallAction
let startCallAction = CXStartCallAction(call: UUID(), handle: handle)
startCallAction.isVideo = videoEnabled
let transaction = CXTransaction(action: startCallAction)
requestTransaction(transaction)
}
func end(call: Call) {
let endCallAction = CXEndCallAction(call: call.uuid)
// wrap action in a transaction
let transaction = CXTransaction(action: endCallAction)
// send transaction to system
requestTransaction(transaction)
}
func setHeld(call: Call, onHold: Bool) {
let setHeldCallAction = CXSetHeldCallAction(call: call.uuid, onHold: onHold)
let transaction = CXTransaction()
transaction.addAction(setHeldCallAction)
requestTransaction(transaction)
}
private func requestTransaction(_ transaction: CXTransaction) {
callController.request(transaction) { error in
if let error = error {
print("Error requesting transaction: \(error)")
} else {
print("Requested transaction successfully")
}
}
}
func remove(call: Call) {
guard let index = calls.index(where: { $0 === call }) else { return }
calls.remove(at: index)
callsChangedHandler?()
}
func removeAllCalls() {
calls.removeAll()
callsChangedHandler?()
}
}

View File

@ -0,0 +1,238 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// ProviderDelegate.swift
// Hotline
//
// Created by Steve Baker on 10/27/17.
// Copyright © 2017 Razeware LLC. All rights reserved.
//
import AVFoundation
import CallKit
import os
class ProviderDelegate: NSObject {
fileprivate let callManager: CallManager
fileprivate let provider: CXProvider
init(callManager: CallManager) {
os_log("ProviderDelegate:init")
self.callManager = callManager
provider = CXProvider(configuration: type(of: self).providerConfiguration)
super.init()
provider.setDelegate(self, queue: nil)
}
// static var belongs to the type
// subclasses can't override static
static var providerConfiguration: CXProviderConfiguration {
// initialize
let providerConfiguration = CXProviderConfiguration(localizedName: "Antidote")
// set call capabilities
providerConfiguration.supportsVideo = true
providerConfiguration.maximumCallsPerCallGroup = 2 // Signal Messenger seems to think 2 is needed
providerConfiguration.supportedHandleTypes = [.generic]
return providerConfiguration
}
func endIncomingCalls() {
os_log("ProviderDelegate:endIncomingCalls")
for call in callManager.calls {
os_log("ProviderDelegate:endcall")
provider.reportCall(with: call.uuid, endedAt: Date(), reason: .remoteEnded)
call.end()
}
callManager.removeAllCalls()
}
func reportIncomingCall(uuid: UUID, handle: String, hasVideo: Bool = false, completion: ((NSError?) -> Void)?) {
os_log("ProviderDelegate:reportIncomingCall")
// prepare update to send to system
let update = CXCallUpdate()
// add call metadata
update.remoteHandle = CXHandle(type: .generic, value: handle)
update.hasVideo = hasVideo
// use provider to notify system
provider.reportNewIncomingCall(with: uuid, update: update) { error in
// now we are inside reportNewIncomingCall's final argument, a completion block
if error == nil {
// no error, so add call
let call = Call(uuid: uuid, handle: handle)
self.callManager.add(call: call)
}
// execute "completion", the final argument that was passed to outer method reportIncomingCall
// execute if it isn't nil
completion?(error as NSError?)
}
}
}
extension ProviderDelegate: CXProviderDelegate {
func providerDidReset(_ provider: CXProvider) {
os_log("ProviderDelegate:providerDidReset")
// stopAudio()
for call in callManager.calls {
call.end()
}
callManager.removeAllCalls()
}
func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
os_log("cc:ProviderDelegate:didActivate")
print("cc:ProviderDelegate:didActivate %@", audioSession)
// HINT: audio session has to be started here!
// also answer Tox Call -------------
// -- HaXX0r --
// -- HaXX0r --
// -- HaXX0r --
let coord = AppDelegate.shared.coordinator.activeCoordinator
let runcoord = coord as! RunningCoordinator
runcoord.activeSessionCoordinator?.callCoordinator.answerCall(enableVideo: false)
// -- HaXX0r --
// -- HaXX0r --
// -- HaXX0r --
// also answer Tox Call -------------
// startAudio()
}
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
os_log("cc:ProviderDelegate:call-answer %@", action)
guard let call = callManager.callWithUUID(uuid: action.callUUID) else {
action.fail()
return
}
// HINT: audio session has to be configured here!
configureAudioSession()
os_log("cc:ProviderDelegate:call-answer:answer()")
call.answer()
// when processing an action, app should fulfill it or fail
os_log("cc:ProviderDelegate:call-answer:fulfill()")
action.fulfill()
}
func configureAudioSession()
{
os_log("cc:ProviderDelegate:configureAudioSession:start")
let session = AVAudioSession.sharedInstance()
do {
try session.setCategory(AVAudioSessionCategoryPlayAndRecord)
os_log("cc:ProviderDelegate:configureAudioSession:try_001")
try session.setMode(AVAudioSessionModeVoiceChat)
os_log("cc:ProviderDelegate:configureAudioSession:try_002")
// try session.setActive(true)
// os_log("cc:ProviderDelegate:configureAudioSession:try_003")
} catch (let error) {
os_log("cc:ProviderDelegate:configureAudioSession:EE_01")
print("cc:ProviderDelegate:configureAudioSession:Error while configuring audio session: \(error)")
}
os_log("ProviderDelegate:configureAudioSession:end")
}
func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
os_log("ProviderDelegate:call-end %@", action)
guard let call = callManager.callWithUUID(uuid: action.callUUID) else {
action.fail()
return
}
// also decline Tox Call -------------
// -- HaXX0r --
// -- HaXX0r --
// -- HaXX0r --
let coord = AppDelegate.shared.coordinator.activeCoordinator
let runcoord = coord as! RunningCoordinator
runcoord.activeSessionCoordinator?.callCoordinator.declineCall(callWasRemoved: false)
// -- HaXX0r --
// -- HaXX0r --
// -- HaXX0r --
// also decline Tox Call -------------
// stopAudio()
// call.end changes the call's status, allows other classes to react to new state
call.end()
action.fulfill()
callManager.remove(call: call)
}
func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) {
os_log("ProviderDelegate:call-held %@", action)
guard let call = callManager.callWithUUID(uuid: action.callUUID) else {
action.fail()
return
}
call.state = action.isOnHold ? .held : .active
if call.state == .held {
// stopAudio()
} else {
// startAudio()
}
action.fulfill()
}
func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
let call = Call(uuid: action.callUUID, outgoing: true, handle: action.handle.value)
// configure. provider(_:didActivate) will start audio
configureAudioSession()
os_log("cc:ProviderDelegate:call-start %s", action.handle.value)
// set connectedStateChanged as a closure to monitor call lifecycle
call.connectedStateChanged = { [weak self, weak call] in
guard let strongSelf = self, let call = call else { return }
if call.connectedState == .pending {
strongSelf.provider.reportOutgoingCall(with: call.uuid, startedConnectingAt: nil)
} else if call.connectedState == .complete {
strongSelf.provider.reportOutgoingCall(with: call.uuid, connectedAt: nil)
}
}
call.start { [weak self, weak call] success in
guard let strongSelf = self, let call = call else { return }
if success {
action.fulfill()
strongSelf.callManager.add(call: call)
} else {
action.fail()
}
}
}
}

View File

@ -0,0 +1,78 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import UIKit
protocol ChangeAutodownloadImagesControllerDelegate: class {
func changeAutodownloadImagesControllerDidChange(_ controller: ChangeAutodownloadImagesController)
}
class ChangeAutodownloadImagesController: StaticTableController {
weak var delegate: ChangeAutodownloadImagesControllerDelegate?
fileprivate let userDefaults: UserDefaultsManager
fileprivate let selectedStatus: UserDefaultsManager.AutodownloadImages
fileprivate let neverModel = StaticTableDefaultCellModel()
fileprivate let wifiModel = StaticTableDefaultCellModel()
fileprivate let alwaysModel = StaticTableDefaultCellModel()
init(theme: Theme) {
self.userDefaults = UserDefaultsManager()
self.selectedStatus = userDefaults.autodownloadImages
super.init(theme: theme, style: .plain, model: [
[
neverModel,
wifiModel,
alwaysModel,
],
])
updateModels()
title = String(localized: "settings_autodownload_images")
}
required convenience init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
private extension ChangeAutodownloadImagesController {
func updateModels() {
neverModel.value = String(localized: "settings_never")
neverModel.didSelectHandler = changeNever
wifiModel.value = String(localized: "settings_using_wifi")
wifiModel.didSelectHandler = changeUsingWifi
alwaysModel.value = String(localized: "settings_always")
alwaysModel.didSelectHandler = changeAlways
switch selectedStatus {
case .Never:
neverModel.rightImageType = .checkmark
case .UsingWiFi:
wifiModel.rightImageType = .checkmark
case .Always:
alwaysModel.rightImageType = .checkmark
}
}
func changeNever(_: StaticTableBaseCell) {
userDefaults.autodownloadImages = .Never
delegate?.changeAutodownloadImagesControllerDidChange(self)
}
func changeUsingWifi(_: StaticTableBaseCell) {
userDefaults.autodownloadImages = .UsingWiFi
delegate?.changeAutodownloadImagesControllerDidChange(self)
}
func changeAlways(_: StaticTableBaseCell) {
userDefaults.autodownloadImages = .Always
delegate?.changeAutodownloadImagesControllerDidChange(self)
}
}

View File

@ -0,0 +1,254 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import UIKit
import SnapKit
private struct Constants {
static let HorizontalOffset = 40.0
static let ButtonVerticalOffset = 20.0
static let FieldsOffset = 10.0
static let MaxFormWidth = 350.0
}
protocol ChangePasswordControllerDelegate: class {
func changePasswordControllerDidFinishPresenting(_ controller: ChangePasswordController)
}
class ChangePasswordController: KeyboardNotificationController {
weak var delegate: ChangePasswordControllerDelegate?
fileprivate let theme: Theme
fileprivate weak var toxManager: OCTManager!
fileprivate var scrollView: UIScrollView!
fileprivate var containerView: IncompressibleView!
fileprivate var oldPasswordField: ExtendedTextField!
fileprivate var newPasswordField: ExtendedTextField!
fileprivate var repeatPasswordField: ExtendedTextField!
fileprivate var button: RoundedButton!
init(theme: Theme, toxManager: OCTManager) {
self.theme = theme
self.toxManager = toxManager
super.init()
edgesForExtendedLayout = UIRectEdge()
addNavigationButtons()
title = String(localized: "change_password")
}
required convenience init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
loadViewWithBackgroundColor(theme.colorForType(.NormalBackground))
createViews()
installConstraints()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if let old = oldPasswordField {
_ = old.becomeFirstResponder()
}
else if let new = newPasswordField {
_ = new.becomeFirstResponder()
}
}
override func keyboardWillShowAnimated(keyboardFrame frame: CGRect) {
scrollView.contentInset.bottom = frame.size.height
scrollView.scrollIndicatorInsets.bottom = frame.size.height
}
override func keyboardWillHideAnimated(keyboardFrame frame: CGRect) {
scrollView.contentInset.bottom = 0.0
scrollView.scrollIndicatorInsets.bottom = 0.0
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
scrollView.contentSize.width = scrollView.frame.size.width
scrollView.contentSize.height = containerView.frame.maxY
}
}
// MARK: Actions
extension ChangePasswordController {
@objc func cancelButtonPressed() {
delegate?.changePasswordControllerDidFinishPresenting(self)
}
@objc func buttonPressed() {
guard validatePasswordFields() else {
return
}
let oldPassword = oldPasswordField.text!
let newPassword = newPasswordField.text!
let hud = JGProgressHUD(style: .dark)
hud?.show(in: view)
DispatchQueue.global(qos: .default).async { [unowned self] in
let result = self.toxManager.changeEncryptPassword(newPassword, oldPassword: oldPassword)
if result {
let keychainManager = KeychainManager()
if keychainManager.toxPasswordForActiveAccount != nil {
keychainManager.toxPasswordForActiveAccount = newPassword
}
}
DispatchQueue.main.async { [unowned self] in
hud?.dismiss()
if result {
self.delegate?.changePasswordControllerDidFinishPresenting(self)
}
else {
handleErrorWithType(.wrongOldPassword)
}
}
}
}
}
extension ChangePasswordController: ExtendedTextFieldDelegate {
func loginExtendedTextFieldReturnKeyPressed(_ field: ExtendedTextField) {
if field === oldPasswordField {
_ = newPasswordField!.becomeFirstResponder()
}
else if field === newPasswordField {
_ = repeatPasswordField!.becomeFirstResponder()
}
else if field === repeatPasswordField {
buttonPressed()
}
}
}
private extension ChangePasswordController {
func addNavigationButtons() {
navigationItem.leftBarButtonItem = UIBarButtonItem(
barButtonSystemItem: .cancel,
target: self,
action: #selector(ChangePasswordController.cancelButtonPressed))
}
func createViews() {
scrollView = UIScrollView()
view.addSubview(scrollView)
containerView = IncompressibleView()
containerView.backgroundColor = .clear
scrollView.addSubview(containerView)
button = RoundedButton(theme: theme, type: .runningPositive)
button.setTitle(String(localized: "change_password_done"), for: UIControlState())
button.addTarget(self, action: #selector(ChangePasswordController.buttonPressed), for: .touchUpInside)
containerView.addSubview(button)
oldPasswordField = createPasswordFieldWithTitle(String(localized: "old_password"))
newPasswordField = createPasswordFieldWithTitle(String(localized: "new_password"))
repeatPasswordField = createPasswordFieldWithTitle(String(localized: "repeat_password"))
oldPasswordField.returnKeyType = .next
newPasswordField.returnKeyType = .next
repeatPasswordField.returnKeyType = .done
}
func createPasswordFieldWithTitle(_ title: String) -> ExtendedTextField {
let field = ExtendedTextField(theme: theme, type: .normal)
field.delegate = self
field.title = title
field.secureTextEntry = true
containerView.addSubview(field)
return field
}
func installConstraints() {
scrollView.snp.makeConstraints {
$0.edges.equalTo(view)
}
containerView.customIntrinsicContentSize.width = CGFloat(Constants.MaxFormWidth)
containerView.snp.makeConstraints {
$0.top.equalTo(scrollView)
$0.centerX.equalTo(scrollView)
$0.width.lessThanOrEqualTo(Constants.MaxFormWidth)
$0.width.lessThanOrEqualTo(scrollView).offset(-2 * Constants.HorizontalOffset)
}
var topConstraint = containerView.snp.top
if installConstraintsForField(oldPasswordField, topConstraint: topConstraint) {
topConstraint = oldPasswordField!.snp.bottom
}
if installConstraintsForField(newPasswordField, topConstraint: topConstraint) {
topConstraint = newPasswordField!.snp.bottom
}
if installConstraintsForField(repeatPasswordField, topConstraint: topConstraint) {
topConstraint = repeatPasswordField!.snp.bottom
}
button.snp.makeConstraints {
$0.top.equalTo(topConstraint).offset(Constants.ButtonVerticalOffset)
$0.leading.trailing.equalTo(containerView)
$0.bottom.equalTo(containerView)
}
}
/**
Returns true if field exists, no otherwise.
*/
func installConstraintsForField(_ field: ExtendedTextField?, topConstraint: ConstraintItem) -> Bool {
guard let field = field else {
return false
}
field.snp.makeConstraints {
$0.top.equalTo(topConstraint).offset(Constants.FieldsOffset)
$0.leading.trailing.equalTo(containerView)
}
return true
}
func validatePasswordFields() -> Bool {
guard let oldText = oldPasswordField.text, !oldText.isEmpty else {
handleErrorWithType(.passwordIsEmpty)
return false
}
guard let newText = newPasswordField.text, !newText.isEmpty else {
handleErrorWithType(.passwordIsEmpty)
return false
}
guard let repeatText = repeatPasswordField.text, !repeatText.isEmpty else {
handleErrorWithType(.passwordIsEmpty)
return false
}
guard newText == repeatText else {
handleErrorWithType(.passwordsDoNotMatch)
return false
}
return true
}
}

View File

@ -0,0 +1,111 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import UIKit
protocol ChangePinTimeoutControllerDelegate: class {
func changePinTimeoutControllerDone(_ controller: ChangePinTimeoutController)
}
class ChangePinTimeoutController: StaticTableController {
weak var delegate: ChangePinTimeoutControllerDelegate?
fileprivate weak var submanagerObjects: OCTSubmanagerObjects!
fileprivate let immediatelyModel = StaticTableDefaultCellModel()
fileprivate let seconds30Model = StaticTableDefaultCellModel()
fileprivate let minute1Model = StaticTableDefaultCellModel()
fileprivate let minute2Model = StaticTableDefaultCellModel()
fileprivate let minute5Model = StaticTableDefaultCellModel()
init(theme: Theme, submanagerObjects: OCTSubmanagerObjects) {
self.submanagerObjects = submanagerObjects
super.init(theme: theme, style: .plain, model: [
[
immediatelyModel,
seconds30Model,
minute1Model,
minute2Model,
minute5Model,
],
])
updateModels()
title = String(localized: "pin_lock_timeout")
}
required convenience init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
private extension ChangePinTimeoutController {
func updateModels() {
let settings = submanagerObjects.getProfileSettings()
immediatelyModel.value = String(localized: "pin_lock_immediately")
immediatelyModel.didSelectHandler = immediatelyHandler
immediatelyModel.rightImageType = .none
seconds30Model.value = String(localized: "pin_lock_30_seconds")
seconds30Model.didSelectHandler = seconds30Handler
seconds30Model.rightImageType = .none
minute1Model.value = String(localized: "pin_lock_1_minute")
minute1Model.didSelectHandler = minute1Handler
minute1Model.rightImageType = .none
minute2Model.value = String(localized: "pin_lock_2_minutes")
minute2Model.didSelectHandler = minute2Handler
minute2Model.rightImageType = .none
minute5Model.value = String(localized: "pin_lock_5_minutes")
minute5Model.didSelectHandler = minute5Handler
minute5Model.rightImageType = .none
switch settings.lockTimeout {
case .Immediately:
immediatelyModel.rightImageType = .checkmark
case .Seconds30:
seconds30Model.rightImageType = .checkmark
case .Minute1:
minute1Model.rightImageType = .checkmark
case .Minute2:
minute2Model.rightImageType = .checkmark
case .Minute5:
minute5Model.rightImageType = .checkmark
}
}
func immediatelyHandler(_: StaticTableBaseCell) {
selectedTimeout(.Immediately)
}
func seconds30Handler(_: StaticTableBaseCell) {
selectedTimeout(.Seconds30)
}
func minute1Handler(_: StaticTableBaseCell) {
selectedTimeout(.Minute1)
}
func minute2Handler(_: StaticTableBaseCell) {
selectedTimeout(.Minute2)
}
func minute5Handler(_: StaticTableBaseCell) {
selectedTimeout(.Minute5)
}
func selectedTimeout(_ timeout: ProfileSettings.LockTimeout) {
let settings = submanagerObjects.getProfileSettings()
settings.lockTimeout = timeout
submanagerObjects.saveProfileSettings(settings)
delegate?.changePinTimeoutControllerDone(self)
}
}

View File

@ -0,0 +1,81 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import UIKit
protocol ChangeUserStatusControllerDelegate: class {
func changeUserStatusController(_ controller: ChangeUserStatusController, selectedStatus: OCTToxUserStatus)
}
class ChangeUserStatusController: StaticTableController {
weak var delegate: ChangeUserStatusControllerDelegate?
fileprivate let selectedStatus: OCTToxUserStatus
fileprivate let onlineModel = StaticTableDefaultCellModel()
fileprivate let awayModel = StaticTableDefaultCellModel()
fileprivate let busyModel = StaticTableDefaultCellModel()
init(theme: Theme, selectedStatus: OCTToxUserStatus) {
self.selectedStatus = selectedStatus
super.init(theme: theme, style: .plain, model: [
[
onlineModel,
awayModel,
busyModel,
],
])
updateModels()
title = String(localized: "status_title")
}
required convenience init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
private extension ChangeUserStatusController {
func updateModels() {
// Hardcoding any connected status to show only online/away/busy statuses here.
let online = UserStatus(connectionStatus: OCTToxConnectionStatus.TCP, userStatus: OCTToxUserStatus.none)
let away = UserStatus(connectionStatus: OCTToxConnectionStatus.TCP, userStatus: OCTToxUserStatus.away)
let busy = UserStatus(connectionStatus: OCTToxConnectionStatus.TCP, userStatus: OCTToxUserStatus.busy)
onlineModel.userStatus = online
onlineModel.value = online.toString()
onlineModel.didSelectHandler = changeOnlineStatus
awayModel.userStatus = away
awayModel.value = away.toString()
awayModel.didSelectHandler = changeAwayStatus
busyModel.userStatus = busy
busyModel.value = busy.toString()
busyModel.didSelectHandler = changeBusyStatus
switch selectedStatus {
case .none:
onlineModel.rightImageType = .checkmark
case .away:
awayModel.rightImageType = .checkmark
case .busy:
busyModel.rightImageType = .checkmark
}
}
func changeOnlineStatus(_: StaticTableBaseCell) {
delegate?.changeUserStatusController(self, selectedStatus: .none)
}
func changeAwayStatus(_: StaticTableBaseCell) {
delegate?.changeUserStatusController(self, selectedStatus: .away)
}
func changeBusyStatus(_: StaticTableBaseCell) {
delegate?.changeUserStatusController(self, selectedStatus: .busy)
}
}

View File

@ -0,0 +1,100 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import UIKit
class ChatBaseTextCell: ChatMovableDateCell {
struct Constants {
static let BubbleVerticalOffset = 1.0
static let BubbleHorizontalOffset = 10.0
}
var bubbleNormalBackground: UIColor?
var bubbleView: BubbleView!
override func setupWithTheme(_ theme: Theme, model: BaseCellModel) {
super.setupWithTheme(theme, model: model)
guard let textModel = model as? ChatBaseTextCellModel else {
assert(false, "Wrong model \(model) passed to cell \(self)")
return
}
canBeCopied = true
bubbleView.text = textModel.message
bubbleView.textColor = theme.colorForType(.NormalText)
}
override func createViews() {
super.createViews()
bubbleView = BubbleView()
contentView.addSubview(bubbleView)
}
override func setEditing(_ editing: Bool, animated: Bool) {
super.setEditing(editing, animated: animated)
bubbleView.isUserInteractionEnabled = !editing
}
override func setHighlighted(_ highlighted: Bool, animated: Bool) {
super.setHighlighted(highlighted, animated: animated)
bubbleView.backgroundColor = bubbleNormalBackground
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
if isEditing {
bubbleView.backgroundColor = bubbleNormalBackground
return
}
if selected {
bubbleView.backgroundColor = bubbleNormalBackground?.darkerColor()
}
else {
bubbleView.backgroundColor = bubbleNormalBackground
}
}
}
// Accessibility
extension ChatBaseTextCell {
override var accessibilityValue: String? {
get {
var value = bubbleView.text!
if let sValue = super.accessibilityValue {
value += ", " + sValue
}
return value
}
set {}
}
}
// ChatEditable
extension ChatBaseTextCell {
override func shouldShowMenu() -> Bool {
return true
}
override func menuTargetRect() -> CGRect {
return bubbleView.frame
}
override func willShowMenu() {
super.willShowMenu()
bubbleView.selectable = false
}
override func willHideMenu() {
super.willHideMenu()
bubbleView.selectable = true
}
}

View File

@ -0,0 +1,9 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import Foundation
class ChatBaseTextCellModel: ChatMovableDateCellModel {
var message: String = ""
}

View File

@ -0,0 +1,88 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// ChatBottomStatusViewManager.swift
// Antidote
//
// Created by Dmytro Vorobiov on 26/04/2017.
// Copyright © 2017 dvor. All rights reserved.
//
import Foundation
class ChatBottomStatusViewManager {
fileprivate weak var submanagerObjects: OCTSubmanagerObjects!
fileprivate let friend: OCTFriend?
fileprivate let undeliveredMessages: Results<OCTMessageAbstract>
fileprivate var friendToken: RLMNotificationToken?
fileprivate var undeliveredMessagesToken: RLMNotificationToken?
init(friend: OCTFriend?, messages: Results<OCTMessageAbstract>, submanagerObjects: OCTSubmanagerObjects) {
self.submanagerObjects = submanagerObjects
self.friend = friend
self.undeliveredMessages = messages.undeliveredMessages()
addFriendNotification()
addMessagesNotification()
}
deinit {
friendToken?.invalidate()
undeliveredMessagesToken?.invalidate()
}
}
private extension ChatBottomStatusViewManager {
func addFriendNotification() {
guard let friend = self.friend else {
return
}
friendToken = submanagerObjects.notificationBlock(for: friend) { [unowned self] change in
switch change {
case .initial:
break
case .update:
self.updateTableHeaderView()
case .error(let error):
break
}
}
}
func addMessagesNotification() {
// self.undeliveredMessagesToken = undeliveredMessages.addNotificationBlock { [unowned self] change in
// guard let tableView = self.tableView else {
// return
// }
// switch change {
// case .initial:
// break
// case .update(_, let deletions, let insertions, let modifications):
// tableView.beginUpdates()
// self.updateTableViewWithDeletions(deletions)
// self.updateTableViewWithInsertions(insertions)
// self.updateTableViewWithModifications(modifications)
// self.visibleMessages = self.visibleMessages + insertions.count - deletions.count
// tableView.endUpdates()
// self.updateTableHeaderView()
// if insertions.contains(0) {
// self.handleNewMessage()
// }
// case .error(let error):
// fatalError("\(error)")
// }
// }
}
func updateTableHeaderView() {
}
}

View File

@ -0,0 +1,29 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import Foundation
/**
Chat cell can confirm to this protocol to support editing with UIMenuController.
*/
protocol ChatEditable {
/**
Return true to show menu for given cell, false otherwise.
*/
func shouldShowMenu() -> Bool
/**
Target rect in view to show menu from.
- Returns: rect to show menu from.
*/
func menuTargetRect() -> CGRect
/**
Methods fired when menu is going to be shown/hide.
If you override this methods, you must call super at some point in your implementation.
*/
func willShowMenu()
func willHideMenu()
}

View File

@ -0,0 +1,49 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import Foundation
import SnapKit
fileprivate struct Constants {
static let verticalOffset = 7.0
static let maxLabelWidth: CGFloat = 280.0
}
class ChatFauxOfflineHeaderView: UIView {
fileprivate var label: UILabel!
init(theme: Theme) {
super.init(frame: CGRect.zero)
backgroundColor = theme.colorForType(.NormalBackground)
createViews(theme: theme)
installConstraints()
}
required convenience init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
private extension ChatFauxOfflineHeaderView {
func createViews(theme: Theme) {
label = UILabel()
label.text = String(localized: "chat_pending_faux_offline_messages")
label.font = UIFont.antidoteFontWithSize(14.0, weight: .medium)
label.textAlignment = .center
label.textColor = theme.colorForType(.ChatInformationText)
label.numberOfLines = 0
label.preferredMaxLayoutWidth = Constants.maxLabelWidth
addSubview(label)
}
func installConstraints() {
label.snp.makeConstraints {
$0.top.equalTo(self).offset(Constants.verticalOffset)
$0.bottom.equalTo(self).offset(-Constants.verticalOffset)
$0.centerX.equalTo(self)
$0.width.equalTo(Constants.maxLabelWidth)
}
}
}

View File

@ -0,0 +1,178 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import UIKit
import SnapKit
class ChatGenericFileCell: ChatMovableDateCell {
var loadingView: LoadingImageView!
var cancelButton: UIButton!
var retryButton: UIButton!
var progressObject: ChatProgressProtocol? {
didSet {
progressObject?.updateProgress = { [weak self] (progress: Float) -> Void in
self?.updateProgress(CGFloat(progress))
}
progressObject?.updateEta = { [weak self] (eta: CFTimeInterval, bytesPerSecond: OCTToxFileSize) -> Void in
self?.updateEta(String(timeInterval: eta))
self?.updateBytesPerSecond(bytesPerSecond)
}
}
}
var state: ChatGenericFileCellModel.State = .waitingConfirmation
var startLoadingHandle: (() -> Void)?
var cancelHandle: (() -> Void)?
var retryHandle: (() -> Void)?
var pauseOrResumeHandle: (() -> Void)?
var openHandle: (() -> Void)?
/**
This method should be called after setupWithTheme:model:
*/
func setButtonImage(_ image: UIImage) {
let square: UIImage
canBeCopied = true
if image.size.width == image.size.height {
square = image
}
else {
let side = min(image.size.width, image.size.height)
let x = (image.size.width - side) / 2
let y = (image.size.height - side) / 2
let rect = CGRect(x: x, y: y, width: side, height: side)
square = image.cropWithRect(rect)
}
loadingView.imageButton.setBackgroundImage(square, for: UIControlState())
if state == .waitingConfirmation || state == .done {
loadingView.centerImageView.image = nil
}
}
override func setupWithTheme(_ theme: Theme, model: BaseCellModel) {
super.setupWithTheme(theme, model: model)
guard let fileModel = model as? ChatGenericFileCellModel else {
assert(false, "Wrong model \(model) passed to cell \(self)")
return
}
state = fileModel.state
startLoadingHandle = fileModel.startLoadingHandle
cancelHandle = fileModel.cancelHandle
retryHandle = fileModel.retryHandle
pauseOrResumeHandle = fileModel.pauseOrResumeHandle
openHandle = fileModel.openHandle
canBeCopied = false
switch state {
case .loading:
loadingView.centerImageView.image = UIImage.templateNamed("chat-file-pause-big")
case .paused:
loadingView.centerImageView.image = UIImage.templateNamed("chat-file-play-big")
case .waitingConfirmation:
fallthrough
case .cancelled:
fallthrough
case .done:
var fileExtension: String? = nil
if let fileName = fileModel.fileName {
fileExtension = (fileName as NSString).pathExtension
}
loadingView.setImageWithUti(fileModel.fileUTI, fileExtension: fileExtension)
}
updateViewsWithState(fileModel.state, fileModel: fileModel)
loadingView.imageButton.setImage(nil, for: UIControlState())
let backgroundColor = theme.colorForType(.FileImageBackgroundActive)
let backgroundImage = UIImage.imageWithColor(backgroundColor, size: CGSize(width: 1.0, height: 1.0))
loadingView.imageButton.setBackgroundImage(backgroundImage, for: UIControlState())
loadingView.progressView.backgroundLineColor = theme.colorForType(.FileImageAcceptButtonTint).withAlphaComponent(0.3)
loadingView.progressView.lineColor = theme.colorForType(.FileImageAcceptButtonTint)
loadingView.centerImageView.tintColor = theme.colorForType(.FileImageAcceptButtonTint)
loadingView.topLabel.textColor = theme.colorForType(.FileImageCancelledText)
loadingView.bottomLabel.textColor = theme.colorForType(.FileImageCancelledText)
cancelButton.tintColor = theme.colorForType(.FileImageCancelButtonTint)
retryButton.tintColor = theme.colorForType(.FileImageCancelButtonTint)
}
override func createViews() {
super.createViews()
loadingView = LoadingImageView()
loadingView.pressedHandle = loadingViewPressed
let cancelImage = UIImage.templateNamed("chat-file-cancel")
cancelButton = UIButton()
cancelButton.setImage(cancelImage, for: UIControlState())
cancelButton.addTarget(self, action: #selector(ChatGenericFileCell.cancelButtonPressed), for: .touchUpInside)
let retryImage = UIImage.templateNamed("chat-file-retry")
retryButton = UIButton()
retryButton.setImage(retryImage, for: UIControlState())
retryButton.addTarget(self, action: #selector(ChatGenericFileCell.retryButtonPressed), for: .touchUpInside)
}
override func setEditing(_ editing: Bool, animated: Bool) {
super.setEditing(editing, animated: animated)
loadingView.isUserInteractionEnabled = !editing
cancelButton.isUserInteractionEnabled = !editing
retryButton.isUserInteractionEnabled = !editing
}
func updateProgress(_ progress: CGFloat) {
loadingView.progressView.progress = progress
}
func updateEta(_ eta: String) {
loadingView.bottomLabel.text = eta
}
func updateBytesPerSecond(_ bytesPerSecond: OCTToxFileSize) {}
@objc func cancelButtonPressed() {
cancelHandle?()
}
@objc func retryButtonPressed() {
retryHandle?()
}
/// Override in subclass
func updateViewsWithState(_ state: ChatGenericFileCellModel.State, fileModel: ChatGenericFileCellModel) {}
/// Override in subclass
func loadingViewPressed() {}
}
// ChatEditable
extension ChatGenericFileCell {
override func shouldShowMenu() -> Bool {
return true
}
override func menuTargetRect() -> CGRect {
return loadingView.frame
}
}

View File

@ -0,0 +1,26 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import Foundation
class ChatGenericFileCellModel: ChatMovableDateCellModel {
enum State {
case waitingConfirmation
case loading
case paused
case cancelled
case done
}
var state: State = .waitingConfirmation
var fileName: String?
var fileSize: String?
var fileUTI: String?
var startLoadingHandle: (() -> Void)?
var cancelHandle: (() -> Void)?
var retryHandle: (() -> Void)?
var pauseOrResumeHandle: (() -> Void)?
var openHandle: (() -> Void)?
}

View File

@ -0,0 +1,86 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import UIKit
import SnapKit
private struct Constants {
static let LeftOffset = 20.0
static let ImageViewToLabelOffset = 5.0
static let ImageViewYOffset = -1.0
static let VerticalOffset = 8.0
}
class ChatIncomingCallCell: ChatMovableDateCell {
fileprivate var callImageView: UIImageView!
fileprivate var label: UILabel!
override func setupWithTheme(_ theme: Theme, model: BaseCellModel) {
super.setupWithTheme(theme, model: model)
guard let incomingModel = model as? ChatIncomingCallCellModel else {
assert(false, "Wrong model \(model) passed to cell \(self)")
return
}
label.textColor = theme.colorForType(.ChatListCellMessage)
callImageView.tintColor = theme.colorForType(.LinkText)
if incomingModel.answered {
label.text = String(localized: "chat_call_message") + String(timeInterval: incomingModel.callDuration)
}
else {
label.text = String(localized: "chat_missed_call_message")
}
}
override func createViews() {
super.createViews()
let image = UIImage.templateNamed("start-call-small")
callImageView = UIImageView(image: image)
contentView.addSubview(callImageView)
label = UILabel()
label.font = UIFont.antidoteFontWithSize(16.0, weight: .light)
contentView.addSubview(label)
}
override func installConstraints() {
super.installConstraints()
callImageView.snp.makeConstraints {
$0.centerY.equalTo(label).offset(Constants.ImageViewYOffset)
$0.leading.equalTo(contentView).offset(Constants.LeftOffset)
}
label.snp.makeConstraints {
$0.top.equalTo(contentView).offset(Constants.VerticalOffset)
$0.bottom.equalTo(contentView).offset(-Constants.VerticalOffset)
$0.leading.equalTo(callImageView.snp.trailing).offset(Constants.ImageViewToLabelOffset)
}
}
}
// Accessibility
extension ChatIncomingCallCell {
override var accessibilityLabel: String? {
get {
return label.text
}
set {}
}
}
// ChatEditable
extension ChatIncomingCallCell {
override func shouldShowMenu() -> Bool {
return true
}
override func menuTargetRect() -> CGRect {
return label.frame
}
}

View File

@ -0,0 +1,10 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import Foundation
class ChatIncomingCallCellModel: ChatMovableDateCellModel {
var callDuration: TimeInterval = 0
var answered: Bool = true
}

View File

@ -0,0 +1,88 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import UIKit
import SnapKit
private struct Constants {
static let BigOffset = 20.0
static let SmallOffset = 8.0
static let ImageButtonSize = 180.0
static let CloseButtonSize = 25.0
}
class ChatIncomingFileCell: ChatGenericFileCell {
override func setButtonImage(_ image: UIImage) {
super.setButtonImage(image)
loadingView.bottomLabel.isHidden = true
}
override func createViews() {
super.createViews()
contentView.addSubview(loadingView)
contentView.addSubview(cancelButton)
}
override func installConstraints() {
super.installConstraints()
loadingView.snp.makeConstraints {
$0.leading.equalTo(contentView).offset(Constants.BigOffset)
$0.top.equalTo(contentView).offset(Constants.SmallOffset)
$0.bottom.equalTo(contentView).offset(-Constants.SmallOffset)
$0.size.equalTo(Constants.ImageButtonSize)
}
cancelButton.snp.makeConstraints {
$0.leading.equalTo(loadingView.snp.trailing).offset(Constants.SmallOffset)
$0.top.equalTo(loadingView)
$0.size.equalTo(Constants.CloseButtonSize)
}
}
override func updateViewsWithState(_ state: ChatGenericFileCellModel.State, fileModel: ChatGenericFileCellModel) {
loadingView.imageButton.isUserInteractionEnabled = true
loadingView.progressView.isHidden = true
loadingView.topLabel.isHidden = false
loadingView.topLabel.text = fileModel.fileName
loadingView.bottomLabel.text = fileModel.fileSize
loadingView.bottomLabel.isHidden = false
cancelButton.isHidden = false
switch state {
case .waitingConfirmation:
loadingView.centerImageView.image = UIImage.templateNamed("chat-file-download-big")
case .loading:
loadingView.progressView.isHidden = false
case .paused:
break
case .cancelled:
loadingView.setCancelledImage()
loadingView.imageButton.isUserInteractionEnabled = false
cancelButton.isHidden = true
loadingView.bottomLabel.text = String(localized: "chat_file_cancelled")
case .done:
cancelButton.isHidden = true
loadingView.topLabel.isHidden = true
loadingView.bottomLabel.text = fileModel.fileName
}
}
override func loadingViewPressed() {
switch state {
case .waitingConfirmation:
startLoadingHandle?()
case .loading:
pauseOrResumeHandle?()
case .paused:
pauseOrResumeHandle?()
case .cancelled:
break
case .done:
openHandle?()
}
}
}

View File

@ -0,0 +1,8 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import Foundation
class ChatIncomingFileCellModel: ChatGenericFileCellModel {
}

View File

@ -0,0 +1,37 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import UIKit
import SnapKit
class ChatIncomingTextCell: ChatBaseTextCell {
override func setupWithTheme(_ theme: Theme, model: BaseCellModel) {
super.setupWithTheme(theme, model: model)
bubbleNormalBackground = theme.colorForType(.ChatIncomingBubble)
bubbleView.backgroundColor = bubbleNormalBackground
bubbleView.tintColor = theme.colorForType(.LinkText)
bubbleView.font = UIFont.preferredFont(forTextStyle: .body)
}
override func installConstraints() {
super.installConstraints()
bubbleView.snp.makeConstraints {
$0.top.equalTo(contentView).offset(ChatBaseTextCell.Constants.BubbleVerticalOffset)
$0.bottom.equalTo(contentView).offset(-ChatBaseTextCell.Constants.BubbleVerticalOffset)
$0.leading.equalTo(contentView).offset(ChatBaseTextCell.Constants.BubbleHorizontalOffset)
}
}
}
// Accessibility
extension ChatIncomingTextCell {
override var accessibilityLabel: String? {
get {
return String(localized: "accessibility_incoming_message_label")
}
set {}
}
}

View File

@ -0,0 +1,217 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import UIKit
import SnapKit
private struct Constants {
static let TopBorderHeight = 0.5
static let Offset: CGFloat = 5.0
static let CameraHorizontalOffset: CGFloat = 10.0
static let CameraBottomOffset: CGFloat = -10.0
static let TextViewMinHeight: CGFloat = 35.0
static let MIN_MYHEIGHT: CGFloat = 45
static let MAX_MYHEIGHT: CGFloat = 90
static let MARGIN_MYHEIGHT: CGFloat = 5
static let MAX_TEXT_INPUT_CHARS = 1000
}
protocol ChatInputViewDelegate: class {
func chatInputViewCameraButtonPressed(_ view: ChatInputView, cameraView: UIView)
func chatInputViewSendButtonPressed(_ view: ChatInputView)
func chatInputViewTextDidChange(_ view: ChatInputView)
}
class ChatInputView: UIView {
weak var delegate: ChatInputViewDelegate?
var text: String {
get {
return textView.text
}
set {
textView.text = newValue
updateViews()
}
}
var maxHeight: CGFloat {
didSet {
updateViews()
}
}
var cameraButtonEnabled: Bool = true{
didSet {
updateViews()
}
}
fileprivate var topBorder: UIView!
fileprivate var cameraButton: UIButton!
fileprivate var textView: UITextView!
fileprivate var sendButton: UIButton!
fileprivate var myHeight: Constraint!
fileprivate var didconstraint = 0
init(theme: Theme) {
self.maxHeight = 0.0
super.init(frame: CGRect.zero)
backgroundColor = theme.colorForType(.ChatInputBackground)
createViews(theme)
installConstraints()
updateViews()
}
required convenience init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func becomeFirstResponder() -> Bool {
return textView.becomeFirstResponder()
}
override func resignFirstResponder() -> Bool {
return textView.resignFirstResponder()
}
}
// MARK: Actions
extension ChatInputView {
@objc func cameraButtonPressed() {
delegate?.chatInputViewCameraButtonPressed(self, cameraView: cameraButton)
}
@objc func sendButtonPressed() {
delegate?.chatInputViewSendButtonPressed(self)
updateTextviewHeight(textView)
}
}
extension ChatInputView: UITextViewDelegate {
func textViewDidChange(_ textView: UITextView) {
updateViews()
updateTextviewHeight(textView)
delegate?.chatInputViewTextDidChange(self)
}
override func didMoveToWindow() {
updateTextviewHeight(textView)
}
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
// get the current text, or use an empty string if that failed
let currentText = textView.text ?? ""
// attempt to read the range they are trying to change, or exit if we can't
guard let stringRange = Range(range, in: currentText) else { return false }
// add their new text to the existing text
let updatedText = currentText.replacingCharacters(in: stringRange, with: text)
// make sure the result is under MAX_TEXT_INPUT_CHARS characters
return updatedText.count <= Constants.MAX_TEXT_INPUT_CHARS
}
}
private extension ChatInputView {
func createViews(_ theme: Theme) {
topBorder = UIView()
topBorder.backgroundColor = theme.colorForType(.SeparatorsAndBorders)
addSubview(topBorder)
let cameraImage = UIImage.templateNamed("chat-camera")
cameraButton = UIButton()
cameraButton.setImage(cameraImage, for: UIControlState())
cameraButton.tintColor = theme.colorForType(.LinkText)
cameraButton.addTarget(self, action: #selector(ChatInputView.cameraButtonPressed), for: .touchUpInside)
cameraButton.setContentCompressionResistancePriority(UILayoutPriority.required, for: .horizontal)
addSubview(cameraButton)
textView = UITextView()
textView.delegate = self
textView.font = UIFont.systemFont(ofSize: 16.0)
textView.backgroundColor = theme.colorForType(.NormalBackground)
textView.layer.cornerRadius = 5.0
textView.layer.borderWidth = 0.5
textView.layer.borderColor = theme.colorForType(.SeparatorsAndBorders).cgColor
textView.layer.masksToBounds = true
textView.setContentHuggingPriority(UILayoutPriority(rawValue: 0.0), for: .horizontal)
textView.autocapitalizationType = .none
addSubview(textView)
sendButton = UIButton(type: .system)
sendButton.setTitle(String(localized: "chat_send_button"), for: UIControlState())
sendButton.titleLabel?.font = UIFont.antidoteFontWithSize(16.0, weight: .bold)
sendButton.addTarget(self, action: #selector(ChatInputView.sendButtonPressed), for: .touchUpInside)
sendButton.setContentCompressionResistancePriority(UILayoutPriority.required, for: .horizontal)
addSubview(sendButton)
}
func installConstraints() {
topBorder.snp.makeConstraints {
$0.top.leading.trailing.equalTo(self)
$0.height.equalTo(Constants.TopBorderHeight)
}
cameraButton.snp.makeConstraints {
$0.leading.equalTo(self).offset(Constants.CameraHorizontalOffset)
$0.bottom.equalTo(self).offset(Constants.CameraBottomOffset)
}
textView.snp.makeConstraints {
$0.leading.equalTo(cameraButton.snp.trailing).offset(Constants.CameraHorizontalOffset)
$0.top.equalTo(self).offset(Constants.Offset)
$0.bottom.equalTo(self).offset(-Constants.Offset)
$0.height.greaterThanOrEqualTo(Constants.TextViewMinHeight)
}
sendButton.snp.makeConstraints {
$0.leading.equalTo(textView.snp.trailing).offset(Constants.Offset)
$0.trailing.equalTo(self).offset(-Constants.Offset)
$0.bottom.equalTo(self).offset(-Constants.Offset)
}
}
func updateTextviewHeight(_ t : UITextView)
{
if (self.didconstraint == 1)
{
self.myHeight.uninstall()
self.didconstraint = 0
}
let text_needs_size = t.sizeThatFits(
CGSize(width: t.frame.size.width,
height: CGFloat.greatestFiniteMagnitude))
var new_height = text_needs_size.height + Constants.MARGIN_MYHEIGHT
if (text_needs_size.height > Constants.MAX_MYHEIGHT)
{
new_height = Constants.MAX_MYHEIGHT
}
else if (text_needs_size.height < Constants.MIN_MYHEIGHT) {
new_height = Constants.MIN_MYHEIGHT
}
if (self.didconstraint == 0) {
self.didconstraint = 1
self.snp.makeConstraints {
self.myHeight = $0.height.equalTo(new_height).constraint
}
}
}
func updateViews() {
textView.isScrollEnabled = true
textView.autocapitalizationType = .none
cameraButton.isEnabled = cameraButtonEnabled
sendButton.isEnabled = !textView.text.isEmpty
}
}

View File

@ -0,0 +1,195 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import Foundation
import MobileCoreServices
import Photos
import os
fileprivate struct Constants {
static let inactivityTimeout = 4.0
}
/**
Manager responsible for sending messages and files, updating typing notification,
saving entered text in database.
*/
class ChatInputViewManager: NSObject {
fileprivate var chat: OCTChat!
fileprivate weak var inputView: ChatInputView?
fileprivate weak var submanagerChats: OCTSubmanagerChats!
fileprivate weak var submanagerFiles: OCTSubmanagerFiles!
fileprivate weak var submanagerObjects: OCTSubmanagerObjects!
fileprivate weak var presentingViewController: UIViewController!
fileprivate var inactivityTimer: Timer?
init(inputView: ChatInputView,
chat: OCTChat,
submanagerChats: OCTSubmanagerChats,
submanagerFiles: OCTSubmanagerFiles,
submanagerObjects: OCTSubmanagerObjects,
presentingViewController: UIViewController) {
self.chat = chat
self.inputView = inputView
self.submanagerChats = submanagerChats
self.submanagerFiles = submanagerFiles
self.submanagerObjects = submanagerObjects
self.presentingViewController = presentingViewController
super.init()
inputView.delegate = self
inputView.text = chat.enteredText ?? ""
}
deinit {
endUserInteraction()
}
}
extension ChatInputViewManager: ChatInputViewDelegate {
func chatInputViewCameraButtonPressed(_ view: ChatInputView, cameraView: UIView) {
let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
alert.popoverPresentationController?.sourceView = cameraView
alert.popoverPresentationController?.sourceRect = CGRect(x: cameraView.frame.size.width / 2, y: cameraView.frame.size.height / 2, width: 1.0, height: 1.0)
func addAction(title: String, sourceType: UIImagePickerControllerSourceType) {
if UIImagePickerController.isSourceTypeAvailable(sourceType) {
alert.addAction(UIAlertAction(title: title, style: .default) { [unowned self] _ -> Void in
let controller = UIImagePickerController()
controller.delegate = self
controller.sourceType = sourceType
controller.mediaTypes = [kUTTypeImage as String, kUTTypeMovie as String]
controller.videoQuality = .typeHigh
self.presentingViewController.present(controller, animated: true, completion: nil)
})
}
}
addAction(title: String(localized: "photo_from_camera"), sourceType: .camera)
addAction(title: String(localized: "photo_from_photo_library"), sourceType: .photoLibrary)
alert.addAction(UIAlertAction(title: String(localized: "alert_cancel"), style: .cancel, handler: nil))
presentingViewController.present(alert, animated: true, completion: nil)
}
func chatInputViewSendButtonPressed(_ view: ChatInputView) {
// HINT: call OCTSubmanagerChatsImpl.m -> sendMessageToChat()
submanagerChats.sendMessage(to: chat, text: view.text, type: .normal, successBlock: nil, failureBlock: nil)
DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
os_log("PUSH:10_seconds")
self.submanagerChats.sendMessagePush(to: self.chat)
}
view.text = ""
endUserInteraction()
}
func chatInputViewTextDidChange(_ view: ChatInputView) {
try? submanagerChats.setIsTyping(true, in: chat)
inactivityTimer?.invalidate()
inactivityTimer = Timer.scheduledTimer(timeInterval: Constants.inactivityTimeout, closure: {[weak self] _ -> Void in
self?.endUserInteraction()
}, repeats: false)
}
}
extension ChatInputViewManager: UIImagePickerControllerDelegate {
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
presentingViewController.dismiss(animated: true, completion: nil)
guard let type = info[UIImagePickerControllerMediaType] as? String else {
return
}
let typeImage = kUTTypeImage as String
let typeMovie = kUTTypeMovie as String
switch type {
case typeImage:
sendImage(imagePickerInfo: info)
case typeMovie:
sendMovie(imagePickerInfo: info)
default:
return
}
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
presentingViewController.dismiss(animated: true, completion: nil)
}
}
extension ChatInputViewManager: UINavigationControllerDelegate {}
fileprivate extension ChatInputViewManager {
func endUserInteraction() {
try? submanagerChats.setIsTyping(false, in: chat)
inactivityTimer?.invalidate()
if let inputView = inputView {
submanagerObjects.change(chat, enteredText: inputView.text)
}
}
func sendImage(imagePickerInfo: [String : Any]) {
guard let image = imagePickerInfo[UIImagePickerControllerOriginalImage] as? UIImage else {
return
}
guard let data = UIImageJPEGRepresentation(image, 0.9) else {
return
}
var fileName: String? = fileNameFromImageInfo(imagePickerInfo)
if fileName == nil {
let dateString = DateFormatter(type: .dateAndTime).string(from: Date())
fileName = "Photo \(dateString).jpg".replacingOccurrences(of: "/", with: "-")
}
submanagerFiles.send(data, withFileName: fileName!, to: chat) { (error: Error) in
handleErrorWithType(.sendFileToFriend, error: error as NSError)
}
}
func sendMovie(imagePickerInfo: [String : Any]) {
guard let url = imagePickerInfo[UIImagePickerControllerMediaURL] as? URL else {
return
}
submanagerFiles.sendFile(atPath: url.path, moveToUploads: true, to: chat) { (error: Error) in
handleErrorWithType(.sendFileToFriend, error: error as NSError)
}
}
func fileNameFromImageInfo(_ info: [String: Any]) -> String? {
guard let url = info[UIImagePickerControllerReferenceURL] as? URL else {
return nil
}
let fetchResult = PHAsset.fetchAssets(withALAssetURLs: [url], options: nil)
guard let asset = fetchResult.firstObject else {
return nil
}
if #available(iOS 9.0, *) {
if let resource = PHAssetResource.assetResources(for: asset).first {
return resource.originalFilename
}
} else {
// Fallback on earlier versions
if let name = asset.value(forKey: "filename") as? String {
return name
}
}
return nil
}
}

160
Antidote/ChatListCell.swift Normal file
View File

@ -0,0 +1,160 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import UIKit
import SnapKit
class ChatListCell: BaseCell {
struct Constants {
static let AvatarSize = 40.0
static let AvatarLeftOffset = 10.0
static let AvatarRightOffset = 16.0
static let NicknameLabelHeight = 22.0
static let MessageLabelHeight = 22.0
static let NicknameToDateMinOffset = 5.0
static let DateToArrowOffset = 5.0
static let RightOffset = -7.0
static let VerticalOffset = 3.0
}
fileprivate var avatarView: ImageViewWithStatus!
fileprivate var nicknameLabel: UILabel!
fileprivate var messageLabel: UILabel!
fileprivate var dateLabel: UILabel!
fileprivate var arrowImageView: UIImageView!
override func setupWithTheme(_ theme: Theme, model: BaseCellModel) {
super.setupWithTheme(theme, model: model)
guard let chatModel = model as? ChatListCellModel else {
assert(false, "Wrong model \(model) passed to cell \(self)")
return
}
separatorInset.left = CGFloat(Constants.AvatarLeftOffset + Constants.AvatarSize + Constants.AvatarRightOffset)
avatarView.imageView.image = chatModel.avatar
avatarView.userStatusView.theme = theme
avatarView.userStatusView.userStatus = chatModel.status
avatarView.userStatusView.connectionStatus = chatModel.connectionstatus
nicknameLabel.text = chatModel.nickname
nicknameLabel.textColor = theme.colorForType(.NormalText)
messageLabel.text = chatModel.message
messageLabel.textColor = theme.colorForType(.ChatListCellMessage)
dateLabel.text = chatModel.dateText
dateLabel.textColor = theme.colorForType(.ChatListCellMessage)
backgroundColor = chatModel.isUnread ? theme.colorForType(.ChatListCellUnreadBackground) : .clear
if (chatModel.isUnread) {
arrowImageView.backgroundColor = theme.colorForType(.ChatListCellUnreadArrowBackground)
} else {
arrowImageView.backgroundColor = .clear
}
// HINT: make the arrow image view a nice circle shape
arrowImageView.layer.cornerRadius = arrowImageView.frame.height / 2
}
override func createViews() {
super.createViews()
avatarView = ImageViewWithStatus()
contentView.addSubview(avatarView)
nicknameLabel = UILabel()
nicknameLabel.font = UIFont.systemFont(ofSize: 18.0)
contentView.addSubview(nicknameLabel)
messageLabel = UILabel()
messageLabel.font = UIFont.systemFont(ofSize: 12.0)
contentView.addSubview(messageLabel)
dateLabel = UILabel()
dateLabel.font = UIFont.antidoteFontWithSize(12.0, weight: .light)
contentView.addSubview(dateLabel)
let image = UIImage(named: "right-arrow")!.flippedToCorrectLayout()
arrowImageView = UIImageView(image: image)
arrowImageView.setContentCompressionResistancePriority(UILayoutPriority.required, for: .horizontal)
contentView.addSubview(arrowImageView)
}
override func installConstraints() {
super.installConstraints()
avatarView.snp.makeConstraints {
$0.leading.equalTo(contentView).offset(Constants.AvatarLeftOffset)
$0.centerY.equalTo(contentView)
$0.size.equalTo(Constants.AvatarSize)
}
nicknameLabel.snp.makeConstraints {
$0.leading.equalTo(avatarView.snp.trailing).offset(Constants.AvatarRightOffset)
$0.top.equalTo(contentView).offset(Constants.VerticalOffset)
$0.height.equalTo(Constants.NicknameLabelHeight)
}
messageLabel.snp.makeConstraints {
$0.leading.equalTo(nicknameLabel)
$0.trailing.equalTo(contentView).offset(Constants.RightOffset)
$0.top.equalTo(nicknameLabel.snp.bottom)
$0.bottom.equalTo(contentView).offset(-Constants.VerticalOffset)
$0.height.equalTo(Constants.MessageLabelHeight)
}
dateLabel.snp.makeConstraints {
$0.leading.greaterThanOrEqualTo(nicknameLabel.snp.trailing).offset(Constants.NicknameToDateMinOffset)
$0.top.equalTo(nicknameLabel)
$0.height.equalTo(nicknameLabel)
}
arrowImageView.snp.makeConstraints {
$0.centerY.equalTo(dateLabel)
$0.leading.greaterThanOrEqualTo(dateLabel.snp.trailing).offset(Constants.DateToArrowOffset)
$0.trailing.equalTo(contentView).offset(Constants.RightOffset)
}
}
}
// Accessibility
extension ChatListCell {
override var isAccessibilityElement: Bool {
get {
return true
}
set {}
}
override var accessibilityLabel: String? {
get {
var label = nicknameLabel.text ?? ""
label += ", " + avatarView.userStatusView.userStatus.toString()
return label
}
set {}
}
override var accessibilityValue: String? {
get {
return messageLabel.text! + ", " + dateLabel.text!
}
set {}
}
override var accessibilityTraits: UIAccessibilityTraits {
get {
return UIAccessibilityTraitSelected
}
set {}
}
}

View File

@ -0,0 +1,18 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import Foundation
class ChatListCellModel: BaseCellModel {
var avatar: UIImage?
var nickname: String = ""
var message: String = ""
var dateText: String = ""
var status: UserStatus = .offline
var connectionstatus: ConnectionStatus = .none
var isUnread: Bool = false
}

View File

@ -0,0 +1,107 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import Foundation
protocol ChatListControllerDelegate: class {
func chatListController(_ controller: ChatListController, didSelectChat chat: OCTChat)
}
class ChatListController: UIViewController {
weak var delegate: ChatListControllerDelegate?
fileprivate let theme: Theme
fileprivate weak var submanagerChats: OCTSubmanagerChats!
fileprivate weak var submanagerObjects: OCTSubmanagerObjects!
fileprivate var placeholderLabel: UILabel!
fileprivate var tableManager: ChatListTableManager!
init(theme: Theme, submanagerChats: OCTSubmanagerChats, submanagerObjects: OCTSubmanagerObjects) {
self.theme = theme
self.submanagerChats = submanagerChats
self.submanagerObjects = submanagerObjects
super.init(nibName: nil, bundle: nil)
edgesForExtendedLayout = UIRectEdge()
title = String(localized: "chats_title")
}
required convenience init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
loadViewWithBackgroundColor(theme.colorForType(.NormalBackground))
createTableView()
createPlaceholderView()
installConstraints()
updateViewsVisibility()
}
override func setEditing(_ editing: Bool, animated: Bool) {
super.setEditing(editing, animated: animated)
tableManager.tableView.setEditing(editing, animated: animated)
}
}
extension ChatListController: ChatListTableManagerDelegate {
func chatListTableManager(_ manager: ChatListTableManager, didSelectChat chat: OCTChat) {
delegate?.chatListController(self, didSelectChat: chat)
}
func chatListTableManager(_ manager: ChatListTableManager, presentAlertController controller: UIAlertController) {
present(controller, animated: true, completion: nil)
}
func chatListTableManagerWasUpdated(_ manager: ChatListTableManager) {
updateViewsVisibility()
}
}
private extension ChatListController {
func updateViewsVisibility() {
navigationItem.leftBarButtonItem = tableManager.isEmpty ? nil : editButtonItem
placeholderLabel.isHidden = !tableManager.isEmpty
}
func createTableView() {
let tableView = UITableView()
tableView.estimatedRowHeight = 44.0
tableView.backgroundColor = theme.colorForType(.NormalBackground)
tableView.sectionIndexColor = theme.colorForType(.LinkText)
// removing separators on empty lines
tableView.tableFooterView = UIView()
view.addSubview(tableView)
tableView.register(ChatListCell.self, forCellReuseIdentifier: ChatListCell.staticReuseIdentifier)
tableManager = ChatListTableManager(theme: theme, tableView: tableView, submanagerChats: submanagerChats, submanagerObjects: submanagerObjects)
tableManager.delegate = self
}
func createPlaceholderView() {
placeholderLabel = UILabel()
placeholderLabel.text = String(localized: "chat_no_chats")
placeholderLabel.textColor = theme.colorForType(.EmptyScreenPlaceholderText)
placeholderLabel.font = UIFont.antidoteFontWithSize(26.0, weight: .light)
view.addSubview(placeholderLabel)
}
func installConstraints() {
tableManager.tableView.snp.makeConstraints {
$0.edges.equalTo(view)
}
placeholderLabel.snp.makeConstraints {
$0.center.equalTo(view)
$0.size.equalTo(placeholderLabel.sizeThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)))
}
}
}

View File

@ -0,0 +1,222 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import Foundation
protocol ChatListTableManagerDelegate: class {
func chatListTableManager(_ manager: ChatListTableManager, didSelectChat chat: OCTChat)
func chatListTableManager(_ manager: ChatListTableManager, presentAlertController controller: UIAlertController)
func chatListTableManagerWasUpdated(_ manager: ChatListTableManager)
}
class ChatListTableManager: NSObject {
weak var delegate: ChatListTableManagerDelegate?
let tableView: UITableView
var isEmpty: Bool {
get {
return chats.count == 0
}
}
fileprivate let theme: Theme
fileprivate let avatarManager: AvatarManager
fileprivate let dateFormatter: DateFormatter
fileprivate let timeFormatter: DateFormatter
fileprivate weak var submanagerChats: OCTSubmanagerChats!
fileprivate let chats: Results<OCTChat>
fileprivate var chatsToken: RLMNotificationToken?
fileprivate let friends: Results<OCTFriend>
fileprivate var friendsToken: RLMNotificationToken?
init(theme: Theme, tableView: UITableView, submanagerChats: OCTSubmanagerChats, submanagerObjects: OCTSubmanagerObjects) {
self.tableView = tableView
self.theme = theme
self.avatarManager = AvatarManager(theme: theme)
self.dateFormatter = DateFormatter(type: .relativeDate)
self.timeFormatter = DateFormatter(type: .time)
self.submanagerChats = submanagerChats
self.chats = submanagerObjects.chats().sortedResultsUsingProperty("lastActivityDateInterval", ascending: false)
self.friends = submanagerObjects.friends()
super.init()
tableView.delegate = self
tableView.dataSource = self
addNotificationBlocks()
}
deinit {
chatsToken?.invalidate()
friendsToken?.invalidate()
}
}
extension ChatListTableManager: UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var avatarData: Data?
var nickname = String(localized: "contact_deleted")
var connectionStatus = OCTToxConnectionStatus.none
var userStatus = OCTToxUserStatus.none
let chat = chats[indexPath.row]
let friend = chat.friends.lastObject() as? OCTFriend
if let friend = friend {
avatarData = friend.avatarData
nickname = friend.nickname
connectionStatus = friend.connectionStatus
userStatus = friend.status
}
let model = ChatListCellModel()
if let data = avatarData {
model.avatar = UIImage(data: data)
}
else {
model.avatar = avatarManager.avatarFromString(
nickname,
diameter: CGFloat(ChatListCell.Constants.AvatarSize))
}
model.nickname = nickname
model.message = lastMessage(in: chat, friend: friend)
if let date = chat.lastActivityDate() {
model.dateText = dateTextFromDate(date)
}
model.status = UserStatus(connectionStatus: connectionStatus, userStatus: userStatus)
model.connectionstatus = ConnectionStatus(connectionStatus: connectionStatus)
model.isUnread = chat.hasUnreadMessages()
let cell = tableView.dequeueReusableCell(withIdentifier: ChatListCell.staticReuseIdentifier) as! ChatListCell
cell.setupWithTheme(theme, model: model)
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return chats.count
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
let alert = UIAlertController(title: String(localized:"delete_chat_title"), message: nil, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: String(localized: "alert_cancel"), style: .default, handler: nil))
alert.addAction(UIAlertAction(title: String(localized: "alert_delete"), style: .destructive) { [unowned self] _ -> Void in
let chat = self.chats[indexPath.row]
self.submanagerChats.removeAllMessages(in: chat, removeChat: true)
})
delegate?.chatListTableManager(self, presentAlertController: alert)
}
}
}
extension ChatListTableManager: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let chat = self.chats[indexPath.row]
delegate?.chatListTableManager(self, didSelectChat: chat)
}
}
private extension ChatListTableManager {
func addNotificationBlocks() {
chatsToken = chats.addNotificationBlock { [unowned self] change in
switch change {
case .initial:
break
case .update(_, let deletions, let insertions, let modifications):
// TODO: fix me, this is a hack to avoid the crash
self.tableView.reloadData()
self.tableView.beginUpdates()
/*
self.tableView.deleteRows(at: deletions.map { IndexPath(row: $0, section: 0) },
with: .automatic)
self.tableView.insertRows(at: insertions.map { IndexPath(row: $0, section: 0) },
with: .automatic)
self.tableView.reloadRows(at: modifications.map { IndexPath(row: $0, section: 0) },
with: .none)
*/
self.tableView.endUpdates()
self.delegate?.chatListTableManagerWasUpdated(self)
case .error(let error):
fatalError("\(error)")
}
}
friendsToken = friends.addNotificationBlock { [unowned self] change in
switch change {
case .initial:
break
case .update(let friends, _, _, let modifications):
guard let friends = friends else {
break
}
for index in modifications {
let friend = friends[index]
let pathsToUpdate = self.tableView.indexPathsForVisibleRows?.filter {
let chat = self.chats[$0.row]
return Int(chat.friends.index(of: friend)) != NSNotFound
}
if let paths = pathsToUpdate {
// TODO: fix me, this crashes
// self.tableView.reloadRows(at: paths, with: .none)
}
}
case .error(let error):
fatalError("\(error)")
}
}
}
func lastMessage(in chat: OCTChat, friend: OCTFriend?) -> String {
guard let message = chat.lastMessage else {
return ""
}
if let friend = friend, friend.isTyping {
return String(localized: "chat_is_typing_text")
}
else if let text = message.messageText {
return text.text ?? ""
}
else if let file = message.messageFile {
let fileName = file.fileName ?? ""
return String(localized: message.isOutgoing() ? "chat_outgoing_file" : "chat_incoming_file") + " \(fileName)"
}
else if let call = message.messageCall {
switch call.callEvent {
case .answered:
let timeString = String(timeInterval: call.callDuration)
return String(localized: "chat_call_finished") + " - \(timeString)"
case .unanswered:
return message.isOutgoing() ? String(localized: "chat_unanwered_call") : String(localized: "chat_missed_call_message")
}
}
return ""
}
func dateTextFromDate(_ date: Date) -> String {
let isToday = (Calendar.current as NSCalendar).compare(Date(), to: date, toUnitGranularity: .day) == .orderedSame
return isToday ? timeFormatter.string(from: date) : dateFormatter.string(from: date)
}
}

View File

@ -0,0 +1,189 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import UIKit
import SnapKit
protocol ChatMovableDateCellDelegate: class {
func chatMovableDateCellCopyPressed(_ cell: ChatMovableDateCell)
func chatMovableDateCellDeletePressed(_ cell: ChatMovableDateCell)
func chatMovableDateCellMorePressed(_ cell: ChatMovableDateCell)
}
class ChatMovableDateCell: BaseCell {
private static var __once: () = {
var items = UIMenuController.shared.menuItems ?? [UIMenuItem]()
items += [
UIMenuItem(title: String(localized: "chat_more_menu_item"), action: #selector(moreAction))
]
UIMenuController.shared.menuItems = items
}()
weak var delegate: ChatMovableDateCellDelegate?
var canBeCopied = false
/**
Superview for content that should move while panning table to the left.
*/
var movableContentView: UIView!
var movableOffset: CGFloat = 0 {
didSet {
var offset = movableOffset
if (UserDefaultsManager().DateonmessageMode == true) {
offset = 39
}
if #available(iOS 9.0, *) {
if UIView.userInterfaceLayoutDirection(for: self.semanticContentAttribute) == .rightToLeft {
offset = -offset
}
}
if offset > 0.0 {
offset = 0.0
}
let minOffset = -dateLabel.frame.size.width - 5.0
if offset < minOffset {
offset = minOffset
}
movableContentViewLeftConstraint.update(offset: offset)
layoutIfNeeded()
}
}
fileprivate var movableContentViewLeftConstraint: Constraint!
fileprivate var dateLabel: UILabel!
fileprivate var isShowingMenu: Bool = false
fileprivate static var setupOnceToken: Int = 0
override func setupWithTheme(_ theme: Theme, model: BaseCellModel) {
super.setupWithTheme(theme, model: model)
guard let movableModel = model as? ChatMovableDateCellModel else {
assert(false, "Wrong model \(model) passed to cell \(self)")
return
}
_ = ChatMovableDateCell.__once
dateLabel.text = movableModel.dateString
dateLabel.numberOfLines = 0 // --> multiline label
dateLabel.textColor = theme.colorForType(.ChatListCellMessage)
}
override func createViews() {
super.createViews()
movableContentView = UIView()
movableContentView.backgroundColor = .clear
contentView.addSubview(movableContentView)
dateLabel = UILabel()
dateLabel.font = UIFont.antidoteFontWithSize(11.0, weight: .medium)
movableContentView.addSubview(dateLabel)
// Using empty view for multiple selection background.
multipleSelectionBackgroundView = UIView()
}
override func installConstraints() {
super.installConstraints()
movableContentView.snp.makeConstraints {
$0.top.equalTo(contentView)
if (UserDefaultsManager().DateonmessageMode == true) {
movableContentViewLeftConstraint = $0.leading.equalTo(contentView).constraint.update(offset: -39)
} else {
movableContentViewLeftConstraint = $0.leading.equalTo(contentView).constraint
}
$0.size.equalTo(contentView)
}
dateLabel.snp.makeConstraints {
$0.centerY.equalTo(movableContentView)
$0.leading.equalTo(movableContentView.snp.trailing)
}
}
override func setSelected(_ selected: Bool, animated: Bool) {
if !isEditing {
// don't call super in case of editing to avoid background change
return
}
super.setSelected(selected, animated: animated)
}
}
// Accessibility
extension ChatMovableDateCell {
override var isAccessibilityElement: Bool {
get {
return true
}
set {}
}
override var accessibilityValue: String? {
get {
return dateLabel.text!
}
set {}
}
}
extension ChatMovableDateCell: ChatEditable {
// Override in subclass to enable menu
@objc func shouldShowMenu() -> Bool {
return false
}
// Override in subclass to enable menu
@objc func menuTargetRect() -> CGRect {
return CGRect.zero
}
@objc func willShowMenu() {
isShowingMenu = true
}
@objc func willHideMenu() {
isShowingMenu = false
}
}
// Methods to make UIMenuController work.
extension ChatMovableDateCell {
func isMenuActionSupportedByCell(_ action: Selector) -> Bool {
switch action {
case #selector(copy(_:)):
return canBeCopied
case #selector(delete(_:)):
return true
case #selector(moreAction):
return true
default:
return false
}
}
override func copy(_ sender: Any?) {
delegate?.chatMovableDateCellCopyPressed(self)
}
override func delete(_ sender: Any?) {
delegate?.chatMovableDateCellDeletePressed(self)
}
@objc func moreAction() {
delegate?.chatMovableDateCellMorePressed(self)
}
}

View File

@ -0,0 +1,9 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import Foundation
class ChatMovableDateCellModel: BaseCellModel {
var dateString: String = ""
}

View File

@ -0,0 +1,86 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import UIKit
import SnapKit
private struct Constants {
static let RightOffset = -20.0
static let ImageViewToLabelOffset = -5.0
static let ImageViewYOffset = -1.0
static let VerticalOffset = 8.0
}
class ChatOutgoingCallCell: ChatMovableDateCell {
fileprivate var callImageView: UIImageView!
fileprivate var label: UILabel!
override func setupWithTheme(_ theme: Theme, model: BaseCellModel) {
super.setupWithTheme(theme, model: model)
guard let outgoingModel = model as? ChatOutgoingCallCellModel else {
assert(false, "Wrong model \(model) passed to cell \(self)")
return
}
label.textColor = theme.colorForType(.ChatListCellMessage)
callImageView.tintColor = theme.colorForType(.LinkText)
if outgoingModel.answered {
label.text = String(localized: "chat_call_message") + String(timeInterval: outgoingModel.callDuration)
}
else {
label.text = String(localized: "chat_unanwered_call")
}
}
override func createViews() {
super.createViews()
let image = UIImage.templateNamed("start-call-small")
callImageView = UIImageView(image: image)
movableContentView.addSubview(callImageView)
label = UILabel()
label.font = UIFont.antidoteFontWithSize(16.0, weight: .light)
movableContentView.addSubview(label)
}
override func installConstraints() {
super.installConstraints()
callImageView.snp.makeConstraints {
$0.centerY.equalTo(label).offset(Constants.ImageViewYOffset)
$0.trailing.equalTo(label.snp.leading).offset(Constants.ImageViewToLabelOffset)
}
label.snp.makeConstraints {
$0.top.equalTo(contentView).offset(Constants.VerticalOffset)
$0.bottom.equalTo(contentView).offset(-Constants.VerticalOffset)
$0.trailing.equalTo(movableContentView).offset(Constants.RightOffset)
}
}
}
// Accessibility
extension ChatOutgoingCallCell {
override var accessibilityLabel: String? {
get {
return label.text
}
set {}
}
}
// ChatEditable
extension ChatOutgoingCallCell {
override func shouldShowMenu() -> Bool {
return true
}
override func menuTargetRect() -> CGRect {
return label.frame
}
}

View File

@ -0,0 +1,10 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import Foundation
class ChatOutgoingCallCellModel: ChatMovableDateCellModel {
var callDuration: TimeInterval = 0
var answered: Bool = true
}

View File

@ -0,0 +1,98 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import UIKit
import SnapKit
private struct Constants {
static let BigOffset = 20.0
static let SmallOffset = 8.0
static let ImageButtonSize = 180.0
static let CloseButtonSize = 25.0
}
class ChatOutgoingFileCell: ChatGenericFileCell {
override func setButtonImage(_ image: UIImage) {
super.setButtonImage(image)
loadingView.bottomLabel.isHidden = true
if state == .cancelled {
loadingView.bottomLabel.isHidden = false
loadingView.centerImageView.image = nil
}
}
override func createViews() {
super.createViews()
movableContentView.addSubview(loadingView)
movableContentView.addSubview(cancelButton)
movableContentView.addSubview(retryButton)
}
override func installConstraints() {
super.installConstraints()
cancelButton.snp.makeConstraints {
$0.trailing.equalTo(loadingView.snp.leading).offset(-Constants.SmallOffset)
$0.top.equalTo(loadingView)
$0.size.equalTo(Constants.CloseButtonSize)
}
retryButton.snp.makeConstraints {
$0.center.equalTo(cancelButton)
$0.size.equalTo(cancelButton)
}
loadingView.snp.makeConstraints {
$0.trailing.equalTo(movableContentView).offset(-Constants.BigOffset)
$0.top.equalTo(movableContentView).offset(Constants.SmallOffset)
$0.bottom.equalTo(movableContentView).offset(-Constants.SmallOffset)
$0.size.equalTo(Constants.ImageButtonSize)
}
}
override func updateViewsWithState(_ state: ChatGenericFileCellModel.State, fileModel: ChatGenericFileCellModel) {
loadingView.imageButton.isUserInteractionEnabled = true
loadingView.progressView.isHidden = true
loadingView.topLabel.isHidden = true
loadingView.bottomLabel.isHidden = false
loadingView.bottomLabel.text = fileModel.fileName
cancelButton.isHidden = false
retryButton.isHidden = true
switch state {
case .waitingConfirmation:
loadingView.imageButton.isUserInteractionEnabled = false
loadingView.bottomLabel.text = String(localized: "chat_waiting")
case .loading:
loadingView.progressView.isHidden = false
case .paused:
break
case .cancelled:
loadingView.bottomLabel.text = String(localized: "chat_file_cancelled")
cancelButton.isHidden = true
retryButton.isHidden = false
case .done:
cancelButton.isHidden = true
}
}
override func loadingViewPressed() {
switch state {
case .waitingConfirmation:
break
case .loading:
pauseOrResumeHandle?()
case .paused:
pauseOrResumeHandle?()
case .cancelled:
openHandle?()
case .done:
openHandle?()
}
}
}

View File

@ -0,0 +1,9 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import Foundation
class ChatOutgoingFileCellModel: ChatGenericFileCellModel {
}

View File

@ -0,0 +1,51 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import UIKit
import SnapKit
class ChatOutgoingTextCell: ChatBaseTextCell {
override func setupWithTheme(_ theme: Theme, model: BaseCellModel) {
super.setupWithTheme(theme, model: model)
guard let textModel = model as? ChatOutgoingTextCellModel else {
assert(false, "Wrong model \(model) passed to cell \(self)")
return
}
bubbleNormalBackground = theme.colorForType(.ChatOutgoingBubble)
if !textModel.delivered {
if !textModel.sentpush {
bubbleNormalBackground = theme.colorForType(.ChatOutgoingUnreadBubble)
} else {
bubbleNormalBackground = theme.colorForType(.ChatOutgoingSentPushBubble)
}
}
bubbleView.textColor = theme.colorForType(.ConnectingText)
bubbleView.backgroundColor = bubbleNormalBackground
bubbleView.tintColor = theme.colorForType(.NormalText)
bubbleView.font = UIFont.preferredFont(forTextStyle: .body)
}
override func installConstraints() {
super.installConstraints()
bubbleView.snp.makeConstraints {
$0.top.equalTo(movableContentView).offset(ChatBaseTextCell.Constants.BubbleVerticalOffset)
$0.bottom.equalTo(movableContentView).offset(-ChatBaseTextCell.Constants.BubbleVerticalOffset)
$0.trailing.equalTo(movableContentView).offset(-ChatBaseTextCell.Constants.BubbleHorizontalOffset)
}
}
}
// Accessibility
extension ChatOutgoingTextCell {
override var accessibilityLabel: String? {
get {
return String(localized: "accessibility_outgoing_message_label")
}
set {}
}
}

View File

@ -0,0 +1,10 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import Foundation
class ChatOutgoingTextCellModel : ChatBaseTextCellModel {
var delivered: Bool = false
var sentpush: Bool = false
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,111 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import UIKit
import SnapKit
private struct Constants {
static let StatusViewLeftOffset: CGFloat = 5.0
static let StatusViewSize: CGFloat = 10.0
}
class ChatPrivateTitleView: UIView {
var name: String {
get {
return nameLabel.text ?? ""
}
set {
nameLabel.text = newValue
updateFrame()
}
}
var userStatus: UserStatus {
get {
return statusView.userStatus
}
set {
statusView.userStatus = newValue
statusLabel.text = newValue.toString()
updateFrame()
}
}
var connectionStatus: ConnectionStatus {
get {
return statusView.connectionStatus
}
set {
statusView.connectionStatus = newValue
updateFrame()
}
}
fileprivate var nameLabel: UILabel!
fileprivate var statusView: UserStatusView!
fileprivate var statusLabel: UILabel!
init(theme: Theme) {
super.init(frame: CGRect.zero)
backgroundColor = .clear
createViews(theme)
}
required convenience init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
private extension ChatPrivateTitleView {
func createViews(_ theme: Theme) {
nameLabel = UILabel()
nameLabel.textAlignment = .center
nameLabel.textColor = theme.colorForType(.NormalText)
nameLabel.font = UIFont.antidoteFontWithSize(16.0, weight: .bold)
addSubview(nameLabel)
statusView = UserStatusView()
statusView.showExternalCircle = false
statusView.theme = theme
addSubview(statusView)
statusLabel = UILabel()
statusLabel.textAlignment = .center
statusLabel.textColor = theme.colorForType(.NormalText)
statusLabel.font = UIFont.antidoteFontWithSize(12.0, weight: .light)
addSubview(statusLabel)
nameLabel.snp.makeConstraints {
$0.top.equalTo(self)
$0.leading.equalTo(self)
}
statusView.snp.makeConstraints {
$0.centerY.equalTo(nameLabel)
$0.leading.equalTo(nameLabel.snp.trailing).offset(Constants.StatusViewLeftOffset)
$0.trailing.equalTo(self)
$0.size.equalTo(Constants.StatusViewSize)
}
statusLabel.snp.makeConstraints {
$0.top.equalTo(nameLabel.snp.bottom)
$0.leading.equalTo(nameLabel)
$0.trailing.equalTo(nameLabel)
$0.bottom.equalTo(self)
}
}
func updateFrame() {
nameLabel.sizeToFit()
statusLabel.sizeToFit()
frame.size.width = max(nameLabel.frame.size.width, statusLabel.frame.size.width) + Constants.StatusViewLeftOffset + Constants.StatusViewSize
frame.size.height = nameLabel.frame.size.height + statusLabel.frame.size.height
}
}

View File

@ -0,0 +1,23 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import Foundation
/**
Bridge between objcTox subscriber and chat progress protocol.
*/
class ChatProgressBridge: NSObject, ChatProgressProtocol {
var updateProgress: ((_ progress: Float) -> Void)?
var updateEta: ((_ eta: CFTimeInterval, _ bytesPerSecond: OCTToxFileSize) -> Void)?
}
extension ChatProgressBridge: OCTSubmanagerFilesProgressSubscriber {
func submanagerFiles(onProgressUpdate progress: Float, message: OCTMessageAbstract) {
updateProgress?(progress)
}
func submanagerFiles(onEtaUpdate eta: CFTimeInterval, bytesPerSecond: OCTToxFileSize, message: OCTMessageAbstract) {
updateEta?(eta, bytesPerSecond)
}
}

View File

@ -0,0 +1,10 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import Foundation
protocol ChatProgressProtocol {
var updateProgress: ((_ progress: Float) -> Void)? { get set }
var updateEta: ((_ eta: CFTimeInterval, _ bytesPerSecond: OCTToxFileSize) -> Void)? { get set }
}

View File

@ -0,0 +1,101 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import Foundation
import SnapKit
fileprivate struct Constants {
static let verticalOffset = 7.0
static let horizontalOffset = 20.0
static let animationStepDuration = 0.7
}
class ChatTypingHeaderView: UIView {
fileprivate let theme: Theme
fileprivate var bubbleView: BubbleView!
fileprivate var label: UILabel!
fileprivate var animationTimer: Timer?
fileprivate var animationStep: Int = 0
init(theme: Theme) {
self.theme = theme
super.init(frame: CGRect.zero)
createViews()
installConstraints()
}
required convenience init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func startAnimation() {
animationTimer = Timer.scheduledTimer(timeInterval: Constants.animationStepDuration, closure: {[weak self] _ -> Void in
guard let sself = self else {
return
}
sself.animationStep += 1
if sself.animationStep > 2 {
sself.animationStep = 0
}
sself.updateDotsString()
}, repeats: true)
}
func stopAnimation() {
animationTimer?.invalidate()
animationTimer = nil
}
}
private extension ChatTypingHeaderView {
func createViews() {
bubbleView = BubbleView()
bubbleView.text = " "
bubbleView.backgroundColor = theme.colorForType(.ChatIncomingBubble)
bubbleView.isUserInteractionEnabled = false
addSubview(bubbleView)
label = UILabel()
addSubview(label)
updateDotsString()
}
func installConstraints() {
bubbleView.snp.makeConstraints() {
$0.top.equalTo(self).offset(Constants.verticalOffset)
$0.bottom.equalTo(self).offset(-Constants.verticalOffset)
$0.leading.equalTo(self).offset(Constants.horizontalOffset)
}
label.snp.makeConstraints() {
$0.center.equalTo(bubbleView)
}
}
func updateDotsString() {
let mutable = NSMutableAttributedString()
let font = UIFont.systemFont(ofSize: 22.0)
let colorNormal = theme.colorForType(.ChatInformationText)
let colorSelected = colorNormal.darkerColor().darkerColor()
for i in 0..<3 {
let color = (i == animationStep) ? colorSelected : colorNormal
// HINT: u{2022} --> UTF-8 BULLET --> e2 80 a2
mutable.append(NSMutableAttributedString(string: "\u{2022}",
attributes: [NSAttributedStringKey.font : font,
NSAttributedStringKey.foregroundColor: color]))
}
label.attributedText = mutable
}
}

View File

@ -0,0 +1,94 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import UIKit
protocol ChatsTabCoordinatorDelegate: class {
func chatsTabCoordinator(_ coordinator: ChatsTabCoordinator, chatWillAppear chat: OCTChat)
func chatsTabCoordinator(_ coordinator: ChatsTabCoordinator, chatWillDisapper chat: OCTChat)
func chatsTabCoordinator(_ coordinator: ChatsTabCoordinator, callToChat chat: OCTChat, enableVideo: Bool)
}
class ChatsTabCoordinator: ActiveSessionNavigationCoordinator {
weak var delegate: ChatsTabCoordinatorDelegate?
fileprivate weak var submanagerObjects: OCTSubmanagerObjects!
fileprivate weak var submanagerChats: OCTSubmanagerChats!
fileprivate weak var submanagerFiles: OCTSubmanagerFiles!
init(theme: Theme, submanagerObjects: OCTSubmanagerObjects, submanagerChats: OCTSubmanagerChats, submanagerFiles: OCTSubmanagerFiles) {
self.submanagerObjects = submanagerObjects
self.submanagerChats = submanagerChats
self.submanagerFiles = submanagerFiles
super.init(theme: theme)
}
override func startWithOptions(_ options: CoordinatorOptions?) {
let controller = ChatListController(theme: theme, submanagerChats: submanagerChats, submanagerObjects: submanagerObjects)
controller.delegate = self
navigationController.pushViewController(controller, animated: false)
}
func showChat(_ chat: OCTChat, animated: Bool) {
if let top = navigationController.topViewController as? ChatPrivateController {
if top.chat == chat {
// controller is already visible
return
}
}
let controller = ChatPrivateController(
theme: theme,
chat: chat,
submanagerChats: submanagerChats,
submanagerObjects: submanagerObjects,
submanagerFiles: submanagerFiles,
delegate: self)
navigationController.popToRootViewController(animated: false)
navigationController.pushViewController(controller, animated: animated)
}
/**
Returns active chat controller if it is visible, nil otherwise.
*/
func activeChatController() -> ChatPrivateController? {
return navigationController.topViewController as? ChatPrivateController
}
}
extension ChatsTabCoordinator: ChatListControllerDelegate {
func chatListController(_ controller: ChatListController, didSelectChat chat: OCTChat) {
showChat(chat, animated: true)
}
}
extension ChatsTabCoordinator: ChatPrivateControllerDelegate {
func chatPrivateControllerWillAppear(_ controller: ChatPrivateController) {
delegate?.chatsTabCoordinator(self, chatWillAppear: controller.chat)
}
func chatPrivateControllerWillDisappear(_ controller: ChatPrivateController) {
delegate?.chatsTabCoordinator(self, chatWillDisapper: controller.chat)
}
func chatPrivateControllerCallToChat(_ controller: ChatPrivateController, enableVideo: Bool) {
delegate?.chatsTabCoordinator(self, callToChat: controller.chat, enableVideo: enableVideo)
}
func chatPrivateControllerShowQuickLookController(
_ controller: ChatPrivateController,
dataSource: QuickLookPreviewControllerDataSource,
selectedIndex: Int)
{
let controller = QuickLookPreviewController()
controller.dataSource = dataSource
controller.dataSourceStorage = dataSource
controller.currentPreviewItemIndex = selectedIndex
navigationController.present(controller, animated: true, completion: nil)
}
}

View File

@ -0,0 +1,24 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import Foundation
enum ConnectionStatus {
case none
case tcp
case udp
init(connectionStatus: OCTToxConnectionStatus) {
switch (connectionStatus) {
case (.none):
self = .none
case (.TCP):
self = .tcp
case (.UDP):
self = .udp
default:
self = .none
}
}
}

View File

@ -0,0 +1,15 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
typealias CoordinatorOptions = [String: Any]
protocol CoordinatorProtocol {
/**
This method will be called when coordinator should start working.
- Parameters:
- options: Options to start with. Options are used for recovering state of coordinator on recreation.
*/
func startWithOptions(_ options: CoordinatorOptions?)
}

58
Antidote/CopyLabel.swift Normal file
View File

@ -0,0 +1,58 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import UIKit
class CopyLabel: UILabel {
var copyable = true {
didSet {
recognizer.isEnabled = copyable
}
}
fileprivate var recognizer: UITapGestureRecognizer!
override init(frame: CGRect) {
super.init(frame: frame)
isUserInteractionEnabled = true
recognizer = UITapGestureRecognizer(target: self, action: #selector(CopyLabel.tapGesture))
addGestureRecognizer(recognizer)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// MARK: Actions
extension CopyLabel {
@objc func tapGesture() {
// This fixes issue with calling UIMenuController after UIActionSheet was presented.
let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.window?.makeKey()
becomeFirstResponder()
let menu = UIMenuController.shared
menu.setTargetRect(frame, in: superview!)
menu.setMenuVisible(true, animated: true)
}
}
// MARK: Copying
extension CopyLabel {
override func copy(_ sender: Any?) {
UIPasteboard.general.string = text
}
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
return action == #selector(copy(_:))
}
override var canBecomeFirstResponder : Bool {
return true
}
}

View File

@ -0,0 +1,148 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import Foundation
import SnapKit
private struct Constants {
static let PinLength = 4
}
protocol EnterPinControllerDelegate: class {
func enterPinController(_ controller: EnterPinController, successWithPin pin: String)
// This method may be called only for ValidatePin state.
func enterPinControllerFailure(_ controller: EnterPinController)
}
class EnterPinController: UIViewController {
enum State {
case validatePin(validPin: String)
case setPin
}
weak var delegate: EnterPinControllerDelegate?
let state: State
var topText: String {
get {
return pinInputView.topText
}
set {
customLoadView()
pinInputView.topText = newValue
}
}
var descriptionText: String? {
get {
return pinInputView.descriptionText
}
set {
customLoadView()
pinInputView.descriptionText = newValue
}
}
fileprivate let theme: Theme
fileprivate var pinInputView: PinInputView!
fileprivate var enteredString: String = ""
init(theme: Theme, state: State) {
self.theme = theme
self.state = state
super.init(nibName: nil, bundle: nil)
}
required convenience init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
loadViewWithBackgroundColor(theme.colorForType(.NormalBackground))
createViews()
installConstraints()
pinInputView.applyColors()
}
override var shouldAutorotate : Bool {
return false
}
override var supportedInterfaceOrientations : UIInterfaceOrientationMask {
return UIInterfaceOrientationMask.portrait
}
func resetEnteredPin() {
enteredString = ""
pinInputView.enteredNumbersCount = enteredString.count
}
}
extension EnterPinController: PinInputViewDelegate {
func pinInputView(_ view: PinInputView, numericButtonPressed i: Int) {
guard enteredString.count < Constants.PinLength else {
return
}
enteredString += "\(i)"
pinInputView.enteredNumbersCount = enteredString.count
if enteredString.count == Constants.PinLength {
switch state {
case .validatePin(let validPin):
if enteredString == validPin {
delegate?.enterPinController(self, successWithPin: enteredString)
}
else {
delegate?.enterPinControllerFailure(self)
}
case .setPin:
delegate?.enterPinController(self, successWithPin: enteredString)
}
}
}
func pinInputViewDeleteButtonPressed(_ view: PinInputView) {
guard enteredString.count > 0 else {
return
}
enteredString = String(enteredString.dropLast())
view.enteredNumbersCount = enteredString.count
}
}
private extension EnterPinController {
func createViews() {
pinInputView = PinInputView(pinLength: Constants.PinLength,
topColor: theme.colorForType(.LockGradientTop),
bottomColor: theme.colorForType(.LockGradientBottom))
pinInputView.delegate = self
view.addSubview(pinInputView)
}
func installConstraints() {
pinInputView.snp.makeConstraints {
$0.center.equalTo(view)
}
}
func customLoadView() {
if #available(iOS 9.0, *) {
loadViewIfNeeded()
} else {
if !isViewLoaded {
// creating view
view.setNeedsDisplay()
}
}
}
}

View File

@ -0,0 +1,337 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import UIKit
enum ErrorHandlerType {
case cannotLoadHTML
case createOCTManager
case toxSetInfoCodeName
case toxSetInfoCodeStatusMessage
case toxAddFriend
case removeFriend
case callToChat
case exportProfile
case deleteProfile
case passwordIsEmpty
case wrongOldPassword
case passwordsDoNotMatch
case answerCall
case routeAudioToSpeaker
case enableVideoSending
case callSwitchCamera
case convertImageToPNG
case changeAvatar
case sendFileToFriend
case acceptIncomingFile
case cancelFileTransfer
case pauseFileTransfer
case pinLogOut
}
/**
Show alert for given error.
- Parameters:
- type: Type of error to handle.
- error: Optional erro to get code from.
- retryBlock: If set user will be asked to retry request once again.
*/
func handleErrorWithType(_ type: ErrorHandlerType, error: NSError? = nil, retryBlock: (() -> Void)? = nil) {
switch type {
case .cannotLoadHTML:
UIAlertController.showErrorWithMessage(String(localized: "error_internal_message"), retryBlock: retryBlock)
case .createOCTManager:
let (title, message) = OCTManagerInitError(rawValue: error!.code)!.strings()
UIAlertController.showWithTitle(title, message: message, retryBlock: retryBlock)
case .toxSetInfoCodeName:
let (title, message) = OCTToxErrorSetInfoCode(rawValue: error!.code)!.nameStrings()
UIAlertController.showWithTitle(title, message: message, retryBlock: retryBlock)
case .toxSetInfoCodeStatusMessage:
let (title, message) = OCTToxErrorSetInfoCode(rawValue: error!.code)!.statusMessageStrings()
UIAlertController.showWithTitle(title, message: message, retryBlock: retryBlock)
case .toxAddFriend:
let (title, message) = OCTToxErrorFriendAdd(rawValue: error!.code)!.strings()
UIAlertController.showWithTitle(title, message: message, retryBlock: retryBlock)
case .removeFriend:
let (title, message) = OCTToxErrorFriendDelete(rawValue: error!.code)!.strings()
UIAlertController.showWithTitle(title, message: message, retryBlock: retryBlock)
case .callToChat:
let (title, message) = OCTToxAVErrorCall(rawValue: error!.code)!.strings()
UIAlertController.showWithTitle(title, message: message, retryBlock: retryBlock)
case .exportProfile:
UIAlertController.showWithTitle(String(localized: "error_title"), message: error!.localizedDescription, retryBlock: retryBlock)
case .deleteProfile:
UIAlertController.showWithTitle(String(localized: "error_title"), message: error!.localizedDescription, retryBlock: retryBlock)
case .passwordIsEmpty:
UIAlertController.showWithTitle(String(localized: "password_is_empty_error"), retryBlock: retryBlock)
case .wrongOldPassword:
UIAlertController.showWithTitle(String(localized: "wrong_old_password"), retryBlock: retryBlock)
case .passwordsDoNotMatch:
UIAlertController.showWithTitle(String(localized: "passwords_do_not_match"), retryBlock: retryBlock)
case .answerCall:
let (title, message) = OCTToxAVErrorAnswer(rawValue: error!.code)!.strings()
UIAlertController.showWithTitle(title, message: message, retryBlock: retryBlock)
case .routeAudioToSpeaker:
UIAlertController.showWithTitle(String(localized: "error_title"), message: String(localized: "error_internal_message"), retryBlock: retryBlock)
case .enableVideoSending:
UIAlertController.showWithTitle(String(localized: "error_title"), message: String(localized: "error_internal_message"), retryBlock: retryBlock)
case .callSwitchCamera:
UIAlertController.showWithTitle(String(localized: "error_title"), message: String(localized: "error_internal_message"), retryBlock: retryBlock)
case .convertImageToPNG:
UIAlertController.showWithTitle(String(localized: "error_title"), message: String(localized: "change_avatar_error_convert_image"), retryBlock: retryBlock)
case .changeAvatar:
let (title, message) = OCTSetUserAvatarError(rawValue: error!.code)!.strings()
UIAlertController.showWithTitle(title, message: message, retryBlock: retryBlock)
case .sendFileToFriend:
let (title, message) = OCTSendFileError(rawValue: error!.code)!.strings()
UIAlertController.showWithTitle(title, message: message, retryBlock: retryBlock)
case .acceptIncomingFile:
let (title, message) = OCTAcceptFileError(rawValue: error!.code)!.strings()
UIAlertController.showWithTitle(title, message: message, retryBlock: retryBlock)
case .cancelFileTransfer:
let (title, message) = OCTFileTransferError(rawValue: error!.code)!.strings()
UIAlertController.showWithTitle(title, message: message, retryBlock: retryBlock)
case .pauseFileTransfer:
let (title, message) = OCTFileTransferError(rawValue: error!.code)!.strings()
UIAlertController.showWithTitle(title, message: message, retryBlock: retryBlock)
case .pinLogOut:
UIAlertController.showWithTitle(String(localized: "error_title"), message: String(localized: "pin_logout_message"), retryBlock: retryBlock)
}
}
extension OCTManagerInitError {
func strings() -> (title: String, message: String) {
switch self {
case .passphraseFailed:
return (String(localized: "error_wrong_password_title"),
String(localized: "error_wrong_password_message"))
case .cannotImportToxSave:
return (String(localized: "error_import_not_exist_title"),
String(localized: "error_import_not_exist_message"))
case .databaseKeyCannotCreateKey:
fallthrough
case .databaseKeyCannotReadKey:
fallthrough
case .databaseKeyMigrationToEncryptedFailed:
return (String(localized: "error_title"),
String(localized: "error_internal_message"))
case .toxFileDecryptNull:
fallthrough
case .databaseKeyDecryptNull:
return (String(localized: "error_decrypt_title"),
String(localized: "error_decrypt_empty_data_message"))
case .toxFileDecryptBadFormat:
fallthrough
case .databaseKeyDecryptBadFormat:
return (String(localized: "error_decrypt_title"),
String(localized: "error_decrypt_bad_format_message"))
case .toxFileDecryptFailed:
fallthrough
case .databaseKeyDecryptFailed:
return (String(localized: "error_decrypt_title"),
String(localized: "error_decrypt_wrong_password_message"))
case .createToxUnknown:
return (String(localized: "error_title"),
String(localized: "error_general_unknown_message"))
case .createToxMemoryError:
return (String(localized: "error_title"),
String(localized: "error_general_no_memory_message"))
case .createToxPortAlloc:
return (String(localized: "error_title"),
String(localized: "error_general_bind_port_message"))
case .createToxProxyBadType:
return (String(localized: "error_proxy_title"),
String(localized: "error_internal_message"))
case .createToxProxyBadHost:
return (String(localized: "error_proxy_title"),
String(localized: "error_proxy_invalid_address_message"))
case .createToxProxyBadPort:
return (String(localized: "error_proxy_title"),
String(localized: "error_proxy_invalid_port_message"))
case .createToxProxyNotFound:
return (String(localized: "error_proxy_title"),
String(localized: "error_proxy_host_not_resolved_message"))
case .createToxEncrypted:
return (String(localized: "error_title"),
String(localized: "error_general_profile_encrypted_message"))
case .createToxBadFormat:
return (String(localized: "error_title"),
String(localized: "error_general_bad_format_message"))
}
}
}
extension OCTToxErrorSetInfoCode {
func nameStrings() -> (title: String, message: String) {
switch self {
case .unknow:
return (String(localized: "error_title"),
String(localized: "error_internal_message"))
case .tooLong:
return (String(localized: "error_title"),
String(localized: "error_name_too_long"))
}
}
func statusMessageStrings() -> (title: String, message: String) {
switch self {
case .unknow:
return (String(localized: "error_title"),
String(localized: "error_internal_message"))
case .tooLong:
return (String(localized: "error_title"),
String(localized: "error_status_message_too_long"))
}
}
}
extension OCTToxErrorFriendAdd {
func strings() -> (title: String, message: String) {
switch self {
case .tooLong:
return (String(localized: "error_title"),
String(localized: "error_contact_request_too_long"))
case .noMessage:
return (String(localized: "error_title"),
String(localized: "error_contact_request_no_message"))
case .ownKey:
return (String(localized: "error_title"),
String(localized: "error_contact_request_own_key"))
case .alreadySent:
return (String(localized: "error_title"),
String(localized: "error_contact_request_already_sent"))
case .badChecksum:
return (String(localized: "error_title"),
String(localized: "error_contact_request_bad_checksum"))
case .setNewNospam:
return (String(localized: "error_title"),
String(localized: "error_contact_request_new_nospam"))
case .malloc:
fallthrough
case .unknown:
return (String(localized: "error_title"),
String(localized: "error_internal_message"))
}
}
}
extension OCTToxErrorFriendDelete {
func strings() -> (title: String, message: String) {
switch self {
case .notFound:
return (String(localized: "error_title"),
String(localized: "error_internal_message"))
}
}
}
extension OCTToxAVErrorCall {
func strings() -> (title: String, message: String) {
switch self {
case .alreadyInCall:
return (String(localized: "error_title"),
String(localized: "call_error_already_in_call"))
case .friendNotConnected:
return (String(localized: "error_title"),
String(localized: "call_error_contact_is_offline"))
case .friendNotFound:
fallthrough
case .invalidBitRate:
fallthrough
case .malloc:
fallthrough
case .sync:
fallthrough
case .unknown:
return (String(localized: "error_title"),
String(localized: "error_internal_message"))
}
}
}
extension OCTToxAVErrorAnswer {
func strings() -> (title: String, message: String) {
switch self {
case .friendNotCalling:
return (String(localized: "error_title"),
String(localized: "call_error_no_active_call"))
case .codecInitialization:
fallthrough
case .sync:
fallthrough
case .invalidBitRate:
fallthrough
case .unknown:
fallthrough
case .friendNotFound:
return (String(localized: "error_title"),
String(localized: "error_internal_message"))
}
}
}
extension OCTSetUserAvatarError {
func strings() -> (title: String, message: String) {
switch self {
case .tooBig:
return (String(localized: "error_title"),
String(localized: "error_internal_message"))
}
}
}
extension OCTSendFileError {
func strings() -> (title: String, message: String) {
switch self {
case .internalError:
fallthrough
case .cannotReadFile:
fallthrough
case .cannotSaveFileToUploads:
fallthrough
case .nameTooLong:
fallthrough
case .friendNotFound:
return (String(localized: "error_title"),
String(localized: "error_internal_message"))
case .friendNotConnected:
return (String(localized: "error_title"),
String(localized: "error_contact_not_connected"))
case .tooMany:
return (String(localized: "error_title"),
String(localized: "error_too_many_files"))
}
}
}
extension OCTAcceptFileError {
func strings() -> (title: String, message: String) {
switch self {
case .internalError:
fallthrough
case .cannotWriteToFile:
fallthrough
case .friendNotFound:
fallthrough
case .wrongMessage:
return (String(localized: "error_title"),
String(localized: "error_internal_message"))
case .friendNotConnected:
return (String(localized: "error_title"),
String(localized: "error_contact_not_connected"))
}
}
}
extension OCTFileTransferError {
func strings() -> (title: String, message: String) {
switch self {
case .wrongMessage:
return (String(localized: "error_title"),
String(localized: "error_internal_message"))
}
}
}

View File

@ -0,0 +1,11 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
#import <Foundation/Foundation.h>
@interface ExceptionHandling : NSObject
+ (void)tryWithBlock:(nonnull void (^)(void))tryBlock catch:(nonnull void (^)(NSException *__nonnull exception))catchBlock;
@end

View File

@ -0,0 +1,19 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
#import "ExceptionHandling.h"
@implementation ExceptionHandling
+ (void)tryWithBlock:(nonnull void (^)(void))tryBlock catch:(nonnull void (^)(NSException *exception))catchBlock
{
@try {
tryBlock();
}
@catch (NSException *exception) {
catchBlock(exception);
}
}
@end

View File

@ -0,0 +1,224 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import UIKit
import SnapKit
private struct Constants {
static let TextFieldHeight = 40.0
static let VerticalOffset = 5.0
}
protocol ExtendedTextFieldDelegate: class {
func loginExtendedTextFieldReturnKeyPressed(_ field: ExtendedTextField)
}
class ExtendedTextField: UIView {
enum FieldType {
case login
case normal
}
weak var delegate: ExtendedTextFieldDelegate?
var maxTextUTF8Length: Int = Int.max
var title: String? {
get {
return titleLabel.text
}
set {
titleLabel.text = newValue
}
}
var placeholder: String? {
get {
return textField.placeholder
}
set {
textField.placeholder = newValue
}
}
var text: String? {
get {
return textField.text
}
set {
textField.text = newValue
}
}
var hint: String? {
get {
return hintLabel.text
}
set {
hintLabel.text = newValue
}
}
var secureTextEntry: Bool {
get {
return textField.isSecureTextEntry
}
set {
textField.isSecureTextEntry = newValue
}
}
var returnKeyType: UIReturnKeyType {
get {
return textField.returnKeyType
}
set {
textField.returnKeyType = newValue
}
}
fileprivate var titleLabel: UILabel!
fileprivate var textField: UITextField!
fileprivate var hintLabel: UILabel!
init(theme: Theme, type: FieldType) {
super.init(frame: CGRect.zero)
createViews(theme: theme, type: type)
installConstraints()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func becomeFirstResponder() -> Bool {
return textField.becomeFirstResponder()
}
}
extension ExtendedTextField: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
delegate?.loginExtendedTextFieldReturnKeyPressed(self)
return false
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let resultText = (textField.text! as NSString).replacingCharacters(in: range, with: string)
if resultText.lengthOfBytes(using: String.Encoding.utf8) <= maxTextUTF8Length {
return true
}
textField.text = resultText.substringToByteLength(maxTextUTF8Length, encoding: String.Encoding.utf8)
return false
}
}
// Accessibility
extension ExtendedTextField {
override var isAccessibilityElement: Bool {
get {
return true
}
set {}
}
override var accessibilityLabel: String? {
get {
return placeholder ?? title
}
set {}
}
override var accessibilityHint: String? {
get {
var result: String?
if placeholder != nil {
// If there is a placeholder also read title as part of the hint.
result = title
}
switch (result, hint) {
case (.none, _):
return hint
case (.some, .none):
return result
case (.some(let r), .some(let s)):
return "\(r), \(s)"
}
}
set {}
}
override var accessibilityValue: String? {
get {
return text
}
set {}
}
override var accessibilityTraits: UIAccessibilityTraits {
get {
return textField.accessibilityTraits
}
set {}
}
}
private extension ExtendedTextField {
func createViews(theme: Theme, type: FieldType) {
textField = UITextField()
textField.delegate = self
textField.borderStyle = .roundedRect
textField.autocapitalizationType = .sentences
textField.enablesReturnKeyAutomatically = true
addSubview(textField)
let textColor: UIColor
switch type {
case .login:
textColor = theme.colorForType(.NormalText)
textField.layer.borderColor = theme.colorForType(.LoginButtonBackground).cgColor
textField.layer.borderWidth = 0.5
textField.layer.masksToBounds = true
textField.layer.cornerRadius = 6.0
case .normal:
textColor = theme.colorForType(.NormalText)
}
titleLabel = UILabel()
titleLabel.textColor = textColor
titleLabel.font = UIFont.systemFont(ofSize: 18.0)
titleLabel.backgroundColor = .clear
addSubview(titleLabel)
hintLabel = UILabel()
hintLabel.textColor = textColor
hintLabel.font = UIFont.antidoteFontWithSize(14.0, weight: .light)
hintLabel.numberOfLines = 0
hintLabel.backgroundColor = .clear
addSubview(hintLabel)
}
func installConstraints() {
titleLabel.snp.makeConstraints {
$0.top.leading.trailing.equalTo(self)
}
textField.snp.makeConstraints {
$0.top.equalTo(titleLabel.snp.bottom).offset(Constants.VerticalOffset)
$0.leading.trailing.equalTo(self)
$0.height.equalTo(Constants.TextFieldHeight)
}
hintLabel.snp.makeConstraints {
$0.top.equalTo(textField.snp.bottom).offset(Constants.VerticalOffset)
$0.leading.trailing.equalTo(self)
$0.bottom.equalTo(self)
}
}
}

View File

@ -0,0 +1,79 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import Foundation
import WebKit
import SnapKit
private struct Constants {
static let FAQURL = "https://github.com/Zoxcore/Antidote/blob/develop/FAQ/en.md"
}
class FAQController: UIViewController {
fileprivate let theme: Theme
fileprivate var webView: WKWebView!
fileprivate var spinner: UIActivityIndicatorView!
init(theme: Theme) {
self.theme = theme
super.init(nibName: nil, bundle: nil)
hidesBottomBarWhenPushed = true
title = String(localized: "settings_faq")
}
required convenience init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
loadViewWithBackgroundColor(theme.colorForType(.NormalBackground))
createViews()
installConstraints()
}
override func viewDidLoad() {
super.viewDidLoad()
let request = URLRequest(url: URL(string: Constants.FAQURL)!)
webView.load(request)
}
}
extension FAQController: WKNavigationDelegate {
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
spinner.startAnimating()
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
spinner.stopAnimating()
}
}
private extension FAQController {
func createViews() {
let configuration = WKWebViewConfiguration()
webView = WKWebView(frame: CGRect.zero, configuration: configuration)
webView.navigationDelegate = self
view.addSubview(webView)
spinner = UIActivityIndicatorView(activityIndicatorStyle: .gray)
spinner.hidesWhenStopped = true
view.addSubview(spinner)
}
func installConstraints() {
webView.snp.makeConstraints {
$0.edges.equalTo(view)
}
spinner.snp.makeConstraints {
$0.center.equalTo(view)
}
}
}

View File

@ -0,0 +1,71 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import Foundation
import QuickLook
private class FilePreviewItem: NSObject, QLPreviewItem {
@objc var previewItemURL: URL?
@objc var previewItemTitle: String?
init(url: URL, title: String?) {
self.previewItemURL = url
self.previewItemTitle = title
}
}
class FilePreviewControllerDataSource: NSObject , QuickLookPreviewControllerDataSource {
weak var previewController: QuickLookPreviewController?
let messages: Results<OCTMessageAbstract>
var messagesToken: RLMNotificationToken?
init(chat: OCTChat, submanagerObjects: OCTSubmanagerObjects) {
let predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
NSPredicate(format: "chatUniqueIdentifier == %@ AND messageFile != nil", chat.uniqueIdentifier),
NSCompoundPredicate(orPredicateWithSubpredicates: [
NSPredicate(format: "messageFile.fileType == \(OCTMessageFileType.ready.rawValue)"),
NSPredicate(format: "senderUniqueIdentifier == nil AND messageFile.fileType == \(OCTMessageFileType.canceled.rawValue)"),
]),
])
self.messages = submanagerObjects.messages(predicate: predicate).sortedResultsUsingProperty("dateInterval", ascending: true)
super.init()
messagesToken = messages.addNotificationBlock { [unowned self] change in
switch change {
case .initial:
break
case .update:
self.previewController?.reloadData()
case .error(let error):
fatalError("\(error)")
}
}
}
deinit {
messagesToken?.invalidate()
}
func indexOfMessage(_ message: OCTMessageAbstract) -> Int? {
return messages.indexOfObject(message)
}
}
extension FilePreviewControllerDataSource: QLPreviewControllerDataSource {
func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
return messages.count
}
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
let message = messages[index]
let url = URL(fileURLWithPath: message.messageFile!.filePath()!)
return FilePreviewItem(url: url, title: message.messageFile!.fileName)
}
}

View File

@ -0,0 +1,186 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import UIKit
protocol FriendCardControllerDelegate: class {
func friendCardControllerChangeNickname(_ controller: FriendCardController, forFriend friend: OCTFriend)
func friendCardControllerOpenChat(_ controller: FriendCardController, forFriend friend: OCTFriend)
func friendCardControllerCall(_ controller: FriendCardController, toFriend friend: OCTFriend)
func friendCardControllerVideoCall(_ controller: FriendCardController, toFriend friend: OCTFriend)
}
class FriendCardController: StaticTableController {
weak var delegate: FriendCardControllerDelegate?
fileprivate weak var submanagerObjects: OCTSubmanagerObjects!
fileprivate let friend: OCTFriend
fileprivate let avatarManager: AvatarManager
fileprivate var friendToken: RLMNotificationToken?
fileprivate let avatarModel: StaticTableAvatarCellModel
fileprivate let chatButtonsModel: StaticTableChatButtonsCellModel
fileprivate let nicknameModel: StaticTableDefaultCellModel
fileprivate let nameModel: StaticTableDefaultCellModel
fileprivate let statusMessageModel: StaticTableDefaultCellModel
fileprivate let publicKeyModel: StaticTableDefaultCellModel
fileprivate let capabilitiesModel: StaticTableDefaultCellModel
fileprivate let pushurlModel: StaticTableDefaultCellModel
init(theme: Theme, friend: OCTFriend, submanagerObjects: OCTSubmanagerObjects) {
self.submanagerObjects = submanagerObjects
self.friend = friend
self.avatarManager = AvatarManager(theme: theme)
avatarModel = StaticTableAvatarCellModel()
chatButtonsModel = StaticTableChatButtonsCellModel()
nicknameModel = StaticTableDefaultCellModel()
nameModel = StaticTableDefaultCellModel()
statusMessageModel = StaticTableDefaultCellModel()
publicKeyModel = StaticTableDefaultCellModel()
capabilitiesModel = StaticTableDefaultCellModel()
pushurlModel = StaticTableDefaultCellModel()
super.init(theme: theme, style: .plain, model: [
[
avatarModel,
chatButtonsModel,
],
[
nicknameModel,
nameModel,
statusMessageModel,
],
[
publicKeyModel,
],
[
capabilitiesModel,
],
[
pushurlModel,
],
])
updateModels()
}
deinit {
friendToken?.invalidate()
}
required convenience init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
let predicate = NSPredicate(format: "uniqueIdentifier == %@", friend.uniqueIdentifier)
let results = submanagerObjects.friends(predicate: predicate)
friendToken = results.addNotificationBlock { [unowned self] change in
switch change {
case .initial:
break
case .update:
self.updateModels()
self.reloadTableView()
case .error(let error):
fatalError("\(error)")
}
}
}
}
private extension FriendCardController {
func updateModels() {
title = friend.nickname
if let data = friend.avatarData {
avatarModel.avatar = UIImage(data: data)
}
else {
avatarModel.avatar = avatarManager.avatarFromString(
friend.nickname,
diameter: StaticTableAvatarCellModel.Constants.AvatarImageSize)
}
avatarModel.userInteractionEnabled = false
chatButtonsModel.chatButtonHandler = { [unowned self] in
self.delegate?.friendCardControllerOpenChat(self, forFriend: self.friend)
}
chatButtonsModel.callButtonHandler = { [unowned self] in
self.delegate?.friendCardControllerCall(self, toFriend: self.friend)
}
chatButtonsModel.videoButtonHandler = { [unowned self] in
self.delegate?.friendCardControllerVideoCall(self, toFriend: self.friend)
}
chatButtonsModel.chatButtonEnabled = true
chatButtonsModel.callButtonEnabled = friend.isConnected
chatButtonsModel.videoButtonEnabled = friend.isConnected
nicknameModel.title = String(localized: "nickname")
nicknameModel.value = friend.nickname
nicknameModel.rightImageType = .arrow
nicknameModel.didSelectHandler = { [unowned self] _ -> Void in
self.delegate?.friendCardControllerChangeNickname(self, forFriend: self.friend)
}
nameModel.title = String(localized: "name")
nameModel.value = friend.name
nameModel.userInteractionEnabled = false
statusMessageModel.title = String(localized: "status_message")
statusMessageModel.value = friend.statusMessage
statusMessageModel.userInteractionEnabled = false
publicKeyModel.title = String(localized: "public_key")
publicKeyModel.value = friend.publicKey
publicKeyModel.userInteractionEnabled = false
publicKeyModel.canCopyValue = true
capabilitiesModel.title = "Tox Capabilities"
let capabilities = friend.capabilities2 ?? ""
if (capabilities.count > 0) {
let caps = NSNumber(value: UInt64(capabilities) ?? 0)
capabilitiesModel.value = capabilitiesToString(caps)
} else {
capabilitiesModel.value = "BASIC"
}
capabilitiesModel.userInteractionEnabled = false
pushurlModel.title = "Push URL"
let pushtoken = friend.pushToken ?? ""
if (pushtoken.count > 0) {
pushurlModel.value = pushtoken
} else {
pushurlModel.value = ""
}
pushurlModel.userInteractionEnabled = false
}
func capabilitiesToString(_ cap: NSNumber) -> String {
var ret: String = "BASIC"
if ((UInt(cap) & 1) > 0) {
ret = ret + " CAPABILITIES"
}
if ((UInt(cap) & 2) > 0) {
ret = ret + " MSGV2"
}
if ((UInt(cap) & 4) > 0) {
ret = ret + " H264"
}
if ((UInt(cap) & 8) > 0) {
ret = ret + " MSGV3"
}
if ((UInt(cap) & 16) > 0) {
ret = ret + " FTV2"
}
return ret;
}
}

View File

@ -0,0 +1,121 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import UIKit
import SnapKit
class FriendListCell: BaseCell {
struct Constants {
static let AvatarSize = 30.0
static let AvatarLeftOffset = 10.0
static let AvatarRightOffset = 16.0
static let TopLabelHeight = 22.0
static let MinimumBottomLabelHeight = 15.0
static let VerticalOffset = 3.0
}
fileprivate var avatarView: ImageViewWithStatus!
fileprivate var topLabel: UILabel!
fileprivate var bottomLabel: UILabel!
fileprivate var arrowImageView: UIImageView!
override func setupWithTheme(_ theme: Theme, model: BaseCellModel) {
super.setupWithTheme(theme, model: model)
guard let friendModel = model as? FriendListCellModel else {
assert(false, "Wrong model \(model) passed to cell \(self)")
return
}
separatorInset.left = CGFloat(Constants.AvatarLeftOffset + Constants.AvatarSize + Constants.AvatarRightOffset)
avatarView.imageView.image = friendModel.avatar
avatarView.userStatusView.theme = theme
avatarView.userStatusView.userStatus = friendModel.status
avatarView.userStatusView.connectionStatus = friendModel.connectionstatus
avatarView.userStatusView.isHidden = friendModel.hideStatus
topLabel.text = friendModel.topText
topLabel.textColor = theme.colorForType(.NormalText)
bottomLabel.text = friendModel.bottomText
bottomLabel.textColor = theme.colorForType(.FriendCellStatus)
bottomLabel.numberOfLines = friendModel.multilineBottomtext ? 0 : 1
accessibilityLabel = friendModel.accessibilityLabel
accessibilityValue = friendModel.accessibilityValue
}
override func createViews() {
super.createViews()
avatarView = ImageViewWithStatus()
contentView.addSubview(avatarView)
topLabel = UILabel()
topLabel.font = UIFont.systemFont(ofSize: 18.0)
contentView.addSubview(topLabel)
bottomLabel = UILabel()
bottomLabel.font = UIFont.antidoteFontWithSize(12.0, weight: .light)
contentView.addSubview(bottomLabel)
let image = UIImage(named: "right-arrow")!.flippedToCorrectLayout()
arrowImageView = UIImageView(image: image)
arrowImageView.setContentCompressionResistancePriority(UILayoutPriority.required, for: .horizontal)
contentView.addSubview(arrowImageView)
}
override func installConstraints() {
super.installConstraints()
avatarView.snp.makeConstraints {
$0.leading.equalTo(contentView).offset(Constants.AvatarLeftOffset)
$0.centerY.equalTo(contentView)
$0.size.equalTo(Constants.AvatarSize)
}
topLabel.snp.makeConstraints {
$0.leading.equalTo(avatarView.snp.trailing).offset(Constants.AvatarRightOffset)
$0.top.equalTo(contentView).offset(Constants.VerticalOffset)
$0.height.equalTo(Constants.TopLabelHeight)
}
bottomLabel.snp.makeConstraints {
$0.leading.trailing.equalTo(topLabel)
$0.top.equalTo(topLabel.snp.bottom)
$0.bottom.equalTo(contentView).offset(-Constants.VerticalOffset)
$0.height.greaterThanOrEqualTo(Constants.MinimumBottomLabelHeight)
}
arrowImageView.snp.makeConstraints {
$0.centerY.equalTo(contentView)
$0.leading.greaterThanOrEqualTo(topLabel.snp.trailing)
$0.trailing.equalTo(contentView)
}
}
}
// Accessibility
extension FriendListCell {
override var isAccessibilityElement: Bool {
get {
return true
}
set {}
}
// Label and value are set in setupWithTheme:model: method
// var accessibilityLabel: String?
// var accessibilityValue: String?
override var accessibilityTraits: UIAccessibilityTraits {
get {
return UIAccessibilityTraitButton
}
set {}
}
}

View File

@ -0,0 +1,20 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import Foundation
class FriendListCellModel: BaseCellModel {
var avatar: UIImage?
var topText: String = ""
var bottomText: String = ""
var multilineBottomtext: Bool = false
var accessibilityLabel = ""
var accessibilityValue = ""
var status: UserStatus = .offline
var connectionstatus: ConnectionStatus = .none
var hideStatus: Bool = false
}

View File

@ -0,0 +1,315 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import UIKit
import SnapKit
protocol FriendListControllerDelegate: class {
func friendListController(_ controller: FriendListController, didSelectFriend friend: OCTFriend)
func friendListController(_ controller: FriendListController, didSelectRequest request: OCTFriendRequest)
func friendListControllerAddFriend(_ controller: FriendListController)
func friendListController(_ controller: FriendListController, showQRCodeWithText text: String)
}
class FriendListController: UIViewController {
weak var delegate: FriendListControllerDelegate?
fileprivate let theme: Theme
fileprivate var dataSource: FriendListDataSource!
fileprivate weak var submanagerObjects: OCTSubmanagerObjects!
fileprivate weak var submanagerFriends: OCTSubmanagerFriends!
fileprivate weak var submanagerChats: OCTSubmanagerChats!
fileprivate weak var submanagerUser: OCTSubmanagerUser!
fileprivate var placeholderView: UITextView!
fileprivate var tableView: UITableView!
init(theme: Theme, submanagerObjects: OCTSubmanagerObjects, submanagerFriends: OCTSubmanagerFriends, submanagerChats: OCTSubmanagerChats, submanagerUser: OCTSubmanagerUser) {
self.theme = theme
self.submanagerObjects = submanagerObjects
self.submanagerFriends = submanagerFriends
self.submanagerChats = submanagerChats
self.submanagerUser = submanagerUser
super.init(nibName: nil, bundle: nil)
addNavigationButtons()
edgesForExtendedLayout = UIRectEdge()
title = String(localized: "contacts_title")
}
required convenience init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
loadViewWithBackgroundColor(theme.colorForType(.NormalBackground))
createTableView()
createPlaceholderView()
installConstraints()
}
override func viewDidLoad() {
super.viewDidLoad()
let friends = submanagerObjects.friends()
let requests = submanagerObjects.friendRequests()
dataSource = FriendListDataSource(theme: theme, friends: friends, requests: requests)
dataSource.delegate = self
// removing separators on empty lines
tableView.tableFooterView = UIView()
updateViewsVisibility()
let message = "Antidote is Tox"
let echobotid = "76518406F6A9F2217E8DC487CC783C25CC16A15EB36FF32E335A235342C48A39218F515C39A6"
print("EchobotAdded=\(UserDefaultsManager().EchobotAdded)")
if (UserDefaultsManager().EchobotAdded == false) {
do {
try self.submanagerFriends.sendFriendRequest(toAddress: echobotid, message: message)
UserDefaultsManager().EchobotAdded = true
}
catch let error as NSError {
return
}
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
updateViewsVisibility()
}
override func setEditing(_ editing: Bool, animated: Bool) {
super.setEditing(editing, animated: animated)
tableView.setEditing(editing, animated: animated)
}
}
extension FriendListController {
@objc func addFriendButtonPressed() {
delegate?.friendListControllerAddFriend(self)
}
}
extension FriendListController: FriendListDataSourceDelegate {
func friendListDataSourceBeginUpdates() {
tableView.beginUpdates()
}
func friendListDataSourceEndUpdates() {
tableView.endUpdates()
updateViewsVisibility()
}
func friendListDataSourceInsertRowsAtIndexPaths(_ indexPaths: [IndexPath]) {
tableView.insertRows(at: indexPaths, with: .automatic)
}
func friendListDataSourceDeleteRowsAtIndexPaths(_ indexPaths: [IndexPath]) {
tableView.deleteRows(at: indexPaths, with: .automatic)
}
func friendListDataSourceReloadRowsAtIndexPaths(_ indexPaths: [IndexPath]) {
tableView.reloadRows(at: indexPaths, with: .automatic)
}
func friendListDataSourceInsertSections(_ sections: IndexSet) {
tableView.insertSections(sections, with: .automatic)
}
func friendListDataSourceDeleteSections(_ sections: IndexSet) {
tableView.deleteSections(sections, with: .automatic)
}
func friendListDataSourceReloadSections(_ sections: IndexSet) {
tableView.reloadSections(sections, with: .automatic)
}
func friendListDataSourceReloadTable() {
tableView.reloadData()
}
}
extension FriendListController: UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: FriendListCell.staticReuseIdentifier) as! FriendListCell
let model = dataSource.modelAtIndexPath(indexPath)
cell.setupWithTheme(theme, model: model)
return cell
}
func numberOfSections(in tableView: UITableView) -> Int {
return dataSource.numberOfSections()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataSource.numberOfRowsInSection(section)
}
func sectionIndexTitles(for tableView: UITableView) -> [String]? {
return dataSource.sectionIndexTitles()
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return dataSource.titleForHeaderInSection(section)
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
let title: String
switch dataSource.objectAtIndexPath(indexPath) {
case .request:
title = String(localized:"delete_contact_request_title")
case .friend:
title = String(localized:"delete_contact_title")
}
let alert = UIAlertController(title: title, message: nil, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: String(localized: "alert_cancel"), style: .default, handler: nil))
alert.addAction(UIAlertAction(title: String(localized: "alert_delete"), style: .destructive) { [unowned self] _ -> Void in
switch self.dataSource.objectAtIndexPath(indexPath) {
case .request(let request):
self.submanagerFriends.remove(request)
case .friend(let friend):
do {
let chat = self.submanagerChats.getOrCreateChat(with: friend)
try self.submanagerFriends.remove(friend)
self.submanagerChats.removeAllMessages(in: chat, removeChat: true)
}
catch let error as NSError {
handleErrorWithType(.removeFriend, error: error)
}
}
})
present(alert, animated: true, completion: nil)
}
}
}
extension FriendListController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
switch dataSource.objectAtIndexPath(indexPath) {
case .request(let request):
didSelectFriendRequest(request)
case .friend(let friend):
delegate?.friendListController(self, didSelectFriend: friend)
}
}
}
extension FriendListController : UITextViewDelegate {
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool {
if textView === placeholderView {
let toxId = submanagerUser.userAddress
let alert = UIAlertController(title: String(localized: "my_tox_id"), message: toxId, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: String(localized: "copy"), style: .default) { _ -> Void in
UIPasteboard.general.string = toxId
})
alert.addAction(UIAlertAction(title: String(localized: "show_qr_code"), style: .default) { [weak self] _ -> Void in
self?.delegate?.friendListController(self!, showQRCodeWithText: toxId)
})
alert.addAction(UIAlertAction(title: String(localized: "alert_cancel"), style: .cancel, handler: nil))
present(alert, animated: true, completion: nil)
}
return false
}
}
private extension FriendListController {
func addNavigationButtons() {
navigationItem.rightBarButtonItem = UIBarButtonItem(
barButtonSystemItem: .add,
target: self,
action: #selector(FriendListController.addFriendButtonPressed))
}
func updateViewsVisibility() {
var isEmpty = true
for section in 0..<dataSource.numberOfSections() {
if dataSource.numberOfRowsInSection(section) > 0 {
isEmpty = false
break
}
}
navigationItem.leftBarButtonItem = isEmpty ? nil : editButtonItem
placeholderView.isHidden = !isEmpty
}
func createTableView() {
tableView = UITableView()
tableView.estimatedRowHeight = 44.0
tableView.dataSource = self
tableView.delegate = self
tableView.backgroundColor = theme.colorForType(.NormalBackground)
tableView.sectionIndexColor = theme.colorForType(.LinkText)
view.addSubview(tableView)
tableView.register(FriendListCell.self, forCellReuseIdentifier: FriendListCell.staticReuseIdentifier)
}
func createPlaceholderView() {
let top = String(localized: "contact_no_contacts_add_contact")
let bottom = String(localized: "contact_no_contacts_share_tox_id")
let text = NSMutableAttributedString(string: "\(top)\(bottom)")
let linkRange = NSRange(location: top.count, length: bottom.count)
let fullRange = NSRange(location: 0, length: text.length)
text.addAttribute(NSAttributedStringKey.foregroundColor, value: theme.colorForType(.EmptyScreenPlaceholderText), range: fullRange)
text.addAttribute(NSAttributedStringKey.font, value: UIFont.antidoteFontWithSize(26.0, weight: .light), range: fullRange)
text.addAttribute(NSAttributedStringKey.link, value: "", range: linkRange)
placeholderView = UITextView()
placeholderView.delegate = self
placeholderView.attributedText = text
placeholderView.isEditable = false
placeholderView.isScrollEnabled = false
placeholderView.textAlignment = .center
placeholderView.linkTextAttributes = [NSAttributedStringKey.foregroundColor.rawValue : theme.colorForType(.LinkText)]
view.addSubview(placeholderView)
}
func installConstraints() {
tableView.snp.makeConstraints {
$0.edges.equalTo(view)
}
placeholderView.snp.makeConstraints {
$0.center.equalTo(view)
$0.size.equalTo(placeholderView.sizeThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)))
}
}
func didSelectFriendRequest(_ request: OCTFriendRequest) {
delegate?.friendListController(self, didSelectRequest: request)
}
}

View File

@ -0,0 +1,230 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import Foundation
private struct Constants {
static let FriendRequestsSection = 0
}
protocol FriendListDataSourceDelegate: class {
func friendListDataSourceBeginUpdates()
func friendListDataSourceEndUpdates()
func friendListDataSourceInsertRowsAtIndexPaths(_ indexPaths: [IndexPath])
func friendListDataSourceDeleteRowsAtIndexPaths(_ indexPaths: [IndexPath])
func friendListDataSourceReloadRowsAtIndexPaths(_ indexPaths: [IndexPath])
func friendListDataSourceInsertSections(_ sections: IndexSet)
func friendListDataSourceDeleteSections(_ sections: IndexSet)
func friendListDataSourceReloadSections(_ sections: IndexSet)
func friendListDataSourceReloadTable()
}
enum FriendListObject {
case request(OCTFriendRequest)
case friend(OCTFriend)
}
class FriendListDataSource: NSObject {
weak var delegate: FriendListDataSourceDelegate?
fileprivate let avatarManager: AvatarManager
fileprivate let dateFormatter: DateFormatter
fileprivate let requests: Results<OCTFriendRequest>?
fileprivate let friends: Results<OCTFriend>
fileprivate var requestsToken: RLMNotificationToken?
fileprivate var friendsToken: RLMNotificationToken?
/// In case if requests is nil friend requests won't be shown.
init(theme: Theme, friends: Results<OCTFriend>, requests: Results<OCTFriendRequest>? = nil) {
self.avatarManager = AvatarManager(theme: theme)
self.dateFormatter = DateFormatter(type: .relativeDateAndTime)
self.requests = requests
self.friends = friends
super.init()
addNotificationBlocks()
}
deinit {
requestsToken?.invalidate()
friendsToken?.invalidate()
}
func numberOfSections() -> Int {
if isRequestsSectionVisible() {
return 2
}
// friends only
return 1
}
func numberOfRowsInSection(_ section: Int) -> Int {
if section == Constants.FriendRequestsSection && isRequestsSectionVisible() {
return requests!.count
}
else {
return friends.count
}
}
func modelAtIndexPath(_ indexPath: IndexPath) -> FriendListCellModel {
let model = FriendListCellModel()
switch objectAtIndexPath(indexPath) {
case .request(let request):
model.avatar = avatarManager.avatarFromString("", diameter: CGFloat(FriendListCell.Constants.AvatarSize))
model.topText = request.publicKey
model.bottomText = request.message ?? ""
model.multilineBottomtext = true
model.hideStatus = true
model.accessibilityLabel = String(localized: "contact_request")
model.accessibilityValue = ""
if let message = request.message {
model.accessibilityValue += String(localized: "add_contact_default_message_title") + ": " + message + ", "
}
model.accessibilityValue += String(localized: "public_key") + ": " + request.publicKey
case .friend(let friend):
if let data = friend.avatarData {
model.avatar = UIImage(data: data)
}
else {
model.avatar = avatarManager.avatarFromString(friend.nickname, diameter: CGFloat(FriendListCell.Constants.AvatarSize))
}
model.topText = friend.nickname
model.status = UserStatus(connectionStatus: friend.connectionStatus, userStatus: friend.status)
model.connectionstatus = ConnectionStatus(connectionStatus: friend.connectionStatus)
model.accessibilityLabel = friend.nickname
model.accessibilityValue = model.status.toString()
if friend.isConnected {
model.bottomText = friend.statusMessage ?? ""
model.accessibilityValue += ", Status: \(model.bottomText)"
}
else if let date = friend.lastSeenOnline() {
model.bottomText = String(localized: "contact_last_seen", dateFormatter.string(from: date))
model.accessibilityValue += ", " + model.bottomText
}
}
return model
}
func objectAtIndexPath(_ indexPath: IndexPath) -> FriendListObject {
if indexPath.section == Constants.FriendRequestsSection && isRequestsSectionVisible() {
return .request(requests![indexPath.row])
}
else {
return .friend(friends[indexPath.row])
}
}
func sectionIndexTitles() -> [String] {
// TODO fix when Realm will support sections
let array = [String]()
return array
}
func titleForHeaderInSection(_ section: Int) -> String? {
if !isRequestsSectionVisible() {
return nil
}
if section == Constants.FriendRequestsSection {
return String(localized: "contact_requests_section")
}
else {
return String(localized: "contacts_title")
}
}
}
private extension FriendListDataSource {
func addNotificationBlocks() {
requestsToken = requests?.addNotificationBlock { [unowned self] change in
switch change {
case .initial:
break
case .update(let requests, let deletions, let insertions, let modifications):
guard let requests = requests else {
return
}
if deletions.count > 0 {
// reloading data on request removal/friend insertion to synchronize requests/friends
self.delegate?.friendListDataSourceReloadTable()
return
}
self.delegate?.friendListDataSourceBeginUpdates()
let countAfter = requests.count
let countBefore = countAfter - insertions.count + deletions.count
if countBefore == 0 && countAfter > 0 {
self.delegate?.friendListDataSourceInsertSections(IndexSet(integer: 0))
}
else if countBefore > 0 && countAfter == 0 {
self.delegate?.friendListDataSourceDeleteSections(IndexSet(integer: 0))
}
else {
self.delegate?.friendListDataSourceDeleteRowsAtIndexPaths(deletions.map { IndexPath(row: $0, section: 0)} )
self.delegate?.friendListDataSourceInsertRowsAtIndexPaths(insertions.map { IndexPath(row: $0, section: 0)} )
self.delegate?.friendListDataSourceReloadRowsAtIndexPaths(modifications.map { IndexPath(row: $0, section: 0)} )
}
self.delegate?.friendListDataSourceEndUpdates()
case .error(let error):
fatalError("\(error)")
}
}
friendsToken = friends.addNotificationBlock { [unowned self] change in
switch change {
case .initial:
break
case .update(_, let deletions, let insertions, let modifications):
if insertions.count > 0 {
// reloading data on request removal/friend insertion to synchronize requests/friends
self.delegate?.friendListDataSourceReloadTable()
return
}
let section = self.isRequestsSectionVisible() ? 1 : 0
let deletions = deletions.map { IndexPath(row: $0, section: section) }
let insertions = insertions.map { IndexPath(row: $0, section: section) }
let modifications = modifications.map { IndexPath(row: $0, section: section) }
self.delegate?.friendListDataSourceBeginUpdates()
self.delegate?.friendListDataSourceDeleteRowsAtIndexPaths(deletions)
self.delegate?.friendListDataSourceInsertRowsAtIndexPaths(insertions)
self.delegate?.friendListDataSourceReloadRowsAtIndexPaths(modifications)
self.delegate?.friendListDataSourceEndUpdates()
case .error(let error):
fatalError("\(error)")
}
}
}
func isRequestsSectionVisible() -> Bool {
guard let requests = requests else {
return false
}
return requests.count > 0
}
}

View File

@ -0,0 +1,90 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import UIKit
protocol FriendRequestControllerDelegate: class {
func friendRequestControllerDidFinish(_ controller: FriendRequestController)
}
class FriendRequestController: StaticTableController {
weak var delegate: FriendRequestControllerDelegate?
fileprivate let request: OCTFriendRequest
fileprivate weak var submanagerFriends: OCTSubmanagerFriends!
fileprivate let publicKeyModel: StaticTableDefaultCellModel
fileprivate let messageModel: StaticTableDefaultCellModel
fileprivate let buttonsModel: StaticTableMultiChoiceButtonCellModel
init(theme: Theme, request: OCTFriendRequest, submanagerFriends: OCTSubmanagerFriends) {
self.request = request
self.submanagerFriends = submanagerFriends
publicKeyModel = StaticTableDefaultCellModel()
messageModel = StaticTableDefaultCellModel()
buttonsModel = StaticTableMultiChoiceButtonCellModel()
super.init(theme: theme, style: .plain, model: [
[
publicKeyModel,
messageModel,
],
[
buttonsModel,
],
])
updateModels()
}
required convenience init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
private extension FriendRequestController {
func updateModels() {
title = String(localized: "contact_request")
publicKeyModel.title = String(localized: "public_key")
publicKeyModel.value = request.publicKey
publicKeyModel.userInteractionEnabled = false
messageModel.title = String(localized: "status_message")
messageModel.value = request.message
messageModel.userInteractionEnabled = false
buttonsModel.buttons = [
StaticTableMultiChoiceButtonCellModel.ButtonModel(title: String(localized: "contact_request_decline"), style: .negative, target: self, action: #selector(FriendRequestController.declineButtonPressed)),
StaticTableMultiChoiceButtonCellModel.ButtonModel(title: String(localized: "contact_request_accept"), style: .positive, target: self, action: #selector(FriendRequestController.acceptButtonPressed)),
]
}
}
extension FriendRequestController {
@objc func declineButtonPressed() {
let alert = UIAlertController(title: String(localized: "contact_request_delete_title"), message: nil, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: String(localized: "alert_cancel"), style: .default, handler: nil))
alert.addAction(UIAlertAction(title: String(localized: "alert_delete"), style: .destructive) { [unowned self] _ -> Void in
self.submanagerFriends.remove(self.request)
self.delegate?.friendRequestControllerDidFinish(self)
})
present(alert, animated: true, completion: nil)
}
@objc func acceptButtonPressed() {
do {
try submanagerFriends.approve(request)
delegate?.friendRequestControllerDidFinish(self)
}
catch let error as NSError {
handleErrorWithType(.toxAddFriend, error: error)
}
}
}

View File

@ -0,0 +1,197 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import UIKit
import SnapKit
protocol FriendSelectControllerDelegate: class {
func friendSelectController(_ controller: FriendSelectController, didSelectFriend friend: OCTFriend)
func friendSelectControllerCancel(_ controller: FriendSelectController)
}
class FriendSelectController: UIViewController {
weak var delegate: FriendSelectControllerDelegate?
var userInfo: AnyObject?
fileprivate let theme: Theme
fileprivate let dataSource: FriendListDataSource
fileprivate var placeholderView: UITextView!
fileprivate var tableView: UITableView!
init(theme: Theme, submanagerObjects: OCTSubmanagerObjects, userInfo: AnyObject? = nil) {
self.theme = theme
self.userInfo = userInfo
let friends = submanagerObjects.friends()
self.dataSource = FriendListDataSource(theme: theme, friends: friends)
super.init(nibName: nil, bundle: nil)
dataSource.delegate = self
addNavigationButtons()
edgesForExtendedLayout = UIRectEdge()
}
required convenience init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
loadViewWithBackgroundColor(theme.colorForType(.NormalBackground))
createTableView()
createPlaceholderView()
installConstraints()
updateViewsVisibility()
}
}
extension FriendSelectController {
@objc func cancelButtonPressed() {
delegate?.friendSelectControllerCancel(self)
}
}
extension FriendSelectController: FriendListDataSourceDelegate {
func friendListDataSourceBeginUpdates() {
tableView.beginUpdates()
}
func friendListDataSourceEndUpdates() {
self.tableView.endUpdates()
updateViewsVisibility()
}
func friendListDataSourceInsertRowsAtIndexPaths(_ indexPaths: [IndexPath]) {
tableView.insertRows(at: indexPaths, with: .automatic)
}
func friendListDataSourceDeleteRowsAtIndexPaths(_ indexPaths: [IndexPath]) {
tableView.deleteRows(at: indexPaths, with: .automatic)
}
func friendListDataSourceReloadRowsAtIndexPaths(_ indexPaths: [IndexPath]) {
tableView.reloadRows(at: indexPaths, with: .automatic)
}
func friendListDataSourceInsertSections(_ sections: IndexSet) {
tableView.insertSections(sections, with: .automatic)
}
func friendListDataSourceDeleteSections(_ sections: IndexSet) {
tableView.deleteSections(sections, with: .automatic)
}
func friendListDataSourceReloadSections(_ sections: IndexSet) {
tableView.reloadSections(sections, with: .automatic)
}
func friendListDataSourceReloadTable() {
tableView.reloadData()
}
}
extension FriendSelectController: UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: FriendListCell.staticReuseIdentifier) as! FriendListCell
let model = dataSource.modelAtIndexPath(indexPath)
cell.setupWithTheme(theme, model: model)
return cell
}
func numberOfSections(in tableView: UITableView) -> Int {
return dataSource.numberOfSections()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataSource.numberOfRowsInSection(section)
}
func sectionIndexTitles(for tableView: UITableView) -> [String]? {
return dataSource.sectionIndexTitles()
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return dataSource.titleForHeaderInSection(section)
}
}
extension FriendSelectController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
switch dataSource.objectAtIndexPath(indexPath) {
case .request:
// nop
break
case .friend(let friend):
delegate?.friendSelectController(self, didSelectFriend: friend)
}
}
}
private extension FriendSelectController {
func addNavigationButtons() {
navigationItem.leftBarButtonItem = UIBarButtonItem(
barButtonSystemItem: .cancel,
target: self,
action: #selector(FriendSelectController.cancelButtonPressed))
}
func updateViewsVisibility() {
var isEmpty = true
for section in 0..<dataSource.numberOfSections() {
if dataSource.numberOfRowsInSection(section) > 0 {
isEmpty = false
break
}
}
placeholderView.isHidden = !isEmpty
}
func createTableView() {
tableView = UITableView()
tableView.estimatedRowHeight = 44.0
tableView.dataSource = self
tableView.delegate = self
tableView.backgroundColor = theme.colorForType(.NormalBackground)
tableView.sectionIndexColor = theme.colorForType(.LinkText)
// removing separators on empty lines
tableView.tableFooterView = UIView()
view.addSubview(tableView)
tableView.register(FriendListCell.self, forCellReuseIdentifier: FriendListCell.staticReuseIdentifier)
}
func createPlaceholderView() {
placeholderView = UITextView()
placeholderView.text = String(localized: "contact_no_contacts")
placeholderView.isEditable = false
placeholderView.isScrollEnabled = false
placeholderView.textAlignment = .center
view.addSubview(placeholderView)
}
func installConstraints() {
tableView.snp.makeConstraints {
$0.edges.equalTo(view)
}
placeholderView.snp.makeConstraints {
$0.center.equalTo(view)
$0.size.equalTo(placeholderView.sizeThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)))
}
}
}

View File

@ -0,0 +1,152 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import Foundation
protocol FriendsTabCoordinatorDelegate: class {
func friendsTabCoordinatorOpenChat(_ coordinator: FriendsTabCoordinator, forFriend friend: OCTFriend)
func friendsTabCoordinatorCall(_ coordinator: FriendsTabCoordinator, toFriend friend: OCTFriend)
func friendsTabCoordinatorVideoCall(_ coordinator: FriendsTabCoordinator, toFriend friend: OCTFriend)
}
class FriendsTabCoordinator: ActiveSessionNavigationCoordinator {
weak var delegate: FriendsTabCoordinatorDelegate?
fileprivate weak var toxManager: OCTManager!
init(theme: Theme, toxManager: OCTManager) {
self.toxManager = toxManager
super.init(theme: theme)
}
override func startWithOptions(_ options: CoordinatorOptions?) {
let controller = FriendListController(theme: theme, submanagerObjects: toxManager.objects, submanagerFriends: toxManager.friends, submanagerChats: toxManager.chats, submanagerUser: toxManager.user)
controller.delegate = self
navigationController.pushViewController(controller, animated: false)
}
func showRequest(_ request: OCTFriendRequest, animated: Bool) {
navigationController.popToRootViewController(animated: false)
let controller = FriendRequestController(theme: theme, request: request, submanagerFriends: toxManager.friends)
controller.delegate = self
navigationController.pushViewController(controller, animated: animated)
}
}
extension FriendsTabCoordinator: FriendListControllerDelegate {
func friendListController(_ controller: FriendListController, didSelectFriend friend: OCTFriend) {
let controller = FriendCardController(theme: theme, friend: friend, submanagerObjects: toxManager.objects)
controller.delegate = self
navigationController.pushViewController(controller, animated: true)
}
func friendListController(_ controller: FriendListController, didSelectRequest request: OCTFriendRequest) {
showRequest(request, animated: true)
}
func friendListControllerAddFriend(_ controller: FriendListController) {
let controller = AddFriendController(theme: theme, submanagerFriends: toxManager.friends)
controller.delegate = self
navigationController.pushViewController(controller, animated: true)
}
func friendListController(_ controller: FriendListController, showQRCodeWithText text: String) {
let controller = QRViewerController(theme: theme, text: text)
controller.delegate = self
let toPresent = UINavigationController(rootViewController: controller)
navigationController.present(toPresent, animated: true, completion: nil)
}
}
extension FriendsTabCoordinator: QRViewerControllerDelegate {
func qrViewerControllerDidFinishPresenting() {
navigationController.dismiss(animated: true, completion: nil)
}
}
extension FriendsTabCoordinator: FriendCardControllerDelegate {
func friendCardControllerChangeNickname(_ controller: FriendCardController, forFriend friend: OCTFriend) {
let title = String(localized: "nickname")
let defaultValue = friend.nickname
let textController = TextEditController(theme: theme, title: title, defaultValue: defaultValue, changeTextHandler: {
[unowned self] newValue -> Void in
self.toxManager.objects.change(friend, nickname: newValue)
}, userFinishedEditing: { [unowned self] in
self.navigationController.popViewController(animated: true)
})
navigationController.pushViewController(textController, animated: true)
}
func friendCardControllerOpenChat(_ controller: FriendCardController, forFriend friend: OCTFriend) {
delegate?.friendsTabCoordinatorOpenChat(self, forFriend: friend)
}
func friendCardControllerCall(_ controller: FriendCardController, toFriend friend: OCTFriend) {
delegate?.friendsTabCoordinatorCall(self, toFriend: friend)
}
func friendCardControllerVideoCall(_ controller: FriendCardController, toFriend friend: OCTFriend) {
delegate?.friendsTabCoordinatorVideoCall(self, toFriend: friend)
}
}
extension FriendsTabCoordinator: FriendRequestControllerDelegate {
func friendRequestControllerDidFinish(_ controller: FriendRequestController) {
navigationController.popViewController(animated: true)
}
}
extension FriendsTabCoordinator: AddFriendControllerDelegate {
func addFriendControllerScanQRCode(_ controller: AddFriendController,
validateCodeHandler: @escaping (String) -> Bool,
didScanHander: @escaping (String) -> Void) {
let scanner = QRScannerController(theme: theme)
scanner.didScanStringsBlock = { [unowned self, scanner] in
let qrCode = $0.filter { validateCodeHandler($0) }.first
if let code = qrCode {
didScanHander(code)
self.navigationController.dismiss(animated: true, completion: nil)
}
else {
scanner.pauseScanning = true
let title = String(localized:"error_title")
let message = String(localized:"add_contact_wrong_qr")
let button = String(localized:"error_ok_button")
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: button, style: .default) { [unowned scanner ] _ -> Void in
scanner.pauseScanning = false
})
scanner.present(alert, animated: true, completion: nil)
}
}
scanner.cancelBlock = { [unowned self] in
self.navigationController.dismiss(animated: true, completion: nil)
}
let scannerNavCon = UINavigationController(rootViewController: scanner)
navigationController.present(scannerNavCon, animated: true, completion: nil)
}
func addFriendControllerDidFinish(_ controller: AddFriendController) {
navigationController.popViewController(animated: true)
}
}

View File

@ -0,0 +1,158 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import UIKit
import SnapKit
private struct Constants {
static let AnimationDuration = 0.3
static let ToolbarHeight: CGFloat = 44.0
}
protocol FullscreenPickerDelegate: class {
func fullscreenPicker(_ picker: FullscreenPicker, willDismissWithSelectedIndex index: Int)
}
class FullscreenPicker: UIView {
weak var delegate: FullscreenPickerDelegate?
fileprivate var theme: Theme
fileprivate var blackoutButton: UIButton!
fileprivate var toolbar: UIToolbar!
fileprivate var picker: UIPickerView!
fileprivate var pickerBottomConstraint: Constraint!
fileprivate let stringsArray: [String]
init(theme: Theme, strings: [String], selectedIndex: Int) {
self.theme = theme
self.stringsArray = strings
super.init(frame: CGRect.zero)
configureSelf()
createSubviews()
installConstraints()
picker.selectRow(selectedIndex, inComponent: 0, animated: false)
}
required convenience init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func showAnimatedInView(_ view: UIView) {
view.addSubview(self)
snp.makeConstraints {
$0.edges.equalTo(view)
}
show()
}
}
// MARK: Actions
extension FullscreenPicker {
@objc func doneButtonPressed() {
delegate?.fullscreenPicker(self, willDismissWithSelectedIndex: picker.selectedRow(inComponent: 0))
hide()
}
}
extension FullscreenPicker: UIPickerViewDataSource {
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return stringsArray.count
}
}
extension FullscreenPicker: UIPickerViewDelegate {
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
return stringsArray[row]
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
picker.reloadAllComponents()
}
}
private extension FullscreenPicker {
func configureSelf() {
backgroundColor = .clear
}
func createSubviews() {
blackoutButton = UIButton()
blackoutButton.backgroundColor = theme.colorForType(.TranslucentBackground)
blackoutButton.addTarget(self, action: #selector(FullscreenPicker.doneButtonPressed), for:.touchUpInside)
blackoutButton.accessibilityElementsHidden = true
blackoutButton.isAccessibilityElement = false
addSubview(blackoutButton)
toolbar = UIToolbar()
toolbar.tintColor = theme.colorForType(.LoginButtonText)
toolbar.barTintColor = theme.loginNavigationBarColor
toolbar.items = [
UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil),
UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(FullscreenPicker.doneButtonPressed))
]
addSubview(toolbar)
picker = UIPickerView()
// Picker is always white, despite choosen theme
picker.backgroundColor = .white
picker.delegate = self
picker.dataSource = self
addSubview(picker)
}
func installConstraints() {
blackoutButton.snp.makeConstraints {
$0.edges.equalTo(self)
}
toolbar.snp.makeConstraints {
$0.bottom.equalTo(self.picker.snp.top)
$0.height.equalTo(Constants.ToolbarHeight)
$0.width.equalTo(self)
}
picker.snp.makeConstraints {
$0.width.equalTo(self)
pickerBottomConstraint = $0.bottom.equalTo(self).constraint
}
}
func show() {
blackoutButton.alpha = 0.0
pickerBottomConstraint.update(offset: picker.frame.size.height + Constants.ToolbarHeight)
layoutIfNeeded()
UIView.animate(withDuration: Constants.AnimationDuration, animations: {
self.blackoutButton.alpha = 1.0
self.pickerBottomConstraint.update(offset: 0.0)
self.layoutIfNeeded()
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, self.picker);
})
}
func hide() {
UIView.animate(withDuration: Constants.AnimationDuration, animations: {
self.blackoutButton.alpha = 0.0
self.pickerBottomConstraint.update(offset: self.picker.frame.size.height + Constants.ToolbarHeight)
self.layoutIfNeeded()
}, completion: { finished in
self.removeFromSuperview()
})
}
}

View File

@ -0,0 +1,19 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import Foundation
func isAddressString(_ string: String) -> Bool {
let nsstring = string as NSString
if nsstring.length != Int(kOCTToxAddressLength) {
return false
}
let validChars = CharacterSet(charactersIn: "1234567890abcdefABCDEF")
let components = nsstring.components(separatedBy: validChars)
let leftChars = components.joined(separator: "")
return leftChars.isEmpty
}

View File

@ -0,0 +1,61 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import UIKit
import SnapKit
private struct Constants {
static let Sqrt2: CGFloat = 1.4142135623731
}
class ImageViewWithStatus: UIView {
var imageView: UIImageView!
var userStatusView: UserStatusView!
fileprivate var userStatusViewCenterConstrant: Constraint!
init() {
super.init(frame: CGRect.zero)
createViews()
installConstraints()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
imageView.layer.cornerRadius = frame.size.width / 2
let offset = bounds.size.width / (2 * Constants.Sqrt2)
userStatusViewCenterConstrant.update(offset: offset)
}
}
private extension ImageViewWithStatus {
func createViews() {
imageView = UIImageView()
imageView.backgroundColor = UIColor.clear
imageView.layer.masksToBounds = true
// imageView.contentMode = .ScaleAspectFit
addSubview(imageView)
userStatusView = UserStatusView()
addSubview(userStatusView)
}
func installConstraints() {
imageView.snp.makeConstraints {
$0.edges.equalTo(self)
}
userStatusView.snp.makeConstraints {
userStatusViewCenterConstrant = $0.center.equalTo(self).constraint
$0.size.equalTo(UserStatusView.Constants.DefaultSize)
}
}
}

View File

@ -0,0 +1,122 @@
{
"images" : [
{
"filename" : "Icon-App-20x20@2x.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "20x20"
},
{
"filename" : "Icon-App-20x20@3x.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "20x20"
},
{
"filename" : "Icon-App-29x29@1x.png",
"idiom" : "iphone",
"scale" : "1x",
"size" : "29x29"
},
{
"filename" : "Icon-App-29x29@2x.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "Icon-App-29x29@3x.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "29x29"
},
{
"filename" : "Icon-App-40x40@2x.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "40x40"
},
{
"filename" : "Icon-App-40x40@3x.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "40x40"
},
{
"filename" : "Icon-App-60x60@2x.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "60x60"
},
{
"filename" : "Icon-App-60x60@3x.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
},
{
"filename" : "Icon-App-20x20@1x.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "20x20"
},
{
"filename" : "Icon-App-20x20@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "20x20"
},
{
"filename" : "Icon-App-29x29@1x.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "29x29"
},
{
"filename" : "Icon-App-29x29@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "Icon-App-40x40@1x.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "40x40"
},
{
"filename" : "Icon-App-40x40@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "40x40"
},
{
"filename" : "Icon-App-76x76@1x.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "76x76"
},
{
"filename" : "Icon-App-76x76@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "76x76"
},
{
"filename" : "Icon-App-83.5x83.5@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"filename" : "ItunesArtwork@2x.png",
"idiom" : "ios-marketing",
"scale" : "1x",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

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