Initial commit
3062
Antidote.xcodeproj/project.pbxproj
Normal file
634
Antidote/ActiveSessionCoordinator.swift
Normal 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)
|
||||
|
||||
})
|
||||
}
|
||||
}
|
24
Antidote/ActiveSessionNavigationCoordinator.swift
Normal 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")
|
||||
}
|
||||
}
|
245
Antidote/AddFriendController.swift
Normal 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)
|
||||
}
|
||||
}
|
50
Antidote/AlertAudioPlayer.swift
Normal 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
|
||||
}
|
||||
}
|
43
Antidote/Antidote-Bridging-Header.h
Normal 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"
|
89
Antidote/Antidote-Info.plist
Normal 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>
|
12
Antidote/Antidote-Prefix.pch
Normal 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 */
|
12
Antidote/Antidote.entitlements
Normal 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>
|
163
Antidote/AppCoordinator.swift
Normal 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
@ -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)
|
||||
}
|
||||
}
|
81
Antidote/AudioPlayer.swift
Normal 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
|
||||
}
|
||||
}
|
110
Antidote/AutomationCoordinator.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
131
Antidote/AvatarManager.swift
Normal 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
@ -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() {}
|
||||
}
|
9
Antidote/BaseCellModel.swift
Normal 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
@ -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
@ -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
|
||||
}
|
||||
}
|
403
Antidote/CallActiveController.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
117
Antidote/CallBaseController.swift
Normal 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
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
396
Antidote/CallCoordinator.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
125
Antidote/CallIncomingController.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
93
Antidote/CallManagement/Call.swift
Normal 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
|
||||
}
|
||||
|
||||
}
|
101
Antidote/CallManagement/CallManager.swift
Normal 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?()
|
||||
}
|
||||
}
|
238
Antidote/CallManagement/ProviderDelegate.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
78
Antidote/ChangeAutodownloadImagesController.swift
Normal 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)
|
||||
}
|
||||
}
|
254
Antidote/ChangePasswordController.swift
Normal 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
|
||||
}
|
||||
}
|
111
Antidote/ChangePinTimeoutController.swift
Normal 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)
|
||||
}
|
||||
}
|
81
Antidote/ChangeUserStatusController.swift
Normal 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)
|
||||
}
|
||||
}
|
100
Antidote/ChatBaseTextCell.swift
Normal 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
|
||||
}
|
||||
}
|
9
Antidote/ChatBaseTextCellModel.swift
Normal 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 = ""
|
||||
}
|
88
Antidote/ChatBottomStatusViewManager.swift
Normal 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() {
|
||||
|
||||
}
|
||||
}
|
29
Antidote/ChatEditable.swift
Normal 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()
|
||||
}
|
49
Antidote/ChatFauxOfflineHeaderView.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
178
Antidote/ChatGenericFileCell.swift
Normal 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
|
||||
}
|
||||
}
|
26
Antidote/ChatGenericFileCellModel.swift
Normal 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)?
|
||||
}
|
86
Antidote/ChatIncomingCallCell.swift
Normal 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
|
||||
}
|
||||
}
|
10
Antidote/ChatIncomingCallCellModel.swift
Normal 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
|
||||
}
|
88
Antidote/ChatIncomingFileCell.swift
Normal 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?()
|
||||
}
|
||||
}
|
||||
}
|
8
Antidote/ChatIncomingFileCellModel.swift
Normal 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 {
|
||||
}
|
37
Antidote/ChatIncomingTextCell.swift
Normal 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 {}
|
||||
}
|
||||
}
|
217
Antidote/ChatInputView.swift
Normal 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
|
||||
}
|
||||
}
|
195
Antidote/ChatInputViewManager.swift
Normal 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
@ -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 {}
|
||||
}
|
||||
}
|
18
Antidote/ChatListCellModel.swift
Normal 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
|
||||
}
|
107
Antidote/ChatListController.swift
Normal 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)))
|
||||
}
|
||||
}
|
||||
}
|
222
Antidote/ChatListTableManager.swift
Normal 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)
|
||||
}
|
||||
}
|
189
Antidote/ChatMovableDateCell.swift
Normal 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)
|
||||
}
|
||||
}
|
9
Antidote/ChatMovableDateCellModel.swift
Normal 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 = ""
|
||||
}
|
86
Antidote/ChatOutgoingCallCell.swift
Normal 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
|
||||
}
|
||||
}
|
10
Antidote/ChatOutgoingCallCellModel.swift
Normal 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
|
||||
}
|
98
Antidote/ChatOutgoingFileCell.swift
Normal 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?()
|
||||
}
|
||||
}
|
||||
}
|
9
Antidote/ChatOutgoingFileCellModel.swift
Normal 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 {
|
||||
|
||||
}
|
51
Antidote/ChatOutgoingTextCell.swift
Normal 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 {}
|
||||
}
|
||||
}
|
10
Antidote/ChatOutgoingTextCellModel.swift
Normal 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
|
||||
}
|
1508
Antidote/ChatPrivateController.swift
Normal file
111
Antidote/ChatPrivateTitleView.swift
Normal 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
|
||||
}
|
||||
}
|
23
Antidote/ChatProgressBridge.swift
Normal 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)
|
||||
}
|
||||
}
|
10
Antidote/ChatProgressProtocol.swift
Normal 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 }
|
||||
}
|
101
Antidote/ChatTypingHeaderView.swift
Normal 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
|
||||
}
|
||||
}
|
94
Antidote/ChatsTabCoordinator.swift
Normal 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)
|
||||
}
|
||||
}
|
24
Antidote/ConnectionStatus.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
15
Antidote/CoordinatorProtocol.swift
Normal 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
@ -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
|
||||
}
|
||||
}
|
148
Antidote/EnterPinController.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
337
Antidote/ErrorHandling.swift
Normal 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"))
|
||||
}
|
||||
}
|
||||
}
|
11
Antidote/ExceptionHandling.h
Normal 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
|
19
Antidote/ExceptionHandling.m
Normal 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
|
224
Antidote/ExtendedTextField.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
79
Antidote/FAQController.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
71
Antidote/FilePreviewControllerDataSource.swift
Normal 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)
|
||||
}
|
||||
}
|
186
Antidote/FriendCardController.swift
Normal 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;
|
||||
}
|
||||
|
||||
}
|
121
Antidote/FriendListCell.swift
Normal 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 {}
|
||||
}
|
||||
}
|
20
Antidote/FriendListCellModel.swift
Normal 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
|
||||
}
|
315
Antidote/FriendListController.swift
Normal 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)
|
||||
}
|
||||
}
|
230
Antidote/FriendListDataSource.swift
Normal 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
|
||||
}
|
||||
}
|
90
Antidote/FriendRequestController.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
197
Antidote/FriendSelectController.swift
Normal 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)))
|
||||
}
|
||||
}
|
||||
}
|
152
Antidote/FriendsTabCoordinator.swift
Normal 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)
|
||||
}
|
||||
}
|
158
Antidote/FullscreenPicker.swift
Normal 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()
|
||||
})
|
||||
}
|
||||
}
|
19
Antidote/HelperFunctions.swift
Normal 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
|
||||
}
|
61
Antidote/ImageViewWithStatus.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
122
Antidote/Images.xcassets/AppIcon.appiconset/Contents.json
Normal 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
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 618 B |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 2.6 KiB |
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 7.7 KiB |
After Width: | Height: | Size: 7.7 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 3.9 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 10 KiB |
BIN
Antidote/Images.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png
Normal file
After Width: | Height: | Size: 43 KiB |
6
Antidote/Images.xcassets/Contents.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|