/* * Copyright (c) 2010-2020 Belledonne Communications SARL. * * This file is part of linphone-iphone * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import UIKit import Foundation import linphonesw import DropDown import PhotosUI import AVFoundation @objc class ChatConversationViewSwift: BackActionsNavigationView, PHPickerViewControllerDelegate, UIDocumentPickerDelegate, UICompositeViewDelegate, UICollectionViewDataSource, UICollectionViewDelegate, UIImagePickerControllerDelegate & UINavigationControllerDelegate{ // Replaces ChatConversationView let controlsView = ControlsView(showVideo: true, controlsViewModel: ChatConversationViewModel.sharedModel) static let compositeDescription = UICompositeViewDescription(ChatConversationViewSwift.self, statusBar: StatusBarView.self, tabBar: nil, sideMenu: SideMenuView.self, fullscreen: false, isLeftFragment: false,fragmentWith: nil) static func compositeViewDescription() -> UICompositeViewDescription! { return compositeDescription } func compositeViewDescription() -> UICompositeViewDescription! { return type(of: self).compositeDescription } let APP_GROUP_ID = "group.belledonne-communications.linphone.widget" var debugEnabled = false var chatRoom: ChatRoom? = nil var chatRoomDelegate: ChatRoomDelegate? = nil var address: String? = nil var participants: String? = nil var activeAlertController = CustomAlertController() @objc let tableController = ChatConversationTableView() let refreshControl = UIRefreshControl() var replyBubble = UIChatReplyBubbleView() var composingVisible = false var contentOriginY = 0.0 var messageViewOriginY = 0.0 var mediaSelectorHeight = 0.0 var mediaSelectorVisible = false let mediaTableView = UITableView() var mediaCollectionView : [UIImage] = [] var mediaVideoCollection : [Bool] = [] var collectionView: UICollectionView = { let top_bar_height = 66.0 let width = UIScreen.main.bounds.width * 0.9 let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout() layout.itemSize = CGSize(width: top_bar_height*2-8, height: top_bar_height*2-8) layout.sectionInset = UIEdgeInsets(top: 4, left: 4, bottom: 4, right: 4) layout.scrollDirection = .horizontal layout.minimumLineSpacing = 4 layout.minimumInteritemSpacing = 20 let collectionView = UICollectionView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: top_bar_height*2), collectionViewLayout: layout) collectionView.translatesAutoresizingMaskIntoConstraints = false collectionView.backgroundColor = UIColor(white: 1, alpha: 0.5) return collectionView }() let menu: DropDown = { let menu = DropDown() menu.dataSource = [""] var images = [ "contact_add_default.png", "contacts_all_default.png", "menu_voip_meeting_schedule", "menu_security_default.png", "ephemeral_messages_default.png", "menu_notifications_off.png", "menu_notifications_on.png", "delete_default.png", "chat_group_informations.png" ] menu.cellNib = UINib(nibName: "DropDownCell", bundle: nil) menu.customCellConfiguration = { index, title, cell in guard let cell = cell as? MyCell else { return } if(index < images.count){ switch menu.dataSource[index] { case VoipTexts.dropdown_menu_chat_conversation_add_to_contact: cell.myImageView.image = UIImage(named: images[0]) case VoipTexts.dropdown_menu_chat_conversation_go_to_contact: cell.myImageView.image = UIImage(named: images[1]) case VoipTexts.conference_schedule_start: cell.myImageView.image = UIImage(named: images[2]) case VoipTexts.dropdown_menu_chat_conversation_conversation_device: cell.myImageView.image = UIImage(named: images[3]) case VoipTexts.dropdown_menu_chat_conversation_ephemeral_messages: cell.myImageView.image = UIImage(named: images[4]) case VoipTexts.dropdown_menu_chat_conversation_mute_notifications: cell.myImageView.image = UIImage(named: images[5]) case VoipTexts.dropdown_menu_chat_conversation_unmute_notifications: cell.myImageView.image = UIImage(named: images[6]) case VoipTexts.dropdown_menu_chat_conversation_delete_messages: cell.myImageView.image = UIImage(named: images[7]) default: cell.myImageView.image = UIImage(named: images[8]) } } } return menu }() override func viewDidLoad() { super.viewDidLoad( backAction: { self.goBackChatListView() }, action1: { self.onCallClick(cChatRoom: self.chatRoom?.getCobject) }, action2: { self.tapChooseMenuItem(self.action2Button) }, action3: { self.alertActionGoToDevicesList() }, action4: { (LinphoneManager.instance().lpConfigInt(forKey: "debugenable_preference") == 1) ? self.showAddressAndIdentityPopup() : self.tapChooseMenuItem(self.action2Button) }, title: address ?? "Error", participants: participants ?? "Error" ) setupViews() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) topBar.backgroundColor = VoipTheme.voipToolbarBackgroundColor.get() self.contentView.addSubview(tableController.tableView) tableController.chatRoom = chatRoom?.getCobject refreshControl.addTarget(self, action: #selector(refreshData), for: .valueChanged) tableController.refreshControl = refreshControl tableController.toggleSelectionButton = action1SelectAllButton messageView.sendButton.onClickAction = onSendClick messageView.pictureButton.onClickAction = selectionMedia chatRoomDelegate = ChatRoomDelegateStub( onIsComposingReceived: { (room: ChatRoom, remoteAddress: Address, isComposing: Bool) -> Void in self.on_chat_room_is_composing_received(room, remoteAddress, isComposing) }, onChatMessageReceived: { (room: ChatRoom, event: EventLog) -> Void in self.on_chat_room_chat_message_received(room, event) }, onChatMessageSending: { (room: ChatRoom, event: EventLog) -> Void in self.on_chat_room_chat_message_sending(room, event) } ) chatRoom?.addDelegate(delegate: chatRoomDelegate!) tableController.tableView.separatorColor = .clear } override func viewWillDisappear(_ animated: Bool) { chatRoom?.removeDelegate(delegate: chatRoomDelegate!) editModeOff() } override func viewDidAppear(_ animated: Bool) { tableController.reloadData() messageView.ephemeralIndicator.isHidden = (linphone_chat_room_ephemeral_enabled(chatRoom?.getCobject) == 0) } override func viewDidDisappear(_ animated: Bool) { mediaSelectorVisible = false mediaSelectorHeight = 0.0 self.isComposingView.transform = CGAffineTransformIdentity; self.mediaSelector.transform = CGAffineTransformIdentity; self.contentView.transform = CGAffineTransformIdentity; } func goBackChatListView() { PhoneMainView.instance().pop(toView: ChatsListView.compositeViewDescription()) } func tapChooseMenuItem(_ sender: UIButton) { menu.anchorView = sender menu.bottomOffset = CGPoint(x: -UIScreen.main.bounds.width * 0.6, y: sender.frame.size.height) menu.show() menu.selectionAction = { [weak self] (index: Int, item: String) in guard let _ = self else { return } print(item) switch item { case VoipTexts.dropdown_menu_chat_conversation_add_to_contact: self!.addOrGoToContact() case VoipTexts.dropdown_menu_chat_conversation_go_to_contact: self!.addOrGoToContact() case VoipTexts.conference_schedule_start: self!.conferenceSchedule() case VoipTexts.dropdown_menu_chat_conversation_group_infos: self!.displayGroupInfo() case VoipTexts.dropdown_menu_chat_conversation_conversation_device: self!.goToDevicesList() case VoipTexts.dropdown_menu_chat_conversation_ephemeral_messages: self!.goToEphemeralSettings() case VoipTexts.dropdown_menu_chat_conversation_mute_notifications: self!.mute_unmute_notifications() self?.menu.dataSource[index] = VoipTexts.dropdown_menu_chat_conversation_unmute_notifications case VoipTexts.dropdown_menu_chat_conversation_unmute_notifications: self!.mute_unmute_notifications() self?.menu.dataSource[index] = VoipTexts.dropdown_menu_chat_conversation_mute_notifications case VoipTexts.dropdown_menu_chat_conversation_delete_messages: self!.onEditionChangeClick() default: self!.showAddressAndIdentityPopup() } self!.menu.clearSelection() } } func goToDevicesList() { let view: DevicesListView = self.VIEW(DevicesListView.compositeViewDescription()) view.room = chatRoom?.getCobject PhoneMainView.instance().changeCurrentView(view.compositeViewDescription()) } func addOrGoToContact() { let firstParticipant = chatRoom?.participants.first let addr = (firstParticipant != nil) ? linphone_participant_get_address(firstParticipant?.getCobject) : linphone_chat_room_get_peer_address(chatRoom?.getCobject) let contact = FastAddressBook.getContactWith(addr) if let contact { let view: ContactDetailsView = self.VIEW(ContactDetailsView.compositeViewDescription()) ContactSelection.setSelectionMode(ContactSelectionModeNone) MagicSearchSingleton.instance().currentFilter = "" view.contact = contact PhoneMainView.instance().changeCurrentView(view.compositeViewDescription()) } else { let lAddress = linphone_address_as_string_uri_only(addr) if let lAddress { var normSip = String(utf8String: lAddress) normSip = normSip?.hasPrefix("sip:") ?? false ? (normSip as NSString?)?.substring(from: 4) : normSip normSip = normSip?.hasPrefix("sips:") ?? false ? (normSip as NSString?)?.substring(from: 5) : normSip ContactSelection.setAddAddress(normSip) ContactSelection.setSelectionMode(ContactSelectionModeEdit) ContactSelection.enableSipFilter(false) PhoneMainView.instance().changeCurrentView(ContactsListView.compositeViewDescription()) } } } func displayGroupInfo() { let contactsArray: NSMutableArray = [] let admins: NSMutableArray = [] let participants = chatRoom?.participants participants?.forEach{ participant in let curi = linphone_address_as_string_uri_only(linphone_participant_get_address(participant.getCobject)) let uri = String(utf8String: curi!) contactsArray.add(uri!) if (linphone_participant_is_admin(participant.getCobject) != 0) { admins.add(uri!) } } let view: ChatConversationInfoView = self.VIEW(ChatConversationInfoView.compositeViewDescription()) view.create = false view.contacts = contactsArray view.oldContacts = contactsArray view.admins = admins view.oldAdmins = admins view.oldSubject = String(utf8String: linphone_chat_room_get_subject(chatRoom?.getCobject)) ?? LINPHONE_DUMMY_SUBJECT view.room = chatRoom?.getCobject let localAddress = linphone_address_as_string(linphone_chat_room_get_local_address(chatRoom?.getCobject)) let peerAddress = linphone_address_as_string(linphone_chat_room_get_peer_address(chatRoom?.getCobject)) view.peerAddress = UnsafePointer(peerAddress) view.localAddress = UnsafePointer(localAddress) PhoneMainView.instance().changeCurrentView(view.compositeViewDescription()) } func goToEphemeralSettings(){ let view: EphemeralSettingsView = self.VIEW(EphemeralSettingsView.compositeViewDescription()) view.room = chatRoom?.getCobject PhoneMainView.instance().changeCurrentView(view.compositeViewDescription()) } func conferenceSchedule(){ ConferenceViewModelBridge.scheduleFromGroupChat(cChatRoom: (chatRoom?.getCobject)!) PhoneMainView.instance().pop(toView: ConferenceSchedulingView.compositeViewDescription()) } func mute_unmute_notifications(){ LinphoneManager.setChatroomPushEnabled(chatRoom?.getCobject, withPushEnabled: !LinphoneManager.getChatroomPushEnabled(chatRoom?.getCobject)) } func onEditionChangeClick() { editModeOn() } func showAddressAndIdentityPopup() { let localAddress = String(utf8String: linphone_address_as_string(linphone_chat_room_get_local_address(chatRoom?.getCobject))) let peerAddress = String(utf8String: linphone_address_as_string(linphone_chat_room_get_peer_address(chatRoom?.getCobject))) var infoMsg: String? = nil if let peerAddress, let localAddress { infoMsg = "Chat room id:\n\(peerAddress)\nLocal account:\n\(localAddress)" } let popupView = UIAlertController(title: NSLocalizedString("Chatroom debug infos", comment: ""), message: infoMsg, preferredStyle: .alert) let defaultAction = UIAlertAction( title: NSLocalizedString("Copy to clipboard", comment: ""), style: .default, handler: { action in let pasteboard = UIPasteboard.general pasteboard.string = infoMsg }) popupView.addAction(defaultAction) present(popupView, animated: true, completion:{ popupView.view.superview?.isUserInteractionEnabled = true popupView.view.superview?.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dismissOnTapOutsideOrCancel))) }) } func initDataSource(groupeChat: Bool, secureLevel: Bool, cChatRoom: OpaquePointer) { menu.dataSource.removeAll() if(groupeChat){ menu.dataSource.append(VoipTexts.conference_schedule_start) menu.dataSource.append(VoipTexts.dropdown_menu_chat_conversation_group_infos) }else{ var contact: Contact? = nil let firstParticipant = chatRoom?.participants.first let addr = (firstParticipant != nil) ? linphone_participant_get_address(firstParticipant?.getCobject) : linphone_chat_room_get_peer_address(cChatRoom) contact = FastAddressBook.getContactWith(addr) if (contact == nil) { menu.dataSource.append(VoipTexts.dropdown_menu_chat_conversation_add_to_contact) } else { menu.dataSource.append(VoipTexts.dropdown_menu_chat_conversation_go_to_contact) } } if(secureLevel){ menu.dataSource.append(VoipTexts.dropdown_menu_chat_conversation_conversation_device) menu.dataSource.append(VoipTexts.dropdown_menu_chat_conversation_ephemeral_messages) } if(LinphoneManager.getChatroomPushEnabled(chatRoom?.getCobject)){ menu.dataSource.append(VoipTexts.dropdown_menu_chat_conversation_mute_notifications) }else{ menu.dataSource.append(VoipTexts.dropdown_menu_chat_conversation_unmute_notifications) } menu.dataSource.append(VoipTexts.dropdown_menu_chat_conversation_delete_messages) messageView.ephemeralIndicator.isHidden = (linphone_chat_room_ephemeral_enabled(chatRoom?.getCobject) == 0) } @objc func initChatRoom(cChatRoom:OpaquePointer) { chatRoom = ChatRoom.getSwiftObject(cObject: cChatRoom) PhoneMainView.instance().currentRoom = cChatRoom address = chatRoom?.peerAddress?.asString() var changeIcon = false let isOneToOneChat = chatRoom!.hasCapability(mask: Int(LinphoneChatRoomCapabilitiesOneToOne.rawValue)) if (isOneToOneChat) { let firstParticipant = chatRoom?.participants.first let addr = (firstParticipant != nil) ? linphone_participant_get_address(firstParticipant?.getCobject) : linphone_chat_room_get_peer_address(cChatRoom); address = FastAddressBook.displayName(for: addr) ?? "unknow" changeIcon = false titleParticipants.isHidden = true } else { address = chatRoom?.subject changeIcon = true titleParticipants.isHidden = false let participants = chatRoom?.participants participantsGroupLabel.text = "" participants?.forEach{ participant in if participantsGroupLabel.text != "" { participantsGroupLabel.text = participantsGroupLabel.text! + ", " } participantsGroupLabel.text = participantsGroupLabel.text! + FastAddressBook.displayName(for: linphone_participant_get_address(participant.getCobject)) } } changeTitle(titleString: address ?? "Error") changeCallIcon(groupChat: changeIcon) let secureLevel = FastAddressBook.image(for: linphone_chat_room_get_security_level(cChatRoom)) changeSecureLevel(secureLevel: secureLevel != nil, imageBadge: secureLevel) initDataSource(groupeChat: !isOneToOneChat, secureLevel: secureLevel != nil, cChatRoom: cChatRoom) } func onCallClick(cChatRoom: OpaquePointer?) { let firstParticipant = chatRoom?.participants.first let addr = (firstParticipant != nil) ? linphone_participant_get_address(firstParticipant?.getCobject) : linphone_chat_room_get_peer_address(cChatRoom); let isOneToOneChat = chatRoom!.hasCapability(mask: Int(LinphoneChatRoomCapabilitiesOneToOne.rawValue)) if (!isOneToOneChat) { alertActionConferenceCall(cChatRoom: cChatRoom) } else { LinphoneManager.instance().call(addr) } } func alertActionConferenceCall(cChatRoom: OpaquePointer?) { let alertController = CustomAlertController(title: VoipTexts.conference_start_group_call_dialog_message, message: nil, preferredStyle: .alert) alertController.setBackgroundColor(color: .darkGray) alertController.setTitle(font: nil, color: .white) alertController.setTint(color: .white) alertController.setMaxWidth(alert: alertController) alertController.addButtonsAlertController(alertController: alertController, buttonsViewHeightV: 60, buttonsAlertHeightV: 40) activeAlertController = alertController self.present(alertController, animated: true, completion:{ alertController.view.superview?.isUserInteractionEnabled = true alertController.view.superview?.subviews[0].addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dismissOnTapOutsideOrCancel))) }) alertController.ok_button_alert.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.onTapOkStartGroupCall))) } @objc func alertActionGoToDevicesList() { let notAskAgain = ConfigManager.instance().lpConfigBoolForKey(key: "confirmation_dialog_before_sas_call_not_ask_again"); if(!notAskAgain){ let alertController = CustomAlertController(title: VoipTexts.alert_dialog_secure_badge_button_chat_conversation_title, message: nil, preferredStyle: .alert) alertController.setBackgroundColor(color: .darkGray) alertController.setTitle(font: nil, color: .white) alertController.setTint(color: .white) alertController.setMaxWidth(alert: alertController) alertController.addButtonsAlertController(alertController: alertController, buttonsViewHeightV: 60, checkboxViewHeightV: 50, buttonsAlertHeightV: 40) activeAlertController = alertController self.present(alertController, animated: true, completion:{ alertController.view.superview?.isUserInteractionEnabled = true alertController.view.superview?.subviews[0].addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dismissOnTapOutsideOrCancel))) }) alertController.ok_button_alert.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.onTapOkGoToDevicesList))) }else{ let view: DevicesListView = self.VIEW(DevicesListView.compositeViewDescription()) view.room = chatRoom?.getCobject PhoneMainView.instance().changeCurrentView(view.compositeViewDescription()) } } @objc func onTapOkStartGroupCall(){ self.dismiss(animated: true, completion: nil) ConferenceViewModelBridge.startGroupCall(cChatRoom: (chatRoom?.getCobject)!) } @objc func onTapOkGoToDevicesList() { self.dismiss(animated: true, completion: nil) if(activeAlertController.isChecked){ ConfigManager.instance().lpConfigSetBool(value: activeAlertController.isChecked, key: "confirmation_dialog_before_sas_call_not_ask_again") } let view: DevicesListView = self.VIEW(DevicesListView.compositeViewDescription()) view.room = chatRoom?.getCobject PhoneMainView.instance().changeCurrentView(view.compositeViewDescription()) } @objc func dismissOnTapOutsideOrCancel(){ self.dismiss(animated: true, completion: nil) } @objc func refreshData() { tableController.refreshData() refreshControl.endRefreshing() if tableController.totalNumberOfItems() == 0 { return } tableController.loadData() tableController.tableView.scrollToRow( at: IndexPath(row: tableController.currentIndex, section: 0), at: .top, animated: false) } override func editModeOn(){ super.editModeOn() tableController.setEditing(true, animated: false) } override func editModeOff(){ super.editModeOff() tableController.setEditing(false, animated: false) } override func selectDeselectAll(){ super.selectDeselectAll() if(action1SelectAllButton.isHidden){ tableController.onSelectionToggle(action1SelectAllButton) }else{ tableController.onSelectionToggle(action1SelectAllButton) } } override func deleteSelected(){ super.deleteSelected() onDeleteClick() } func onDeleteClick() { let msg = NSLocalizedString("Do you want to delete the selected messages?", comment: "") UIConfirmationDialog.show( withMessage: msg, cancelMessage: nil, confirmMessage: nil, onCancelClick: { [self] in onEditionChangeClick()}, onConfirmationClick: { self.tableController.removeSelection(nil) self.editModeOff() self.tableController.loadData() } ) } func sendMessage(message: String?, withExterlBodyUrl externalUrl: URL?, rootMessage: ChatMessage?) -> Bool { if chatRoom == nil { return false } let msg = rootMessage let basic = ChatConversationView.isBasicChatRoom(chatRoom?.getCobject) let params = linphone_account_get_params(linphone_core_get_default_account(LinphoneManager.getLc())) let cpimEnabled = linphone_account_params_cpim_in_basic_chat_room_enabled(params) if (!basic || (cpimEnabled != 0)) && (message != nil) && message!.count > 0 { linphone_chat_message_add_utf8_text_content(msg?.getCobject, message) } if (externalUrl != nil) { linphone_chat_message_set_external_body_url(msg?.getCobject, externalUrl!.absoluteString) } let contentList = linphone_chat_message_get_contents(msg?.getCobject) if bctbx_list_size(contentList) > 0 { linphone_chat_message_send(msg?.getCobject) } if basic && (cpimEnabled == 0) && (message != nil) && message!.count > 0 { linphone_chat_message_send(linphone_chat_room_create_message_from_utf8(chatRoom?.getCobject, message)) } return true } func sendMessageInMessageField(rootMessage: ChatMessage?) { if sendMessage(message: messageView.messageText.text, withExterlBodyUrl: nil, rootMessage: rootMessage) { messageView.messageText.text = "" } } func onSendClick() { //let rootMessage = replyBubble ? linphone_chat_room_create_reply_message(chatRoom?.getCobject, replyBubble.message) : linphone_chat_room_create_empty_message(chatRoom?.getCobject) let rootMessage = linphone_chat_room_create_empty_message(chatRoom?.getCobject) /* if replyBubble != nil { closePendingReply() } if isPendingVoiceRecord && voiceRecorder && linphone_recorder_get_file(voiceRecorder) { let voiceContent = linphone_recorder_create_content(voiceRecorder) isPendingVoiceRecord = false cancelVoiceRecording() stopVoiceRecordPlayer() linphone_chat_message_add_content(rootMessage, voiceContent) } if fileContext.count() > 0 { if linphone_chat_room_get_capabilities(chatRoom?.getCobject) & LinphoneChatRoomCapabilitiesConference != 0 { startMultiFilesUpload(rootMessage) } else { var i = 0 for i in 0..<(fileContext.count() - 1) { startUploadData(fileContext.datasArray[i], withType: fileContext.typesArray[i], withName: fileContext.namesArray[i], andMessage: nil, rootMessage: nil) } if isOneToOne { startUploadData(fileContext.datasArray[i], withType: fileContext.typesArray[i], withName: fileContext.namesArray[i], andMessage: nil, rootMessage: nil) if messageView.messageText.text != "" { sendMessage(message: messageView.messageText.text, withExterlBodyUrl: nil, rootMessage: rootMessage) } } else { startUploadData(fileContext.datasArray[i], withType: fileContext.typesArray[i], withName: fileContext.namesArray[i], andMessage: messageField.text(), rootMessage: rootMessage) } } clearMessageView() return } */ let result = ChatMessage.getSwiftObject(cObject: rootMessage!) sendMessageInMessageField(rootMessage: result) } func on_chat_room_chat_message_received(_ cr: ChatRoom?, _ event_log: EventLog?) { let chat = event_log?.chatMessage if chat == nil { return } var hasFile = false // if auto_download is available and file is downloaded if (linphone_core_get_max_size_for_auto_download_incoming_files(LinphoneManager.getLc()) > -1) && (chat?.fileTransferInformation != nil) { hasFile = true } var returnValue = false; chat?.contents.forEach({ content in if !content.isFileTransfer && !content.isText && !content.isVoiceRecording && !hasFile { returnValue = true } }) if returnValue { return } let from = chat?.fromAddress if from == nil { return } let isDisplayingBottomOfTable = tableController.tableView.indexPathsForVisibleRows?.last?.row == (tableController.totalNumberOfItems() ) - 1 tableController.addEventEntry(event_log?.getCobject) if isDisplayingBottomOfTable { tableController.scroll(toBottom: true) tableController.scrollBadge!.text = nil tableController.scrollBadge!.isHidden = true } else { tableController.scrollBadge!.isHidden = false let unread_msg = linphone_chat_room_get_unread_messages_count(cr?.getCobject) tableController.scrollBadge!.text = "\(unread_msg)" } } func on_chat_room_chat_message_sending(_ cr: ChatRoom?, _ event_log: EventLog?) { tableController.addEventEntry(event_log?.getCobject) tableController.scroll(toBottom: true) } func on_chat_room_is_composing_received(_ cr: ChatRoom?, _ remoteAddr: Address?, _ isComposing: Bool) { let composing = (linphone_chat_room_is_remote_composing(cr?.getCobject) != 0) || bctbx_list_size(linphone_chat_room_get_composing_addresses(cr?.getCobject)) > 0 setComposingVisible(composing, withDelay: 0.3) } func setComposingVisible(_ visible: Bool, withDelay delay: CGFloat) { let shouldAnimate = composingVisible != visible if visible { let addresses = chatRoom!.composingAddresses var composingAddresses : String? = "" if addresses.count == 1 { composingAddresses = FastAddressBook.displayName(for: addresses.first?.getCobject) isComposingTextView.text = String.localizedStringWithFormat(NSLocalizedString("%@ is writing...", comment: ""), composingAddresses!) } else { addresses.forEach({ addressItem in if composingAddresses != "" { composingAddresses = composingAddresses! + ", " } composingAddresses = composingAddresses! + FastAddressBook.displayName(for: addressItem.getCobject) }) isComposingTextView.text = String.localizedStringWithFormat(NSLocalizedString("%@ are writing...", comment: ""), composingAddresses!) } } composingVisible = visible if !shouldAnimate { return } var isBottomOfView = false if (tableController.tableView.contentOffset.y + self.isComposingView.frame.size.height >= (tableController.tableView.contentSize.height - tableController.tableView.frame.size.height)) { isBottomOfView = true } UIView.animate( withDuration: delay, animations: { self.contentOriginY = self.contentView.frame.origin.y self.messageViewOriginY = self.isComposingView.frame.origin.y if visible { if(isBottomOfView && self.tableController.totalNumberOfItems() > 3){ self.contentView.transform = self.contentView.transform.translatedBy(x: 0, y: -self.mediaSelectorHeight - self.top_bar_height/2) self.contentView.transform = CGAffineTransform(translationX: 0, y: -self.mediaSelectorHeight - self.top_bar_height/2) } self.isComposingView.transform = self.isComposingView.transform.translatedBy(x: 0, y: -self.mediaSelectorHeight - self.top_bar_height/2) self.isComposingView.transform = CGAffineTransform(translationX: 0, y: -self.mediaSelectorHeight - self.top_bar_height/2) }else{ if(isBottomOfView && self.tableController.totalNumberOfItems() > 3){ self.contentView.transform = self.contentView.transform.translatedBy(x: 0, y: -self.mediaSelectorHeight) self.contentView.transform = CGAffineTransform(translationX: 0, y: -self.mediaSelectorHeight) } self.isComposingView.transform = self.isComposingView.transform.translatedBy(x: 0, y: 0) self.isComposingView.transform = CGAffineTransform(translationX: 0, y: 0) } }) } func selectionMedia() { self.alertAction() mediaSelectorVisible = !mediaSelectorVisible mediaSelectorHeight = mediaSelectorVisible ? top_bar_height*2 : 0.0 var isBottomOfView = false if (tableController.tableView.contentOffset.y + self.isComposingView.frame.size.height >= (tableController.tableView.contentSize.height - tableController.tableView.frame.size.height)) { isBottomOfView = true } UIView.animate( withDuration: 0.3, animations: { [self] in self.contentOriginY = self.contentView.frame.origin.y self.messageViewOriginY = self.mediaSelector.frame.origin.y if self.mediaSelectorVisible { if(composingVisible){ self.isComposingView.transform = self.isComposingView.transform.translatedBy(x: 0, y: -self.mediaSelectorHeight - self.top_bar_height/2) self.isComposingView.transform = CGAffineTransform(translationX: 0, y: -self.mediaSelectorHeight - self.top_bar_height/2) } if(isBottomOfView && tableController.totalNumberOfItems() > 3){ self.contentView.transform = self.contentView.transform.translatedBy(x: 0, y: -self.mediaSelectorHeight) self.contentView.transform = CGAffineTransform(translationX: 0, y: -self.mediaSelectorHeight) } self.mediaSelector.transform = self.mediaSelector.transform.translatedBy(x: 0, y: -self.mediaSelectorHeight) self.mediaSelector.transform = CGAffineTransform(translationX: 0, y: -self.mediaSelectorHeight) }else{ if(self.composingVisible){ self.isComposingView.transform = self.isComposingView.transform.translatedBy(x: 0, y: -self.top_bar_height/2) self.isComposingView.transform = CGAffineTransform(translationX: 0, y: -self.top_bar_height/2) } if(isBottomOfView && tableController.totalNumberOfItems() > 3){ self.contentView.transform = self.contentView.transform.translatedBy(x: 0, y: 0) self.contentView.transform = CGAffineTransform(translationX: 0, y: 0) } self.mediaSelector.transform = self.mediaSelector.transform.translatedBy(x: 0, y: 0) self.mediaSelector.transform = CGAffineTransform(translationX: 0, y: 0) } }) } class func autoDownload(_ message: ChatMessage?) { let content = linphone_chat_message_get_file_transfer_information(message?.getCobject) let name = String(utf8String: linphone_content_get_name(content)) let fileType = String(utf8String: linphone_content_get_type(content)) let key = ChatConversationView.getKeyFromFileType(fileType, fileName: name) LinphoneManager.setValueInMessageAppData(name, forKey: key, in: message?.getCobject) DispatchQueue.main.async(execute: { if !VFSUtil.vfsEnabled(groupName: kLinphoneMsgNotificationAppGroupId) && ConfigManager.instance().lpConfigBoolForKey(key: "auto_write_to_gallery_preference") { ChatConversationViewSwift.writeMediaToGallery(name: name, fileType: fileType) } }) } class func writeMediaToGallery(name: String?, fileType: String?) { let filePath = LinphoneManager.validFilePath(name) let fileManager = FileManager.default if fileManager.fileExists(atPath: filePath!) { let data = NSData(contentsOfFile: filePath!) as Data? let block: (() -> Void)? = { if fileType == "image" { // we're finished, save the image and update the message let image = UIImage(data: data!) if image == nil { showFileDownloadError() return } var placeHolder: PHObjectPlaceholder? = nil PHPhotoLibrary.shared().performChanges({ let request = PHAssetCreationRequest.creationRequestForAsset(from: image!) placeHolder = request.placeholderForCreatedAsset }) { success, error in DispatchQueue.main.async(execute: { if error != nil { Log.e("Cannot save image data downloaded \(error!.localizedDescription)") let errView = UIAlertController( title: NSLocalizedString("Transfer error", comment: ""), message: NSLocalizedString("Cannot write image to photo library", comment: ""), preferredStyle: .alert) let defaultAction = UIAlertAction( title: "OK", style: .default, handler: { action in }) errView.addAction(defaultAction) PhoneMainView.instance()!.present(errView, animated: true) } else { Log.i("Image saved to \(placeHolder!.localIdentifier)") } }) } } else if fileType == "video" { var placeHolder: PHObjectPlaceholder? PHPhotoLibrary.shared().performChanges({ let request = PHAssetCreationRequest.creationRequestForAssetFromVideo(atFileURL: URL(fileURLWithPath: filePath!)) placeHolder = request?.placeholderForCreatedAsset }) { success, error in DispatchQueue.main.async(execute: { if error != nil { Log.e("Cannot save video data downloaded \(error!.localizedDescription)") let errView = UIAlertController( title: NSLocalizedString("Transfer error", comment: ""), message: NSLocalizedString("Cannot write video to photo library", comment: ""), preferredStyle: .alert) let defaultAction = UIAlertAction( title: "OK", style: .default, handler: { action in }) errView.addAction(defaultAction) PhoneMainView.instance()!.present(errView, animated: true) } else { Log.i("video saved to \(placeHolder!.localIdentifier)") } }) } } } if PHPhotoLibrary.authorizationStatus() == .authorized { block!() } else { PHPhotoLibrary.requestAuthorization({ status in DispatchQueue.main.async(execute: { if PHPhotoLibrary.authorizationStatus() == .authorized { block!() } else { UIAlertView(title: NSLocalizedString("Photo's permission", comment: ""), message: NSLocalizedString("Photo not authorized", comment: ""), delegate: nil, cancelButtonTitle: "", otherButtonTitles: "Continue").show() } }) }) } } } class func showFileDownloadError() { let errView = UIAlertController( title: NSLocalizedString("File download error", comment: ""), message: NSLocalizedString( """ Error while downloading the file.\n\ The file is probably encrypted.\n\ Please retry to download this file after activating LIME. """, comment: ""), preferredStyle: .alert) let defaultAction = UIAlertAction( title: "OK", style: .default, handler: { action in }) errView.addAction(defaultAction) PhoneMainView.instance()!.present(errView, animated: true) } func alertAction() { let alertController = UIAlertController(title: VoipTexts.image_picker_view_alert_action_title, message: nil, preferredStyle: .actionSheet) let alert_action_camera = UIAlertAction(title: VoipTexts.image_picker_view_alert_action_camera, style: .default, handler: { (action) -> Void in self.imageCamera() }) let alert_action_photo_library = UIAlertAction(title: VoipTexts.image_picker_view_alert_action_photo_library, style: .default, handler: { (action) -> Void in self.pickPhotos() }) let alert_action_document = UIAlertAction(title: VoipTexts.image_picker_view_alert_action_document, style: .default, handler: { (action) -> Void in self.openDocumentPicker() }) let cancel = UIAlertAction(title: VoipTexts.cancel, style: .cancel) { (action) -> Void in } alertController.addAction(cancel) alertController.addAction(alert_action_camera) alertController.addAction(alert_action_photo_library) alertController.addAction(alert_action_document) alertController.popoverPresentationController?.sourceView = PhoneMainView.instance().mainViewController.statusBarView PhoneMainView.instance().mainViewController.present(alertController, animated: true) } func imageCamera(){ let imagePicker = UIImagePickerController() imagePicker.sourceType = .camera imagePicker.mediaTypes = ["public.image", "public.movie"] imagePicker.delegate = self PhoneMainView.instance().mainViewController.present(imagePicker, animated: true) } func pickPhotos() { if #available(iOS 14.0, *) { var config = PHPickerConfiguration() config.selectionLimit = 0 let pickerViewController = PHPickerViewController(configuration: config) pickerViewController.delegate = self PhoneMainView.instance().mainViewController.present(pickerViewController, animated: true) } else { let imagePicker = UIImagePickerController() imagePicker.sourceType = .photoLibrary imagePicker.mediaTypes = ["public.image", "public.movie"] imagePicker.delegate = self PhoneMainView.instance().mainViewController.present(imagePicker, animated: true) } } func openDocumentPicker() { let documentPicker = UIDocumentPickerViewController(documentTypes: ["public.jpeg","com.compuserve.gif","public.url","public.movie","com.apple.mapkit.map-item","com.adobe.pdf","public.png","public.image", "public.data", "public.text"], in: .import) documentPicker.delegate = self documentPicker.modalPresentationStyle = .overFullScreen documentPicker.allowsMultipleSelection = true PhoneMainView.instance().mainViewController.present(documentPicker, animated: true) } func createThumbnailOfVideoFromFileURL(videoURL: String) -> UIImage? { let asset = AVAsset(url: URL(string: videoURL)!) let assetImgGenerate = AVAssetImageGenerator(asset: asset) assetImgGenerate.appliesPreferredTrackTransform = true do { let img = try assetImgGenerate.copyCGImage(at: CMTimeMake(value: 1, timescale: 10), actualTime: nil) let thumbnail = UIImage(cgImage: img) return thumbnail } catch _{ return nil } } func setupViews() { mediaSelector.addSubview(collectionView) collectionView.dataSource = self collectionView.delegate = self collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "cell") } @objc func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return mediaCollectionView.count } @objc(collectionView:cellForItemAtIndexPath:) func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) let viewCell: UIView = UIView(frame: cell.contentView.frame) let deleteButton = CallControlButton(width: 22, height: 22, buttonTheme:VoipTheme.nav_black_button("reply_cancel")) let imageCell = mediaCollectionView[indexPath.row] let myImageView = UIImageView(image: imageCell) cell.addSubview(viewCell) myImageView.size(w: (viewCell.frame.width * 0.9)-2, h: (viewCell.frame.height * 0.9)-2).done() viewCell.addSubview(myImageView) viewCell.addSubview(deleteButton) myImageView.alignParentBottom(withMargin: 4).alignParentLeft(withMargin: 4).done() if(mediaVideoCollection[indexPath.row]){ var imagePlay = UIImage() if #available(iOS 13.0, *) { imagePlay = (UIImage(named: "vr_play")!.withTintColor(.white)) } else { imagePlay = UIImage(named: "vr_play")! } let myImagePlayView = UIImageView(image: imagePlay) viewCell.addSubview(myImagePlayView) myImagePlayView.size(w: viewCell.frame.width/4, h: viewCell.frame.height/4).done() myImagePlayView.alignHorizontalCenterWith(viewCell).alignVerticalCenterWith(viewCell).done() } myImageView.contentMode = .scaleAspectFill myImageView.clipsToBounds = true deleteButton.alignParentRight().done() return cell } @available(iOS 14.0, *) func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { picker.dismiss(animated: true, completion: nil) let itemProviders = results.map(\.itemProvider) for item in itemProviders { if item.canLoadObject(ofClass: UIImage.self) { item.loadObject(ofClass: UIImage.self) { (image, error) in DispatchQueue.main.async { if let image = image as? UIImage { self.collectionView.performBatchUpdates({ let indexPath = IndexPath(row: self.mediaCollectionView.count, section: 0) self.mediaCollectionView.append(image) self.collectionView.insertItems(at: [indexPath]) self.mediaVideoCollection.append(false) }, completion: nil) } } } } else if item.hasItemConformingToTypeIdentifier(UTType.movie.identifier) { item.loadFileRepresentation(forTypeIdentifier: UTType.movie.identifier) { urlFile, err in if let url = urlFile { DispatchQueue.main.sync { var image = self.createThumbnailOfVideoFromFileURL(videoURL: url.relativeString) if image == nil { image = UIImage(named: "chat_error")} self.collectionView.performBatchUpdates({ let indexPath = IndexPath(row: self.mediaCollectionView.count, section: 0) self.mediaCollectionView.append(image!) self.collectionView.insertItems(at: [indexPath]) self.mediaVideoCollection.append(true) }, completion: nil) } } } } } } func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { picker.dismiss(animated: true, completion: nil) } func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { let mediaType = info[UIImagePickerController.InfoKey.mediaType] as! String switch mediaType { case "public.image": let image = info[UIImagePickerController.InfoKey.originalImage] as! UIImage self.collectionView.performBatchUpdates({ let indexPath = IndexPath(row: self.mediaCollectionView.count, section: 0) self.mediaCollectionView.append(image) self.collectionView.insertItems(at: [indexPath]) self.mediaVideoCollection.append(false) }, completion: nil) case "public.movie": let videoUrl = info[UIImagePickerController.InfoKey.mediaURL] as! URL var image = createThumbnailOfVideoFromFileURL(videoURL: videoUrl.relativeString) if image == nil { image = UIImage(named: "chat_error")} self.collectionView.performBatchUpdates({ let indexPath = IndexPath(row: self.mediaCollectionView.count, section: 0) self.mediaCollectionView.append(image!) self.collectionView.insertItems(at: [indexPath]) self.mediaVideoCollection.append(true) }, completion: nil) default: print("Mismatched type: \(mediaType)") } picker.dismiss(animated: true, completion: nil) } public func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) { controller.dismiss(animated: true) } public func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { if(controller.documentPickerMode == .import){ urls.forEach { url in let imageExtension = ["png", "jpg", "jpeg", "bmp", "heic"] let videoExtension = ["mkv", "avi", "mov", "mp4"] if(imageExtension.contains(url.pathExtension.lowercased())){ let image = UIImage(contentsOfFile: url.path) self.collectionView.performBatchUpdates({ let indexPath = IndexPath(row: self.mediaCollectionView.count, section: 0) self.mediaCollectionView.append(image!) self.collectionView.insertItems(at: [indexPath]) self.mediaVideoCollection.append(false) }, completion: nil) }else if(videoExtension.contains(url.pathExtension.lowercased())){ let thumbnail = createThumbnailOfVideoFromFileURL(videoURL: url.relativeString) self.collectionView.performBatchUpdates({ let indexPath = IndexPath(row: self.mediaCollectionView.count, section: 0) self.mediaCollectionView.append(thumbnail!) self.collectionView.insertItems(at: [indexPath]) self.mediaVideoCollection.append(true) }, completion: nil) }else{ let otherFile = UIImage(named: "chat_error") self.collectionView.performBatchUpdates({ let indexPath = IndexPath(row: self.mediaCollectionView.count, section: 0) self.mediaCollectionView.append(otherFile!) self.collectionView.insertItems(at: [indexPath]) self.mediaVideoCollection.append(false) }, completion: nil) } } } controller.dismiss(animated: true) } }