Commit 00695c69 authored by Kateryna Kostiuk's avatar Kateryna Kostiuk Committed by Andreas Traczyk

contact request: create conversation

Create conversation and generate messages when:
- request was sent to peer
- request was accepted by peer
- request from peer was accepted

Change-Id: I9b66384a35a9b948c66492113ca3cc3fae2896ac
Reviewed-by: Andreas Traczyk's avatarAndreas Traczyk <andreas.traczyk@savoirfairelinux.com>
parent 40d80977
......@@ -80,6 +80,8 @@
04399B141D1C341A00E99CD9 /* libx264.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 04399AE21D1C341A00E99CD9 /* libx264.a */; };
04399B151D1C341A00E99CD9 /* libyaml-cpp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 04399AE31D1C341A00E99CD9 /* libyaml-cpp.a */; };
0586C94B1F684DF600613517 /* UIImage+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0586C94A1F684DF600613517 /* UIImage+Helpers.swift */; };
0E403F811F7D797300C80BC2 /* MessageCellGenerated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E403F801F7D797300C80BC2 /* MessageCellGenerated.swift */; };
0E403F831F7D79B000C80BC2 /* MessageCellGenerated.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0E403F821F7D79B000C80BC2 /* MessageCellGenerated.xib */; };
0EE1B54E1F75ACDE00BA98EE /* CNContactVCardSerialization+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EE1B54D1F75ACDE00BA98EE /* CNContactVCardSerialization+Helpers.swift */; };
0EE1B5501F75AD4700BA98EE /* VCardUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EE1B54F1F75AD4700BA98EE /* VCardUtils.swift */; };
0EEFBA3C1F83DA21000EDBAD /* libsecp256k1.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 0EEFBA3B1F83DA21000EDBAD /* libsecp256k1.a */; };
......@@ -296,6 +298,8 @@
04399AE21D1C341A00E99CD9 /* libx264.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libx264.a; path = ../fat/lib/libx264.a; sourceTree = "<group>"; };
04399AE31D1C341A00E99CD9 /* libyaml-cpp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libyaml-cpp.a"; path = "../fat/lib/libyaml-cpp.a"; sourceTree = "<group>"; };
0586C94A1F684DF600613517 /* UIImage+Helpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImage+Helpers.swift"; sourceTree = "<group>"; };
0E403F801F7D797300C80BC2 /* MessageCellGenerated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageCellGenerated.swift; sourceTree = "<group>"; };
0E403F821F7D79B000C80BC2 /* MessageCellGenerated.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MessageCellGenerated.xib; sourceTree = "<group>"; };
0EE1B54D1F75ACDE00BA98EE /* CNContactVCardSerialization+Helpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CNContactVCardSerialization+Helpers.swift"; sourceTree = "<group>"; };
0EE1B54F1F75AD4700BA98EE /* VCardUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VCardUtils.swift; sourceTree = "<group>"; };
0EEFBA3B1F83DA21000EDBAD /* libsecp256k1.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libsecp256k1.a; path = ../DEPS/arm64/lib/libsecp256k1.a; sourceTree = "<group>"; };
......@@ -940,6 +944,8 @@
1A2D18F21F292D7200B2C785 /* MessageCellReceived.xib */,
1A2D18F31F292D7200B2C785 /* MessageCellSent.swift */,
1A2D18F41F292D7200B2C785 /* MessageCellSent.xib */,
0E403F801F7D797300C80BC2 /* MessageCellGenerated.swift */,
0E403F821F7D79B000C80BC2 /* MessageCellGenerated.xib */,
);
path = Cells;
sourceTree = "<group>";
......@@ -1152,6 +1158,7 @@
1A2D18F61F292D7200B2C785 /* MessageCellReceived.xib in Resources */,
1A2D18EF1F291A0100B2C785 /* MeDetailViewController.storyboard in Resources */,
1A2D18B11F2915B600B2C785 /* SmartlistViewController.storyboard in Resources */,
0E403F831F7D79B000C80BC2 /* MessageCellGenerated.xib in Resources */,
04399A031D1C2D9D00E99CD9 /* Images.xcassets in Resources */,
1A2041841F1EA0FC00C08435 /* CreateAccountViewController.storyboard in Resources */,
1A2D18ED1F2919D800B2C785 /* MeViewController.storyboard in Resources */,
......@@ -1294,6 +1301,7 @@
1A5DC0371F35675E0075E8EF /* ContactRequestCell.swift in Sources */,
1A20417C1F1E56FF00C08435 /* WelcomeViewModel.swift in Sources */,
1A5DC03D1F35678D0075E8EF /* ContactRequestItem.swift in Sources */,
0E403F811F7D797300C80BC2 /* MessageCellGenerated.swift in Sources */,
1A2041821F1E906B00C08435 /* CreateProfileViewModel.swift in Sources */,
1A0C4EE31F1D673600550433 /* InjectionBag.swift in Sources */,
564C44641E943E1E000F92B1 /* NameRegistrationAdapterDelegate.swift in Sources */,
......
......@@ -54,5 +54,8 @@ extension Chameleon {
MessageBubble.appearance(whenContainedInInstancesOf: [MessageCellReceived.self]).tintColor = secondaryContentColor
MessageBubble.appearance(whenContainedInInstancesOf: [MessageCellReceived.self]).backgroundColor = secondaryColor
UILabel.appearance(whenContainedInInstancesOf: [MessageBubble.self, MessageCellReceived.self]).textColor = secondaryContentColor
MessageBubble.appearance(whenContainedInInstancesOf: [MessageCellGenerated.self]).tintColor = UIColor.clear
MessageBubble.appearance(whenContainedInInstancesOf: [MessageCellGenerated.self]).backgroundColor = UIColor.clear
}
}
......@@ -26,13 +26,16 @@ class ContactRequestsViewModel: ViewModel {
let contactsService: ContactsService
let accountsService: AccountsService
let conversationService: ConversationsService
let nameService: NameService
fileprivate let disposeBag = DisposeBag()
fileprivate let log = SwiftyBeaver.self
required init(with injectionBag: InjectionBag) {
self.contactsService = injectionBag.contactsService
self.accountsService = injectionBag.accountService
self.conversationService = injectionBag.conversationsService
self.nameService = injectionBag.nameService
}
......@@ -62,6 +65,26 @@ class ContactRequestsViewModel: ViewModel {
func accept(withItem item: ContactRequestItem) -> Observable<Void> {
let acceptCompleted = self.contactsService.accept(contactRequest: item.contactRequest, withAccount: self.accountsService.currentAccount!)
let accountHelper = AccountModelHelper(withAccount: self.accountsService.currentAccount!)
self.conversationService.saveMessage(withContent:
GeneratedMessageType.receivedContactRequest.rawValue,
byAuthor: accountHelper.ringId!,
toConversationWith: item.contactRequest.ringId,
currentAccountId: (self.accountsService.currentAccount?.id)!, generated: true)
.subscribe(onCompleted: { [unowned self] in
self.log.debug("Message saved")
})
.disposed(by: disposeBag)
self.conversationService.saveMessage(withContent:
GeneratedMessageType.contactRequestAccepted.rawValue,
byAuthor: accountHelper.ringId!,
toConversationWith: item.contactRequest.ringId,
currentAccountId: (self.accountsService.currentAccount?.id)!, generated: true)
.subscribe(onCompleted: { [unowned self] in
self.log.debug("Message saved")
})
.disposed(by: disposeBag)
if let vCard = item.contactRequest.vCard {
let saveVCardCompleted = self.contactsService.saveVCard(vCard: vCard, forContactWithRingId: item.contactRequest.ringId)
return Observable<Void>.zip(acceptCompleted, saveVCardCompleted) { _, _ in
......
/*
* Copyright (C) 2017 Savoir-faire Linux Inc.
*
* Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
*
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
import Foundation
import Reusable
class MessageCellGenerated: UITableViewCell, NibReusable {
@IBOutlet weak var bubble: MessageBubble!
@IBOutlet weak var messageLabel: UILabel!
}
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableViewCell contentMode="scaleToFill" selectionStyle="none" indentationWidth="10" rowHeight="60" id="3QB-g7-MaS" customClass="MessageCellGenerated" customModule="Ring" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="510" height="47"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="3QB-g7-MaS" id="Dkz-SA-3Af">
<rect key="frame" x="0.0" y="0.0" width="510" height="46.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<view clipsSubviews="YES" contentMode="scaleToFill" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="xVQ-Jk-Sxy" customClass="MessageBubble" customModule="Ring" customModuleProvider="target">
<rect key="frame" x="179" y="8" width="152.5" height="30.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label Label Label Label " lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ijf-jY-sqW">
<rect key="frame" x="8" y="4" width="136.5" height="22.5"/>
<fontDescription key="fontDescription" type="system" pointSize="12"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
<constraints>
<constraint firstAttribute="bottom" secondItem="ijf-jY-sqW" secondAttribute="bottom" constant="4" id="SxF-yH-rea"/>
<constraint firstItem="ijf-jY-sqW" firstAttribute="leading" secondItem="xVQ-Jk-Sxy" secondAttribute="leading" constant="8" id="Wqz-dB-u71"/>
<constraint firstItem="ijf-jY-sqW" firstAttribute="top" secondItem="xVQ-Jk-Sxy" secondAttribute="top" constant="4" id="bc9-iJ-EjK"/>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="30" id="hS0-Te-OEU"/>
<constraint firstAttribute="trailing" secondItem="ijf-jY-sqW" secondAttribute="trailing" constant="8" id="jne-iL-4tV"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="cornerRadius">
<real key="value" value="4"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</view>
</subviews>
<color key="tintColor" name="controlColor" catalog="System" colorSpace="catalog"/>
<constraints>
<constraint firstItem="xVQ-Jk-Sxy" firstAttribute="centerX" secondItem="Dkz-SA-3Af" secondAttribute="centerX" id="2E6-jB-N9L"/>
<constraint firstAttribute="bottom" secondItem="xVQ-Jk-Sxy" secondAttribute="bottom" constant="8" id="Qbn-zO-KWj"/>
<constraint firstItem="xVQ-Jk-Sxy" firstAttribute="top" secondItem="Dkz-SA-3Af" secondAttribute="top" constant="8" id="R6Q-PY-A3m"/>
</constraints>
</tableViewCellContentView>
<connections>
<outlet property="bubble" destination="xVQ-Jk-Sxy" id="dRd-NH-FPh"/>
<outlet property="messageLabel" destination="ijf-jY-sqW" id="Wcu-8D-wWf"/>
</connections>
<point key="canvasLocation" x="-411" y="-132"/>
</tableViewCell>
</objects>
</document>
......@@ -81,7 +81,8 @@ class ConversationViewController: UIViewController, UITextFieldDelegate, Storybo
//invite button
let inviteItem = UIBarButtonItem()
inviteItem.image = UIImage(named: "add_person")
inviteItem.rx.tap.subscribe(onNext: { [unowned self] in
inviteItem.rx.tap.throttle(0.5, scheduler: MainScheduler.instance)
.subscribe(onNext: { [unowned self] in
self.inviteItemTapped()
}).disposed(by: self.disposeBag)
......@@ -118,6 +119,7 @@ class ConversationViewController: UIViewController, UITextFieldDelegate, Storybo
//Register cell
self.tableView.register(cellType: MessageCellSent.self)
self.tableView.register(cellType: MessageCellReceived.self)
self.tableView.register(cellType: MessageCellGenerated.self)
//Bind the TableView to the ViewModel
self.viewModel.messages.subscribe(onNext: { [weak self] (messageViewModels) in
......@@ -203,6 +205,12 @@ extension ConversationViewController: UITableViewDataSource {
return cell
}
if messageViewModel.bubblePosition() == .generated {
let cell = tableView.dequeueReusableCell(for: indexPath, cellType: MessageCellGenerated.self)
cell.messageLabel.text = messageViewModel.content
return cell
}
let cell = tableView.dequeueReusableCell(for: indexPath, cellType: MessageCellSent.self)
cell.messageLabel.text = messageViewModel.content
return cell
......
......@@ -2,6 +2,7 @@
* Copyright (C) 2017 Savoir-faire Linux Inc.
*
* Author: Silbino Gonçalves Matado <silbino.gmatado@savoirfairelinux.com>
* Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
*
* 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
......@@ -85,9 +86,16 @@ class ConversationViewModel: ViewModel {
if let contact = contact {
self.inviteButtonIsAvailable.onNext(!contact.confirmed)
}
self.contactsService.contactStatus.subscribe(onNext: { contact in
self.inviteButtonIsAvailable.onNext(!contact.confirmed)
}).disposed(by: self.disposeBag)
self.contactsService.contactStatus.filter({ cont in
return cont.ringId == contact?.ringId
})
.subscribe(onNext: { [unowned self] cont in
self.inviteButtonIsAvailable.onNext(!cont.confirmed)
if cont.confirmed {
self.generateMessage(ofType: GeneratedMessageType.contactRequestAccepted)
}
}).disposed(by: self.disposeBag)
// subscribe to presence updates for the conversation's associated contact
self.presenceService
......@@ -216,13 +224,13 @@ class ConversationViewModel: ViewModel {
to: self.conversation.recipientRingId)
.subscribe(onCompleted: { [unowned self] in
let accountHelper = AccountModelHelper(withAccount: self.accountService.currentAccount!)
self.saveMessage(withContent: content, byAuthor: accountHelper.ringId!, toConversationWith: self.conversation.recipientRingId)
self.saveMessage(withContent: content, byAuthor: accountHelper.ringId!, toConversationWith: self.conversation.recipientRingId, generated: false)
}).disposed(by: self.disposeBag)
}
fileprivate func saveMessage(withContent content: String, byAuthor author: String, toConversationWith account: String) {
fileprivate func saveMessage(withContent content: String, byAuthor author: String, toConversationWith account: String, generated: Bool) {
self.conversationsService
.saveMessage(withContent: content, byAuthor: author, toConversationWith: account, currentAccountId: (accountService.currentAccount?.id)!)
.saveMessage(withContent: content, byAuthor: author, toConversationWith: account, currentAccountId: (accountService.currentAccount?.id)!, generated: generated)
.subscribe(onCompleted: { [unowned self] in
self.log.debug("Message saved")
})
......@@ -245,12 +253,45 @@ class ConversationViewModel: ViewModel {
}
func sendContactRequest() {
let contactExists = self.contactsService.contact(withRingId: self.conversation.recipientRingId) != nil ? true : false
self.accountService.loadVCard(forAccounr: self.accountService.currentAccount!)
.subscribe(onSuccess: { card in
self.contactsService.sendContactRequest(toContactRingId: self.conversation.recipientRingId, vCard: card, withAccount: self.accountService.currentAccount!).subscribe(onCompleted: {
self.log.info("contact request sent")
}).disposed(by: self.disposeBag)
.subscribe(onSuccess: { [unowned self] (card) in
self.contactsService.sendContactRequest(toContactRingId: self.conversation.recipientRingId, vCard: card, withAccount: self.accountService.currentAccount!)
.subscribe(onCompleted: {
if !contactExists {
self.generateMessage(ofType: GeneratedMessageType.sendContactRequest)
}
self.log.info("contact request sent")
}).disposed(by: self.disposeBag)
}, onError: { [unowned self] _ in
self.contactsService.sendContactRequest(toContactRingId: self.conversation.recipientRingId, vCard: nil, withAccount: self.accountService.currentAccount!)
.subscribe(onCompleted: {
if !contactExists {
self.generateMessage(ofType: GeneratedMessageType.sendContactRequest)
}
self.log.info("contact request sent")
}).disposed(by: self.disposeBag)
}).disposed(by: self.disposeBag)
}
func generateMessage(ofType messageType: GeneratedMessageType) {
if self.generatedMessageExists(ofType: messageType) {
return
}
let accountHelper = AccountModelHelper(withAccount: self.accountService.currentAccount!)
self.saveMessage(withContent:
messageType.rawValue, byAuthor: accountHelper.ringId!, toConversationWith: self.conversation.recipientRingId, generated: true)
}
func generatedMessageExists(ofType messageType: GeneratedMessageType) -> Bool {
for message in self.conversation.messages
where message.content == messageType.rawValue {
return true
}
return false
}
}
......@@ -23,6 +23,13 @@ import RxSwift
enum BubblePosition {
case received
case sent
case generated
}
enum GeneratedMessageType: String {
case sendContactRequest = "The invitation has been sent"
case receivedContactRequest = "Contact request received"
case contactRequestAccepted = "ACCEPTED"
}
class MessageViewModel {
......@@ -41,13 +48,15 @@ class MessageViewModel {
}
func bubblePosition() -> BubblePosition {
if self.message.isGenerated {
return .generated
}
let accountHelper = AccountModelHelper(withAccount: accountService.currentAccount!)
if self.message.author == accountHelper.ringId! {
return .sent
} else {
return .received
return.received
}
}
}
......@@ -27,6 +27,7 @@ class MessageModel: Object {
dynamic var content: String = ""
dynamic var author: String = ""
dynamic var status: MessageStatus = .unknown
dynamic var isGenerated: Bool = false
convenience init(withId id: Int64, receivedDate: Date, content: String, author: String) {
self.init()
......@@ -35,5 +36,6 @@ class MessageModel: Object {
self.content = content
self.author = author
self.status = .unknown
self.isGenerated = false
}
}
......@@ -70,7 +70,9 @@ class ContactsService {
return ContactModel(withDictionary: contactDict)
}) {
for contact in contacts {
self.contacts.value.append(contact)
if self.contacts.value.index(of: contact) == nil {
self.contacts.value.append(contact)
}
}
}
}
......@@ -212,16 +214,11 @@ extension ContactsService: ContactsAdapterDelegate {
func contactAdded(contact uri: String, withAccountId accountId: String, confirmed: Bool) {
//Update trust request list
self.removeContactRequest(withRingId: uri)
//if contact list is empty thats mean app just starts and we receive all contacts was added
if self.contacts.value.isEmpty {
return
}
// update contact status
if let contact = self.contact(withRingId: uri) {
if contact.confirmed != confirmed {
contact.confirmed = confirmed
contactStatus.onNext(contact)
self.contactStatus.onNext(contact)
}
}
//sync contacts with daemon contacts
......
......@@ -40,6 +40,7 @@ class ConversationsService: MessagesAdapterDelegate {
var conversations: Observable<Results<ConversationModel>>
init(withMessageAdapter adapter: MessagesAdapter) {
guard let realm = try? Realm() else {
fatalError("Enable to instantiate Realm")
}
......@@ -50,6 +51,7 @@ class ConversationsService: MessagesAdapterDelegate {
conversations = Observable.collection(from: results, synchronousStart: true)
MessagesAdapter.delegate = self
}
func sendMessage(withContent content: String,
......@@ -63,7 +65,11 @@ class ConversationsService: MessagesAdapterDelegate {
let accountHelper = AccountModelHelper(withAccount: senderAccount)
if accountHelper.ringId! != recipientRingId {
_ = self.saveMessage(withContent: content, byAuthor: accountHelper.ringId!, toConversationWith: recipientRingId, currentAccountId: senderAccount.id)
_ = self.saveMessage(withContent: content,
byAuthor: accountHelper.ringId!,
toConversationWith: recipientRingId,
currentAccountId: senderAccount.id,
generated: false)
}
completable(.completed)
......@@ -90,10 +96,14 @@ class ConversationsService: MessagesAdapterDelegate {
func saveMessage(withContent content: String,
byAuthor author: String,
toConversationWith recipientRingId: String,
currentAccountId: String) -> Completable {
currentAccountId: String,
generated: Bool?) -> Completable {
return Completable.create(subscribe: { [unowned self] completable in
let message = MessageModel(withId: 0, receivedDate: Date(), content: content, author: author)
if let generated = generated {
message.isGenerated = generated
}
//Get conversations for this sender
var currentConversation = self.results.filter({ conversation in
......@@ -185,7 +195,8 @@ class ConversationsService: MessagesAdapterDelegate {
self.saveMessage(withContent: content,
byAuthor: senderAccount,
toConversationWith: senderAccount,
currentAccountId: receiverAccountId)
currentAccountId: receiverAccountId,
generated: false)
.subscribe(onCompleted: { [unowned self] in
self.log.info("Message saved")
})
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment