Smartlist: Add a search bar to filter conversations and lookup users

Add the search bar on the top of the smartlist and the list of found
results.

Add an other list for search results. This list is splited in 2
sections for filered and found results.

Add "No Conversations" label if there is no conversations

If the contact is not found locally, a new conversation is created

Change-Id: I7985f0644f97063875bfb02159d44dd1aa8b731e
parent 37f2ea9b
......@@ -114,7 +114,6 @@
564C44601E943C37000F92B1 /* NameRegistrationAdapter.mm in Sources */ = {isa = PBXBuildFile; fileRef = 564C445F1E943C37000F92B1 /* NameRegistrationAdapter.mm */; };
564C44621E943DE6000F92B1 /* NameService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564C44611E943DE6000F92B1 /* NameService.swift */; };
564C44641E943E1E000F92B1 /* NameRegistrationAdapterDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564C44631E943E1E000F92B1 /* NameRegistrationAdapterDelegate.swift */; };
56559B171EEED50D00BF20E1 /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56559B161EEED50D00BF20E1 /* Colors.swift */; };
5669A7FA1EA904AF003C7B93 /* SwitchCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5669A7F91EA904AF003C7B93 /* SwitchCell.xib */; };
5669A7FC1EA904D2003C7B93 /* TextFieldCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5669A7FB1EA904D2003C7B93 /* TextFieldCell.xib */; };
5669A7FE1EA904E4003C7B93 /* TextCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5669A7FD1EA904E4003C7B93 /* TextCell.xib */; };
......@@ -149,6 +148,8 @@
56BBC9DF1EDDC9D300CDAF8B /* LookupNameResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 56BBC9DE1EDDC9D300CDAF8B /* LookupNameResponse.m */; };
56BBC9E01EDDC9E600CDAF8B /* ConversationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BBC9AF1ED7155700CDAF8B /* ConversationViewModel.swift */; };
56BBC9E11EDDCA5900CDAF8B /* Walkthrough.strings in Resources */ = {isa = PBXBuildFile; fileRef = 56AC64DB1E8012CA00EA1AA9 /* Walkthrough.strings */; };
56BBC9E31EDDCC8100CDAF8B /* ConversationSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BBC9E21EDDCC8100CDAF8B /* ConversationSection.swift */; };
56BBC9E71EDE1DDF00CDAF8B /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BBC9E61EDE1DDF00CDAF8B /* Colors.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
......@@ -327,6 +328,8 @@
56BBC9D31EDC7A6D00CDAF8B /* libargon2.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libargon2.a; path = ../fat/lib/libargon2.a; sourceTree = "<group>"; };
56BBC9DD1EDDC9D300CDAF8B /* LookupNameResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LookupNameResponse.h; sourceTree = "<group>"; };
56BBC9DE1EDDC9D300CDAF8B /* LookupNameResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LookupNameResponse.m; sourceTree = "<group>"; };
56BBC9E21EDDCC8100CDAF8B /* ConversationSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationSection.swift; sourceTree = "<group>"; };
56BBC9E61EDE1DDF00CDAF8B /* Colors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Colors.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
......@@ -722,6 +725,7 @@
564C44571E8D7F68000F92B1 /* Constants */ = {
isa = PBXGroup;
children = (
56BBC9E61EDE1DDF00CDAF8B /* Colors.swift */,
564C44581E8D7F8F000F92B1 /* LocalizedStringTableNames.swift */,
564C445A1E8EA44E000F92B1 /* Durations.swift */,
56559B161EEED50D00BF20E1 /* Colors.swift */,
......@@ -769,6 +773,7 @@
isa = PBXGroup;
children = (
562FB6CC1EFAD18A00C61A78 /* ConversationViewController.swift */,
56BBC9E21EDDCC8100CDAF8B /* ConversationSection.swift */,
56BBC9B21ED7156500CDAF8B /* ConversationCell.swift */,
56BBC9B31ED7156500CDAF8B /* ConversationCell.xib */,
56BBC9AE1ED7155700CDAF8B /* ConversationModel.swift */,
......@@ -950,6 +955,7 @@
"$(SRCROOT)/Carthage/Build/iOS/RxSwift.framework",
"$(SRCROOT)/Carthage/Build/iOS/RxCocoa.framework",
"$(SRCROOT)/Carthage/Build/iOS/PKHUD.framework",
"$(SRCROOT)/Carthage/Build/iOS/RxDataSources.framework",
);
outputPaths = (
);
......@@ -977,6 +983,7 @@
02C9B63F1E1D4E8C00F82F0C /* ServiceEvent.swift in Sources */,
56BBC9D21EDC5E7000CDAF8B /* MessageViewModel.swift in Sources */,
02DD80CD1E1EB2E4009A3510 /* ConfigKeyModel.swift in Sources */,
56BBC9E31EDDCC8100CDAF8B /* ConversationSection.swift in Sources */,
56BBC9A81ED7152300CDAF8B /* SmartlistViewController.swift in Sources */,
5516C29F1E71CEFF009D3D2D /* AccountModelHelper.swift in Sources */,
56308BA71EA00E5700660275 /* NameRegistrationResponse.m in Sources */,
......@@ -990,6 +997,7 @@
02B22DFC1DF755BB000358C9 /* AccountModel.swift in Sources */,
043866331D22CE8C00E06CE2 /* MeViewController.swift in Sources */,
56AC64DF1E804ECC00EA1AA9 /* SwitchCell.swift in Sources */,
56BBC9E71EDE1DDF00CDAF8B /* Colors.swift in Sources */,
56BBC9B91ED715FE00CDAF8B /* ContactModel.swift in Sources */,
56BBC9BA1ED715FE00CDAF8B /* ContactHelper.swift in Sources */,
04399AAE1D1C304300E99CD9 /* Utils.mm in Sources */,
......@@ -1003,7 +1011,6 @@
56BBC9DF1EDDC9D300CDAF8B /* LookupNameResponse.m in Sources */,
56BBC9CD1EDC5E7000CDAF8B /* MessageAccessoryView.swift in Sources */,
02DD80CA1E1EAF1A009A3510 /* AccountCredentialsModel.swift in Sources */,
56559B171EEED50D00BF20E1 /* Colors.swift in Sources */,
0273C3081E0C68BF00CF00BA /* RoundedButton.swift in Sources */,
56BBC9BC1ED7161200CDAF8B /* Date+Helpers.swift in Sources */,
562FB6CD1EFAD18A00C61A78 /* ConversationViewController.swift in Sources */,
......
......@@ -234,19 +234,80 @@
<viewControllerLayoutGuide type="bottom" id="aoH-Yk-Qrn"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="khr-49-0iv">
<rect key="frame" x="0.0" y="0.0" width="320" height="568"/>
<rect key="frame" x="0.0" y="64" width="320" height="504"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView clipsSubviews="YES" contentMode="scaleToFill" fixedFrame="YES" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="B6Y-MZ-L7L">
<rect key="frame" x="0.0" y="0.0" width="320" height="519"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<searchBar contentMode="redraw" placeholder="Enter name..." translatesAutoresizingMaskIntoConstraints="NO" id="uCX-a5-egQ">
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="tintColor" white="0.33333333333333331" alpha="1" colorSpace="calibratedWhite"/>
<offsetWrapper key="searchFieldBackgroundPositionAdjustment" horizontal="0.0" vertical="0.0"/>
<textInputTraits key="textInputTraits" autocorrectionType="no" spellCheckingType="no" keyboardType="namePhonePad" returnKeyType="done"/>
<connections>
<outlet property="delegate" destination="NIj-Cd-aWO" id="nrQ-TN-RFw"/>
</connections>
</searchBar>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="B6Y-MZ-L7L">
<rect key="frame" x="0.0" y="44" width="320" height="411"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</tableView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="aeB-7A-alJ">
<rect key="frame" x="0.0" y="44" width="320" height="411"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="No conversations" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Tuk-H4-adP">
<rect key="frame" x="93.5" y="195" width="133" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="0.33333333333333331" alpha="1" colorSpace="calibratedWhite"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<constraints>
<constraint firstItem="Tuk-H4-adP" firstAttribute="centerY" secondItem="aeB-7A-alJ" secondAttribute="centerY" id="DWU-Br-H6b"/>
<constraint firstItem="Tuk-H4-adP" firstAttribute="centerX" secondItem="aeB-7A-alJ" secondAttribute="centerX" id="EhK-Zq-lbK"/>
</constraints>
</view>
<tableView hidden="YES" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="grouped" separatorStyle="default" rowHeight="44" sectionHeaderHeight="1" sectionFooterHeight="1" translatesAutoresizingMaskIntoConstraints="NO" id="1pl-Jb-V2A">
<rect key="frame" x="0.0" y="44" width="320" height="411"/>
<color key="backgroundColor" cocoaTouchSystemColor="groupTableViewBackgroundColor"/>
<label key="tableHeaderView" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="IQr-J3-Dyb">
<rect key="frame" x="0.0" y="0.0" width="320" height="24"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" white="0.33333333333333331" alpha="1" colorSpace="calibratedWhite"/>
<nil key="highlightedColor"/>
</label>
</tableView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="1pl-Jb-V2A" firstAttribute="top" secondItem="uCX-a5-egQ" secondAttribute="bottom" id="0kW-Z4-4kE"/>
<constraint firstAttribute="trailing" secondItem="aeB-7A-alJ" secondAttribute="trailing" id="6lb-NM-Kv1"/>
<constraint firstItem="aoH-Yk-Qrn" firstAttribute="top" secondItem="B6Y-MZ-L7L" secondAttribute="bottom" id="8dr-z2-qYT"/>
<constraint firstItem="aoH-Yk-Qrn" firstAttribute="top" secondItem="1pl-Jb-V2A" secondAttribute="bottom" id="DRW-kt-vzQ"/>
<constraint firstItem="B6Y-MZ-L7L" firstAttribute="leading" secondItem="khr-49-0iv" secondAttribute="leading" id="I1L-uJ-KI4"/>
<constraint firstItem="1pl-Jb-V2A" firstAttribute="leading" secondItem="khr-49-0iv" secondAttribute="leading" id="N3f-tu-YZd"/>
<constraint firstItem="B6Y-MZ-L7L" firstAttribute="top" secondItem="uCX-a5-egQ" secondAttribute="bottom" id="QM6-tE-fRQ"/>
<constraint firstAttribute="trailing" secondItem="uCX-a5-egQ" secondAttribute="trailing" id="XcJ-y7-DKa"/>
<constraint firstAttribute="trailing" secondItem="B6Y-MZ-L7L" secondAttribute="trailing" id="b7u-DV-d1S"/>
<constraint firstItem="uCX-a5-egQ" firstAttribute="leading" secondItem="khr-49-0iv" secondAttribute="leading" id="bB2-ML-Tck"/>
<constraint firstItem="aeB-7A-alJ" firstAttribute="leading" secondItem="khr-49-0iv" secondAttribute="leading" id="dWi-wQ-oVH"/>
<constraint firstItem="uCX-a5-egQ" firstAttribute="top" secondItem="s1e-Lp-B2j" secondAttribute="bottom" id="sBi-PG-yh3"/>
<constraint firstItem="aeB-7A-alJ" firstAttribute="top" secondItem="uCX-a5-egQ" secondAttribute="bottom" id="wlD-ZX-Pgo"/>
<constraint firstItem="aoH-Yk-Qrn" firstAttribute="top" secondItem="aeB-7A-alJ" secondAttribute="bottom" id="zub-RB-pYy"/>
<constraint firstAttribute="trailing" secondItem="1pl-Jb-V2A" secondAttribute="trailing" id="zul-Kh-urI"/>
</constraints>
</view>
<extendedEdge key="edgesForExtendedLayout" bottom="YES"/>
<tabBarItem key="tabBarItem" title="Home" id="1QA-0Y-BFL"/>
<navigationItem key="navigationItem" id="b8m-eG-Q9D"/>
<connections>
<outlet property="conversationsTableView" destination="B6Y-MZ-L7L" id="1qp-yP-v0E"/>
<outlet property="noConversationsView" destination="aeB-7A-alJ" id="LsS-ch-rh0"/>
<outlet property="searchBar" destination="uCX-a5-egQ" id="xsm-gp-Yjb"/>
<outlet property="searchResultsTableView" destination="1pl-Jb-V2A" id="Ywb-Lm-6S7"/>
<outlet property="searchTableViewLabel" destination="IQr-J3-Dyb" id="wOe-wg-q30"/>
<outlet property="tableView" destination="B6Y-MZ-L7L" id="dXp-J4-x68"/>
<segue destination="Qlv-cA-wRT" kind="show" identifier="ShowMessages" id="X75-kM-dPZ"/>
</connections>
......@@ -263,7 +324,7 @@
<viewControllerLayoutGuide type="top" id="wEb-Zj-bvJ"/>
<viewControllerLayoutGuide type="bottom" id="S9d-I1-nWj"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" misplaced="YES" id="jPi-CC-dFO">
<view key="view" contentMode="scaleToFill" id="jPi-CC-dFO">
<rect key="frame" x="0.0" y="64" width="320" height="455"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
......
......@@ -24,34 +24,24 @@ import RxSwift
class ContactHelper {
fileprivate static var cache = [String : String]()
static func lookupUserName(forRingId ringId: String, nameService: NameService, disposeBag: DisposeBag) -> Variable<String> {
let userName = Variable("")
if ContactHelper.cache[ringId] == nil {
//Lookup the user name observer
nameService.usernameLookupStatus
.observeOn(MainScheduler.instance)
.filter({ lookupNameResponse in
return lookupNameResponse.address != nil && lookupNameResponse.address == ringId
}).subscribe(onNext: { lookupNameResponse in
if lookupNameResponse.state == .found {
self.cache[ringId] = lookupNameResponse.name
userName.value = lookupNameResponse.name
} else {
self.cache[ringId] = lookupNameResponse.address
userName.value = lookupNameResponse.address
}
}).addDisposableTo(disposeBag)
nameService.lookupAddress(withAccount: "", nameserver: "", address: ringId)
} else {
userName.value = self.cache[ringId]!
}
//Lookup the user name observer
nameService.usernameLookupStatus
.observeOn(MainScheduler.instance)
.filter({ lookupNameResponse in
return lookupNameResponse.address != nil && lookupNameResponse.address == ringId
}).subscribe(onNext: { lookupNameResponse in
if lookupNameResponse.state == .found {
userName.value = lookupNameResponse.name
} else {
userName.value = lookupNameResponse.address
}
}).addDisposableTo(disposeBag)
nameService.lookupAddress(withAccount: "", nameserver: "", address: ringId)
return userName
}
......
......@@ -23,6 +23,7 @@ import UIKit
class ContactModel {
var ringId: String
var userName: String?
init(withRingId ringId: String) {
self.ringId = ringId
......
/*
* Copyright (C) 2017 Savoir-faire Linux Inc.
*
* Author: Silbino Gonçalves Matado <silbino.gmatado@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 RxSwift
class ContactViewModel {
private let nameService = AppDelegate.nameService
private let disposeBag = DisposeBag()
private let contact: ContactModel
let userName = Variable("")
init(withContact contact: ContactModel) {
self.contact = contact
if let userName = self.contact.userName {
self.userName.value = userName
} else {
self.lookupUserName()
}
}
func lookupUserName() {
nameService.usernameLookupStatus
.observeOn(MainScheduler.instance)
.filter({ [unowned self] lookupNameResponse in
return lookupNameResponse.address != nil && lookupNameResponse.address == self.contact.ringId
}).subscribe(onNext: { [unowned self] lookupNameResponse in
if lookupNameResponse.state == .found {
self.contact.userName = lookupNameResponse.name
self.userName.value = lookupNameResponse.name
} else {
self.userName.value = lookupNameResponse.address
}
}).addDisposableTo(disposeBag)
nameService.lookupAddress(withAccount: "", nameserver: "", address: self.contact.ringId)
}
}
......@@ -15,7 +15,7 @@
<rect key="frame" x="0.0" y="0.0" width="358" height="76"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
<rect key="frame" x="0.0" y="0.0" width="358" height="75.5"/>
<rect key="frame" x="0.0" y="0.0" width="358" height="76"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="ic_contact_picture" translatesAutoresizingMaskIntoConstraints="NO" id="pFB-Jn-TNP">
......@@ -31,20 +31,20 @@
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" horizontalCompressionResistancePriority="751" text="Yesterday" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="7Yv-cC-LKx">
<rect key="frame" x="281" y="30.5" width="61" height="14.5"/>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" text="Yesterday" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="7Yv-cC-LKx">
<rect key="frame" x="281.5" y="30.5" width="60.5" height="14.5"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="12"/>
<color key="textColor" white="0.33333333333333331" alpha="1" colorSpace="calibratedWhite"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="2fJ-Wf-1e0">
<rect key="frame" x="60" y="4" width="217" height="41"/>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" text="Name" textAlignment="natural" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="2fJ-Wf-1e0">
<rect key="frame" x="60" y="8" width="217.5" height="39"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="14"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="eug-ak-r49">
<rect key="frame" x="60" y="49" width="282" height="22.5"/>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="Preview" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="eug-ak-r49">
<rect key="frame" x="60" y="51" width="217.5" height="17"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
......@@ -79,16 +79,17 @@
</subviews>
<constraints>
<constraint firstItem="2fJ-Wf-1e0" firstAttribute="leading" secondItem="pFB-Jn-TNP" secondAttribute="trailing" constant="4" id="2NV-6m-dri"/>
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="7Yv-cC-LKx" secondAttribute="bottom" constant="8" id="2O6-wC-voj"/>
<constraint firstItem="eug-ak-r49" firstAttribute="leading" secondItem="pFB-Jn-TNP" secondAttribute="trailing" constant="4" id="9ah-Ed-RlY"/>
<constraint firstItem="pFB-Jn-TNP" firstAttribute="centerY" secondItem="H2p-sc-9uM" secondAttribute="centerY" id="9mO-5E-3lA"/>
<constraint firstItem="pFB-Jn-TNP" firstAttribute="centerY" secondItem="H2p-sc-9uM" secondAttribute="centerY" id="9mO-5E-3lA"/>
<constraint firstItem="7Yv-cC-LKx" firstAttribute="leading" secondItem="2fJ-Wf-1e0" secondAttribute="trailing" constant="4" id="BzU-Ya-2ME"/>
<constraint firstAttribute="trailing" secondItem="eug-ak-r49" secondAttribute="trailing" constant="16" id="ITl-14-BeZ"/>
<constraint firstItem="JTE-eF-Y5s" firstAttribute="trailing" secondItem="pFB-Jn-TNP" secondAttribute="trailing" id="MgK-cd-QXM"/>
<constraint firstAttribute="trailing" secondItem="7Yv-cC-LKx" secondAttribute="trailing" constant="16" id="UOx-Og-IuZ"/>
<constraint firstItem="JTE-eF-Y5s" firstAttribute="top" secondItem="pFB-Jn-TNP" secondAttribute="top" id="W3A-IX-eXJ"/>
<constraint firstItem="2fJ-Wf-1e0" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" constant="4" id="omS-kb-QbN"/>
<constraint firstItem="eug-ak-r49" firstAttribute="top" secondItem="7Yv-cC-LKx" secondAttribute="bottom" constant="4" id="oox-mY-e8b"/>
<constraint firstAttribute="bottom" secondItem="eug-ak-r49" secondAttribute="bottom" constant="4" id="rzH-w5-tpt"/>
<constraint firstItem="7Yv-cC-LKx" firstAttribute="top" relation="greaterThanOrEqual" secondItem="H2p-sc-9uM" secondAttribute="top" constant="8" id="Wei-7X-4zv"/>
<constraint firstItem="eug-ak-r49" firstAttribute="trailing" secondItem="2fJ-Wf-1e0" secondAttribute="trailing" id="mdZ-Uh-312"/>
<constraint firstItem="2fJ-Wf-1e0" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" constant="8" id="omS-kb-QbN"/>
<constraint firstAttribute="bottom" secondItem="eug-ak-r49" secondAttribute="bottom" constant="8" id="rzH-w5-tpt"/>
<constraint firstItem="eug-ak-r49" firstAttribute="top" secondItem="2fJ-Wf-1e0" secondAttribute="bottom" constant="4" id="se0-Ur-K7G"/>
<constraint firstItem="pFB-Jn-TNP" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" constant="16" id="suq-ak-BYg"/>
<constraint firstItem="7Yv-cC-LKx" firstAttribute="centerY" secondItem="H2p-sc-9uM" secondAttribute="centerY" id="xVO-gP-WdP"/>
......
/*
* Copyright (C) 2017 Savoir-faire Linux Inc.
*
* Author: Silbino Gonçalves Matado <silbino.gmatado@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 RxDataSources
struct ConversationSection {
var header: String
var items: [ConversationViewModel]
}
extension ConversationSection: SectionModelType {
typealias Item = ConversationViewModel
init(original: ConversationSection, items: [Item]) {
self = original
self.items = items
}
}
......@@ -70,7 +70,7 @@ class ConversationViewController: UIViewController, UITextFieldDelegate {
}
func setupUI() {
self.viewModel?.userName.bind(to: self.navigationItem.rx.title).addDisposableTo(disposeBag)
self.viewModel?.userName.asObservable().bind(to: self.navigationItem.rx.title).addDisposableTo(disposeBag)
self.tableView.contentInset.bottom = messageAccessoryView.frame.size.height
self.tableView.scrollIndicatorInsets.bottom = messageAccessoryView.frame.size.height
......@@ -131,8 +131,11 @@ class ConversationViewController: UIViewController, UITextFieldDelegate {
}
fileprivate func scrollToBottom(animated: Bool) {
let last = IndexPath(row: self.tableView.numberOfRows(inSection: 0) - 1, section: 0)
self.tableView.scrollToRow(at: last, at: .bottom, animated: animated)
let numberOfRows = self.tableView.numberOfRows(inSection: 0)
if numberOfRows > 0 {
let last = IndexPath(row: numberOfRows - 1, section: 0)
self.tableView.scrollToRow(at: last, at: .bottom, animated: animated)
}
}
fileprivate var isBottomContentOffset: Bool {
......
......@@ -24,7 +24,6 @@ import RxSwift
class ConversationViewModel {
let conversation: ConversationModel
let userName: Observable<String>
//Displays the entire date ( for messages received before the current week )
private let dateFormatter = DateFormatter()
......@@ -36,6 +35,7 @@ class ConversationViewModel {
let messages :Observable<[MessageViewModel]>
//Services
private let conversationsService = AppDelegate.conversationsService
private let accountService = AppDelegate.accountService
......@@ -45,10 +45,6 @@ class ConversationViewModel {
dateFormatter.dateStyle = .medium
hourFormatter.dateFormat = "HH:mm"
self.userName = ContactHelper.lookupUserName(forRingId: self.conversation.recipient.ringId,
nameService: AppDelegate.nameService,
disposeBag: self.disposeBag).asObservable()
//Create observable from sorted conversations and flatMap them to view models
self.messages = self.conversationsService.conversations.asObservable().map({ conversations in
return conversations.filter({ currentConversation in
......@@ -62,6 +58,23 @@ class ConversationViewModel {
}
lazy var userName: Variable<String> = {
if let userName = self.conversation.recipient.userName {
return Variable(userName)
} else {
let tmp :Variable<String> = ContactHelper.lookupUserName(forRingId: self.conversation.recipient.ringId,
nameService: AppDelegate.nameService,
disposeBag: self.disposeBag)
tmp.asObservable().subscribe(onNext: { userNameFound in
self.conversation.recipient.userName = userNameFound
}).addDisposableTo(self.disposeBag)
return tmp
}
}()
var unreadMessages: String {
return self.unreadMessagesCount.description
}
......@@ -113,6 +126,10 @@ class ConversationViewModel {
return self.unreadMessagesCount == 0
}
var hideDate: Bool {
return self.conversation.messages.count == 0
}
func sendMessage(withContent content: String) {
self.conversationsService
.sendMessage(withContent: content,
......@@ -126,7 +143,7 @@ class ConversationViewModel {
fileprivate func saveMessage(withContent content: String, byAuthor author: String, toConversationWith account: String) {
self.conversationsService
.saveMessage(withContent: content, byAuthor: author, toConversationWith: account)
.saveMessage(withContent: content, byAuthor: author, toConversationWith: account, currentAccountId: (accountService.currentAccount?.id)!)
.subscribe(onCompleted: {
print("Message saved")
})
......
......@@ -32,29 +32,43 @@ class ConversationsService: MessagesAdapterDelegate {
init(withMessageAdapter messageAdapter: MessagesAdapter) {
self.messageAdapter = messageAdapter
MessagesAdapter.delegate = self
}
func sendMessage(withContent content: String, from senderAccount: AccountModel, to recipient: ContactModel) -> Completable {
func sendMessage(withContent content: String,
from senderAccount: AccountModel,
to recipient: ContactModel) -> Completable {
return Completable.create(subscribe: { [unowned self] completable in
let contentDict = [self.textPlainMIMEType : content]
self.messageAdapter.sendMessage(withContent: contentDict, withAccountId: senderAccount.id, to: recipient.ringId)
let accountHelper = AccountModelHelper(withAccount: senderAccount)
if accountHelper.ringId! != recipient.ringId {
_ = self.saveMessage(withContent: content, byAuthor: accountHelper.ringId!, toConversationWith: recipient.ringId, currentAccountId: senderAccount.id)
}
completable(.completed)
return Disposables.create {}
})
}
func saveMessage(withContent content: String, byAuthor author: String, toConversationWith account: String) -> Completable {
func addConversation(conversation: ConversationModel) {
self.conversations.value.append(conversation)
}
func saveMessage(withContent content: String,
byAuthor author: String,
toConversationWith recipientRingId: String,
currentAccountId: String) -> Completable {
return Completable.create(subscribe: { [unowned self] completable in
let message = MessageModel(withId: nil, receivedDate: Date(), content: content, author: author)
//Get conversations for this sender
var currentConversation = self.conversations.value.filter({ conversation in
return conversation.recipient.ringId == account
return conversation.recipient.ringId == recipientRingId
}).first
//Get the current array of conversations
......@@ -62,7 +76,7 @@ class ConversationsService: MessagesAdapterDelegate {
//Create a new conversation for this sender if not exists
if currentConversation == nil {
currentConversation = ConversationModel(withRecipient: ContactModel(withRingId: account), accountId: author)
currentConversation = ConversationModel(withRecipient: ContactModel(withRingId: recipientRingId), accountId: currentAccountId)
currentConversations.append(currentConversation!)
}
......@@ -115,7 +129,7 @@ class ConversationsService: MessagesAdapterDelegate {
to receiverAccountId: String) {
if let content = message[textPlainMIMEType] {
self.saveMessage(withContent: content, byAuthor: senderAccount, toConversationWith: senderAccount)
self.saveMessage(withContent: content, byAuthor: senderAccount, toConversationWith: senderAccount, currentAccountId: receiverAccountId)
.subscribe(onCompleted: {
print("Message saved")
})
......
/*
* Copyright (C) 2017 Savoir-faire Linux Inc.
*
* Author: Silbino Gonçalves Matado <silbino.gmatado@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 UIKit
import RxSwift
class MessagesService: MessagesAdapterDelegate {
fileprivate let messageAdapter :MessagesAdapter
fileprivate let disposeBag = DisposeBag()
fileprivate let textPlainMIMEType = "text/plain"
var conversations = Variable([ConversationModel]())
init(withMessageAdapter messageAdapter: MessagesAdapter) {
self.messageAdapter = messageAdapter
MessagesAdapter.delegate = self
}
func sendMessage(withContent content: String, from senderAccount: AccountModel, to recipient: ContactModel) {
let contentDict = [textPlainMIMEType : content]
self.messageAdapter.sendMessage(withContent: contentDict, withAccountId: senderAccount.id, to: recipient.ringId)