From 497b2005968483daecd2f4025b0f9e171b0e5272 Mon Sep 17 00:00:00 2001 From: Silbino Goncalves Matado <silbino.gmatado@savoirfairelinux.com> Date: Mon, 24 Apr 2017 17:12:01 -0400 Subject: [PATCH] Smartlist: Add the conversations list for received messages Add adapter and service for messages (send, receive and statuses) Add the smartlist to see the users that sent messages Add lookup address into NameService to get the user name from the RingId Change-Id: I512bfafc7d3e77888a3560a183a085434220337b --- Ring/Cartfile | 1 + Ring/Cartfile.resolved | 3 +- Ring/Ring.xcodeproj/project.pbxproj | 102 +++++++++++++++- Ring/Ring/AppDelegate.swift | 1 + .../ic_contact_picture.imageset/Contents.json | 21 ++++ .../ic_contact_picture.png | Bin 0 -> 4599 bytes Ring/Ring/Base.lproj/Main.storyboard | 31 +++-- Ring/Ring/Bridging/Ring-Bridging-Header.h | 1 + Ring/Ring/Colors.swift | 28 +++++ Ring/Ring/ContactHelper.swift | 59 ++++++++++ Ring/Ring/ContactModel.swift | 34 ++++++ Ring/Ring/ConversationCell.swift | 47 ++++++++ Ring/Ring/ConversationCell.xib | 111 ++++++++++++++++++ Ring/Ring/ConversationModel.swift | 32 +++++ Ring/Ring/ConversationViewModel.swift | 99 ++++++++++++++++ Ring/Ring/ConversationsService.swift | 76 ++++++++++++ Ring/Ring/Extensions/Date+Helpers.swift | 31 +++++ Ring/Ring/Global.strings | 21 ++++ .../MainTabBar/MainTabBarViewController.swift | 6 + Ring/Ring/MessageModel.swift | 37 ++++++ Ring/Ring/MessagesAdapter.h | 43 +++++++ Ring/Ring/MessagesAdapter.mm | 102 ++++++++++++++++ Ring/Ring/MessagesAdapterDelegate.swift | 28 +++++ Ring/Ring/NameRegistrationAdapter.h | 4 + Ring/Ring/NameRegistrationAdapter.mm | 4 + Ring/Ring/NameService.swift | 16 ++- Ring/Ring/Smartlist.strings | 22 ++++ Ring/Ring/SmartlistViewController.swift | 76 ++++++++++++ Ring/Ring/SmartlistViewModel.swift | 66 +++++++++++ 29 files changed, 1086 insertions(+), 16 deletions(-) create mode 100644 Ring/Ring/Assets.xcassets/ic_contact_picture.imageset/Contents.json create mode 100644 Ring/Ring/Assets.xcassets/ic_contact_picture.imageset/ic_contact_picture.png create mode 100644 Ring/Ring/Colors.swift create mode 100644 Ring/Ring/ContactHelper.swift create mode 100644 Ring/Ring/ContactModel.swift create mode 100644 Ring/Ring/ConversationCell.swift create mode 100644 Ring/Ring/ConversationCell.xib create mode 100644 Ring/Ring/ConversationModel.swift create mode 100644 Ring/Ring/ConversationViewModel.swift create mode 100644 Ring/Ring/ConversationsService.swift create mode 100644 Ring/Ring/Extensions/Date+Helpers.swift create mode 100644 Ring/Ring/Global.strings create mode 100644 Ring/Ring/MessageModel.swift create mode 100644 Ring/Ring/MessagesAdapter.h create mode 100644 Ring/Ring/MessagesAdapter.mm create mode 100644 Ring/Ring/MessagesAdapterDelegate.swift create mode 100644 Ring/Ring/Smartlist.strings create mode 100644 Ring/Ring/SmartlistViewController.swift create mode 100644 Ring/Ring/SmartlistViewModel.swift diff --git a/Ring/Cartfile b/Ring/Cartfile index 79d55a06e..e336d1898 100644 --- a/Ring/Cartfile +++ b/Ring/Cartfile @@ -1,2 +1,3 @@ github "ReactiveX/RxSwift" == 3.0.1 +github "RxSwiftCommunity/RxDataSources" ~> 1.0 github "pkluz/PKHUD" ~> 4.0 diff --git a/Ring/Cartfile.resolved b/Ring/Cartfile.resolved index 8c2f05780..306fc01f4 100644 --- a/Ring/Cartfile.resolved +++ b/Ring/Cartfile.resolved @@ -1,2 +1,3 @@ -github "pkluz/PKHUD" "4.2.1" github "ReactiveX/RxSwift" "3.0.1" +github "RxSwiftCommunity/RxDataSources" "1.0.3" +github "pkluz/PKHUD" "4.2.3" diff --git a/Ring/Ring.xcodeproj/project.pbxproj b/Ring/Ring.xcodeproj/project.pbxproj index 57383a8de..2777f220a 100644 --- a/Ring/Ring.xcodeproj/project.pbxproj +++ b/Ring/Ring.xcodeproj/project.pbxproj @@ -115,6 +115,7 @@ 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 */; }; @@ -126,8 +127,23 @@ 56AC64E11E80542300EA1AA9 /* TextFieldCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56AC64E01E80542300EA1AA9 /* TextFieldCell.swift */; }; 56AC64E31E805F0200EA1AA9 /* TextCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56AC64E21E805F0200EA1AA9 /* TextCell.swift */; }; 56AC650E1E85694D00EA1AA9 /* RoundedTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56AC650D1E85694D00EA1AA9 /* RoundedTextField.swift */; }; + 56BBC99F1ED714CB00CDAF8B /* MessagesAdapter.mm in Sources */ = {isa = PBXBuildFile; fileRef = 56BBC99E1ED714CB00CDAF8B /* MessagesAdapter.mm */; }; + 56BBC9A21ED714DF00CDAF8B /* MessagesAdapterDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BBC9A01ED714DF00CDAF8B /* MessagesAdapterDelegate.swift */; }; + 56BBC9A31ED714DF00CDAF8B /* ConversationsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BBC9A11ED714DF00CDAF8B /* ConversationsService.swift */; }; + 56BBC9A61ED7151500CDAF8B /* MessageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BBC9A51ED7151500CDAF8B /* MessageModel.swift */; }; + 56BBC9A81ED7152300CDAF8B /* SmartlistViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BBC9A71ED7152300CDAF8B /* SmartlistViewController.swift */; }; + 56BBC9AA1ED7153800CDAF8B /* Global.strings in Resources */ = {isa = PBXBuildFile; fileRef = 56BBC9A91ED7153800CDAF8B /* Global.strings */; }; + 56BBC9AC1ED7154300CDAF8B /* Smartlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 56BBC9AB1ED7154300CDAF8B /* Smartlist.strings */; }; + 56BBC9B01ED7155700CDAF8B /* ConversationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BBC9AE1ED7155700CDAF8B /* ConversationModel.swift */; }; + 56BBC9B41ED7156500CDAF8B /* ConversationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BBC9B21ED7156500CDAF8B /* ConversationCell.swift */; }; + 56BBC9B51ED7156500CDAF8B /* ConversationCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 56BBC9B31ED7156500CDAF8B /* ConversationCell.xib */; }; + 56BBC9B91ED715FE00CDAF8B /* ContactModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BBC9B71ED715FE00CDAF8B /* ContactModel.swift */; }; + 56BBC9BA1ED715FE00CDAF8B /* ContactHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BBC9B81ED715FE00CDAF8B /* ContactHelper.swift */; }; + 56BBC9BC1ED7161200CDAF8B /* Date+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BBC9BB1ED7161200CDAF8B /* Date+Helpers.swift */; }; + 56BBC9BF1ED7168400CDAF8B /* SmartlistViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BBC9BE1ED7168400CDAF8B /* SmartlistViewModel.swift */; }; 56BBC9C51ED8BF3300CDAF8B /* libargon2.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 56BBC9C41ED8BF3300CDAF8B /* libargon2.a */; }; - 56BBC9DA1EDDC0B400CDAF8B /* LookupNameResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 56BBC9D91EDDC0B400CDAF8B /* LookupNameResponse.m */; }; + 56BBC9DB1EDDC7F700CDAF8B /* LookupNameResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 56BBC9D91EDDC0B400CDAF8B /* LookupNameResponse.m */; }; + 56BBC9DC1EDDC82600CDAF8B /* ConversationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BBC9AF1ED7155700CDAF8B /* ConversationViewModel.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -269,6 +285,7 @@ 564C445F1E943C37000F92B1 /* NameRegistrationAdapter.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = NameRegistrationAdapter.mm; sourceTree = "<group>"; }; 564C44611E943DE6000F92B1 /* NameService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NameService.swift; sourceTree = "<group>"; }; 564C44631E943E1E000F92B1 /* NameRegistrationAdapterDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NameRegistrationAdapterDelegate.swift; sourceTree = "<group>"; }; + 56559B161EEED50D00BF20E1 /* Colors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Colors.swift; sourceTree = "<group>"; }; 5669A7F91EA904AF003C7B93 /* SwitchCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SwitchCell.xib; sourceTree = "<group>"; }; 5669A7FB1EA904D2003C7B93 /* TextFieldCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = TextFieldCell.xib; sourceTree = "<group>"; }; 5669A7FD1EA904E4003C7B93 /* TextCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = TextCell.xib; sourceTree = "<group>"; }; @@ -280,6 +297,22 @@ 56AC64E01E80542300EA1AA9 /* TextFieldCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFieldCell.swift; sourceTree = "<group>"; }; 56AC64E21E805F0200EA1AA9 /* TextCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextCell.swift; sourceTree = "<group>"; }; 56AC650D1E85694D00EA1AA9 /* RoundedTextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoundedTextField.swift; sourceTree = "<group>"; }; + 56BBC99D1ED714CB00CDAF8B /* MessagesAdapter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MessagesAdapter.h; sourceTree = "<group>"; }; + 56BBC99E1ED714CB00CDAF8B /* MessagesAdapter.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MessagesAdapter.mm; sourceTree = "<group>"; }; + 56BBC9A01ED714DF00CDAF8B /* MessagesAdapterDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesAdapterDelegate.swift; sourceTree = "<group>"; }; + 56BBC9A11ED714DF00CDAF8B /* ConversationsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationsService.swift; sourceTree = "<group>"; }; + 56BBC9A51ED7151500CDAF8B /* MessageModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageModel.swift; sourceTree = "<group>"; }; + 56BBC9A71ED7152300CDAF8B /* SmartlistViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SmartlistViewController.swift; sourceTree = "<group>"; }; + 56BBC9A91ED7153800CDAF8B /* Global.strings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; path = Global.strings; sourceTree = "<group>"; }; + 56BBC9AB1ED7154300CDAF8B /* Smartlist.strings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; path = Smartlist.strings; sourceTree = "<group>"; }; + 56BBC9AE1ED7155700CDAF8B /* ConversationModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationModel.swift; sourceTree = "<group>"; }; + 56BBC9AF1ED7155700CDAF8B /* ConversationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationViewModel.swift; sourceTree = "<group>"; }; + 56BBC9B21ED7156500CDAF8B /* ConversationCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationCell.swift; sourceTree = "<group>"; }; + 56BBC9B31ED7156500CDAF8B /* ConversationCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ConversationCell.xib; sourceTree = "<group>"; }; + 56BBC9B71ED715FE00CDAF8B /* ContactModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactModel.swift; sourceTree = "<group>"; }; + 56BBC9B81ED715FE00CDAF8B /* ContactHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactHelper.swift; sourceTree = "<group>"; }; + 56BBC9BB1ED7161200CDAF8B /* Date+Helpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Date+Helpers.swift"; path = "Extensions/Date+Helpers.swift"; sourceTree = "<group>"; }; + 56BBC9BE1ED7168400CDAF8B /* SmartlistViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SmartlistViewModel.swift; sourceTree = "<group>"; }; 56BBC9C41ED8BF3300CDAF8B /* libargon2.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libargon2.a; path = ../fat/lib/libargon2.a; sourceTree = "<group>"; }; 56BBC9D81EDDC0B400CDAF8B /* LookupNameResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LookupNameResponse.h; sourceTree = "<group>"; }; 56BBC9D91EDDC0B400CDAF8B /* LookupNameResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LookupNameResponse.m; sourceTree = "<group>"; }; @@ -427,6 +460,8 @@ 02E1A0261DDE4C2E00D75B59 /* Services */ = { isa = PBXGroup; children = ( + 56BBC9A01ED714DF00CDAF8B /* MessagesAdapterDelegate.swift */, + 56BBC9A11ED714DF00CDAF8B /* ConversationsService.swift */, 02B22E081DF7585F000358C9 /* DaemonService.swift */, 02B22DFE1DF755DB000358C9 /* AccountsService.swift */, 0273C2FE1E0C438F00CF00BA /* AccountAdapterDelegate.swift */, @@ -453,6 +488,8 @@ 02EFCACF1E0C3DD600FD8ED1 /* Bridging */ = { isa = PBXGroup; children = ( + 56BBC99D1ED714CB00CDAF8B /* MessagesAdapter.h */, + 56BBC99E1ED714CB00CDAF8B /* MessagesAdapter.mm */, 04399AA91D1C304300E99CD9 /* Ring-Bridging-Header.h */, 557086501E8ADB9D001A7CE4 /* SystemAdapter.h */, 557086511E8ADB9D001A7CE4 /* SystemAdapter.mm */, @@ -497,6 +534,7 @@ 043866391D2307C000E06CE2 /* Extensions */ = { isa = PBXGroup; children = ( + 56BBC9BB1ED7161200CDAF8B /* Date+Helpers.swift */, 043866371D2304A700E06CE2 /* BoolStringExtension.swift */, ); name = Extensions; @@ -529,6 +567,10 @@ 043999F51D1C2D9D00E99CD9 /* Ring */ = { isa = PBXGroup; children = ( + 56BBC9BD1ED7165800CDAF8B /* Smartlist */, + 56BBC9B61ED7158600CDAF8B /* Contacts */, + 56BBC9AD1ED7154800CDAF8B /* Conversations */, + 56BBC9A41ED7150200CDAF8B /* Messages */, 564C44571E8D7F68000F92B1 /* Constants */, 56AC64D61E80121200EA1AA9 /* Internationalization */, 0273C3021E0C689600CF00BA /* Walkthrough */, @@ -671,6 +713,7 @@ children = ( 564C44581E8D7F8F000F92B1 /* LocalizedStringTableNames.swift */, 564C445A1E8EA44E000F92B1 /* Durations.swift */, + 56559B161EEED50D00BF20E1 /* Colors.swift */, ); name = Constants; sourceTree = "<group>"; @@ -678,6 +721,8 @@ 56AC64D61E80121200EA1AA9 /* Internationalization */ = { isa = PBXGroup; children = ( + 56BBC9AB1ED7154300CDAF8B /* Smartlist.strings */, + 56BBC9A91ED7153800CDAF8B /* Global.strings */, 56AC64DB1E8012CA00EA1AA9 /* Walkthrough.strings */, ); name = Internationalization; @@ -696,6 +741,43 @@ name = Cells; sourceTree = "<group>"; }; + 56BBC9A41ED7150200CDAF8B /* Messages */ = { + isa = PBXGroup; + children = ( + 56BBC9A51ED7151500CDAF8B /* MessageModel.swift */, + ); + name = Messages; + sourceTree = "<group>"; + }; + 56BBC9AD1ED7154800CDAF8B /* Conversations */ = { + isa = PBXGroup; + children = ( + 56BBC9B21ED7156500CDAF8B /* ConversationCell.swift */, + 56BBC9B31ED7156500CDAF8B /* ConversationCell.xib */, + 56BBC9AE1ED7155700CDAF8B /* ConversationModel.swift */, + 56BBC9AF1ED7155700CDAF8B /* ConversationViewModel.swift */, + ); + name = Conversations; + sourceTree = "<group>"; + }; + 56BBC9B61ED7158600CDAF8B /* Contacts */ = { + isa = PBXGroup; + children = ( + 56BBC9B71ED715FE00CDAF8B /* ContactModel.swift */, + 56BBC9B81ED715FE00CDAF8B /* ContactHelper.swift */, + ); + name = Contacts; + sourceTree = "<group>"; + }; + 56BBC9BD1ED7165800CDAF8B /* Smartlist */ = { + isa = PBXGroup; + children = ( + 56BBC9BE1ED7168400CDAF8B /* SmartlistViewModel.swift */, + 56BBC9A71ED7152300CDAF8B /* SmartlistViewController.swift */, + ); + name = Smartlist; + sourceTree = "<group>"; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -809,9 +891,12 @@ 02B22E031DF755F7000358C9 /* WalkthroughStoryboard.storyboard in Resources */, 5669A7FE1EA904E4003C7B93 /* TextCell.xib in Resources */, 04399A061D1C2D9D00E99CD9 /* LaunchScreen.storyboard in Resources */, + 56BBC9AC1ED7154300CDAF8B /* Smartlist.strings in Resources */, + 56BBC9AA1ED7153800CDAF8B /* Global.strings in Resources */, 04399A031D1C2D9D00E99CD9 /* Assets.xcassets in Resources */, 04399A011D1C2D9D00E99CD9 /* Main.storyboard in Resources */, 5669A7FA1EA904AF003C7B93 /* SwitchCell.xib in Resources */, + 56BBC9B51ED7156500CDAF8B /* ConversationCell.xib in Resources */, 5669A7FC1EA904D2003C7B93 /* TextFieldCell.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -856,41 +941,54 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 56BBC9DC1EDDC82600CDAF8B /* ConversationViewModel.swift in Sources */, 557086521E8ADB9D001A7CE4 /* SystemAdapter.mm in Sources */, 5669A8031EAA58E6003C7B93 /* LinkDeviceToAccountViewController.swift in Sources */, 0273C3051E0C68B100CF00BA /* CreateProfileViewController.swift in Sources */, 02E1A0251DDE4ABA00D75B59 /* BoolStringExtension.swift in Sources */, 04399AAC1D1C304300E99CD9 /* AccountAdapter.mm in Sources */, 02DD80C81E1EAD70009A3510 /* AccountConfigModel.swift in Sources */, - 56BBC9DA1EDDC0B400CDAF8B /* LookupNameResponse.m in Sources */, 02B22E091DF7585F000358C9 /* DaemonService.swift in Sources */, 0273C3061E0C68B100CF00BA /* CreateRingAccountViewController.swift in Sources */, + 56BBC99F1ED714CB00CDAF8B /* MessagesAdapter.mm in Sources */, 02C9B63F1E1D4E8C00F82F0C /* ServiceEvent.swift in Sources */, 02DD80CD1E1EB2E4009A3510 /* ConfigKeyModel.swift in Sources */, + 56BBC9A81ED7152300CDAF8B /* SmartlistViewController.swift in Sources */, 5516C29F1E71CEFF009D3D2D /* AccountModelHelper.swift in Sources */, 56308BA71EA00E5700660275 /* NameRegistrationResponse.m in Sources */, 56AC64E11E80542300EA1AA9 /* TextFieldCell.swift in Sources */, 56AC64E31E805F0200EA1AA9 /* TextCell.swift in Sources */, 56AC650E1E85694D00EA1AA9 /* RoundedTextField.swift in Sources */, + 56BBC9BF1ED7168400CDAF8B /* SmartlistViewModel.swift in Sources */, 02B22E011DF755E5000358C9 /* MainTabBarViewController.swift in Sources */, + 56BBC9DB1EDDC7F700CDAF8B /* LookupNameResponse.m in Sources */, 564C44641E943E1E000F92B1 /* NameRegistrationAdapterDelegate.swift in Sources */, 043999F71D1C2D9D00E99CD9 /* AppDelegate.swift in Sources */, 02B22DFC1DF755BB000358C9 /* AccountModel.swift in Sources */, 043866331D22CE8C00E06CE2 /* MeViewController.swift in Sources */, 56AC64DF1E804ECC00EA1AA9 /* SwitchCell.swift in Sources */, + 56BBC9B91ED715FE00CDAF8B /* ContactModel.swift in Sources */, + 56BBC9BA1ED715FE00CDAF8B /* ContactHelper.swift in Sources */, 04399AAE1D1C304300E99CD9 /* Utils.mm in Sources */, + 56BBC9A31ED714DF00CDAF8B /* ConversationsService.swift in Sources */, + 56BBC9B01ED7155700CDAF8B /* ConversationModel.swift in Sources */, 563AEC771EA664C0003A5641 /* RegistrationResponse.m in Sources */, 02B22DFD1DF755BB000358C9 /* CreateRingAccountViewModel.swift in Sources */, 043999FA1D1C2D9D00E99CD9 /* Ring.xcdatamodeld in Sources */, 564C445B1E8EA44E000F92B1 /* Durations.swift in Sources */, 0438663B1D2313B700E06CE2 /* AccountDetailsViewController.swift in Sources */, 02DD80CA1E1EAF1A009A3510 /* AccountCredentialsModel.swift in Sources */, + 56559B171EEED50D00BF20E1 /* Colors.swift in Sources */, 0273C3081E0C68BF00CF00BA /* RoundedButton.swift in Sources */, + 56BBC9BC1ED7161200CDAF8B /* Date+Helpers.swift in Sources */, 564C44621E943DE6000F92B1 /* NameService.swift in Sources */, 043866361D22D06500E06CE2 /* AccountTableViewCell.swift in Sources */, 04399AAD1D1C304300E99CD9 /* DRingAdapter.mm in Sources */, 0273C2FF1E0C438F00CF00BA /* AccountAdapterDelegate.swift in Sources */, 02B22DFF1DF755DB000358C9 /* AccountsService.swift in Sources */, + 56BBC9A61ED7151500CDAF8B /* MessageModel.swift in Sources */, + 56BBC9A21ED714DF00CDAF8B /* MessagesAdapterDelegate.swift in Sources */, + 56BBC9B41ED7156500CDAF8B /* ConversationCell.swift in Sources */, 564C44601E943C37000F92B1 /* NameRegistrationAdapter.mm in Sources */, 564C44591E8D7F8F000F92B1 /* LocalizedStringTableNames.swift in Sources */, 56AC64D51E7C7F4000EA1AA9 /* WelcomeViewController.swift in Sources */, diff --git a/Ring/Ring/AppDelegate.swift b/Ring/Ring/AppDelegate.swift index e670d0351..1baaf7479 100644 --- a/Ring/Ring/AppDelegate.swift +++ b/Ring/Ring/AppDelegate.swift @@ -29,6 +29,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { static let daemonService = DaemonService(dRingAdaptor: DRingAdapter()) static let accountService = AccountsService(withAccountAdapter: AccountAdapter()) static let nameService = NameService(withNameRegistrationAdapter: NameRegistrationAdapter()) + static let conversationsService = ConversationsService(withMessageAdapter: MessagesAdapter()) func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { SystemAdapter().registerConfigurationHandler() diff --git a/Ring/Ring/Assets.xcassets/ic_contact_picture.imageset/Contents.json b/Ring/Ring/Assets.xcassets/ic_contact_picture.imageset/Contents.json new file mode 100644 index 000000000..f8e96decf --- /dev/null +++ b/Ring/Ring/Assets.xcassets/ic_contact_picture.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_contact_picture.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Ring/Ring/Assets.xcassets/ic_contact_picture.imageset/ic_contact_picture.png b/Ring/Ring/Assets.xcassets/ic_contact_picture.imageset/ic_contact_picture.png new file mode 100644 index 0000000000000000000000000000000000000000..b84a9511860c02c2da725d0e2f94a24675eb81c7 GIT binary patch literal 4599 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 PFfe$!`njxgN@xNAg#+oo literal 0 HcmV?d00001 diff --git a/Ring/Ring/Base.lproj/Main.storyboard b/Ring/Ring/Base.lproj/Main.storyboard index cee4ad0e1..e764877f8 100644 --- a/Ring/Ring/Base.lproj/Main.storyboard +++ b/Ring/Ring/Base.lproj/Main.storyboard @@ -1,11 +1,11 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="11762" systemVersion="15G31" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="qdG-Sd-QaE"> +<?xml version="1.0" encoding="UTF-8"?> +<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12120" systemVersion="16A323" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="qdG-Sd-QaE"> <device id="retina4_0" orientation="portrait"> <adaptation id="fullscreen"/> </device> <dependencies> <deployment identifier="iOS"/> - <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11757"/> + <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12088"/> <capability name="Constraints to layout margins" minToolsVersion="6.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> </dependencies> @@ -22,7 +22,7 @@ <rect key="frame" x="0.0" y="0.0" width="320" height="568"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <subviews> - <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="PERSON PLACEHOLDER" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="vcL-Nj-NcH"> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="PERSON PLACEHOLDER" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="vcL-Nj-NcH"> <rect key="frame" x="16" y="102" width="180" height="64"/> <constraints> <constraint firstAttribute="width" constant="180" id="CUM-z9-ubY"/> @@ -50,7 +50,7 @@ <rect key="frame" x="0.0" y="28" width="320" height="44"/> <autoresizingMask key="autoresizingMask"/> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="i1O-Yc-WGd" id="Bz1-A3-Z3f"> - <rect key="frame" x="0.0" y="0.0" width="320" height="43"/> + <rect key="frame" x="0.0" y="0.0" width="320" height="44"/> <autoresizingMask key="autoresizingMask"/> <subviews> <switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="bm0-lC-K2F"> @@ -96,7 +96,7 @@ <rect key="frame" x="0.0" y="72" width="320" height="44"/> <autoresizingMask key="autoresizingMask"/> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="Ok2-8L-eMm" id="m64-AI-t2h"> - <rect key="frame" x="0.0" y="0.0" width="320" height="43"/> + <rect key="frame" x="0.0" y="0.0" width="320" height="44"/> <autoresizingMask key="autoresizingMask"/> <subviews> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Add Account" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="kZv-uf-BsD" userLabel="Account Name Label"> @@ -192,9 +192,11 @@ <objects> <navigationController id="acv-jH-RCt" sceneMemberID="viewController"> <tabBarItem key="tabBarItem" title="Me" id="jo3-1i-bFH"/> - <navigationBar key="navigationBar" contentMode="scaleToFill" id="tAT-cg-hut"> + <navigationBar key="navigationBar" contentMode="scaleToFill" barStyle="black" id="tAT-cg-hut"> <rect key="frame" x="0.0" y="0.0" width="320" height="44"/> <autoresizingMask key="autoresizingMask"/> + <color key="tintColor" white="1" alpha="1" colorSpace="calibratedWhite"/> + <color key="barTintColor" red="0.039215686270000001" green="0.4549019608" blue="0.53725490200000003" alpha="1" colorSpace="calibratedRGB"/> </navigationBar> <connections> <segue destination="Adn-sB-m3h" kind="relationship" relationship="rootViewController" id="yoa-Sb-o2W"/> @@ -204,14 +206,16 @@ </objects> <point key="canvasLocation" x="-954" y="-438"/> </scene> - <!--Item--> + <!--Home--> <scene sceneID="RrV-5t-V1t"> <objects> <navigationController id="m3Y-hs-SzS" sceneMemberID="viewController"> - <tabBarItem key="tabBarItem" title="Item" id="vc8-sv-IF0"/> - <navigationBar key="navigationBar" contentMode="scaleToFill" id="G6y-tt-W3p"> + <tabBarItem key="tabBarItem" title="Home" id="vc8-sv-IF0"/> + <navigationBar key="navigationBar" contentMode="scaleToFill" barStyle="black" id="G6y-tt-W3p"> <rect key="frame" x="0.0" y="0.0" width="375" height="44"/> <autoresizingMask key="autoresizingMask"/> + <color key="tintColor" white="1" alpha="1" colorSpace="calibratedWhite"/> + <color key="barTintColor" red="0.039215686270000001" green="0.4549019608" blue="0.53725490200000003" alpha="1" colorSpace="calibratedRGB"/> </navigationBar> <connections> <segue destination="NIj-Cd-aWO" kind="relationship" relationship="rootViewController" id="GUx-m9-dbP"/> @@ -224,7 +228,7 @@ <!--Home--> <scene sceneID="UuZ-iE-GB0"> <objects> - <viewController title="Home" id="NIj-Cd-aWO" sceneMemberID="viewController"> + <viewController title="Home" id="NIj-Cd-aWO" customClass="SmartlistViewController" customModule="Ring" customModuleProvider="target" sceneMemberID="viewController"> <layoutGuides> <viewControllerLayoutGuide type="top" id="s1e-Lp-B2j"/> <viewControllerLayoutGuide type="bottom" id="aoH-Yk-Qrn"/> @@ -241,8 +245,10 @@ </subviews> <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> </view> - <tabBarItem key="tabBarItem" title="Home" id="1QA-0Y-BFL"/> <navigationItem key="navigationItem" id="b8m-eG-Q9D"/> + <connections> + <outlet property="tableView" destination="B6Y-MZ-L7L" id="dXp-J4-x68"/> + </connections> </viewController> <placeholder placeholderIdentifier="IBFirstResponder" id="rzQ-ll-5bo" userLabel="First Responder" sceneMemberID="firstResponder"/> </objects> @@ -256,6 +262,7 @@ <rect key="frame" x="0.0" y="0.0" width="320" height="49"/> <autoresizingMask key="autoresizingMask"/> <color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/> + <color key="tintColor" red="0.039215686270000001" green="0.4549019608" blue="0.53725490200000003" alpha="1" colorSpace="calibratedRGB"/> </tabBar> <connections> <segue destination="m3Y-hs-SzS" kind="relationship" relationship="viewControllers" id="tnG-cB-lXh"/> diff --git a/Ring/Ring/Bridging/Ring-Bridging-Header.h b/Ring/Ring/Bridging/Ring-Bridging-Header.h index b0aecf060..600ce0985 100644 --- a/Ring/Ring/Bridging/Ring-Bridging-Header.h +++ b/Ring/Ring/Bridging/Ring-Bridging-Header.h @@ -29,3 +29,4 @@ #import "LookupNameResponse.h" #import "RegistrationResponse.h" #import "NameRegistrationResponse.h" +#import "MessagesAdapter.h" diff --git a/Ring/Ring/Colors.swift b/Ring/Ring/Colors.swift new file mode 100644 index 000000000..94d112f16 --- /dev/null +++ b/Ring/Ring/Colors.swift @@ -0,0 +1,28 @@ +/* + * 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 + +class Colors { + static let ringMainColor = UIColor(colorLiteralRed: 10.0/255.0, + green: 116.0/255.0, + blue: 137.0/255.0, + alpha: 1.0) +} diff --git a/Ring/Ring/ContactHelper.swift b/Ring/Ring/ContactHelper.swift new file mode 100644 index 000000000..67378a955 --- /dev/null +++ b/Ring/Ring/ContactHelper.swift @@ -0,0 +1,59 @@ +/* + * 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 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]! + } + + return userName + } + +} diff --git a/Ring/Ring/ContactModel.swift b/Ring/Ring/ContactModel.swift new file mode 100644 index 000000000..f4a8f16e1 --- /dev/null +++ b/Ring/Ring/ContactModel.swift @@ -0,0 +1,34 @@ +/* + * 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 + +class ContactModel { + + var ringId: String + + init(withRingId ringId: String) { + self.ringId = ringId + } + + public static func ==(lhs: ContactModel, rhs: ContactModel) -> Bool { + return lhs.ringId == rhs.ringId + } +} diff --git a/Ring/Ring/ConversationCell.swift b/Ring/Ring/ConversationCell.swift new file mode 100644 index 000000000..d3fd4fd62 --- /dev/null +++ b/Ring/Ring/ConversationCell.swift @@ -0,0 +1,47 @@ +/* + * 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 ConversationCell: UITableViewCell { + + @IBOutlet weak var profileImage: UIImageView! + @IBOutlet weak var nameLabel: UILabel! + @IBOutlet weak var newMessagesIndicator: UIView! + @IBOutlet weak var newMessagesLabel: UILabel! + @IBOutlet weak var lastMessageDateLabel: UILabel! + @IBOutlet weak var lastMessagePreviewLabel: UILabel! + + override func awakeFromNib() { + super.awakeFromNib() + + } + + override func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + self.newMessagesIndicator.backgroundColor = UIColor.red + } + + override func setHighlighted(_ highlighted: Bool, animated: Bool) { + super.setHighlighted(highlighted, animated: animated) + self.newMessagesIndicator.backgroundColor = UIColor.red + } +} diff --git a/Ring/Ring/ConversationCell.xib b/Ring/Ring/ConversationCell.xib new file mode 100644 index 000000000..c48708bf6 --- /dev/null +++ b/Ring/Ring/ConversationCell.xib @@ -0,0 +1,111 @@ +<?xml version="1.0" encoding="UTF-8"?> +<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="12120" systemVersion="16A323" 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="12088"/> + <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="default" indentationWidth="10" reuseIdentifier="ConversationCellId" rowHeight="76" id="KGk-i7-Jjw" customClass="ConversationCell" customModule="Ring" customModuleProvider="target"> + <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"/> + <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"> + <rect key="frame" x="16" y="18" width="40" height="40"/> + <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/> + <constraints> + <constraint firstAttribute="width" constant="40" id="Ptm-nU-6Xi"/> + <constraint firstAttribute="height" constant="40" id="eES-QW-paO"/> + </constraints> + <userDefinedRuntimeAttributes> + <userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius"> + <integer key="value" value="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"/> + <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"/> + <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"/> + <fontDescription key="fontDescription" type="system" pointSize="14"/> + <nil key="textColor"/> + <nil key="highlightedColor"/> + </label> + <view clipsSubviews="YES" contentMode="scaleToFill" horizontalHuggingPriority="249" translatesAutoresizingMaskIntoConstraints="NO" id="JTE-eF-Y5s"> + <rect key="frame" x="42" y="18" width="14" height="14"/> + <subviews> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="1" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="P5S-4k-0yx"> + <rect key="frame" x="0.0" y="0.0" width="14" height="14"/> + <constraints> + <constraint firstAttribute="width" relation="greaterThanOrEqual" constant="14" id="VEi-Hm-z85"/> + </constraints> + <fontDescription key="fontDescription" type="system" pointSize="10"/> + <color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/> + <nil key="highlightedColor"/> + </label> + </subviews> + <color key="backgroundColor" red="1" green="0.0" blue="0.0" alpha="1" colorSpace="calibratedRGB"/> + <constraints> + <constraint firstItem="P5S-4k-0yx" firstAttribute="top" secondItem="JTE-eF-Y5s" secondAttribute="top" id="BH8-rD-0Oo"/> + <constraint firstAttribute="height" constant="14" id="Qxc-Mv-ssH"/> + <constraint firstItem="P5S-4k-0yx" firstAttribute="leading" secondItem="JTE-eF-Y5s" secondAttribute="leading" id="ZE1-mR-DoI"/> + <constraint firstAttribute="trailing" secondItem="P5S-4k-0yx" secondAttribute="trailing" id="a9D-mE-0qK"/> + <constraint firstAttribute="bottom" secondItem="P5S-4k-0yx" secondAttribute="bottom" id="qMW-uw-IoS"/> + </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 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="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="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"/> + </constraints> + </tableViewCellContentView> + <connections> + <outlet property="lastMessageDateLabel" destination="7Yv-cC-LKx" id="pFf-hW-zD8"/> + <outlet property="lastMessagePreviewLabel" destination="eug-ak-r49" id="3jj-Lx-jbU"/> + <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="profileImage" destination="pFB-Jn-TNP" id="zuf-CZ-9wL"/> + </connections> + <point key="canvasLocation" x="70" y="-92"/> + </tableViewCell> + </objects> + <resources> + <image name="ic_contact_picture" width="128" height="128"/> + </resources> +</document> diff --git a/Ring/Ring/ConversationModel.swift b/Ring/Ring/ConversationModel.swift new file mode 100644 index 000000000..d1624cda7 --- /dev/null +++ b/Ring/Ring/ConversationModel.swift @@ -0,0 +1,32 @@ +/* + * 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. + */ + +class ConversationModel { + + var messages = [MessageModel]() + var recipient: ContactModel + var accountId: String + + init(withRecipient recipient: ContactModel, accountId: String) { + self.recipient = recipient + self.accountId = accountId + } + +} diff --git a/Ring/Ring/ConversationViewModel.swift b/Ring/Ring/ConversationViewModel.swift new file mode 100644 index 000000000..19537035c --- /dev/null +++ b/Ring/Ring/ConversationViewModel.swift @@ -0,0 +1,99 @@ +/* + * 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 ConversationViewModel { + + let conversation: ConversationModel + let userName: Observable<String> + + //Displays the entire date ( for messages received before the current week ) + private let dateFormatter = DateFormatter() + + //Displays the hour of the message reception ( for messages received today ) + private let hourFormatter = DateFormatter() + + private let disposeBag = DisposeBag() + + init(withConversation conversation: ConversationModel) { + self.conversation = conversation + dateFormatter.dateStyle = .medium + hourFormatter.dateFormat = "HH:mm" + + self.userName = ContactHelper.lookupUserName(forRingId: self.conversation.recipient.ringId, + nameService: AppDelegate.nameService, + disposeBag: self.disposeBag).asObservable() + } + + var unreadMessages: String { + return self.unreadMessagesCount.description + } + + fileprivate var unreadMessagesCount: Int { + return self.conversation.messages.filter({ message in + return message.status != .read + }).count + } + + var hasUnreadMessages: Bool { + return conversation.messages.count > 0 + } + + var lastMessage: String { + if let lastMessage = conversation.messages.last?.content { + return lastMessage + } else { + return "" + } + } + + var lastMessageReceivedDate: String { + + guard let lastMessageDate = self.conversation.messages.last?.receivedDate else { + return "" + } + + let dateToday = Date() + + //Get components from today date + let todayWeekOfYear = Calendar.current.component(.weekOfYear, from: dateToday) + let todayDay = Calendar.current.component(.day, from: dateToday) + let todayMonth = Calendar.current.component(.month, from: dateToday) + let todayYear = Calendar.current.component(.year, from: dateToday) + + //Get components from last message date + let weekOfYear = Calendar.current.component(.weekOfYear, from: lastMessageDate) + let day = Calendar.current.component(.day, from: lastMessageDate) + let month = Calendar.current.component(.month, from: lastMessageDate) + let year = Calendar.current.component(.year, from: lastMessageDate) + + if todayDay == day && todayMonth == month && todayYear == year { + return hourFormatter.string(from: lastMessageDate) + } else if day == todayDay - 1 { + return NSLocalizedString("Yesterday", tableName: "Smartlist", comment: "") + } else if todayYear == year && todayWeekOfYear == weekOfYear { + return lastMessageDate.dayOfWeek() + } else { + return dateFormatter.string(from: lastMessageDate) + } + } +} diff --git a/Ring/Ring/ConversationsService.swift b/Ring/Ring/ConversationsService.swift new file mode 100644 index 000000000..0d77c07f2 --- /dev/null +++ b/Ring/Ring/ConversationsService.swift @@ -0,0 +1,76 @@ +/* + * 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 ConversationsService: MessagesAdapterDelegate { + + fileprivate let messageAdapter :MessagesAdapter + fileprivate let disposeBag = DisposeBag() + fileprivate let textPlainMIMEType = "text/plain" + + let conversations = Variable([ConversationModel]()) + + init(withMessageAdapter messageAdapter: MessagesAdapter) { + self.messageAdapter = messageAdapter + MessagesAdapter.delegate = self + } + + func status(forMessageId messageId: UInt64) -> MessageStatus { + return self.messageAdapter.status(forMessageId: messageId) + } + + //MARK: Message Adapter delegate + + func didReceiveMessage(_ message: Dictionary<String, String>, from senderAccount: String, + to receiverAccountId: String) { + + if let content = message[textPlainMIMEType] { + let message = MessageModel(withId: nil, receivedDate: Date(), content: content, author: senderAccount) + + //Get conversations for this sender + var currentConversation = conversations.value.filter({ conversation in + return conversation.recipient.ringId == senderAccount + }).first + + //Get the current array of conversations + var currentConversations = self.conversations.value + + //Create a new conversation for this sender if not exists + if currentConversation == nil { + currentConversation = ConversationModel(withRecipient: ContactModel(withRingId: senderAccount), accountId: receiverAccountId) + currentConversations.append(currentConversation!) + } + + //Add the received message into the conversation + currentConversation?.messages.append(message) + + //Upate the value of the Variable + self.conversations.value = currentConversations + } + } + + func messageStatusChanged(_ status: MessageStatus, for messageId: UInt64, + from senderAccountId: String, to receiverAccount: String) { + + print("messageStatusChanged: \(status.rawValue) for: \(messageId) from: \(senderAccountId) to: \(receiverAccount)") + } +} diff --git a/Ring/Ring/Extensions/Date+Helpers.swift b/Ring/Ring/Extensions/Date+Helpers.swift new file mode 100644 index 000000000..19a58ed73 --- /dev/null +++ b/Ring/Ring/Extensions/Date+Helpers.swift @@ -0,0 +1,31 @@ +/* + * 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. + */ + +extension Date { + func dayNumberOfWeek() -> Int? { + return Calendar.current.dateComponents([.weekday], from: self).weekday + } + + func dayOfWeek() -> String { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "EEEE" + return dateFormatter.string(from: self) + } +} diff --git a/Ring/Ring/Global.strings b/Ring/Ring/Global.strings new file mode 100644 index 000000000..0cc9f97f3 --- /dev/null +++ b/Ring/Ring/Global.strings @@ -0,0 +1,21 @@ +/* + * 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. + */ + +"HomeTabBarTitle" = "Home"; diff --git a/Ring/Ring/MainTabBar/MainTabBarViewController.swift b/Ring/Ring/MainTabBar/MainTabBarViewController.swift index 678d30a0b..72cdb8bfa 100644 --- a/Ring/Ring/MainTabBar/MainTabBarViewController.swift +++ b/Ring/Ring/MainTabBar/MainTabBarViewController.swift @@ -23,6 +23,12 @@ import UIKit class MainTabBarViewController: UITabBarController { fileprivate let accountService = AppDelegate.accountService + override func viewDidLoad() { + super.viewDidLoad() + UITabBarItem.appearance() + .setTitleTextAttributes( [NSForegroundColorAttributeName : Colors.ringMainColor], for: .selected) + } + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) if !accountService.hasAccounts() { diff --git a/Ring/Ring/MessageModel.swift b/Ring/Ring/MessageModel.swift new file mode 100644 index 000000000..000e4858c --- /dev/null +++ b/Ring/Ring/MessageModel.swift @@ -0,0 +1,37 @@ +/* + * 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. + */ + +class MessageModel { + + var id: UInt64? + var receivedDate: Date + var content: String + var author: String + var status: MessageStatus + + init(withId id: UInt64?, receivedDate: Date, content: String, author: String) { + self.id = id + self.receivedDate = receivedDate + self.content = content + self.author = author + self.status = .unknown + } + +} diff --git a/Ring/Ring/MessagesAdapter.h b/Ring/Ring/MessagesAdapter.h new file mode 100644 index 000000000..4cc6c8553 --- /dev/null +++ b/Ring/Ring/MessagesAdapter.h @@ -0,0 +1,43 @@ +/* + * 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 <Foundation/Foundation.h> + +typedef NS_ENUM(int, MessageStatus) { + MessageStatusUnknown = 0, + MessageStatusIdle, + MessageStatusSending, + MessageStatusSent, + MessageStatusRead, + MessageStatusFailure +}; + +@protocol MessagesAdapterDelegate; + +@interface MessagesAdapter : NSObject + +@property (class, nonatomic, weak) id <MessagesAdapterDelegate> delegate; + +- (NSUInteger)sendMessageWithContent:(NSDictionary*)content withAccountId:(NSString*)accountId + to:(NSString*)toAccountId; + +- (MessageStatus)statusForMessageId:(uint64_t)messageId; + +@end diff --git a/Ring/Ring/MessagesAdapter.mm b/Ring/Ring/MessagesAdapter.mm new file mode 100644 index 000000000..69f90d4a3 --- /dev/null +++ b/Ring/Ring/MessagesAdapter.mm @@ -0,0 +1,102 @@ +/* + * 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 "MessagesAdapter.h" + +#import "Ring-Swift.h" +#import "Utils.h" +#import "dring/configurationmanager_interface.h" + +@implementation MessagesAdapter + +using namespace DRing; + +/// Static delegate that will receive the propagated daemon events +static id <MessagesAdapterDelegate> _delegate; + +#pragma mark Init +- (id)init { + if (self = [super init]) { + [self registerConfigurationHandler]; + } + return self; +} +#pragma mark - + +#pragma mark Callbacks registration +- (void)registerConfigurationHandler { + std::map<std::string, std::shared_ptr<CallbackWrapperBase>> confHandlers; + + confHandlers.insert(exportable_callback<ConfigurationSignal::IncomingAccountMessage>([&](const std::string& account_id, + const std::string& from, + const std::map<std::string, + std::string>& payloads) { + if (MessagesAdapter.delegate) { + NSDictionary* message = [Utils mapToDictionnary:payloads]; + NSString* fromAccount = [NSString stringWithUTF8String:from.c_str()]; + NSString* toAccountId = [NSString stringWithUTF8String:account_id.c_str()]; + [MessagesAdapter.delegate didReceiveMessage:message from:fromAccount to:toAccountId]; + } + })); + + confHandlers.insert(exportable_callback<ConfigurationSignal::AccountMessageStatusChanged>([&](const std::string& account_id, uint64_t message_id, const std::string& to, int state) { + if (MessagesAdapter.delegate) { + NSString* fromAccountId = [NSString stringWithUTF8String:account_id.c_str()]; + NSString* toAccount = [NSString stringWithUTF8String:to.c_str()]; + [MessagesAdapter.delegate messageStatusChanged:(MessageStatus)state + for:message_id from:fromAccountId + to:toAccount]; + } + })); + + confHandlers.insert(exportable_callback<DebugSignal::MessageSend>([&](const std::string& message) { + if (MessagesAdapter.delegate) { + NSString* messageSend = [NSString stringWithUTF8String:message.c_str()]; + NSLog(@"MessageSend = %@",messageSend); + } + })); + + registerConfHandlers(confHandlers); +} +#pragma mark - + +- (NSUInteger)sendMessageWithContent:(NSDictionary*)content withAccountId:(NSString*)accountId + to:(NSString*)toAccountId { + + return (NSUInteger) sendAccountTextMessage(std::string([accountId UTF8String]), + std::string([toAccountId UTF8String]), + [Utils dictionnaryToMap:content]); +} + +- (MessageStatus)statusForMessageId:(uint64_t)messageId { + return (MessageStatus)getMessageStatus(messageId); +} + +#pragma mark AccountAdapterDelegate ++ (id <MessagesAdapterDelegate>)delegate { + return _delegate; +} + ++ (void) setDelegate:(id<MessagesAdapterDelegate>)delegate { + _delegate = delegate; +} +#pragma mark - + +@end diff --git a/Ring/Ring/MessagesAdapterDelegate.swift b/Ring/Ring/MessagesAdapterDelegate.swift new file mode 100644 index 000000000..dc50514e3 --- /dev/null +++ b/Ring/Ring/MessagesAdapterDelegate.swift @@ -0,0 +1,28 @@ +/* + * 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. + */ + +@objc protocol MessagesAdapterDelegate { + + func didReceiveMessage(_ message: Dictionary<String, String>, from senderAccount: String, + to receiverAccountId: String) + + func messageStatusChanged(_ status: MessageStatus, for messageId: UInt64, from senderAccountId: String, + to receiverAccount: String) +} diff --git a/Ring/Ring/NameRegistrationAdapter.h b/Ring/Ring/NameRegistrationAdapter.h index 2147523a9..6806055bb 100644 --- a/Ring/Ring/NameRegistrationAdapter.h +++ b/Ring/Ring/NameRegistrationAdapter.h @@ -32,4 +32,8 @@ - (void)registerNameWithAccount:(NSString*)account password:(NSString*)password name:(NSString*)name; +- (void)lookupAddressWithAccount:(NSString*)account nameserver:(NSString*)nameserver + address:(NSString*)address; + + @end diff --git a/Ring/Ring/NameRegistrationAdapter.mm b/Ring/Ring/NameRegistrationAdapter.mm index 84a5881f6..99a9a0357 100644 --- a/Ring/Ring/NameRegistrationAdapter.mm +++ b/Ring/Ring/NameRegistrationAdapter.mm @@ -79,6 +79,10 @@ static id <NameRegistrationAdapterDelegate> _delegate; lookupName(std::string([account UTF8String]),std::string([nameserver UTF8String]),std::string([name UTF8String])); } +- (void)lookupAddressWithAccount:(NSString*)account nameserver:(NSString*)nameserver address:(NSString*)address { + lookupAddress(std::string([account UTF8String]), std::string([nameserver UTF8String]), std::string([address UTF8String])); +} + - (void)registerNameWithAccount:(NSString*)account password:(NSString*)password name:(NSString*)name { registerName(std::string([account UTF8String]), std::string([password UTF8String]), std::string([name UTF8String])); } diff --git a/Ring/Ring/NameService.swift b/Ring/Ring/NameService.swift index 214fd00ed..c15dceb81 100644 --- a/Ring/Ring/NameService.swift +++ b/Ring/Ring/NameService.swift @@ -44,7 +44,7 @@ class NameService: NameRegistrationAdapterDelegate { fileprivate let lookupNameCallDelay = 0.5 /** - Status of the current username lookup request + Status of the current username validation request */ var usernameValidationStatus = PublishSubject<UsernameValidationStatus>() @@ -53,6 +53,11 @@ class NameService: NameRegistrationAdapterDelegate { NameRegistrationAdapter.delegate = self } + /** + Status of the current username lookup request + */ + var usernameLookupStatus = PublishSubject<LookupNameResponse>() + /** Make a username lookup request to the daemon */ @@ -75,6 +80,13 @@ class NameService: NameRegistrationAdapterDelegate { } } + /** + Make an address lookup request to the daemon + */ + func lookupAddress(withAccount account: String, nameserver: String, address: String) { + self.nameRegistrationAdapter.lookupAddress(withAccount: account, nameserver: nameserver, address: address) + } + /** Register the username into the the blockchain */ @@ -95,6 +107,8 @@ class NameService: NameRegistrationAdapterDelegate { } else { print("Lookup name error") } + + usernameLookupStatus.onNext(response) } internal func nameRegistrationEnded(with response: NameRegistrationResponse) { diff --git a/Ring/Ring/Smartlist.strings b/Ring/Ring/Smartlist.strings new file mode 100644 index 000000000..e7d37ca1d --- /dev/null +++ b/Ring/Ring/Smartlist.strings @@ -0,0 +1,22 @@ +/* + * 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. + */ + +"Yesterday" = "Yesterday"; + diff --git a/Ring/Ring/SmartlistViewController.swift b/Ring/Ring/SmartlistViewController.swift new file mode 100644 index 000000000..9a76c1117 --- /dev/null +++ b/Ring/Ring/SmartlistViewController.swift @@ -0,0 +1,76 @@ +/* + * 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 +import RxDataSources + +class SmartlistViewController: UIViewController, UITableViewDelegate { + + @IBOutlet weak var tableView: UITableView! + + fileprivate let viewModel = SmartlistViewModel(withConversationsService: AppDelegate.conversationsService) + fileprivate let disposeBag = DisposeBag() + fileprivate let SmartlistRowHeight :CGFloat = 64.0 + + override func viewDidLoad() { + super.viewDidLoad() + self.setupUI() + self.setupTableView() + } + + func setupUI() { + + let title = NSLocalizedString("HomeTabBarTitle", tableName: "Global", comment: "") + + self.title = title + self.navigationItem.title = title + } + + func setupTableView() { + + //Set row height + self.tableView.rowHeight = SmartlistRowHeight + + //Register Cell + self.tableView.register(UINib.init(nibName: "ConversationCell", bundle: nil), forCellReuseIdentifier: "ConversationCellId") + + //Bind the TableView to the ViewModel + self.viewModel.conversationsObservable.bindTo(tableView.rx.items(cellIdentifier: "ConversationCellId", cellType: ConversationCell.self) ) { index, viewModel, cell in + viewModel.userName.bindTo(cell.nameLabel.rx.text).addDisposableTo(self.disposeBag) + cell.newMessagesLabel.text = viewModel.unreadMessages + cell.lastMessageDateLabel.text = viewModel.lastMessageReceivedDate + cell.newMessagesIndicator.isHidden = !viewModel.hasUnreadMessages + cell.lastMessagePreviewLabel.text = viewModel.lastMessage + }.addDisposableTo(disposeBag) + + self.tableView.rx.itemSelected.asObservable().subscribe(onNext: { indexPath in + self.tableView.deselectRow(at: indexPath, animated: true) + }).addDisposableTo(disposeBag) + } + + // MARK: - Navigation + + // In a storyboard-based application, you will often want to do a little preparation before navigation + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + + } + +} diff --git a/Ring/Ring/SmartlistViewModel.swift b/Ring/Ring/SmartlistViewModel.swift new file mode 100644 index 000000000..3c0ab418b --- /dev/null +++ b/Ring/Ring/SmartlistViewModel.swift @@ -0,0 +1,66 @@ +/* + * 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 SmartlistViewModel: NSObject { + + fileprivate let conversationsService: ConversationsService + fileprivate var conversationViewModels :[ConversationViewModel] + + let conversationsObservable :Observable<[ConversationViewModel]> + + init(withConversationsService conversationsService: ConversationsService) { + self.conversationsService = conversationsService + + var conversationViewModels = [ConversationViewModel]() + self.conversationViewModels = conversationViewModels + + //Create observable from sorted conversations and flatMap them to view models + self.conversationsObservable = self.conversationsService.conversations.asObservable().map({ conversations in + return conversations.sorted(by: { conversation1, conversations2 in + + guard let lastMessage1 = conversation1.messages.last, + let lastMessage2 = conversations2.messages.last else { + return true + } + + return lastMessage1.receivedDate > lastMessage2.receivedDate + }).flatMap({ conversationModel in + + var conversationViewModel: ConversationViewModel? + + //Get the current ConversationViewModel if exists or create it + if let foundConversationViewModel = conversationViewModels.filter({ conversationViewModel in + return conversationViewModel.conversation === conversationModel + }).first { + conversationViewModel = foundConversationViewModel + } else { + conversationViewModel = ConversationViewModel(withConversation: conversationModel) + conversationViewModels.append(conversationViewModel!) + } + + return conversationViewModel + }) + }).observeOn(MainScheduler.instance) + } + +} -- GitLab