diff --git a/Ring/Ring.xcodeproj/project.pbxproj b/Ring/Ring.xcodeproj/project.pbxproj index 0e0389be657c42853c664fb6d9ebfc4e9e4a0c56..38947b0e373e717b15c8597c44da8a0dc556f5cb 100644 --- a/Ring/Ring.xcodeproj/project.pbxproj +++ b/Ring/Ring.xcodeproj/project.pbxproj @@ -183,6 +183,9 @@ 56BBC9D41EDC7A6D00CDAF8B /* libargon2.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 56BBC9D31EDC7A6D00CDAF8B /* libargon2.a */; }; 56BBC9DF1EDDC9D300CDAF8B /* LookupNameResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 56BBC9DE1EDDC9D300CDAF8B /* LookupNameResponse.m */; }; 56C715FF1F0D36C600770048 /* ContactsAdapter.mm in Sources */ = {isa = PBXBuildFile; fileRef = 56C715FE1F0D36C600770048 /* ContactsAdapter.mm */; }; + 62A88D371F6C2ED400F8AB18 /* PresenceAdapterDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62A88D361F6C2ED400F8AB18 /* PresenceAdapterDelegate.swift */; }; + 62A88D391F6C323500F8AB18 /* PresenceAdapter.mm in Sources */ = {isa = PBXBuildFile; fileRef = 62A88D381F6C323500F8AB18 /* PresenceAdapter.mm */; }; + 62A88D3B1F6C3ACC00F8AB18 /* PresenceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62A88D3A1F6C3ACC00F8AB18 /* PresenceService.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -406,6 +409,10 @@ 56C715FE1F0D36C600770048 /* ContactsAdapter.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ContactsAdapter.mm; sourceTree = "<group>"; }; 56C716001F0D36D900770048 /* ContactsAdapterDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactsAdapterDelegate.swift; sourceTree = "<group>"; }; 56C716021F0D466100770048 /* ContactsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactsService.swift; sourceTree = "<group>"; }; + 62A88D351F6C2E5F00F8AB18 /* PresenceAdapter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PresenceAdapter.h; sourceTree = "<group>"; }; + 62A88D361F6C2ED400F8AB18 /* PresenceAdapterDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresenceAdapterDelegate.swift; sourceTree = "<group>"; }; + 62A88D381F6C323500F8AB18 /* PresenceAdapter.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = PresenceAdapter.mm; sourceTree = "<group>"; }; + 62A88D3A1F6C3ACC00F8AB18 /* PresenceService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresenceService.swift; sourceTree = "<group>"; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -553,6 +560,8 @@ 56C716001F0D36D900770048 /* ContactsAdapterDelegate.swift */, 1A5DC01D1F355DA70075E8EF /* ContactsAdapterDelegate.swift */, 1A5DC01F1F355DCF0075E8EF /* ContactsService.swift */, + 62A88D361F6C2ED400F8AB18 /* PresenceAdapterDelegate.swift */, + 62A88D3A1F6C3ACC00F8AB18 /* PresenceService.swift */, ); path = Services; sourceTree = "<group>"; @@ -583,6 +592,8 @@ 1A5DC00D1F3559070075E8EF /* ContactsAdapter.mm */, 563AEC741EA66487003A5641 /* AccountCreation */, 563AEC731EA6627F003A5641 /* NameRegistration */, + 62A88D351F6C2E5F00F8AB18 /* PresenceAdapter.h */, + 62A88D381F6C323500F8AB18 /* PresenceAdapter.mm */, ); path = Bridging; sourceTree = "<group>"; @@ -1270,6 +1281,7 @@ 1A3D28A71F0EB9DB00B524EE /* Bool+String.swift in Sources */, 5516C29F1E71CEFF009D3D2D /* AccountModelHelper.swift in Sources */, 1ABE07D31F0D8FE800D36361 /* Storyboards.swift in Sources */, + 62A88D3B1F6C3ACC00F8AB18 /* PresenceService.swift in Sources */, 1A2D18E51F29197100B2C785 /* MessageAccessoryView.swift in Sources */, 1A2D18C61F29180700B2C785 /* ConversationModel.swift in Sources */, 56308BA71EA00E5700660275 /* NameRegistrationResponse.m in Sources */, @@ -1289,6 +1301,7 @@ 1A2041861F1EA19600C08435 /* CreateAccountViewController.swift in Sources */, 1A2D18C21F29180700B2C785 /* AccountCredentialsModel.swift in Sources */, 1A2D18FF1F29352D00B2C785 /* MeViewModel.swift in Sources */, + 62A88D391F6C323500F8AB18 /* PresenceAdapter.mm in Sources */, 1A2D18B71F29164700B2C785 /* SmartlistViewModel.swift in Sources */, 04399AAE1D1C304300E99CD9 /* Utils.mm in Sources */, 56BBC9A31ED714DF00CDAF8B /* ConversationsService.swift in Sources */, @@ -1316,6 +1329,7 @@ 1A2D19011F29353A00B2C785 /* MeDetailViewModel.swift in Sources */, 1A2D18A41F27EF5200B2C785 /* AppCoordinator.swift in Sources */, 1A2D18C31F29180700B2C785 /* AccountModel.swift in Sources */, + 62A88D371F6C2ED400F8AB18 /* PresenceAdapterDelegate.swift in Sources */, 1A2D18EB1F29197100B2C785 /* MessageViewModel.swift in Sources */, 02B22DFF1DF755DB000358C9 /* AccountsService.swift in Sources */, 1A5DC0421F3567DF0075E8EF /* ContactRequestsCoordinator.swift in Sources */, diff --git a/Ring/Ring/AppDelegate.swift b/Ring/Ring/AppDelegate.swift index 2c4c76c9b126acbe68eedbd3a682978f2c08fd65..6deb2d7830e053db795efccb3f64a10e6e833e2e 100644 --- a/Ring/Ring/AppDelegate.swift +++ b/Ring/Ring/AppDelegate.swift @@ -27,8 +27,6 @@ import RxSwift import Chameleon import Contacts -import Contacts - @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { @@ -38,13 +36,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate { private let nameService = NameService(withNameRegistrationAdapter: NameRegistrationAdapter()) private let conversationsService = ConversationsService(withMessageAdapter: MessagesAdapter()) private let contactsService = ContactsService(withContactsAdapter: ContactsAdapter()) + private let presenceService = PresenceService(withPresenceAdapter: PresenceAdapter()) public lazy var injectionBag: InjectionBag = { return InjectionBag(withDaemonService: self.daemonService, withAccountService: self.accountService, withNameService: self.nameService, withConversationService: self.conversationsService, - withContactsService: self.contactsService) + withContactsService: self.contactsService, + withPresenceService: self.presenceService) }() private lazy var appCoordinator: AppCoordinator = { return AppCoordinator(with: self.injectionBag) @@ -77,12 +77,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate { if let currentAccount = self.accountService.currentAccount { self.contactsService.loadContacts(withAccount: currentAccount) self.contactsService.loadContactRequests(withAccount: currentAccount) + self.presenceService.subscribeBuddies(withAccount: currentAccount, withContacts: self.contactsService.contacts.value) } - self.window?.rootViewController = self.appCoordinator.rootViewController - self.window?.makeKeyAndVisible() - self.appCoordinator.start() }.disposed(by: self.disposeBag) + self.window?.rootViewController = self.appCoordinator.rootViewController + self.window?.makeKeyAndVisible() + self.appCoordinator.start() + return true } diff --git a/Ring/Ring/Bridging/PresenceAdapter.h b/Ring/Ring/Bridging/PresenceAdapter.h new file mode 100644 index 0000000000000000000000000000000000000000..e9423d97c4ca3ee6a9f7ec06739cda23f84ccb46 --- /dev/null +++ b/Ring/Ring/Bridging/PresenceAdapter.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2017 Savoir-faire Linux Inc. + * + * Author: Andreas Traczyk <andreas.traczyk@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/Foundation.h> + +@protocol PresenceAdapterDelegate; + +@interface PresenceAdapter : NSObject + +@property (class, nonatomic, weak) id <PresenceAdapterDelegate> delegate; + +- (void)subscribeBuddyWithURI:(NSString*)uri WithAccountId:(NSString*)accountId WithFlag:(BOOL)flag; + +@end diff --git a/Ring/Ring/Bridging/PresenceAdapter.mm b/Ring/Ring/Bridging/PresenceAdapter.mm new file mode 100644 index 0000000000000000000000000000000000000000..6648e76ee52726c928f67eb5a20de6a11614458b --- /dev/null +++ b/Ring/Ring/Bridging/PresenceAdapter.mm @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2017 Savoir-faire Linux Inc. + * + * Author: Andreas Traczyk <andreas.traczyk@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 "PresenceAdapter.h" +#import "Utils.h" +#import "dring/presencemanager_interface.h" +#import "Ring-Swift.h" + +using namespace DRing; + +@implementation PresenceAdapter + +// Static delegate that will receive the propagated daemon events +static id <PresenceAdapterDelegate> _delegate; + +#pragma mark Init + +- (id)init { + if (self = [super init]) { + [self registerPresenceHandlers]; + } + return self; +} + +#pragma mark - + +#pragma mark Callbacks registration + +- (void)registerPresenceHandlers { + std::map<std::string, std::shared_ptr<CallbackWrapperBase>> presenceHandlers; + + // Incoming buddy notification + presenceHandlers.insert(exportable_callback<PresenceSignal::NewBuddyNotification>([&](const std::string& account_id, + const std::string& uri, + int status, + const std::string& lineStatus) { + if(PresenceAdapter.delegate) { + NSString* accountId = [NSString stringWithUTF8String:account_id.c_str()]; + NSString* uriString = [NSString stringWithUTF8String:uri.c_str()]; + NSString* lineStatusString = [NSString stringWithUTF8String:lineStatus.c_str()]; + + [PresenceAdapter.delegate newBuddyNotificationWithAccountId:accountId withUri:uriString withStatus:(NSInteger)status withLineStatus:lineStatusString]; + } + })); + + registerPresHandlers(presenceHandlers); +} + +#pragma mark - + +- (void)subscribeBuddyWithURI:(NSString*)uri WithAccountId:(NSString*)accountId WithFlag:(BOOL)flag { + DRing::subscribeBuddy(std::string([accountId UTF8String]), std::string([uri UTF8String]), (bool)flag); +} + +#pragma mark PresenceAdapterDelegate + ++ (id <PresenceAdapterDelegate>)delegate { + return _delegate; +} + ++ (void) setDelegate:(id<PresenceAdapterDelegate>)delegate { + _delegate = delegate; +} + +#pragma mark - + +@end diff --git a/Ring/Ring/Bridging/Ring-Bridging-Header.h b/Ring/Ring/Bridging/Ring-Bridging-Header.h index 657359142aa34b1210fca00dcdc64cc084125931..a1af5df7c4445f9e1996e88edcaf44c81205687c 100644 --- a/Ring/Ring/Bridging/Ring-Bridging-Header.h +++ b/Ring/Ring/Bridging/Ring-Bridging-Header.h @@ -32,3 +32,4 @@ #import "MessagesAdapter.h" #import "Chameleon/Chameleon.h" #import "ContactsAdapter.h" +#import "PresenceAdapter.h" diff --git a/Ring/Ring/Coordinators/InjectionBag.swift b/Ring/Ring/Coordinators/InjectionBag.swift index 10188aad501f2d31afbb21d03f03287654b4c0dd..5c003c8571921510632426710c0040348c6a1439 100644 --- a/Ring/Ring/Coordinators/InjectionBag.swift +++ b/Ring/Ring/Coordinators/InjectionBag.swift @@ -28,17 +28,20 @@ class InjectionBag { let nameService: NameService let conversationsService: ConversationsService let contactsService: ContactsService + let presenceService: PresenceService init (withDaemonService daemonService: DaemonService, withAccountService accountService: AccountsService, withNameService nameService: NameService, withConversationService conversationService: ConversationsService, - withContactsService contactsService: ContactsService) { + withContactsService contactsService: ContactsService, + withPresenceService presenceService: PresenceService) { self.daemonService = daemonService self.accountService = accountService self.nameService = nameService self.conversationsService = conversationService self.contactsService = contactsService + self.presenceService = presenceService } } diff --git a/Ring/Ring/Features/Conversations/Conversation/ConversationViewModel.swift b/Ring/Ring/Features/Conversations/Conversation/ConversationViewModel.swift index ce8fc613ffe1295585f5fc47e5f6dfa4dd661ef6..841b27087515faaf8dd44de2e44eace749630380 100644 --- a/Ring/Ring/Features/Conversations/Conversation/ConversationViewModel.swift +++ b/Ring/Ring/Features/Conversations/Conversation/ConversationViewModel.swift @@ -30,6 +30,26 @@ class ConversationViewModel: ViewModel { */ private let log = SwiftyBeaver.self + //Services + private let conversationsService: ConversationsService + private let accountService: AccountsService + private let nameService: NameService + private let contactsService: ContactsService + private let presenceService: PresenceService + private let injectionBag: InjectionBag + + required init(with injectionBag: InjectionBag) { + self.injectionBag = injectionBag + self.accountService = injectionBag.accountService + self.conversationsService = injectionBag.conversationsService + self.nameService = injectionBag.nameService + self.contactsService = injectionBag.contactsService + self.presenceService = injectionBag.presenceService + + dateFormatter.dateStyle = .medium + hourFormatter.dateFormat = "HH:mm" + } + var conversation: ConversationModel! { didSet { //Create observable from sorted conversations and flatMap them to view models @@ -50,20 +70,34 @@ class ConversationViewModel: ViewModel { let contact = self.contactsService.contact(withRingId: self.conversation.recipientRingId) - if let contact = contact { + 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) + // subscribe to presence updates for the conversation's associated contact + self.presenceService + .sharedResponseStream + .filter({ presenceUpdateEvent in + return presenceUpdateEvent.eventType == ServiceEventType.presenceUpdated + && presenceUpdateEvent.getEventInput(.uri) == contact?.ringId + }) + .subscribe(onNext: { [unowned self] presenceUpdateEvent in + if let uri: String = presenceUpdateEvent.getEventInput(.uri) { + self.contactPresence.onNext(self.presenceService.contactPresence[uri]!) + } + }) + .disposed(by: disposeBag) + if let contactUserName = contact?.userName { self.userName.onNext(contactUserName) } else { let recipientRingId = self.conversation.recipientRingId - //Return an observer for the username lookup + // Return an observer for the username lookup self.nameService.usernameLookupStatus .filter({ lookupNameResponse in return lookupNameResponse.address != nil && @@ -101,25 +135,10 @@ class ConversationViewModel: ViewModel { var messages: Observable<[MessageViewModel]>! var userName = BehaviorSubject(value: "") - var inviteButtonIsAvailable = BehaviorSubject(value: true) - - //Services - private let conversationsService: ConversationsService - private let accountService: AccountsService - private let nameService: NameService - private let contactsService: ContactsService - private let injectionBag: InjectionBag - required init(with injectionBag: InjectionBag) { - self.injectionBag = injectionBag - self.accountService = injectionBag.accountService - self.conversationsService = injectionBag.conversationsService - self.nameService = injectionBag.nameService - self.contactsService = injectionBag.contactsService + var inviteButtonIsAvailable = BehaviorSubject(value: true) - dateFormatter.dateStyle = .medium - hourFormatter.dateFormat = "HH:mm" - } + var contactPresence = BehaviorSubject(value: false) var unreadMessages: String { return self.unreadMessagesCount.description diff --git a/Ring/Ring/Features/Conversations/SmartList/Cells/ConversationCell.swift b/Ring/Ring/Features/Conversations/SmartList/Cells/ConversationCell.swift index 12082a6f9a7f5cd0fb2c7c0f568418b3f92d7e10..76c96e87bfa7121e01078042080f96a4f1555dc1 100644 --- a/Ring/Ring/Features/Conversations/SmartList/Cells/ConversationCell.swift +++ b/Ring/Ring/Features/Conversations/SmartList/Cells/ConversationCell.swift @@ -30,6 +30,7 @@ class ConversationCell: UITableViewCell, NibReusable { @IBOutlet weak var newMessagesLabel: UILabel! @IBOutlet weak var lastMessageDateLabel: UILabel! @IBOutlet weak var lastMessagePreviewLabel: UILabel! + @IBOutlet weak var presenceIndicator: UIView! override func setSelected(_ selected: Bool, animated: Bool) { super.setSelected(selected, animated: animated) diff --git a/Ring/Ring/Features/Conversations/SmartList/Cells/ConversationCell.xib b/Ring/Ring/Features/Conversations/SmartList/Cells/ConversationCell.xib index a82c301ea75eb6599b07f9b812b8c7a65ecf3a81..f5e558f555bfe56fdecbbf040a0ee3a623b9c7cb 100644 --- a/Ring/Ring/Features/Conversations/SmartList/Cells/ConversationCell.xib +++ b/Ring/Ring/Features/Conversations/SmartList/Cells/ConversationCell.xib @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="UTF-8"?> -<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="12121" systemVersion="16E195" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES"> +<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="12121" systemVersion="16F2073" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES"> <device id="retina4_7" orientation="portrait"> <adaptation id="fullscreen"/> </device> @@ -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"> @@ -32,19 +32,19 @@ </userDefinedRuntimeAttributes> </imageView> <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"/> + <rect key="frame" x="281" y="31" width="61" height="15"/> <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" 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"/> + <rect key="frame" x="60" y="8" width="217" height="39"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="14"/> <nil key="textColor"/> <nil key="highlightedColor"/> </label> <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"/> + <rect key="frame" x="60" y="51" width="217" height="17"/> <fontDescription key="fontDescription" type="system" pointSize="14"/> <nil key="textColor"/> <nil key="highlightedColor"/> @@ -76,14 +76,29 @@ </userDefinedRuntimeAttribute> </userDefinedRuntimeAttributes> </view> + <view clipsSubviews="YES" contentMode="scaleToFill" horizontalHuggingPriority="249" translatesAutoresizingMaskIntoConstraints="NO" id="Fpi-20-ZYV" userLabel="Presence Indicator"> + <rect key="frame" x="42" y="44" width="14" height="14"/> + <color key="backgroundColor" red="0.0" green="1" blue="0.0" alpha="1" colorSpace="calibratedRGB"/> + <constraints> + <constraint firstAttribute="height" constant="14" id="0m2-aN-1GT"/> + <constraint firstAttribute="width" constant="14" id="m3b-My-KNY"/> + </constraints> + <userDefinedRuntimeAttributes> + <userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius"> + <integer key="value" value="6"/> + </userDefinedRuntimeAttribute> + </userDefinedRuntimeAttributes> + </view> </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="Fpi-20-ZYV" firstAttribute="bottom" secondItem="pFB-Jn-TNP" secondAttribute="bottom" id="BMe-Il-GoN"/> <constraint firstItem="7Yv-cC-LKx" firstAttribute="leading" secondItem="2fJ-Wf-1e0" secondAttribute="trailing" constant="4" id="BzU-Ya-2ME"/> <constraint firstItem="JTE-eF-Y5s" firstAttribute="trailing" secondItem="pFB-Jn-TNP" secondAttribute="trailing" id="MgK-cd-QXM"/> + <constraint firstItem="Fpi-20-ZYV" firstAttribute="trailing" secondItem="pFB-Jn-TNP" secondAttribute="trailing" id="Oav-3c-X7k"/> <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="7Yv-cC-LKx" firstAttribute="top" relation="greaterThanOrEqual" secondItem="H2p-sc-9uM" secondAttribute="top" constant="8" id="Wei-7X-4zv"/> @@ -101,6 +116,7 @@ <outlet property="nameLabel" destination="2fJ-Wf-1e0" id="0Mb-yC-vh6"/> <outlet property="newMessagesIndicator" destination="JTE-eF-Y5s" id="9kR-8x-Zpk"/> <outlet property="newMessagesLabel" destination="P5S-4k-0yx" id="WlA-Z8-sNC"/> + <outlet property="presenceIndicator" destination="Fpi-20-ZYV" id="RL9-sx-sBF"/> <outlet property="profileImage" destination="pFB-Jn-TNP" id="zuf-CZ-9wL"/> </connections> <point key="canvasLocation" x="70" y="-92"/> diff --git a/Ring/Ring/Features/Conversations/SmartList/SmartlistViewController.swift b/Ring/Ring/Features/Conversations/SmartList/SmartlistViewController.swift index a56fd6f0b0ace4c8f16cae9236b1569ce22320b4..4c23e9e9447ea29216d7712a473ac5ae2003808e 100644 --- a/Ring/Ring/Features/Conversations/SmartList/SmartlistViewController.swift +++ b/Ring/Ring/Features/Conversations/SmartList/SmartlistViewController.swift @@ -117,12 +117,22 @@ class SmartlistViewController: UIViewController, StoryboardBased, ViewModelBased let cell = tableView.dequeueReusableCell(for: indexPath, cellType: ConversationCell.self) - item.userName.asObservable().observeOn(MainScheduler.instance).bind(to: cell.nameLabel.rx.text).disposed(by: self.disposeBag) + item.userName.asObservable() + .observeOn(MainScheduler.instance) + .bind(to: cell.nameLabel.rx.text) + .disposed(by: self.disposeBag) + cell.newMessagesLabel.text = item.unreadMessages cell.lastMessageDateLabel.text = item.lastMessageReceivedDate cell.newMessagesIndicator.isHidden = item.hideNewMessagesLabel cell.lastMessagePreviewLabel.text = item.lastMessage + item.contactPresence.asObservable() + .observeOn(MainScheduler.instance) + .map { value in !value } + .bind(to: cell.presenceIndicator.rx.isHidden) + .disposed(by: self.disposeBag) + return cell } diff --git a/Ring/Ring/Services/PresenceAdapterDelegate.swift b/Ring/Ring/Services/PresenceAdapterDelegate.swift new file mode 100644 index 0000000000000000000000000000000000000000..1f97df75b1ea4259eb73d502d009f3a2c17f47d8 --- /dev/null +++ b/Ring/Ring/Services/PresenceAdapterDelegate.swift @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2017 Savoir-faire Linux Inc. + * + * Author: Andreas Traczyk <andreas.traczyk@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. + */ + +@objc protocol PresenceAdapterDelegate { + func newBuddyNotification(withAccountId accountId: String, + withUri uri: String, + withStatus status: Int, + withLineStatus lineStatus: String) +} diff --git a/Ring/Ring/Services/PresenceService.swift b/Ring/Ring/Services/PresenceService.swift new file mode 100644 index 0000000000000000000000000000000000000000..c318df8967307f5ae9643d22feb4851fae2c5f75 --- /dev/null +++ b/Ring/Ring/Services/PresenceService.swift @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2017 Savoir-faire Linux Inc. + * + * Author: Andreas Traczyk <andreas.traczyk@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 SwiftyBeaver +import RxSwift + +class PresenceService { + + fileprivate let presenceAdapter: PresenceAdapter + fileprivate let log = SwiftyBeaver.self + var contactPresence: [String: Bool] + + fileprivate let disposeBag = DisposeBag() + fileprivate let responseStream = PublishSubject<ServiceEvent>() + var sharedResponseStream: Observable<ServiceEvent> + + init(withPresenceAdapter presenceAdapter: PresenceAdapter) { + self.responseStream.disposed(by: disposeBag) + self.sharedResponseStream = responseStream.share() + self.contactPresence = [String: Bool]() + self.presenceAdapter = presenceAdapter + PresenceAdapter.delegate = self + } + + func subscribeBuddies(withAccount account: AccountModel, withContacts contacts: [ContactModel]) { + for contact in contacts { + subscribeBuddy(withAccountId: account.id, + withUri: contact.ringId, + withFlag: true) + } + } + + func subscribeBuddy(withAccountId accountId: String, + withUri uri: String, + withFlag flag: Bool) { + presenceAdapter.subscribeBuddy(withURI: uri, withAccountId: accountId, withFlag: flag) + contactPresence[uri] = false + } +} + +extension PresenceService: PresenceAdapterDelegate { + func newBuddyNotification(withAccountId accountId: String, + withUri uri: String, + withStatus status: Int, + withLineStatus lineStatus: String) { + contactPresence[uri] = status > 0 ? true : false + + /* + The subscriber is intended to query the contactPresence dictionary + with the contact's ringId + */ + var event = ServiceEvent(withEventType: .presenceUpdated) + event.addEventInput(.uri, value: uri) + self.responseStream.onNext(event) + + log.debug("newBuddyNotification: uri=\(uri), status=\(status)") + } +} diff --git a/Ring/Ring/Services/ServiceEvent.swift b/Ring/Ring/Services/ServiceEvent.swift index 0e40e8662fabac4d03a44b3711e4de13a7bf1420..836d4b7bf1eb4a5773500ccc954148d14fd48f95 100644 --- a/Ring/Ring/Services/ServiceEvent.swift +++ b/Ring/Ring/Services/ServiceEvent.swift @@ -28,6 +28,7 @@ enum ServiceEventType { case accountAdded case accountsChanged case registrationStateChanged + case presenceUpdated } /** @@ -37,6 +38,8 @@ enum ServiceEventInput { case id case state case registrationState + case uri + case presenceStatus } /**