├── .gitignore ├── .gitmodules ├── CouchChat.xcodeproj ├── project.pbxproj └── project.xcworkspace │ └── contents.xcworkspacedata ├── CouchChat ├── AppDelegate.h ├── AppDelegate.m ├── ChatController.h ├── ChatController.m ├── ChatListController.h ├── ChatListController.m ├── ChatRoom.h ├── ChatRoom.m ├── ChatStore.h ├── ChatStore.m ├── CouchChat-Info.plist ├── CouchChat-Prefix.pch ├── Default-568h@2x.png ├── Default.png ├── Default@2x.png ├── LoginController.h ├── LoginController.m ├── SyncManager.h ├── SyncManager.m ├── UserPickerController.h ├── UserPickerController.m ├── UserProfile.h ├── UserProfile.m ├── UserProfile_Private.h ├── en.lproj │ ├── ChatController_iPad.xib │ ├── ChatController_iPhone.xib │ ├── ChatListController_iPad.xib │ ├── ChatListController_iPhone.xib │ └── InfoPlist.strings └── main.m ├── Frameworks └── README.txt ├── README.md ├── Resources ├── Camera.png ├── ChatIcon.png ├── ChatIcon@2x.png └── double_lined.png ├── iPhone icon.png ├── iPhone icon@2x.png └── sync-gateway-config.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.framework 3 | *~ 4 | xcuserdata/ 5 | build/ 6 | DerivedData/ 7 | obj/ 8 | derived_src/ 9 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/UIBubbleTableView"] 2 | path = vendor/UIBubbleTableView 3 | url = git://github.com/snej/UIBubbleTableView.git 4 | [submodule "vendor/browserid"] 5 | path = vendor/browserid 6 | url = git://github.com/couchbaselabs/browserid-ios.git 7 | [submodule "vendor/THContactPicker"] 8 | path = vendor/THContactPicker 9 | url = git://github.com/snej/THContactPicker.git 10 | -------------------------------------------------------------------------------- /CouchChat.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 279B32471721F458004F06A7 /* iPhone icon.png in Resources */ = {isa = PBXBuildFile; fileRef = 279B32461721F458004F06A7 /* iPhone icon.png */; }; 11 | 279B32491721F45E004F06A7 /* iPhone icon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 279B32481721F45E004F06A7 /* iPhone icon@2x.png */; }; 12 | 27CB04DC16CC4B420040C9F8 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27CB04DB16CC4B420040C9F8 /* UIKit.framework */; }; 13 | 27CB04DE16CC4B420040C9F8 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27CB04DD16CC4B420040C9F8 /* Foundation.framework */; }; 14 | 27CB04E616CC4B420040C9F8 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 27CB04E416CC4B420040C9F8 /* InfoPlist.strings */; }; 15 | 27CB04E816CC4B420040C9F8 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 27CB04E716CC4B420040C9F8 /* main.m */; }; 16 | 27CB04EC16CC4B420040C9F8 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 27CB04EB16CC4B420040C9F8 /* AppDelegate.m */; }; 17 | 27CB04EE16CC4B420040C9F8 /* Default.png in Resources */ = {isa = PBXBuildFile; fileRef = 27CB04ED16CC4B420040C9F8 /* Default.png */; }; 18 | 27CB04F016CC4B420040C9F8 /* Default@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 27CB04EF16CC4B420040C9F8 /* Default@2x.png */; }; 19 | 27CB04F216CC4B420040C9F8 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 27CB04F116CC4B420040C9F8 /* Default-568h@2x.png */; }; 20 | 27CB04F516CC4B420040C9F8 /* ChatListController.m in Sources */ = {isa = PBXBuildFile; fileRef = 27CB04F416CC4B420040C9F8 /* ChatListController.m */; }; 21 | 27CB04F816CC4B420040C9F8 /* ChatController.m in Sources */ = {isa = PBXBuildFile; fileRef = 27CB04F716CC4B420040C9F8 /* ChatController.m */; }; 22 | 27CB04FB16CC4B420040C9F8 /* ChatListController_iPhone.xib in Resources */ = {isa = PBXBuildFile; fileRef = 27CB04F916CC4B420040C9F8 /* ChatListController_iPhone.xib */; }; 23 | 27CB04FE16CC4B420040C9F8 /* ChatListController_iPad.xib in Resources */ = {isa = PBXBuildFile; fileRef = 27CB04FC16CC4B420040C9F8 /* ChatListController_iPad.xib */; }; 24 | 27CB050116CC4B420040C9F8 /* ChatController_iPhone.xib in Resources */ = {isa = PBXBuildFile; fileRef = 27CB04FF16CC4B420040C9F8 /* ChatController_iPhone.xib */; }; 25 | 27CB050416CC4B420040C9F8 /* ChatController_iPad.xib in Resources */ = {isa = PBXBuildFile; fileRef = 27CB050216CC4B420040C9F8 /* ChatController_iPad.xib */; }; 26 | 27CB051016CC4C8B0040C9F8 /* ChatStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 27CB050D16CC4C8B0040C9F8 /* ChatStore.m */; }; 27 | 27CB051116CC4C8B0040C9F8 /* ChatRoom.m in Sources */ = {isa = PBXBuildFile; fileRef = 27CB050F16CC4C8B0040C9F8 /* ChatRoom.m */; }; 28 | 27CB051416CC4E400040C9F8 /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27CB051316CC4E400040C9F8 /* CFNetwork.framework */; }; 29 | 27CB051616CC4E460040C9F8 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27CB051516CC4E460040C9F8 /* Security.framework */; }; 30 | 27CB051816CC4E4C0040C9F8 /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 27CB051716CC4E4C0040C9F8 /* libz.dylib */; }; 31 | 27CB051A16CC4E560040C9F8 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27CB051916CC4E560040C9F8 /* SystemConfiguration.framework */; }; 32 | 27CB051C16CC4E5B0040C9F8 /* libsqlite3.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 27CB051B16CC4E5B0040C9F8 /* libsqlite3.dylib */; }; 33 | 27CB053916CC4FF60040C9F8 /* bubbleMine.png in Resources */ = {isa = PBXBuildFile; fileRef = 27CB052F16CC4FF60040C9F8 /* bubbleMine.png */; }; 34 | 27CB053A16CC4FF60040C9F8 /* bubbleMine@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 27CB053016CC4FF60040C9F8 /* bubbleMine@2x.png */; }; 35 | 27CB053B16CC4FF60040C9F8 /* bubbleSomeone.png in Resources */ = {isa = PBXBuildFile; fileRef = 27CB053116CC4FF60040C9F8 /* bubbleSomeone.png */; }; 36 | 27CB053C16CC4FF60040C9F8 /* bubbleSomeone@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 27CB053216CC4FF60040C9F8 /* bubbleSomeone@2x.png */; }; 37 | 27CB053D16CC4FF60040C9F8 /* missingAvatar.png in Resources */ = {isa = PBXBuildFile; fileRef = 27CB053316CC4FF60040C9F8 /* missingAvatar.png */; }; 38 | 27CB053E16CC4FF60040C9F8 /* missingAvatar@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 27CB053416CC4FF60040C9F8 /* missingAvatar@2x.png */; }; 39 | 27CB053F16CC4FF60040C9F8 /* typingMine.png in Resources */ = {isa = PBXBuildFile; fileRef = 27CB053516CC4FF60040C9F8 /* typingMine.png */; }; 40 | 27CB054016CC4FF60040C9F8 /* typingMine@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 27CB053616CC4FF60040C9F8 /* typingMine@2x.png */; }; 41 | 27CB054116CC4FF60040C9F8 /* typingSomeone.png in Resources */ = {isa = PBXBuildFile; fileRef = 27CB053716CC4FF60040C9F8 /* typingSomeone.png */; }; 42 | 27CB054216CC4FF60040C9F8 /* typingSomeone@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 27CB053816CC4FF60040C9F8 /* typingSomeone@2x.png */; }; 43 | 27CB054716CC75400040C9F8 /* Camera.png in Resources */ = {isa = PBXBuildFile; fileRef = 27CB054516CC75400040C9F8 /* Camera.png */; }; 44 | 27CB054816CC75400040C9F8 /* double_lined.png in Resources */ = {isa = PBXBuildFile; fileRef = 27CB054616CC75400040C9F8 /* double_lined.png */; }; 45 | 27CB054B16CD764B0040C9F8 /* ChatIcon.png in Resources */ = {isa = PBXBuildFile; fileRef = 27CB054916CD764B0040C9F8 /* ChatIcon.png */; }; 46 | 27CB054C16CD764B0040C9F8 /* ChatIcon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 27CB054A16CD764B0040C9F8 /* ChatIcon@2x.png */; }; 47 | 27CB055616CD81700040C9F8 /* PersonaController+UIKit.m in Sources */ = {isa = PBXBuildFile; fileRef = 27CB055116CD81700040C9F8 /* PersonaController+UIKit.m */; }; 48 | 27CB055716CD81700040C9F8 /* PersonaController.js in Resources */ = {isa = PBXBuildFile; fileRef = 27CB055316CD81700040C9F8 /* PersonaController.js */; }; 49 | 27CB055816CD81700040C9F8 /* PersonaController.m in Sources */ = {isa = PBXBuildFile; fileRef = 27CB055416CD81700040C9F8 /* PersonaController.m */; }; 50 | 27CB055B16CD82840040C9F8 /* SyncManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 27CB055A16CD82840040C9F8 /* SyncManager.m */; }; 51 | 27CB055E16CD82A30040C9F8 /* LoginController.m in Sources */ = {isa = PBXBuildFile; fileRef = 27CB055D16CD82980040C9F8 /* LoginController.m */; }; 52 | 27CB056116CDCBCD0040C9F8 /* CouchbaseLite.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27CB056016CDCBCD0040C9F8 /* CouchbaseLite.framework */; }; 53 | 27CB056F16CEEB7C0040C9F8 /* UserProfile.m in Sources */ = {isa = PBXBuildFile; fileRef = 27CB056E16CEEB7C0040C9F8 /* UserProfile.m */; }; 54 | 27F41BA616E51C3D005572BC /* THBubbleColor.m in Sources */ = {isa = PBXBuildFile; fileRef = 27F41B9716E51C3D005572BC /* THBubbleColor.m */; }; 55 | 27F41BA716E51C3D005572BC /* THContactBubble.m in Sources */ = {isa = PBXBuildFile; fileRef = 27F41B9916E51C3D005572BC /* THContactBubble.m */; }; 56 | 27F41BA816E51C3D005572BC /* THContactPickerView.m in Sources */ = {isa = PBXBuildFile; fileRef = 27F41B9B16E51C3D005572BC /* THContactPickerView.m */; }; 57 | 27F41BA916E51C3D005572BC /* THContactPickerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 27F41B9D16E51C3D005572BC /* THContactPickerViewController.m */; }; 58 | 27F41BAC16E51CDB005572BC /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27F41BAB16E51CDA005572BC /* QuartzCore.framework */; }; 59 | 27F41BAD16E51CEE005572BC /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27CB04DF16CC4B420040C9F8 /* CoreGraphics.framework */; }; 60 | 27F41BB016E522CC005572BC /* UserPickerController.m in Sources */ = {isa = PBXBuildFile; fileRef = 27F41BAF16E522CC005572BC /* UserPickerController.m */; }; 61 | 27F41BBC16E55A18005572BC /* BTVBubbleData.m in Sources */ = {isa = PBXBuildFile; fileRef = 27F41BB216E55A17005572BC /* BTVBubbleData.m */; }; 62 | 27F41BBD16E55A18005572BC /* BTVBubbleHeaderTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 27F41BB416E55A17005572BC /* BTVBubbleHeaderTableViewCell.m */; }; 63 | 27F41BBE16E55A18005572BC /* BTVBubbleTableView.m in Sources */ = {isa = PBXBuildFile; fileRef = 27F41BB616E55A17005572BC /* BTVBubbleTableView.m */; }; 64 | 27F41BBF16E55A18005572BC /* BTVBubbleTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 27F41BB816E55A18005572BC /* BTVBubbleTableViewCell.m */; }; 65 | 27F41BC016E55A18005572BC /* BTVBubbleTypingTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 27F41BBB16E55A18005572BC /* BTVBubbleTypingTableViewCell.m */; }; 66 | 27F41BC516E7C05E005572BC /* THContactPickerViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 27F41BC416E7C05D005572BC /* THContactPickerViewController.xib */; }; 67 | /* End PBXBuildFile section */ 68 | 69 | /* Begin PBXFileReference section */ 70 | 279B32461721F458004F06A7 /* iPhone icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "iPhone icon.png"; path = "../iPhone icon.png"; sourceTree = ""; }; 71 | 279B32481721F45E004F06A7 /* iPhone icon@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "iPhone icon@2x.png"; path = "../iPhone icon@2x.png"; sourceTree = ""; }; 72 | 279B324A1721F74D004F06A7 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = text; path = README.md; sourceTree = ""; }; 73 | 27CB04D816CC4B420040C9F8 /* CouchChat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CouchChat.app; sourceTree = BUILT_PRODUCTS_DIR; }; 74 | 27CB04DB16CC4B420040C9F8 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; 75 | 27CB04DD16CC4B420040C9F8 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 76 | 27CB04DF16CC4B420040C9F8 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; 77 | 27CB04E316CC4B420040C9F8 /* CouchChat-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "CouchChat-Info.plist"; sourceTree = ""; }; 78 | 27CB04E516CC4B420040C9F8 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 79 | 27CB04E716CC4B420040C9F8 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 80 | 27CB04E916CC4B420040C9F8 /* CouchChat-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CouchChat-Prefix.pch"; sourceTree = ""; }; 81 | 27CB04EA16CC4B420040C9F8 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 82 | 27CB04EB16CC4B420040C9F8 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 83 | 27CB04ED16CC4B420040C9F8 /* Default.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Default.png; sourceTree = ""; }; 84 | 27CB04EF16CC4B420040C9F8 /* Default@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default@2x.png"; sourceTree = ""; }; 85 | 27CB04F116CC4B420040C9F8 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-568h@2x.png"; sourceTree = ""; }; 86 | 27CB04F316CC4B420040C9F8 /* ChatListController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ChatListController.h; sourceTree = ""; }; 87 | 27CB04F416CC4B420040C9F8 /* ChatListController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ChatListController.m; sourceTree = ""; }; 88 | 27CB04F616CC4B420040C9F8 /* ChatController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ChatController.h; sourceTree = ""; }; 89 | 27CB04F716CC4B420040C9F8 /* ChatController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ChatController.m; sourceTree = ""; }; 90 | 27CB04FA16CC4B420040C9F8 /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/ChatListController_iPhone.xib; sourceTree = ""; }; 91 | 27CB04FD16CC4B420040C9F8 /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/ChatListController_iPad.xib; sourceTree = ""; }; 92 | 27CB050016CC4B420040C9F8 /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/ChatController_iPhone.xib; sourceTree = ""; }; 93 | 27CB050316CC4B420040C9F8 /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/ChatController_iPad.xib; sourceTree = ""; }; 94 | 27CB050C16CC4C8B0040C9F8 /* ChatStore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ChatStore.h; sourceTree = ""; }; 95 | 27CB050D16CC4C8B0040C9F8 /* ChatStore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ChatStore.m; sourceTree = ""; }; 96 | 27CB050E16CC4C8B0040C9F8 /* ChatRoom.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ChatRoom.h; sourceTree = ""; }; 97 | 27CB050F16CC4C8B0040C9F8 /* ChatRoom.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ChatRoom.m; sourceTree = ""; }; 98 | 27CB051316CC4E400040C9F8 /* CFNetwork.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CFNetwork.framework; path = System/Library/Frameworks/CFNetwork.framework; sourceTree = SDKROOT; }; 99 | 27CB051516CC4E460040C9F8 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; 100 | 27CB051716CC4E4C0040C9F8 /* libz.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libz.dylib; path = usr/lib/libz.dylib; sourceTree = SDKROOT; }; 101 | 27CB051916CC4E560040C9F8 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; 102 | 27CB051B16CC4E5B0040C9F8 /* libsqlite3.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libsqlite3.dylib; path = usr/lib/libsqlite3.dylib; sourceTree = SDKROOT; }; 103 | 27CB052F16CC4FF60040C9F8 /* bubbleMine.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = bubbleMine.png; sourceTree = ""; }; 104 | 27CB053016CC4FF60040C9F8 /* bubbleMine@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "bubbleMine@2x.png"; sourceTree = ""; }; 105 | 27CB053116CC4FF60040C9F8 /* bubbleSomeone.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = bubbleSomeone.png; sourceTree = ""; }; 106 | 27CB053216CC4FF60040C9F8 /* bubbleSomeone@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "bubbleSomeone@2x.png"; sourceTree = ""; }; 107 | 27CB053316CC4FF60040C9F8 /* missingAvatar.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = missingAvatar.png; sourceTree = ""; }; 108 | 27CB053416CC4FF60040C9F8 /* missingAvatar@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "missingAvatar@2x.png"; sourceTree = ""; }; 109 | 27CB053516CC4FF60040C9F8 /* typingMine.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = typingMine.png; sourceTree = ""; }; 110 | 27CB053616CC4FF60040C9F8 /* typingMine@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "typingMine@2x.png"; sourceTree = ""; }; 111 | 27CB053716CC4FF60040C9F8 /* typingSomeone.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = typingSomeone.png; sourceTree = ""; }; 112 | 27CB053816CC4FF60040C9F8 /* typingSomeone@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "typingSomeone@2x.png"; sourceTree = ""; }; 113 | 27CB054516CC75400040C9F8 /* Camera.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Camera.png; sourceTree = ""; }; 114 | 27CB054616CC75400040C9F8 /* double_lined.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = double_lined.png; sourceTree = ""; }; 115 | 27CB054916CD764B0040C9F8 /* ChatIcon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = ChatIcon.png; sourceTree = ""; }; 116 | 27CB054A16CD764B0040C9F8 /* ChatIcon@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "ChatIcon@2x.png"; sourceTree = ""; }; 117 | 27CB055016CD81700040C9F8 /* PersonaController+UIKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "PersonaController+UIKit.h"; sourceTree = ""; }; 118 | 27CB055116CD81700040C9F8 /* PersonaController+UIKit.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "PersonaController+UIKit.m"; sourceTree = ""; }; 119 | 27CB055216CD81700040C9F8 /* PersonaController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PersonaController.h; sourceTree = ""; }; 120 | 27CB055316CD81700040C9F8 /* PersonaController.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = PersonaController.js; sourceTree = ""; }; 121 | 27CB055416CD81700040C9F8 /* PersonaController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PersonaController.m; sourceTree = ""; }; 122 | 27CB055916CD82840040C9F8 /* SyncManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SyncManager.h; sourceTree = ""; }; 123 | 27CB055A16CD82840040C9F8 /* SyncManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SyncManager.m; sourceTree = ""; }; 124 | 27CB055C16CD82980040C9F8 /* LoginController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LoginController.h; sourceTree = ""; }; 125 | 27CB055D16CD82980040C9F8 /* LoginController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LoginController.m; sourceTree = ""; }; 126 | 27CB056016CDCBCD0040C9F8 /* CouchbaseLite.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CouchbaseLite.framework; path = Frameworks/CouchbaseLite.framework; sourceTree = ""; }; 127 | 27CB056D16CEEB7C0040C9F8 /* UserProfile.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UserProfile.h; sourceTree = ""; }; 128 | 27CB056E16CEEB7C0040C9F8 /* UserProfile.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UserProfile.m; sourceTree = ""; }; 129 | 27F41B8816E164E2005572BC /* UserProfile_Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UserProfile_Private.h; sourceTree = ""; }; 130 | 27F41B9616E51C3D005572BC /* THBubbleColor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = THBubbleColor.h; sourceTree = ""; }; 131 | 27F41B9716E51C3D005572BC /* THBubbleColor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = THBubbleColor.m; sourceTree = ""; }; 132 | 27F41B9816E51C3D005572BC /* THContactBubble.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = THContactBubble.h; sourceTree = ""; }; 133 | 27F41B9916E51C3D005572BC /* THContactBubble.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = THContactBubble.m; sourceTree = ""; }; 134 | 27F41B9A16E51C3D005572BC /* THContactPickerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = THContactPickerView.h; sourceTree = ""; }; 135 | 27F41B9B16E51C3D005572BC /* THContactPickerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = THContactPickerView.m; sourceTree = ""; }; 136 | 27F41B9C16E51C3D005572BC /* THContactPickerViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = THContactPickerViewController.h; sourceTree = ""; }; 137 | 27F41B9D16E51C3D005572BC /* THContactPickerViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = THContactPickerViewController.m; sourceTree = ""; }; 138 | 27F41BAB16E51CDA005572BC /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; 139 | 27F41BAE16E522CC005572BC /* UserPickerController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UserPickerController.h; sourceTree = ""; }; 140 | 27F41BAF16E522CC005572BC /* UserPickerController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UserPickerController.m; sourceTree = ""; }; 141 | 27F41BB116E55A17005572BC /* BTVBubbleData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BTVBubbleData.h; sourceTree = ""; }; 142 | 27F41BB216E55A17005572BC /* BTVBubbleData.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BTVBubbleData.m; sourceTree = ""; }; 143 | 27F41BB316E55A17005572BC /* BTVBubbleHeaderTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BTVBubbleHeaderTableViewCell.h; sourceTree = ""; }; 144 | 27F41BB416E55A17005572BC /* BTVBubbleHeaderTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BTVBubbleHeaderTableViewCell.m; sourceTree = ""; }; 145 | 27F41BB516E55A17005572BC /* BTVBubbleTableView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BTVBubbleTableView.h; sourceTree = ""; }; 146 | 27F41BB616E55A17005572BC /* BTVBubbleTableView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BTVBubbleTableView.m; sourceTree = ""; }; 147 | 27F41BB716E55A18005572BC /* BTVBubbleTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BTVBubbleTableViewCell.h; sourceTree = ""; }; 148 | 27F41BB816E55A18005572BC /* BTVBubbleTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BTVBubbleTableViewCell.m; sourceTree = ""; }; 149 | 27F41BB916E55A18005572BC /* BTVBubbleTableViewDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BTVBubbleTableViewDataSource.h; sourceTree = ""; }; 150 | 27F41BBA16E55A18005572BC /* BTVBubbleTypingTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BTVBubbleTypingTableViewCell.h; sourceTree = ""; }; 151 | 27F41BBB16E55A18005572BC /* BTVBubbleTypingTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BTVBubbleTypingTableViewCell.m; sourceTree = ""; }; 152 | 27F41BC416E7C05D005572BC /* THContactPickerViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = THContactPickerViewController.xib; sourceTree = ""; }; 153 | /* End PBXFileReference section */ 154 | 155 | /* Begin PBXFrameworksBuildPhase section */ 156 | 27CB04D516CC4B420040C9F8 /* Frameworks */ = { 157 | isa = PBXFrameworksBuildPhase; 158 | buildActionMask = 2147483647; 159 | files = ( 160 | 27CB056116CDCBCD0040C9F8 /* CouchbaseLite.framework in Frameworks */, 161 | 27CB051C16CC4E5B0040C9F8 /* libsqlite3.dylib in Frameworks */, 162 | 27CB051816CC4E4C0040C9F8 /* libz.dylib in Frameworks */, 163 | 27CB051416CC4E400040C9F8 /* CFNetwork.framework in Frameworks */, 164 | 27CB051616CC4E460040C9F8 /* Security.framework in Frameworks */, 165 | 27CB051A16CC4E560040C9F8 /* SystemConfiguration.framework in Frameworks */, 166 | 27F41BAC16E51CDB005572BC /* QuartzCore.framework in Frameworks */, 167 | 27F41BAD16E51CEE005572BC /* CoreGraphics.framework in Frameworks */, 168 | 27CB04DC16CC4B420040C9F8 /* UIKit.framework in Frameworks */, 169 | 27CB04DE16CC4B420040C9F8 /* Foundation.framework in Frameworks */, 170 | ); 171 | runOnlyForDeploymentPostprocessing = 0; 172 | }; 173 | /* End PBXFrameworksBuildPhase section */ 174 | 175 | /* Begin PBXGroup section */ 176 | 27CB04CF16CC4B420040C9F8 = { 177 | isa = PBXGroup; 178 | children = ( 179 | 279B324A1721F74D004F06A7 /* README.md */, 180 | 27CB04E116CC4B420040C9F8 /* CouchChat */, 181 | 27CB055F16CD82AF0040C9F8 /* Sync UI */, 182 | 27CB051216CC4C900040C9F8 /* Model */, 183 | 27F41B8916E51B90005572BC /* vendor */, 184 | 27CB054316CC74D10040C9F8 /* Resources */, 185 | 27CB04DA16CC4B420040C9F8 /* Frameworks */, 186 | 27CB04D916CC4B420040C9F8 /* Products */, 187 | ); 188 | sourceTree = ""; 189 | }; 190 | 27CB04D916CC4B420040C9F8 /* Products */ = { 191 | isa = PBXGroup; 192 | children = ( 193 | 27CB04D816CC4B420040C9F8 /* CouchChat.app */, 194 | ); 195 | name = Products; 196 | sourceTree = ""; 197 | }; 198 | 27CB04DA16CC4B420040C9F8 /* Frameworks */ = { 199 | isa = PBXGroup; 200 | children = ( 201 | 27F41BAB16E51CDA005572BC /* QuartzCore.framework */, 202 | 27CB056016CDCBCD0040C9F8 /* CouchbaseLite.framework */, 203 | 27CB051B16CC4E5B0040C9F8 /* libsqlite3.dylib */, 204 | 27CB051916CC4E560040C9F8 /* SystemConfiguration.framework */, 205 | 27CB051716CC4E4C0040C9F8 /* libz.dylib */, 206 | 27CB051516CC4E460040C9F8 /* Security.framework */, 207 | 27CB051316CC4E400040C9F8 /* CFNetwork.framework */, 208 | 27CB04DB16CC4B420040C9F8 /* UIKit.framework */, 209 | 27CB04DD16CC4B420040C9F8 /* Foundation.framework */, 210 | 27CB04DF16CC4B420040C9F8 /* CoreGraphics.framework */, 211 | ); 212 | name = Frameworks; 213 | sourceTree = ""; 214 | }; 215 | 27CB04E116CC4B420040C9F8 /* CouchChat */ = { 216 | isa = PBXGroup; 217 | children = ( 218 | 27CB04EA16CC4B420040C9F8 /* AppDelegate.h */, 219 | 27CB04EB16CC4B420040C9F8 /* AppDelegate.m */, 220 | 27CB04F316CC4B420040C9F8 /* ChatListController.h */, 221 | 27CB04F416CC4B420040C9F8 /* ChatListController.m */, 222 | 27CB04F616CC4B420040C9F8 /* ChatController.h */, 223 | 27CB04F716CC4B420040C9F8 /* ChatController.m */, 224 | 27F41BAE16E522CC005572BC /* UserPickerController.h */, 225 | 27F41BAF16E522CC005572BC /* UserPickerController.m */, 226 | 27CB04F916CC4B420040C9F8 /* ChatListController_iPhone.xib */, 227 | 27CB04FC16CC4B420040C9F8 /* ChatListController_iPad.xib */, 228 | 27CB04FF16CC4B420040C9F8 /* ChatController_iPhone.xib */, 229 | 27CB050216CC4B420040C9F8 /* ChatController_iPad.xib */, 230 | 27CB04E216CC4B420040C9F8 /* Supporting Files */, 231 | ); 232 | path = CouchChat; 233 | sourceTree = ""; 234 | }; 235 | 27CB04E216CC4B420040C9F8 /* Supporting Files */ = { 236 | isa = PBXGroup; 237 | children = ( 238 | 27CB04E316CC4B420040C9F8 /* CouchChat-Info.plist */, 239 | 27CB04E416CC4B420040C9F8 /* InfoPlist.strings */, 240 | 27CB04E716CC4B420040C9F8 /* main.m */, 241 | 27CB04E916CC4B420040C9F8 /* CouchChat-Prefix.pch */, 242 | 27CB04ED16CC4B420040C9F8 /* Default.png */, 243 | 27CB04EF16CC4B420040C9F8 /* Default@2x.png */, 244 | 27CB04F116CC4B420040C9F8 /* Default-568h@2x.png */, 245 | 279B32481721F45E004F06A7 /* iPhone icon@2x.png */, 246 | 279B32461721F458004F06A7 /* iPhone icon.png */, 247 | ); 248 | name = "Supporting Files"; 249 | sourceTree = ""; 250 | }; 251 | 27CB051216CC4C900040C9F8 /* Model */ = { 252 | isa = PBXGroup; 253 | children = ( 254 | 27CB050C16CC4C8B0040C9F8 /* ChatStore.h */, 255 | 27CB050D16CC4C8B0040C9F8 /* ChatStore.m */, 256 | 27CB050E16CC4C8B0040C9F8 /* ChatRoom.h */, 257 | 27CB050F16CC4C8B0040C9F8 /* ChatRoom.m */, 258 | 27CB056D16CEEB7C0040C9F8 /* UserProfile.h */, 259 | 27F41B8816E164E2005572BC /* UserProfile_Private.h */, 260 | 27CB056E16CEEB7C0040C9F8 /* UserProfile.m */, 261 | ); 262 | name = Model; 263 | path = CouchChat; 264 | sourceTree = ""; 265 | }; 266 | 27CB051D16CC4FDE0040C9F8 /* UIBubbleTableView */ = { 267 | isa = PBXGroup; 268 | children = ( 269 | 27F41BB116E55A17005572BC /* BTVBubbleData.h */, 270 | 27F41BB216E55A17005572BC /* BTVBubbleData.m */, 271 | 27F41BB316E55A17005572BC /* BTVBubbleHeaderTableViewCell.h */, 272 | 27F41BB416E55A17005572BC /* BTVBubbleHeaderTableViewCell.m */, 273 | 27F41BB516E55A17005572BC /* BTVBubbleTableView.h */, 274 | 27F41BB616E55A17005572BC /* BTVBubbleTableView.m */, 275 | 27F41BB716E55A18005572BC /* BTVBubbleTableViewCell.h */, 276 | 27F41BB816E55A18005572BC /* BTVBubbleTableViewCell.m */, 277 | 27F41BB916E55A18005572BC /* BTVBubbleTableViewDataSource.h */, 278 | 27F41BBA16E55A18005572BC /* BTVBubbleTypingTableViewCell.h */, 279 | 27F41BBB16E55A18005572BC /* BTVBubbleTypingTableViewCell.m */, 280 | 27CB052E16CC4FF60040C9F8 /* images */, 281 | ); 282 | name = UIBubbleTableView; 283 | path = UIBubbleTableView/src; 284 | sourceTree = ""; 285 | }; 286 | 27CB052E16CC4FF60040C9F8 /* images */ = { 287 | isa = PBXGroup; 288 | children = ( 289 | 27CB052F16CC4FF60040C9F8 /* bubbleMine.png */, 290 | 27CB053016CC4FF60040C9F8 /* bubbleMine@2x.png */, 291 | 27CB053116CC4FF60040C9F8 /* bubbleSomeone.png */, 292 | 27CB053216CC4FF60040C9F8 /* bubbleSomeone@2x.png */, 293 | 27CB053316CC4FF60040C9F8 /* missingAvatar.png */, 294 | 27CB053416CC4FF60040C9F8 /* missingAvatar@2x.png */, 295 | 27CB053516CC4FF60040C9F8 /* typingMine.png */, 296 | 27CB053616CC4FF60040C9F8 /* typingMine@2x.png */, 297 | 27CB053716CC4FF60040C9F8 /* typingSomeone.png */, 298 | 27CB053816CC4FF60040C9F8 /* typingSomeone@2x.png */, 299 | ); 300 | name = images; 301 | path = vendor/UIBubbleTableView/images; 302 | sourceTree = SOURCE_ROOT; 303 | }; 304 | 27CB054316CC74D10040C9F8 /* Resources */ = { 305 | isa = PBXGroup; 306 | children = ( 307 | 27CB054916CD764B0040C9F8 /* ChatIcon.png */, 308 | 27CB054A16CD764B0040C9F8 /* ChatIcon@2x.png */, 309 | 27CB054516CC75400040C9F8 /* Camera.png */, 310 | 27CB054616CC75400040C9F8 /* double_lined.png */, 311 | ); 312 | path = Resources; 313 | sourceTree = ""; 314 | }; 315 | 27CB054D16CD81700040C9F8 /* Persona */ = { 316 | isa = PBXGroup; 317 | children = ( 318 | 27CB055016CD81700040C9F8 /* PersonaController+UIKit.h */, 319 | 27CB055116CD81700040C9F8 /* PersonaController+UIKit.m */, 320 | 27CB055216CD81700040C9F8 /* PersonaController.h */, 321 | 27CB055316CD81700040C9F8 /* PersonaController.js */, 322 | 27CB055416CD81700040C9F8 /* PersonaController.m */, 323 | ); 324 | name = Persona; 325 | path = browserid/Sources; 326 | sourceTree = ""; 327 | }; 328 | 27CB055F16CD82AF0040C9F8 /* Sync UI */ = { 329 | isa = PBXGroup; 330 | children = ( 331 | 27CB055916CD82840040C9F8 /* SyncManager.h */, 332 | 27CB055A16CD82840040C9F8 /* SyncManager.m */, 333 | 27CB055C16CD82980040C9F8 /* LoginController.h */, 334 | 27CB055D16CD82980040C9F8 /* LoginController.m */, 335 | ); 336 | name = "Sync UI"; 337 | path = CouchChat; 338 | sourceTree = ""; 339 | }; 340 | 27F41B8916E51B90005572BC /* vendor */ = { 341 | isa = PBXGroup; 342 | children = ( 343 | 27CB054D16CD81700040C9F8 /* Persona */, 344 | 27CB051D16CC4FDE0040C9F8 /* UIBubbleTableView */, 345 | 27F41B8B16E51C3D005572BC /* THContactPicker */, 346 | ); 347 | path = vendor; 348 | sourceTree = ""; 349 | }; 350 | 27F41B8B16E51C3D005572BC /* THContactPicker */ = { 351 | isa = PBXGroup; 352 | children = ( 353 | 27F41B9616E51C3D005572BC /* THBubbleColor.h */, 354 | 27F41B9716E51C3D005572BC /* THBubbleColor.m */, 355 | 27F41B9816E51C3D005572BC /* THContactBubble.h */, 356 | 27F41B9916E51C3D005572BC /* THContactBubble.m */, 357 | 27F41B9A16E51C3D005572BC /* THContactPickerView.h */, 358 | 27F41B9B16E51C3D005572BC /* THContactPickerView.m */, 359 | 27F41B9C16E51C3D005572BC /* THContactPickerViewController.h */, 360 | 27F41B9D16E51C3D005572BC /* THContactPickerViewController.m */, 361 | 27F41BC416E7C05D005572BC /* THContactPickerViewController.xib */, 362 | ); 363 | name = THContactPicker; 364 | path = THContactPicker/ContactPicker; 365 | sourceTree = ""; 366 | }; 367 | /* End PBXGroup section */ 368 | 369 | /* Begin PBXNativeTarget section */ 370 | 27CB04D716CC4B420040C9F8 /* CouchChat */ = { 371 | isa = PBXNativeTarget; 372 | buildConfigurationList = 27CB050716CC4B420040C9F8 /* Build configuration list for PBXNativeTarget "CouchChat" */; 373 | buildPhases = ( 374 | 27CB04D416CC4B420040C9F8 /* Sources */, 375 | 27CB04D516CC4B420040C9F8 /* Frameworks */, 376 | 27CB04D616CC4B420040C9F8 /* Resources */, 377 | ); 378 | buildRules = ( 379 | ); 380 | dependencies = ( 381 | ); 382 | name = CouchChat; 383 | productName = CouchChat; 384 | productReference = 27CB04D816CC4B420040C9F8 /* CouchChat.app */; 385 | productType = "com.apple.product-type.application"; 386 | }; 387 | /* End PBXNativeTarget section */ 388 | 389 | /* Begin PBXProject section */ 390 | 27CB04D016CC4B420040C9F8 /* Project object */ = { 391 | isa = PBXProject; 392 | attributes = { 393 | LastUpgradeCheck = 0460; 394 | ORGANIZATIONNAME = Couchbase; 395 | }; 396 | buildConfigurationList = 27CB04D316CC4B420040C9F8 /* Build configuration list for PBXProject "CouchChat" */; 397 | compatibilityVersion = "Xcode 3.2"; 398 | developmentRegion = English; 399 | hasScannedForEncodings = 0; 400 | knownRegions = ( 401 | en, 402 | ); 403 | mainGroup = 27CB04CF16CC4B420040C9F8; 404 | productRefGroup = 27CB04D916CC4B420040C9F8 /* Products */; 405 | projectDirPath = ""; 406 | projectRoot = ""; 407 | targets = ( 408 | 27CB04D716CC4B420040C9F8 /* CouchChat */, 409 | ); 410 | }; 411 | /* End PBXProject section */ 412 | 413 | /* Begin PBXResourcesBuildPhase section */ 414 | 27CB04D616CC4B420040C9F8 /* Resources */ = { 415 | isa = PBXResourcesBuildPhase; 416 | buildActionMask = 2147483647; 417 | files = ( 418 | 27CB04E616CC4B420040C9F8 /* InfoPlist.strings in Resources */, 419 | 27CB04EE16CC4B420040C9F8 /* Default.png in Resources */, 420 | 27CB04F016CC4B420040C9F8 /* Default@2x.png in Resources */, 421 | 27CB04F216CC4B420040C9F8 /* Default-568h@2x.png in Resources */, 422 | 27CB04FB16CC4B420040C9F8 /* ChatListController_iPhone.xib in Resources */, 423 | 27CB04FE16CC4B420040C9F8 /* ChatListController_iPad.xib in Resources */, 424 | 27CB050116CC4B420040C9F8 /* ChatController_iPhone.xib in Resources */, 425 | 27CB050416CC4B420040C9F8 /* ChatController_iPad.xib in Resources */, 426 | 27CB055716CD81700040C9F8 /* PersonaController.js in Resources */, 427 | 27CB053916CC4FF60040C9F8 /* bubbleMine.png in Resources */, 428 | 27CB053A16CC4FF60040C9F8 /* bubbleMine@2x.png in Resources */, 429 | 27CB053B16CC4FF60040C9F8 /* bubbleSomeone.png in Resources */, 430 | 27CB053C16CC4FF60040C9F8 /* bubbleSomeone@2x.png in Resources */, 431 | 27CB053D16CC4FF60040C9F8 /* missingAvatar.png in Resources */, 432 | 27CB053E16CC4FF60040C9F8 /* missingAvatar@2x.png in Resources */, 433 | 27CB053F16CC4FF60040C9F8 /* typingMine.png in Resources */, 434 | 27CB054016CC4FF60040C9F8 /* typingMine@2x.png in Resources */, 435 | 27CB054116CC4FF60040C9F8 /* typingSomeone.png in Resources */, 436 | 27CB054216CC4FF60040C9F8 /* typingSomeone@2x.png in Resources */, 437 | 27CB054716CC75400040C9F8 /* Camera.png in Resources */, 438 | 27CB054816CC75400040C9F8 /* double_lined.png in Resources */, 439 | 27CB054B16CD764B0040C9F8 /* ChatIcon.png in Resources */, 440 | 27CB054C16CD764B0040C9F8 /* ChatIcon@2x.png in Resources */, 441 | 27F41BC516E7C05E005572BC /* THContactPickerViewController.xib in Resources */, 442 | 279B32471721F458004F06A7 /* iPhone icon.png in Resources */, 443 | 279B32491721F45E004F06A7 /* iPhone icon@2x.png in Resources */, 444 | ); 445 | runOnlyForDeploymentPostprocessing = 0; 446 | }; 447 | /* End PBXResourcesBuildPhase section */ 448 | 449 | /* Begin PBXSourcesBuildPhase section */ 450 | 27CB04D416CC4B420040C9F8 /* Sources */ = { 451 | isa = PBXSourcesBuildPhase; 452 | buildActionMask = 2147483647; 453 | files = ( 454 | 27CB04E816CC4B420040C9F8 /* main.m in Sources */, 455 | 27CB04EC16CC4B420040C9F8 /* AppDelegate.m in Sources */, 456 | 27CB04F516CC4B420040C9F8 /* ChatListController.m in Sources */, 457 | 27CB04F816CC4B420040C9F8 /* ChatController.m in Sources */, 458 | 27CB051016CC4C8B0040C9F8 /* ChatStore.m in Sources */, 459 | 27CB051116CC4C8B0040C9F8 /* ChatRoom.m in Sources */, 460 | 27CB055616CD81700040C9F8 /* PersonaController+UIKit.m in Sources */, 461 | 27CB055816CD81700040C9F8 /* PersonaController.m in Sources */, 462 | 27CB055B16CD82840040C9F8 /* SyncManager.m in Sources */, 463 | 27CB055E16CD82A30040C9F8 /* LoginController.m in Sources */, 464 | 27CB056F16CEEB7C0040C9F8 /* UserProfile.m in Sources */, 465 | 27F41BA616E51C3D005572BC /* THBubbleColor.m in Sources */, 466 | 27F41BA716E51C3D005572BC /* THContactBubble.m in Sources */, 467 | 27F41BA816E51C3D005572BC /* THContactPickerView.m in Sources */, 468 | 27F41BA916E51C3D005572BC /* THContactPickerViewController.m in Sources */, 469 | 27F41BB016E522CC005572BC /* UserPickerController.m in Sources */, 470 | 27F41BBC16E55A18005572BC /* BTVBubbleData.m in Sources */, 471 | 27F41BBD16E55A18005572BC /* BTVBubbleHeaderTableViewCell.m in Sources */, 472 | 27F41BBE16E55A18005572BC /* BTVBubbleTableView.m in Sources */, 473 | 27F41BBF16E55A18005572BC /* BTVBubbleTableViewCell.m in Sources */, 474 | 27F41BC016E55A18005572BC /* BTVBubbleTypingTableViewCell.m in Sources */, 475 | ); 476 | runOnlyForDeploymentPostprocessing = 0; 477 | }; 478 | /* End PBXSourcesBuildPhase section */ 479 | 480 | /* Begin PBXVariantGroup section */ 481 | 27CB04E416CC4B420040C9F8 /* InfoPlist.strings */ = { 482 | isa = PBXVariantGroup; 483 | children = ( 484 | 27CB04E516CC4B420040C9F8 /* en */, 485 | ); 486 | name = InfoPlist.strings; 487 | sourceTree = ""; 488 | }; 489 | 27CB04F916CC4B420040C9F8 /* ChatListController_iPhone.xib */ = { 490 | isa = PBXVariantGroup; 491 | children = ( 492 | 27CB04FA16CC4B420040C9F8 /* en */, 493 | ); 494 | name = ChatListController_iPhone.xib; 495 | sourceTree = ""; 496 | }; 497 | 27CB04FC16CC4B420040C9F8 /* ChatListController_iPad.xib */ = { 498 | isa = PBXVariantGroup; 499 | children = ( 500 | 27CB04FD16CC4B420040C9F8 /* en */, 501 | ); 502 | name = ChatListController_iPad.xib; 503 | sourceTree = ""; 504 | }; 505 | 27CB04FF16CC4B420040C9F8 /* ChatController_iPhone.xib */ = { 506 | isa = PBXVariantGroup; 507 | children = ( 508 | 27CB050016CC4B420040C9F8 /* en */, 509 | ); 510 | name = ChatController_iPhone.xib; 511 | sourceTree = ""; 512 | }; 513 | 27CB050216CC4B420040C9F8 /* ChatController_iPad.xib */ = { 514 | isa = PBXVariantGroup; 515 | children = ( 516 | 27CB050316CC4B420040C9F8 /* en */, 517 | ); 518 | name = ChatController_iPad.xib; 519 | sourceTree = ""; 520 | }; 521 | /* End PBXVariantGroup section */ 522 | 523 | /* Begin XCBuildConfiguration section */ 524 | 27CB050516CC4B420040C9F8 /* Debug */ = { 525 | isa = XCBuildConfiguration; 526 | buildSettings = { 527 | ALWAYS_SEARCH_USER_PATHS = NO; 528 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 529 | CLANG_CXX_LIBRARY = "libc++"; 530 | CLANG_ENABLE_OBJC_ARC = YES; 531 | CLANG_WARN_CONSTANT_CONVERSION = YES; 532 | CLANG_WARN_EMPTY_BODY = YES; 533 | CLANG_WARN_ENUM_CONVERSION = YES; 534 | CLANG_WARN_INT_CONVERSION = YES; 535 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 536 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 537 | COPY_PHASE_STRIP = NO; 538 | GCC_C_LANGUAGE_STANDARD = gnu99; 539 | GCC_DYNAMIC_NO_PIC = NO; 540 | GCC_OPTIMIZATION_LEVEL = 0; 541 | GCC_PREPROCESSOR_DEFINITIONS = ( 542 | "DEBUG=1", 543 | "$(inherited)", 544 | ); 545 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 546 | GCC_TREAT_WARNINGS_AS_ERRORS = YES; 547 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 548 | GCC_WARN_UNDECLARED_SELECTOR = YES; 549 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 550 | GCC_WARN_UNUSED_VARIABLE = YES; 551 | IPHONEOS_DEPLOYMENT_TARGET = 6.1; 552 | ONLY_ACTIVE_ARCH = YES; 553 | SDKROOT = iphoneos; 554 | TARGETED_DEVICE_FAMILY = "1,2"; 555 | WARNING_CFLAGS = ( 556 | "-Wall", 557 | "-Wformat-security", 558 | "-Wshorten-64-to-32", 559 | "-Wmissing-declarations", 560 | "-Woverriding-method-mismatch", 561 | "-Wbool-conversion", 562 | ); 563 | }; 564 | name = Debug; 565 | }; 566 | 27CB050616CC4B420040C9F8 /* Release */ = { 567 | isa = XCBuildConfiguration; 568 | buildSettings = { 569 | ALWAYS_SEARCH_USER_PATHS = NO; 570 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 571 | CLANG_CXX_LIBRARY = "libc++"; 572 | CLANG_ENABLE_OBJC_ARC = YES; 573 | CLANG_WARN_CONSTANT_CONVERSION = YES; 574 | CLANG_WARN_EMPTY_BODY = YES; 575 | CLANG_WARN_ENUM_CONVERSION = YES; 576 | CLANG_WARN_INT_CONVERSION = YES; 577 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 578 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 579 | COPY_PHASE_STRIP = YES; 580 | GCC_C_LANGUAGE_STANDARD = gnu99; 581 | GCC_TREAT_WARNINGS_AS_ERRORS = YES; 582 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 583 | GCC_WARN_UNDECLARED_SELECTOR = YES; 584 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 585 | GCC_WARN_UNUSED_VARIABLE = YES; 586 | IPHONEOS_DEPLOYMENT_TARGET = 6.1; 587 | OTHER_CFLAGS = "-DNS_BLOCK_ASSERTIONS=1"; 588 | SDKROOT = iphoneos; 589 | TARGETED_DEVICE_FAMILY = "1,2"; 590 | VALIDATE_PRODUCT = YES; 591 | WARNING_CFLAGS = ( 592 | "-Wall", 593 | "-Wformat-security", 594 | "-Wshorten-64-to-32", 595 | "-Wmissing-declarations", 596 | "-Woverriding-method-mismatch", 597 | "-Wbool-conversion", 598 | ); 599 | }; 600 | name = Release; 601 | }; 602 | 27CB050816CC4B420040C9F8 /* Debug */ = { 603 | isa = XCBuildConfiguration; 604 | buildSettings = { 605 | FRAMEWORK_SEARCH_PATHS = ( 606 | "$(inherited)", 607 | "\"$(SRCROOT)\"", 608 | "\"$(SRCROOT)/Frameworks\"", 609 | ); 610 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 611 | GCC_PREFIX_HEADER = "CouchChat/CouchChat-Prefix.pch"; 612 | INFOPLIST_FILE = "CouchChat/CouchChat-Info.plist"; 613 | IPHONEOS_DEPLOYMENT_TARGET = 6.0; 614 | OTHER_LDFLAGS = "-ObjC"; 615 | PRODUCT_NAME = "$(TARGET_NAME)"; 616 | TARGETED_DEVICE_FAMILY = 1; 617 | WRAPPER_EXTENSION = app; 618 | }; 619 | name = Debug; 620 | }; 621 | 27CB050916CC4B420040C9F8 /* Release */ = { 622 | isa = XCBuildConfiguration; 623 | buildSettings = { 624 | FRAMEWORK_SEARCH_PATHS = ( 625 | "$(inherited)", 626 | "\"$(SRCROOT)\"", 627 | "\"$(SRCROOT)/Frameworks\"", 628 | ); 629 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 630 | GCC_PREFIX_HEADER = "CouchChat/CouchChat-Prefix.pch"; 631 | INFOPLIST_FILE = "CouchChat/CouchChat-Info.plist"; 632 | IPHONEOS_DEPLOYMENT_TARGET = 6.0; 633 | OTHER_LDFLAGS = "-ObjC"; 634 | PRODUCT_NAME = "$(TARGET_NAME)"; 635 | TARGETED_DEVICE_FAMILY = 1; 636 | WRAPPER_EXTENSION = app; 637 | }; 638 | name = Release; 639 | }; 640 | /* End XCBuildConfiguration section */ 641 | 642 | /* Begin XCConfigurationList section */ 643 | 27CB04D316CC4B420040C9F8 /* Build configuration list for PBXProject "CouchChat" */ = { 644 | isa = XCConfigurationList; 645 | buildConfigurations = ( 646 | 27CB050516CC4B420040C9F8 /* Debug */, 647 | 27CB050616CC4B420040C9F8 /* Release */, 648 | ); 649 | defaultConfigurationIsVisible = 0; 650 | defaultConfigurationName = Release; 651 | }; 652 | 27CB050716CC4B420040C9F8 /* Build configuration list for PBXNativeTarget "CouchChat" */ = { 653 | isa = XCConfigurationList; 654 | buildConfigurations = ( 655 | 27CB050816CC4B420040C9F8 /* Debug */, 656 | 27CB050916CC4B420040C9F8 /* Release */, 657 | ); 658 | defaultConfigurationIsVisible = 0; 659 | defaultConfigurationName = Release; 660 | }; 661 | /* End XCConfigurationList section */ 662 | }; 663 | rootObject = 27CB04D016CC4B420040C9F8 /* Project object */; 664 | } 665 | -------------------------------------------------------------------------------- /CouchChat.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CouchChat/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // CouchChat 4 | // 5 | // Created by Jens Alfke on 2/13/13. 6 | // Copyright (c) 2013 Couchbase. All rights reserved. 7 | // 8 | 9 | #import 10 | @class ChatStore; 11 | 12 | @interface AppDelegate : UIResponder 13 | 14 | @property (strong, nonatomic) UIWindow *window; 15 | 16 | @property (strong, nonatomic) UINavigationController *navigationController; 17 | 18 | @property (strong, nonatomic) UISplitViewController *splitViewController; 19 | 20 | @property (readonly) ChatStore* chatStore; 21 | 22 | - (void)showAlert: (NSString*)message error: (NSError*)error fatal: (BOOL)fatal; 23 | 24 | @end 25 | 26 | 27 | extern AppDelegate* gAppDelegate; 28 | -------------------------------------------------------------------------------- /CouchChat/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // CouchChat 4 | // 5 | // Created by Jens Alfke on 2/13/13. 6 | // Copyright (c) 2013 Couchbase. All rights reserved. 7 | // 8 | 9 | #import "AppDelegate.h" 10 | #import "ChatListController.h" 11 | #import "ChatController.h" 12 | #import "ChatStore.h" 13 | #import "UserProfile.h" 14 | #import "SyncManager.h" 15 | #import "PersonaController+UIKit.h" 16 | #import 17 | 18 | 19 | // URL of the server-side database; you will need to change the hostname/port to your server. 20 | #define kServerDBURLString @"http://jens.local:4984/chat" 21 | 22 | 23 | AppDelegate* gAppDelegate; 24 | 25 | 26 | @interface AppDelegate () 27 | @end 28 | 29 | 30 | @implementation AppDelegate 31 | { 32 | CBLDatabase* _database; 33 | SyncManager* _syncManager; 34 | PersonaController* _personaController; 35 | } 36 | 37 | 38 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 39 | { 40 | gAppDelegate = self; 41 | self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; 42 | 43 | // Initialize CouchbaseLite: 44 | NSError* error; 45 | _database = [[CBLManager sharedInstance] databaseNamed: @"chat" error: &error]; 46 | if (!_database) 47 | [self showAlert: @"Couldn't open database" error: error fatal: YES]; 48 | 49 | _chatStore = [[ChatStore alloc] initWithDatabase: _database]; 50 | 51 | if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) { 52 | // iPhone UI: 53 | ChatListController *listController = [[ChatListController alloc] initWithNibName:@"ChatListController_iPhone" bundle:nil]; 54 | self.navigationController = [[UINavigationController alloc] initWithRootViewController:listController]; 55 | self.window.rootViewController = self.navigationController; 56 | } else { 57 | // iPad UI: 58 | ChatListController *listController = [[ChatListController alloc] initWithNibName:@"ChatListController_iPad" bundle:nil]; 59 | UINavigationController *masterNavigationController = [[UINavigationController alloc] initWithRootViewController:listController]; 60 | 61 | ChatController *chatController = [[ChatController alloc] initWithNibName:@"ChatController_iPad" bundle:nil]; 62 | UINavigationController *detailNavigationController = [[UINavigationController alloc] initWithRootViewController:chatController]; 63 | self.navigationController = detailNavigationController; 64 | 65 | listController.chatController = chatController; 66 | 67 | self.splitViewController = [[UISplitViewController alloc] init]; 68 | self.splitViewController.delegate = chatController; 69 | self.splitViewController.viewControllers = @[masterNavigationController, detailNavigationController]; 70 | 71 | self.window.rootViewController = self.splitViewController; 72 | } 73 | [self.window makeKeyAndVisible]; 74 | 75 | [self setupSync]; 76 | 77 | return YES; 78 | } 79 | 80 | 81 | #pragma mark - SYNC & LOGIN: 82 | 83 | 84 | - (void) setupSync { 85 | _syncManager = [[SyncManager alloc] initWithDatabase: _database]; 86 | _syncManager.delegate = self; 87 | // Configure replication: 88 | _syncManager.continuous = YES; 89 | _syncManager.syncURL = [NSURL URLWithString: kServerDBURLString]; 90 | } 91 | 92 | 93 | - (void) syncManagerProgressChanged: (SyncManager*)manager { 94 | } 95 | 96 | 97 | - (bool) syncManagerShouldPromptForLogin: (SyncManager*)manager { 98 | // Display Persona login panel, not the default username/password one: 99 | if (!_personaController) { 100 | _personaController = [[PersonaController alloc] init]; 101 | NSArray* replications = _syncManager.replications; 102 | if (replications.count > 0) 103 | _personaController.origin = [replications[0] personaOrigin]; 104 | _personaController.delegate = self; 105 | [_personaController presentModalInController: self.navigationController]; 106 | } 107 | return false; 108 | } 109 | 110 | 111 | - (void) personaControllerDidCancel: (PersonaController*) personaController { 112 | [_personaController.viewController dismissViewControllerAnimated: YES completion: NULL]; 113 | _personaController = nil; 114 | } 115 | 116 | - (void) personaController: (PersonaController*) personaController 117 | didFailWithReason: (NSString*) reason 118 | { 119 | [self personaControllerDidCancel: personaController]; 120 | } 121 | 122 | - (void) personaController: (PersonaController*) personaController 123 | didSucceedWithAssertion: (NSString*) assertion 124 | { 125 | NSLog(@"Chat username = '%@'", personaController.emailAddress); 126 | _chatStore.username = personaController.emailAddress; 127 | 128 | [self personaControllerDidCancel: personaController]; 129 | for (CBLReplication* repl in _syncManager.replications) { 130 | repl.authenticator = [CBLAuthenticator personaAuthenticatorWithAssertion: assertion]; 131 | [repl restart]; 132 | } 133 | } 134 | 135 | 136 | #pragma mark - ALERT: 137 | 138 | 139 | // Display an error alert, without blocking. 140 | // If 'fatal' is true, the app will quit when it's pressed. 141 | - (void)showAlert: (NSString*)message error: (NSError*)error fatal: (BOOL)fatal { 142 | if (error) { 143 | message = [NSString stringWithFormat: @"%@\n\n%@", message, error.localizedDescription]; 144 | } 145 | UIAlertView* alert = [[UIAlertView alloc] initWithTitle: (fatal ? @"Fatal Error" : @"Error") 146 | message: message 147 | delegate: (fatal ? self : nil) 148 | cancelButtonTitle: (fatal ? @"Quit" : @"Sorry") 149 | otherButtonTitles: nil]; 150 | [alert show]; 151 | } 152 | 153 | - (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex { 154 | exit(0); 155 | } 156 | 157 | 158 | @end 159 | -------------------------------------------------------------------------------- /CouchChat/ChatController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ChatController.h 3 | // CouchChat 4 | // 5 | // Created by Jens Alfke on 2/13/13. 6 | // Copyright (c) 2013 Couchbase. All rights reserved. 7 | // 8 | 9 | #import 10 | @class ChatRoom; 11 | 12 | @interface ChatController : UIViewController 13 | 14 | @property (strong, nonatomic) ChatRoom* chatRoom; 15 | 16 | @property (weak, nonatomic) IBOutlet UILabel *detailDescriptionLabel; 17 | 18 | - (IBAction) addPicture:(id)sender; 19 | - (IBAction) configureSync; 20 | - (IBAction) addUsers; 21 | 22 | @end 23 | -------------------------------------------------------------------------------- /CouchChat/ChatController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ChatController.m 3 | // CouchChat 4 | // 5 | // Created by Jens Alfke on 2/13/13. 6 | // Copyright (c) 2013 Couchbase. All rights reserved. 7 | // 8 | 9 | #import "ChatController.h" 10 | #import "ChatRoom.h" 11 | #import "ChatStore.h" 12 | #import "UserProfile.h" 13 | #import "UserPickerController.h" 14 | #import "BTVBubbleTableView.h" 15 | #import 16 | 17 | 18 | #define kMaxPicturePixelDimensions 800 19 | 20 | 21 | @interface ChatController () 24 | @property (strong, nonatomic) UIPopoverController *masterPopoverController; 25 | @end 26 | 27 | 28 | @implementation ChatController 29 | { 30 | NSString* _username; 31 | NSArray* _rows; 32 | ChatStore* _chatStore; 33 | ChatRoom* _chatRoom; 34 | CBLLiveQuery* _query; 35 | IBOutlet BTVBubbleTableView* _bubbles; 36 | IBOutlet UITextField* _inputLine; 37 | UIButton* _pickerButton; 38 | UIPopoverController* _imagePickerPopover; 39 | } 40 | 41 | 42 | - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { 43 | self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; 44 | if (self) { 45 | _chatStore = [ChatStore sharedInstance]; 46 | _username = _chatStore.username; 47 | } 48 | return self; 49 | } 50 | 51 | 52 | - (void)dealloc { 53 | [_query removeObserver: self forKeyPath: @"rows"]; 54 | } 55 | 56 | 57 | - (void)viewDidLoad { 58 | [super viewDidLoad]; 59 | 60 | [[NSNotificationCenter defaultCenter] addObserver: self 61 | selector: @selector(keyboardWillShow:) 62 | name: UIKeyboardWillShowNotification 63 | object: nil]; 64 | [[NSNotificationCenter defaultCenter] addObserver: self 65 | selector: @selector(keyboardWillHide:) 66 | name: UIKeyboardWillHideNotification 67 | object: nil]; 68 | 69 | UIButton* inviteButton = [UIButton buttonWithType: UIButtonTypeContactAdd]; 70 | [inviteButton addTarget: self action: @selector(addUsers) 71 | forControlEvents: UIControlEventTouchUpInside]; 72 | self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView: inviteButton]; 73 | 74 | UIImage* pattern = [UIImage imageNamed: @"double_lined.png"]; 75 | self.view.backgroundColor = [UIColor colorWithPatternImage: pattern]; 76 | 77 | _pickerButton = [UIButton buttonWithType: UIButtonTypeCustom]; 78 | [_pickerButton setImage: [UIImage imageNamed: @"Camera.png"] forState: UIControlStateNormal]; 79 | _pickerButton.frame = CGRectMake(0, 0, 24, 24); 80 | [_pickerButton addTarget: self action: @selector(addPicture:) 81 | forControlEvents: UIControlEventTouchUpInside]; 82 | _inputLine.rightView = _pickerButton; 83 | _inputLine.rightViewMode = UITextFieldViewModeAlways; 84 | 85 | _bubbles.showAvatars = YES; 86 | [_bubbles reloadData]; 87 | [self scrollToBottom]; 88 | 89 | UIGestureRecognizer* g = [[UITapGestureRecognizer alloc] initWithTarget: self action: @selector(bubblesTouched)]; 90 | [_bubbles addGestureRecognizer: g]; 91 | } 92 | 93 | 94 | - (void) viewDidAppear:(BOOL)animated { 95 | [super viewDidAppear: animated]; 96 | [_chatRoom markAsRead]; 97 | } 98 | 99 | 100 | - (void) viewWillDisappear:(BOOL)animated { 101 | [super viewWillDisappear: animated]; 102 | // Bad Stuff happens if the keyboard remains visible while this view is hidden. 103 | [_inputLine resignFirstResponder]; 104 | } 105 | 106 | 107 | - (void)setChatRoom:(ChatRoom*)newChatRoom { 108 | if (_chatRoom != newChatRoom) { 109 | _chatRoom = newChatRoom; 110 | 111 | [_query removeObserver: self forKeyPath: @"rows"]; 112 | _query = _chatRoom.chatMessagesQuery.asLiveQuery; 113 | [_query addObserver: self forKeyPath: @"rows" options: 0 context: NULL]; 114 | [self reloadFromQuery]; 115 | 116 | self.title = newChatRoom ? newChatRoom.displayName : @""; 117 | 118 | if (self.view.superview != nil) 119 | [_chatRoom markAsRead]; 120 | } 121 | 122 | if (self.masterPopoverController != nil) { 123 | [self.masterPopoverController dismissPopoverAnimated:YES]; 124 | } 125 | } 126 | 127 | 128 | - (IBAction) configureSync { 129 | // TODO 130 | } 131 | 132 | 133 | #pragma mark - MESSAGE DISPLAY: 134 | 135 | 136 | - (void) reloadFromQuery { 137 | CBLQueryEnumerator* rowEnum = _query.rows; 138 | if (rowEnum) { 139 | _rows = rowEnum.allObjects; 140 | NSLog(@"ChatController: Showing %lu messages", (unsigned long)_rows.count); 141 | [_bubbles reloadData]; 142 | [self scrollToBottom]; 143 | } 144 | } 145 | 146 | 147 | - (void) scrollToBottom { 148 | CGRect bottom = {{0, 0}, {1, 1}}; 149 | bottom.origin.y =_bubbles.contentSize.height - 1; 150 | [_bubbles scrollRectToVisible: bottom animated: YES]; 151 | } 152 | 153 | 154 | - (void) observeValueForKeyPath: (NSString*)keyPath ofObject: (id)object 155 | change: (NSDictionary*)change context: (void*)context 156 | { 157 | if (object == _query) 158 | [self reloadFromQuery]; 159 | } 160 | 161 | 162 | - (NSInteger) numberOfRowsForBubbleTable: (BTVBubbleTableView *)tableView { 163 | return _rows.count; 164 | } 165 | 166 | - (BTVBubbleData*) bubbleTableView: (BTVBubbleTableView*)tableView dataForRow: (NSInteger)row { 167 | // See map block definition in ChatStore.m 168 | CBLQueryRow* r = _rows[row]; 169 | NSArray* key = r.key; 170 | NSArray* value = r.value; 171 | NSString* sender = value[0]; 172 | NSString* text = value[1]; 173 | bool hasPicture = [value[2] boolValue]; 174 | bool isAnnouncement = [value[3] boolValue]; 175 | NSDate* date = [CBLJSON dateWithJSONObject: key[1]]; 176 | BOOL mine = [sender isEqual: _username]; 177 | 178 | UIImage* image = nil; 179 | if (hasPicture) { 180 | CBLAttachment* att = [r.document.currentRevision attachmentNamed: @"picture"]; 181 | NSData* imageData = att.content; 182 | if (imageData) 183 | image = [[UIImage alloc] initWithData: imageData]; 184 | } 185 | 186 | BTVBubbleData* bubble; 187 | if (image) { 188 | bubble = [BTVBubbleData dataWithImage: image 189 | date: date 190 | type: (mine ? BubbleTypeMine : BubbleTypeSomeoneElse)]; 191 | //FIX: If doc has markdown as well as image, the text won't be shown! 192 | } else { 193 | //FIX: Render the markdown 194 | bubble = [BTVBubbleData dataWithText: text 195 | date: date 196 | type: (mine ? BubbleTypeMine : BubbleTypeSomeoneElse)]; 197 | } 198 | 199 | if (isAnnouncement) 200 | bubble.hasBubble = bubble.hasAvatar = false; 201 | else 202 | bubble.avatar = [_chatStore pictureForUsername: sender]; 203 | 204 | return bubble; 205 | } 206 | 207 | 208 | - (void) bubblesTouched { 209 | [_inputLine resignFirstResponder]; 210 | } 211 | 212 | 213 | #pragma mark - INPUT LINE: 214 | 215 | 216 | - (void) setFrameMaxY: (CGFloat)maxY { 217 | CGRect frame = self.view.frame; 218 | frame.size.height = maxY - frame.origin.y; 219 | self.view.frame = frame; 220 | } 221 | 222 | 223 | - (void) keyboardWillShow: (NSNotification*)n { 224 | CGRect kbdFrame = [(NSValue*)n.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; 225 | kbdFrame = [self.view.window convertRect: kbdFrame fromWindow: nil]; 226 | kbdFrame = [self.view.superview convertRect: kbdFrame fromView: nil]; 227 | [self setFrameMaxY: kbdFrame.origin.y]; 228 | } 229 | 230 | 231 | - (void) keyboardWillHide: (NSNotification*)n { 232 | [self setFrameMaxY: CGRectGetMaxY(self.view.superview.bounds)]; 233 | } 234 | 235 | 236 | - (BOOL)textFieldShouldReturn:(UITextField *)textField { 237 | NSString* message = _inputLine.text; 238 | if (message.length == 0) 239 | return NO; 240 | 241 | if (![_chatRoom addChatMessage: message announcement: false picture: nil]) 242 | return NO; 243 | _inputLine.text = @""; 244 | return YES; 245 | } 246 | 247 | 248 | #pragma mark - IMAGE PICKER: 249 | 250 | 251 | - (IBAction) addPicture:( id)sender { 252 | if ([UIImagePickerController isSourceTypeAvailable: UIImagePickerControllerSourceTypeCamera]) { 253 | NSString* message = @"Take a photo with the camera, or choose an existing photo?"; 254 | UIAlertView* alert = [[UIAlertView alloc] initWithTitle: @"Add Picture" 255 | message: message 256 | delegate: self 257 | cancelButtonTitle: @"Cancel" 258 | otherButtonTitles: @"Use Camera", @"Choose Photo", 259 | nil]; 260 | [alert show]; 261 | } else { 262 | [self pickPictureFromSource: UIImagePickerControllerSourceTypePhotoLibrary]; 263 | } 264 | } 265 | 266 | 267 | - (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex { 268 | switch (buttonIndex) { 269 | case 1: 270 | [self pickPictureFromSource: UIImagePickerControllerSourceTypeCamera]; 271 | break; 272 | case 2: 273 | [self pickPictureFromSource: UIImagePickerControllerSourceTypePhotoLibrary]; 274 | break; 275 | } 276 | } 277 | 278 | 279 | - (void) pickPictureFromSource: (UIImagePickerControllerSourceType)source { 280 | if (![UIImagePickerController isSourceTypeAvailable: source]) 281 | return; 282 | UIImagePickerController *imagePicker = [[UIImagePickerController alloc] init]; 283 | imagePicker.delegate = self; 284 | //imagePicker.allowsEditing = YES; // unfortunately this forces square crop & small dimensions 285 | imagePicker.sourceType = source; 286 | 287 | if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone 288 | || source == UIImagePickerControllerSourceTypeCamera) { 289 | [self presentViewController:imagePicker animated:YES completion: nil]; 290 | } else { 291 | _imagePickerPopover = [[UIPopoverController alloc] initWithContentViewController: imagePicker]; 292 | _imagePickerPopover.delegate = self; 293 | [_imagePickerPopover presentPopoverFromRect: [_pickerButton bounds] 294 | inView: _pickerButton 295 | permittedArrowDirections: UIPopoverArrowDirectionAny 296 | animated: YES]; 297 | } 298 | } 299 | 300 | 301 | - (void)imagePickerController:(UIImagePickerController *)picker 302 | didFinishPickingMediaWithInfo:(NSDictionary *)info 303 | { 304 | UIImage* picture = info[UIImagePickerControllerEditedImage]; 305 | if (!picture) 306 | picture = info[UIImagePickerControllerOriginalImage]; 307 | [self closeImagePicker]; 308 | 309 | if (picture) { 310 | picture = [self scaleImage: picture maxPixels: kMaxPicturePixelDimensions]; 311 | [_chatRoom addChatMessage: nil 312 | announcement: false 313 | picture: picture]; 314 | } 315 | } 316 | 317 | 318 | - (void) closeImagePicker { 319 | if (_imagePickerPopover) 320 | [_imagePickerPopover dismissPopoverAnimated: YES]; 321 | else 322 | [self dismissViewControllerAnimated: YES completion: nil]; 323 | } 324 | 325 | 326 | - (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker { 327 | [self closeImagePicker]; 328 | } 329 | 330 | 331 | - (void)popoverControllerDidDismissPopover:(UIPopoverController *)popoverController { 332 | _imagePickerPopover = nil; 333 | } 334 | 335 | 336 | - (UIImage*) scaleImage: (UIImage*)image maxPixels: (CGFloat)maxPixels { 337 | // Compute the pixel dimensions: 338 | double scale = image.scale; 339 | CGSize pointSize = image.size; 340 | double shrinkFactor = MIN(maxPixels / (pointSize.width * scale), 341 | maxPixels / (pointSize.height * scale)); 342 | if (shrinkFactor >= 1.0) 343 | return image; // no shrinking needed 344 | 345 | scale *= shrinkFactor; 346 | if (scale < 1.0) { 347 | // If scale would drop below 72dpi, we do need to reduce the pixel count. 348 | pointSize.width *= scale; 349 | pointSize.height *= scale; 350 | scale = 1.0; 351 | } 352 | 353 | UIGraphicsBeginImageContextWithOptions(pointSize, YES, scale); 354 | CGRect imageRect = CGRectMake(0.0, 0.0, pointSize.width, pointSize.height); 355 | [image drawInRect:imageRect]; 356 | image = UIGraphicsGetImageFromCurrentImageContext(); 357 | UIGraphicsEndImageContext(); 358 | return image; 359 | } 360 | 361 | 362 | #pragma mark - SPLIT VIEW: 363 | 364 | 365 | - (void)splitViewController:(UISplitViewController *)splitController 366 | willHideViewController:(UIViewController *)viewController 367 | withBarButtonItem:(UIBarButtonItem *)barButtonItem 368 | forPopoverController:(UIPopoverController *)popoverController 369 | { 370 | barButtonItem.title = NSLocalizedString(@"Chats", @"Chats"); 371 | [self.navigationItem setLeftBarButtonItem:barButtonItem animated:YES]; 372 | self.masterPopoverController = popoverController; 373 | } 374 | 375 | - (void)splitViewController:(UISplitViewController *)splitController 376 | willShowViewController:(UIViewController *)viewController 377 | invalidatingBarButtonItem:(UIBarButtonItem *)barButtonItem 378 | { 379 | // Called when the view is shown again in the split view, invalidating the button and popover controller. 380 | [self.navigationItem setLeftBarButtonItem:nil animated:YES]; 381 | self.masterPopoverController = nil; 382 | } 383 | 384 | 385 | #pragma mark - ADDING USERS: 386 | 387 | 388 | - (IBAction) addUsers { 389 | UserPickerController *picker = [[UserPickerController alloc] initWithUsers: _chatStore.allOtherUsers 390 | delegate: self]; 391 | for (UserProfile* user in _chatRoom.allMemberProfiles) 392 | [picker selectContact: user]; 393 | self.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle: @"Cancel" style:UIBarButtonItemStylePlain target:nil action:NULL]; 394 | picker.navigationItem.rightBarButtonItem.title = @"Done"; 395 | picker.title = @"Edit Member List"; 396 | [self.navigationController pushViewController: picker animated: YES]; 397 | } 398 | 399 | 400 | - (void) userPickerController: (UserPickerController*)controller 401 | pickedUsers: (NSArray*)users 402 | { 403 | self.navigationItem.backBarButtonItem = nil; 404 | [self.navigationController popToViewController: self animated: NO]; 405 | if (users == nil ) 406 | return; 407 | 408 | users = [users arrayByAddingObject: _chatStore.user]; 409 | 410 | // Post invite/kick messages: 411 | NSSet* newMembers = [NSSet setWithArray: users]; 412 | NSSet* oldMembers = _chatRoom.allMemberProfiles.set; 413 | [self addMessageForChange: @"invited" forUsers: newMembers notUsers: oldMembers]; 414 | [self addMessageForChange: @"removed" forUsers: oldMembers notUsers: newMembers]; 415 | 416 | // Update the membership: 417 | NSMutableArray* memberNames = [NSMutableArray array]; 418 | for (UserProfile* user in users) 419 | [memberNames addObject: user.username]; 420 | //TODO: This makes everyone an owner. Fix this if we add support for non-owner members. 421 | _chatRoom.owners = memberNames; 422 | _chatRoom.members = [NSArray array]; 423 | 424 | // Changing membership may change the display name of this chat room: 425 | self.title = _chatRoom.displayName; 426 | } 427 | 428 | 429 | - (void) addMessageForChange: (NSString*)change forUsers: (NSSet*)users notUsers: (NSSet*)notUsers { 430 | NSMutableSet* diff = [users mutableCopy]; 431 | [diff minusSet: notUsers]; 432 | if (diff.count == 0) 433 | return; 434 | NSString* message = [NSString stringWithFormat: @"%@ %@ %@.", 435 | _chatStore.user.displayName, change, [UserProfile listOfNames: diff]]; 436 | [_chatRoom addChatMessage: message announcement: true picture: nil]; 437 | } 438 | 439 | 440 | @end 441 | -------------------------------------------------------------------------------- /CouchChat/ChatListController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ChatListController.h 3 | // CouchChat 4 | // 5 | // Created by Jens Alfke on 2/13/13. 6 | // Copyright (c) 2013 Couchbase. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | @class ChatController, ChatStore, ChatRoom; 12 | 13 | 14 | @interface ChatListController : UIViewController 15 | 16 | @property (strong, nonatomic) ChatController *chatController; 17 | 18 | @property (readonly, nonatomic) ChatStore* chatStore; 19 | @property (strong, nonatomic) ChatRoom* chat; 20 | 21 | - (void) createChatWithTitle: (NSString*)title 22 | otherUsers: (NSArray*)otherUsers; 23 | 24 | @end 25 | -------------------------------------------------------------------------------- /CouchChat/ChatListController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ChatListController.m 3 | // CouchChat 4 | // 5 | // Created by Jens Alfke on 2/13/13. 6 | // Copyright (c) 2013 Couchbase. All rights reserved. 7 | // 8 | 9 | #import "ChatListController.h" 10 | #import "ChatController.h" 11 | #import "AppDelegate.h" 12 | #import "ChatStore.h" 13 | #import "ChatRoom.h" 14 | #import "UserProfile.h" 15 | #import "UserPickerController.h" 16 | #import 17 | 18 | 19 | @interface ChatListController () 20 | @end 21 | 22 | 23 | @implementation ChatListController 24 | { 25 | IBOutlet UITableView* _table; 26 | UIBarButtonItem* _newChatButton; 27 | NSArray* _chats; 28 | } 29 | 30 | 31 | - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { 32 | self = [super initWithNibName: nibNameOrNil bundle: nibBundleOrNil]; 33 | if (self) { 34 | _chatStore = [ChatStore sharedInstance]; 35 | self.contentSizeForViewInPopover = CGSizeMake(320.0, 600.0); 36 | self.restorationIdentifier = @"ChatListController"; 37 | } 38 | return self; 39 | } 40 | 41 | - (void) viewDidLoad { 42 | [super viewDidLoad]; 43 | 44 | self.title = @"Chats"; 45 | _newChatButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd 46 | target:self 47 | action:@selector(newChat:)]; 48 | self.navigationItem.rightBarButtonItem = _newChatButton; 49 | 50 | //FIX: This is a workaround until chats can persistently store their unread counts. On launch, 51 | // reset every chat's unread count to 0. That way, only messages received after the app 52 | // launches will appear as unread, instead of _all_ messages ever received. 53 | for (ChatRoom* chat in _chatStore.allChats) 54 | [chat markAsRead]; 55 | 56 | [_chatStore addObserver: self forKeyPath: @"allChats" 57 | options: NSKeyValueObservingOptionInitial context: NULL]; 58 | // Use NSNotification to listen for status changes, because otherwise we'd have to observe 59 | // each item of _chats, but NSArray doesn't support KVO :( 60 | [[NSNotificationCenter defaultCenter] addObserver: self 61 | selector: @selector(chatStatusChanged:) 62 | name: kChatRoomStatusChangedNotification 63 | object: nil]; 64 | [self selectChat: _chatController.chatRoom]; 65 | } 66 | 67 | 68 | - (void) viewWillAppear: (BOOL)animated { 69 | [super viewWillAppear: animated]; 70 | if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) { 71 | NSIndexPath* sel = _table.indexPathForSelectedRow; 72 | if (sel) 73 | [_table deselectRowAtIndexPath: sel animated: NO]; 74 | } 75 | } 76 | 77 | - (void)dealloc { 78 | [_chatStore removeObserver: self forKeyPath: @"allChats"]; 79 | [[NSNotificationCenter defaultCenter] removeObserver: self]; 80 | } 81 | 82 | 83 | #pragma mark - NEW CHAT: 84 | 85 | 86 | - (IBAction) newChat: (id)sender { 87 | if (!_chatStore.username) { 88 | UIAlertView* alert; 89 | alert = [[UIAlertView alloc] initWithTitle: @"Not Logged In" 90 | message: @"Please log in and sync first." 91 | delegate: self 92 | cancelButtonTitle: @"Login" 93 | otherButtonTitles: nil]; 94 | [alert show]; 95 | return; 96 | } 97 | 98 | NSArray* users = _chatStore.allOtherUsers; 99 | UserPickerController *picker = [[UserPickerController alloc] initWithUsers: users 100 | delegate: self]; 101 | [self.navigationController pushViewController: picker animated: YES]; 102 | } 103 | 104 | - (void)alertView:(UIAlertView *)alert didDismissWithButtonIndex:(NSInteger)buttonIndex { 105 | if (buttonIndex >= 0) 106 | [_chatController configureSync]; 107 | } 108 | 109 | - (void) userPickerController: (UserPickerController*)controller 110 | pickedUsers: (NSArray*)users 111 | { 112 | [self.navigationController popToViewController: self animated: NO]; 113 | if (users.count > 0) 114 | [self createChatWithTitle: nil otherUsers: users]; 115 | } 116 | 117 | 118 | - (void) createChatWithTitle: (NSString*)title otherUsers: (NSArray*)otherUsers { 119 | if (title.length == 0) { 120 | NSDateFormatter* fmt = [[NSDateFormatter alloc] init]; 121 | fmt.dateStyle = NSDateFormatterShortStyle; 122 | fmt.timeStyle = NSDateFormatterShortStyle; 123 | title = [fmt stringFromDate: [NSDate date]]; 124 | } 125 | 126 | ChatRoom* chat = [_chatStore newChatWithTitle: title]; 127 | 128 | NSMutableArray* allUsernames = [NSMutableArray arrayWithObject: _chatStore.username]; 129 | NSMutableArray* otherDisplaynames = [NSMutableArray array]; 130 | for (UserProfile* user in otherUsers) { 131 | [allUsernames addObject: user.username]; 132 | [otherDisplaynames addObject: user.displayName]; 133 | } 134 | chat.owners = allUsernames; 135 | 136 | NSError* error; 137 | if (![chat save: &error]) { 138 | [gAppDelegate showAlert: @"Couldn't create chat" error: error fatal: NO]; 139 | } 140 | 141 | NSString* msg = [NSString stringWithFormat: @"%@ started the chat, inviting %@.", 142 | _chatStore.user.displayName, 143 | [UserProfile listOfNames: otherUsers]]; 144 | [chat addChatMessage: msg announcement: true picture: nil]; 145 | 146 | [self showChat: chat]; 147 | } 148 | 149 | 150 | - (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object 151 | change:(NSDictionary *)change context:(void *)context 152 | { 153 | if (object == _chatStore) { 154 | _chats = _chatStore.allChats; 155 | [_table reloadData]; 156 | } else if ([keyPath isEqualToString: @"chatController.chatRoom"]) { 157 | [self selectChat: _chatController.chatRoom]; 158 | } 159 | } 160 | 161 | 162 | - (ChatRoom*) chatForPath: (NSIndexPath*)indexPath { 163 | return _chats[indexPath.row]; 164 | } 165 | 166 | 167 | - (NSIndexPath*) pathForChat: (ChatRoom*)chat { 168 | NSUInteger row = [_chats indexOfObjectIdenticalTo: chat]; 169 | if (row == NSNotFound) 170 | return nil; 171 | return [NSIndexPath indexPathForRow: row inSection: 0]; 172 | } 173 | 174 | 175 | #pragma mark - SELECTION: 176 | 177 | 178 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { 179 | [self showChat: [self chatForPath: indexPath]]; 180 | } 181 | 182 | 183 | - (void) showChat: (ChatRoom*)chat { 184 | if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) { 185 | if (!_chatController) { 186 | self.chatController = [[ChatController alloc] initWithNibName:@"ChatController_iPhone" 187 | bundle:nil]; 188 | } 189 | _chatController.chatRoom = chat; 190 | [self.navigationController pushViewController: _chatController animated: YES]; 191 | } else { 192 | if (chat != _chatController.chatRoom) 193 | _chatController.chatRoom = chat; 194 | } 195 | } 196 | 197 | 198 | - (bool) selectChat: (ChatRoom*)chat { 199 | NSIndexPath* path = [self pathForChat: chat]; 200 | if (!path) 201 | return false; 202 | [_table selectRowAtIndexPath: path 203 | animated: NO 204 | scrollPosition: UITableViewScrollPositionMiddle]; 205 | [self showChat: chat]; 206 | return true; 207 | } 208 | 209 | 210 | #pragma mark - TABLE DISPLAY: 211 | 212 | 213 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { 214 | return _chats.count; 215 | } 216 | 217 | - (UITableViewCell *)tableView:(UITableView *)tableView 218 | cellForRowAtIndexPath:(NSIndexPath *)indexPath 219 | { 220 | ChatRoom* chat = [self chatForPath: indexPath]; 221 | 222 | UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier: @"Chat"]; 223 | if (!cell) { 224 | cell = [[UITableViewCell alloc] initWithStyle: UITableViewCellStyleSubtitle 225 | reuseIdentifier: @"Chat"]; 226 | if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) 227 | cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; 228 | } 229 | [self updateCell: cell forChat: chat]; 230 | return cell; 231 | } 232 | 233 | 234 | 235 | - (void) chatStatusChanged: (NSNotification*)n { 236 | ChatRoom* chat = n.object; 237 | NSIndexPath* path = [self pathForChat: chat]; 238 | if (!path) 239 | return; 240 | [_table reloadData]; 241 | } 242 | 243 | 244 | - (void) updateCells { 245 | for (UITableViewCell* cell in _table.visibleCells) { 246 | ChatRoom* chat = [self chatForPath: [_table indexPathForCell: cell]]; 247 | [self updateCell: cell forChat: chat]; 248 | } 249 | } 250 | 251 | 252 | - (void) updateCell: (UITableViewCell*)cell forChat: (ChatRoom*)chat { 253 | static NSDateFormatter* sDateFormat; 254 | if (!sDateFormat) { 255 | sDateFormat = [[NSDateFormatter alloc] init]; 256 | sDateFormat.dateStyle = NSDateFormatterMediumStyle; 257 | sDateFormat.timeStyle = NSDateFormatterShortStyle; 258 | } 259 | 260 | cell.textLabel.text = chat.displayName; 261 | 262 | NSString* detail = nil; 263 | unsigned unread = chat.unreadMessageCount; 264 | UserProfile* lastSender = chat.lastSender; 265 | if (unread) 266 | detail = [NSString stringWithFormat: @"%u unread; latest by %@", 267 | unread, lastSender.displayName]; 268 | else 269 | detail = [NSString stringWithFormat: @"last updated %@", 270 | [sDateFormat stringFromDate: chat.modDate]]; 271 | cell.detailTextLabel.text = detail; 272 | 273 | UIImage *lastSenderImage = nil; 274 | if (unread > 0 && lastSender) 275 | lastSenderImage = [_chatStore pictureForUsername: lastSender.username]; 276 | 277 | cell.imageView.image = lastSenderImage ?: [UIImage imageNamed: @"ChatIcon"]; 278 | } 279 | 280 | 281 | - (void)tableView:(UITableView *)tableView 282 | willDisplayCell:(UITableViewCell *)cell 283 | forRowAtIndexPath:(NSIndexPath *)indexPath 284 | { 285 | ChatRoom* chat = [self chatForPath: indexPath]; 286 | unsigned unread = chat.unreadMessageCount; 287 | cell.backgroundColor = unread ? [UIColor yellowColor] : [UIColor clearColor]; 288 | } 289 | 290 | 291 | #pragma mark - EDITING: 292 | 293 | 294 | - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { 295 | return YES; 296 | } 297 | 298 | 299 | - (void)tableView:(UITableView *)tableView 300 | commitEditingStyle:(UITableViewCellEditingStyle)editingStyle 301 | forRowAtIndexPath:(NSIndexPath *)indexPath 302 | { 303 | if (editingStyle != UITableViewCellEditingStyleDelete) 304 | return; 305 | 306 | ChatRoom* chat = [self chatForPath: indexPath]; 307 | if (!chat.isMember) 308 | return; 309 | 310 | // Delete the row from the table data source. 311 | [_table deleteRowsAtIndexPaths: @[indexPath] withRowAnimation: UITableViewRowAnimationFade]; 312 | 313 | NSString* msg = [NSString stringWithFormat: @"%@ left the chat.", 314 | _chatStore.user.displayName]; 315 | [chat removeMember: _chatStore.user withMessage: msg]; 316 | } 317 | 318 | 319 | @end 320 | -------------------------------------------------------------------------------- /CouchChat/ChatRoom.h: -------------------------------------------------------------------------------- 1 | // 2 | // Chat.h 3 | // CouchChat 4 | // 5 | // Created by Jens Alfke on 12/14/12. 6 | // Copyright (c) 2012 Couchbase. All rights reserved. 7 | // 8 | 9 | #import 10 | @class ChatStore, UserProfile; 11 | 12 | 13 | /** One chat in the database. */ 14 | @interface ChatRoom : CBLModel 15 | 16 | + (id) chatWithTitle: (NSString*)title 17 | inChatStore: (ChatStore*)chatStore; 18 | 19 | @property (readonly) ChatStore* chatStore; 20 | 21 | @property (readonly) NSString* chatID; 22 | 23 | @property (readwrite) NSString* title; 24 | @property (readonly) NSString* displayName; 25 | 26 | @property (readonly) NSDate* modDate; 27 | @property (readonly) UserProfile* lastSender; 28 | 29 | // Membership: 30 | 31 | @property (copy) NSArray* members; 32 | @property (copy) NSArray* owners; 33 | 34 | @property (readonly) NSOrderedSet* allMemberProfiles; 35 | 36 | @property (readonly) bool isMember; 37 | @property (readonly) bool isOwner; 38 | 39 | - (void) addMembers: (NSArray*)newMembers; 40 | - (bool) removeMember: (UserProfile*)member 41 | withMessage: (NSString*)message; 42 | 43 | // Messages: 44 | 45 | @property (readonly) unsigned unreadMessageCount; 46 | 47 | - (void) markAsRead; 48 | 49 | @property (readonly) CBLQuery* chatMessagesQuery; 50 | 51 | - (BOOL) addChatMessage: (NSString*)markdown 52 | announcement: (bool)announcement 53 | picture: (UIImage*)picture; 54 | 55 | // Internal use only 56 | - (void) setMessageCount: (NSUInteger)messageCount 57 | modDate: (NSDate*)modDate 58 | lastSender: (NSString*)lastSender; 59 | 60 | @end 61 | 62 | 63 | // Posted when a chat room's unreadCount or modDate change 64 | extern NSString* const kChatRoomStatusChangedNotification; 65 | 66 | 67 | /* A chat root document in JSON form: 68 | { 69 | "_id": "5737529525067657", 70 | "_rev": "29-d3aad012fc362578e8a9b652918f419d", 71 | "type": "room", 72 | "channel_id" : "5737529525067657", 73 | "title": "Mobile Dev", 74 | "tags": "Mobile, Dev, Couchbase", 75 | "members": ["@jchris", "@snej", "@mschoch"], 76 | "owners": ["@amysue"], 77 | "markdown": "Topics in Mobile!\n\n- HostedCouchbase\n- AccessControl\n- SyncProtocol\n- DeveloperFlow\n- CouchbaseServer\n", 78 | "created_at": "2012-12-09T06:05:55.031Z", 79 | "updated_at": "2012-12-16T01:33:01.909Z" 80 | } 81 | */ -------------------------------------------------------------------------------- /CouchChat/ChatRoom.m: -------------------------------------------------------------------------------- 1 | // 2 | // Chat.m 3 | // CouchChat 4 | // 5 | // Created by Jens Alfke on 12/14/12. 6 | // Copyright (c) 2012 Couchbase. All rights reserved. 7 | // 8 | 9 | #import "ChatRoom.h" 10 | #import "ChatStore.h" 11 | #import "UserProfile.h" 12 | #import 13 | #import 14 | 15 | 16 | NSString* const kChatRoomStatusChangedNotification = @"ChatRoomStatusChanged"; 17 | 18 | 19 | @interface ChatRoom () 20 | @property (readwrite) unsigned unreadMessageCount; 21 | @property (readwrite) NSDate* modDate; 22 | @end 23 | 24 | 25 | @implementation ChatRoom 26 | { 27 | CBLLiveQuery* _allPagesQuery; 28 | NSSet* _allPageTitles; 29 | NSUInteger _messageCount; 30 | NSString* _lastSenderID; 31 | } 32 | 33 | @dynamic title, owners, members; 34 | 35 | @synthesize modDate = _modDate, unreadMessageCount = _unreadMessageCount; 36 | 37 | 38 | + (instancetype) modelForDocument: (CBLDocument*)document { 39 | // This is the designated initializer that's always called 40 | ChatRoom *room = [super modelForDocument: document]; 41 | room.autosaves = true; 42 | [room loadLocalState]; 43 | return room; 44 | } 45 | 46 | 47 | // New-document initializer 48 | + (id) chatWithTitle: (NSString*)title inChatStore: (ChatStore*)chatStore { 49 | NSAssert(chatStore.username, @"No username set up yet"); 50 | ChatRoom *room = [super modelForNewDocumentInDatabase: chatStore.database]; 51 | room.owners = [NSArray arrayWithObject:chatStore.username]; 52 | [room setValue: @"room" ofProperty: @"type"]; 53 | [room setValue: [room chatID] ofProperty: @"channel_id"]; 54 | room.title = title; 55 | return self; 56 | } 57 | 58 | 59 | - (NSString*) description { 60 | return [NSString stringWithFormat: @"%@[%@ '%@']", 61 | self.class, self.document.abbreviatedID, self.title]; 62 | } 63 | 64 | 65 | - (NSString*) chatID { 66 | return self.document.documentID; 67 | } 68 | 69 | 70 | - (ChatStore*) chatStore { 71 | return [ChatStore sharedInstance]; //FIX? 72 | } 73 | 74 | 75 | - (NSString*) displayName { 76 | NSString* name = self.title; 77 | if (name) 78 | return name; 79 | return [NSString stringWithFormat: @"with %@", 80 | [UserProfile listOfNames: self.allMemberProfiles]]; 81 | } 82 | 83 | 84 | #pragma mark - MEMBERSHIP: 85 | 86 | 87 | - (UserProfile*) lastSender { 88 | if (!_lastSenderID) 89 | return nil; 90 | return [self.chatStore profileWithUsername: _lastSenderID]; 91 | } 92 | 93 | 94 | - (NSOrderedSet*) allMemberProfiles { 95 | NSMutableOrderedSet* profiles = [NSMutableOrderedSet orderedSet]; 96 | for (NSString* username in self.owners) 97 | [profiles addObject: [self.chatStore profileWithUsername: username]]; 98 | for (NSString* username in self.members) 99 | [profiles addObject: [self.chatStore profileWithUsername: username]]; 100 | return profiles; 101 | } 102 | 103 | 104 | - (bool) isMember { 105 | NSString* username = self.chatStore.username; 106 | return username && ([self.members containsObject: username] || 107 | [self.owners containsObject: username]); 108 | } 109 | 110 | 111 | - (bool) isOwner { 112 | NSString* username = self.chatStore.username; 113 | return username && [self.owners containsObject: username]; 114 | } 115 | 116 | 117 | - (void) addMembers: (NSArray*)newMembers { 118 | NSArray* oldMembers = self.members; 119 | if (!oldMembers) { 120 | self.members = newMembers; 121 | return; 122 | } 123 | NSMutableOrderedSet* members = [NSMutableOrderedSet orderedSetWithArray: self.members]; 124 | [members addObjectsFromArray: newMembers]; 125 | self.members = members.array; 126 | } 127 | 128 | 129 | static NSArray* removeFromArray(NSArray* array, id item) { 130 | NSMutableArray* nuArray = [array mutableCopy]; 131 | [nuArray removeObject: item]; 132 | return nuArray; 133 | } 134 | 135 | 136 | - (bool) removeMember: (UserProfile*)member 137 | withMessage: (NSString*)message 138 | { 139 | NSString* memberID = member.username; 140 | if (![self.members containsObject: memberID]) 141 | return true; // If they're not a member, it's a no-op 142 | if (memberID != self.chatStore.username && !self.isOwner) 143 | return false; // If *you're* not an owner, you can only remove yourself, not other people 144 | 145 | if (message) 146 | [self addChatMessage: message announcement: true picture: nil]; 147 | self.owners = removeFromArray(self.owners, memberID); 148 | self.members = removeFromArray(self.members, memberID); 149 | return true; 150 | } 151 | 152 | 153 | #pragma mark - MESSAGES: 154 | 155 | 156 | - (CBLQuery*) chatMessagesQuery { 157 | CBLQuery* query = [[self.database viewNamed: @"chatMessages"] createQuery]; 158 | query.startKey = @[self.chatID]; 159 | query.endKey = @[self.chatID, @{}]; 160 | query.mapOnly = true; 161 | return query; 162 | } 163 | 164 | 165 | - (BOOL) addChatMessage: (NSString*)markdown 166 | announcement: (bool)announcement 167 | picture: (UIImage*)picture 168 | { 169 | NSString* createdAt = [CBLJSON JSONObjectWithDate: [NSDate date]]; 170 | CBLUnsavedRevision* rev = self.database.createDocument.newRevision; 171 | [rev.properties addEntriesFromDictionary: @{@"type": @"chat", 172 | @"channel_id": self.chatID, 173 | @"author": self.chatStore.username, 174 | @"created_at": createdAt}]; 175 | rev[@"markdown"] = markdown; 176 | if (announcement) 177 | rev[@"style"] = @"announcement"; 178 | if (picture) { 179 | [rev setAttachmentNamed: @"picture" 180 | withContentType: @"image/jpeg" 181 | content: UIImageJPEGRepresentation(picture, 0.6)]; 182 | } 183 | 184 | // Bumping the message count has the effect of not treating this newly-added message as 185 | // unread -- when the ChatStore's view updates and it calls -setMessageCount:modDate: on me, 186 | // the new message count will match my _messageCount so I won't change my _unreadCount. 187 | ++_messageCount; 188 | 189 | NSError* error; 190 | if (![rev save: &error]) { 191 | --_messageCount; // back out the bump 192 | NSLog(@"WARNING: Couldn't save chat picture message: %@", error); 193 | return NO; 194 | } 195 | return YES; 196 | } 197 | 198 | 199 | - (void) postStatusChanged { 200 | NSLog(@"STATUS: %@: unread = %u, modDate = %@ by %@", 201 | self, _unreadMessageCount, _modDate, _lastSenderID); 202 | [[NSNotificationCenter defaultCenter] postNotificationName: kChatRoomStatusChangedNotification 203 | object: self]; 204 | [self saveLocalState]; 205 | } 206 | 207 | 208 | - (void) setMessageCount: (NSUInteger)messageCount 209 | modDate: (NSDate*)modDate 210 | lastSender: (NSString*)lastSender 211 | { 212 | bool changed = false; 213 | int delta = ((int)messageCount - (int)_messageCount); 214 | if (delta != 0) { 215 | _messageCount = messageCount; 216 | self.unreadMessageCount += delta; 217 | changed = true; 218 | } 219 | if (![modDate isEqualToDate: _modDate]) { 220 | self.modDate = modDate; 221 | changed = true; 222 | } 223 | _lastSenderID = lastSender; 224 | if (changed) 225 | [self postStatusChanged]; 226 | } 227 | 228 | 229 | - (void) markAsRead { 230 | if (_unreadMessageCount == 0) 231 | return; 232 | NSLog(@"MARK READ: %@", self); 233 | self.unreadMessageCount = 0; 234 | [self postStatusChanged]; 235 | } 236 | 237 | 238 | - (NSString*) localStateDocID { 239 | return [@"chatState-" stringByAppendingString: self.document.documentID]; 240 | } 241 | 242 | 243 | - (void) saveLocalState { 244 | NSUInteger readCount = _messageCount - _unreadMessageCount; 245 | NSError* error; 246 | if (![self.database putLocalDocument: @{@"readCount": @(readCount)} 247 | withID: self.localStateDocID 248 | error: &error]) 249 | NSLog(@"Warning: Couldn't save local doc: %@", error); 250 | } 251 | 252 | 253 | - (void) loadLocalState { 254 | if (self.document) { 255 | NSDictionary* state = [self.database existingLocalDocumentWithID: self.localStateDocID]; 256 | _messageCount = [state[@"readCount"] unsignedIntValue]; 257 | } 258 | } 259 | 260 | 261 | @end 262 | -------------------------------------------------------------------------------- /CouchChat/ChatStore.h: -------------------------------------------------------------------------------- 1 | // 2 | // ChatStore.h 3 | // CouchChat 4 | // 5 | // Created by Jens Alfke on 12/18/12. 6 | // Copyright (c) 2012 Couchbase. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | @class ChatRoom, UserProfile; 12 | 13 | 14 | /** Chat interface to a CouchbaseLite database. This is the root of the object model. */ 15 | @interface ChatStore : NSObject 16 | 17 | - (id) initWithDatabase: (CBLDatabase*)database; 18 | 19 | + (ChatStore*) sharedInstance; 20 | 21 | @property (readonly) CBLDatabase* database; 22 | 23 | // CHATS: 24 | 25 | @property (readonly, copy) NSArray* allChats; 26 | 27 | - (ChatRoom*) newChatWithTitle: (NSString*)title; 28 | 29 | // USERS: 30 | 31 | /** The local logged-in user */ 32 | @property (nonatomic, readonly) UserProfile* user; 33 | 34 | /** The local logged-in user's username. */ 35 | @property (nonatomic, copy) NSString* username; 36 | 37 | /** Gets a UserProfile for a user given their username. */ 38 | - (UserProfile*) profileWithUsername: (NSString*)username; 39 | 40 | /** Looks up a picture for a username, either from a UserProfile or from gravatar.com. */ 41 | - (UIImage*) pictureForUsername: (NSString*)username; 42 | 43 | - (void) setMyProfileName: (NSString*)name nick: (NSString*)nick; 44 | - (void) setMyProfilePicture:(UIImage *)picture; 45 | 46 | @property (readonly) CBLQuery* allUsersQuery; 47 | @property (readonly) NSArray* allOtherUsers; /**< UserProfile objects of other users */ 48 | 49 | @end 50 | -------------------------------------------------------------------------------- /CouchChat/ChatStore.m: -------------------------------------------------------------------------------- 1 | // 2 | // ChatStore.m 3 | // CouchChat 4 | // 5 | // Created by Jens Alfke on 12/18/12. 6 | // Copyright (c) 2012 Couchbase. All rights reserved. 7 | // 8 | 9 | #import "ChatStore.h" 10 | #import "ChatRoom.h" 11 | #import "UserProfile_Private.h" 12 | #import 13 | #import 14 | 15 | 16 | static ChatStore* sInstance; 17 | 18 | 19 | @interface ChatStore () 20 | @property (readwrite, copy) NSArray* allChats; 21 | @end 22 | 23 | 24 | @implementation ChatStore 25 | { 26 | CBLView* _usersView; 27 | CBLLiveQuery* _chatModDatesQuery; 28 | } 29 | 30 | 31 | @synthesize username=_username, allChats=_allChats; 32 | 33 | 34 | - (id) initWithDatabase: (CBLDatabase*)database { 35 | self = [super init]; 36 | if (self) { 37 | NSAssert(!sInstance, @"Cannot create more than one ChatStore"); 38 | sInstance = self; 39 | _database = database; 40 | _username = [[NSUserDefaults standardUserDefaults] stringForKey: @"UserName"]; 41 | 42 | [_database.modelFactory registerClass: [ChatRoom class] forDocumentType: @"room"]; 43 | [_database.modelFactory registerClass: [UserProfile class] forDocumentType: @"profile"]; 44 | 45 | // Map function for getting chat messages for each chat, sorted by date 46 | CBLView* view = [_database viewNamed: @"chatMessages"]; 47 | [view setMapBlock: MAPBLOCK({ 48 | if ([doc[@"type"] isEqualToString: @"chat"]) { 49 | NSString* markdown = doc[@"markdown"] ?: @""; 50 | NSString* channelID = doc[@"channel_id"]; 51 | if (!channelID) 52 | return; 53 | bool hasAttachments = [doc[@"_attachments"] count] > 0; 54 | bool isAnnouncement = [doc[@"style"] isEqualToString: @"announcement"]; 55 | emit(@[channelID, doc[@"created_at"]], 56 | @[doc[@"author"], markdown, @(hasAttachments), @(isAnnouncement)]); 57 | } 58 | }) reduceBlock: REDUCEBLOCK({ 59 | // Reduce function returns [mod_date, message_count] 60 | NSString* maxDate = [NSDate distantPast]; 61 | NSUInteger count = 0; 62 | NSString* lastSender = @""; 63 | if (rereduce) { 64 | for (NSArray* reducedItem in values) { 65 | count += [reducedItem[1] unsignedIntValue]; 66 | NSString* date = reducedItem[0]; 67 | if ([date compare: maxDate] > 0) { 68 | maxDate = date; 69 | lastSender = reducedItem[2]; 70 | } 71 | } 72 | } else { 73 | maxDate = [keys.lastObject objectAtIndex: 1]; // since keys are in order 74 | lastSender = [values.lastObject objectAtIndex: 0]; 75 | count = values.count; 76 | } 77 | return (@[maxDate, @(count), lastSender]); 78 | }) version: @"7"]; 79 | 80 | _chatModDatesQuery = [[view createQuery] asLiveQuery]; 81 | _chatModDatesQuery.groupLevel = 1; 82 | [_chatModDatesQuery addObserver: self forKeyPath: @"rows" 83 | options: NSKeyValueObservingOptionInitial context: NULL]; 84 | 85 | // View for getting user profiles by name 86 | _usersView = [_database viewNamed: @"usersByName"]; 87 | [_usersView setMapBlock: MAPBLOCK({ 88 | if ([doc[@"type"] isEqualToString: @"profile"]) { 89 | NSString* name = doc[@"nick"] ?: [UserProfile usernameFromDocID: doc[@"_id"]]; 90 | if (name) 91 | emit(name.lowercaseString, name); 92 | } 93 | }) version: @"3"]; 94 | 95 | #if 0 96 | [self createFakeUsers]; 97 | #endif 98 | 99 | } 100 | return self; 101 | } 102 | 103 | 104 | - (void)dealloc 105 | { 106 | [_chatModDatesQuery removeObserver: self forKeyPath: @"rows"]; 107 | } 108 | 109 | 110 | - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context 111 | { 112 | if (object == _chatModDatesQuery) { 113 | [self refreshChatList]; 114 | } else { 115 | [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; 116 | } 117 | } 118 | 119 | 120 | + (ChatStore*) sharedInstance { 121 | return sInstance; 122 | } 123 | 124 | 125 | #pragma mark - CHATS: 126 | 127 | 128 | - (ChatRoom*) newChatWithTitle: (NSString*)title { 129 | return [ChatRoom chatWithTitle: title inChatStore: self]; 130 | } 131 | 132 | 133 | - (void) refreshChatList { 134 | NSMutableArray* chats = [NSMutableArray array]; 135 | for (CBLQueryRow* row in _chatModDatesQuery.rows) { 136 | CBLDocument* document = [_database documentWithID: row.key0]; 137 | ChatRoom* chat = [ChatRoom modelForDocument: document]; 138 | if (chat.isMember) { 139 | [chats addObject: chat]; 140 | NSArray* value = row.value; 141 | NSDate* modDate = [CBLJSON dateWithJSONObject: value[0]]; 142 | NSUInteger count = [value[1] unsignedIntegerValue]; 143 | NSString* lastSender = value[2]; 144 | [chat setMessageCount: count modDate: modDate lastSender: lastSender]; 145 | } 146 | } 147 | [chats sortUsingComparator: ^NSComparisonResult(ChatRoom *chat1, ChatRoom *chat2) { 148 | return [chat2.modDate compare: chat1.modDate]; // descending order! 149 | }]; 150 | 151 | if (![chats isEqual: _allChats]) { 152 | self.allChats = chats; 153 | } 154 | } 155 | 156 | 157 | #pragma mark - USERS: 158 | 159 | 160 | #if 0 161 | - (void) createFakeUsers { 162 | UserProfile* profile = [self profileWithUsername: @"foo@example.com"]; 163 | if (!profile) { 164 | profile = [UserProfile createInDatabase: _database 165 | withUsername: @"foo@example.com"]; 166 | [profile setName: @"Foo Bar" nick: @"foobar"]; 167 | } 168 | profile = [self profileWithUsername: @"pupshaw@example.com"]; 169 | if (!profile) { 170 | profile = [UserProfile createInDatabase: _database 171 | withUsername: @"pupshaw@example.com"]; 172 | [profile setName: @"Pupshaw" nick: nil]; 173 | } 174 | } 175 | #endif 176 | 177 | 178 | - (void) setUsername:(NSString *)username { 179 | if (![username isEqualToString: _username]) { 180 | NSLog(@"Setting chat username to '%@'", username); 181 | _username = [username copy]; 182 | [[NSUserDefaults standardUserDefaults] setObject: username forKey: @"UserName"]; 183 | 184 | UserProfile* myProfile = [self profileWithUsername: _username]; 185 | if (!myProfile) { 186 | myProfile = [UserProfile createInDatabase: _database 187 | withUsername: _username]; 188 | NSLog(@"Created user profile %@", myProfile); 189 | } 190 | } 191 | } 192 | 193 | 194 | - (UserProfile*) user { 195 | if (!_username) 196 | return nil; 197 | UserProfile* user = [self profileWithUsername: _username]; 198 | if (!user) { 199 | user = [UserProfile createInDatabase: _database 200 | withUsername: _username]; 201 | } 202 | return user; 203 | } 204 | 205 | 206 | - (UserProfile*) profileWithUsername: (NSString*)username { 207 | NSString* docID = [UserProfile docIDForUsername: username]; 208 | CBLDocument* doc = [self.database documentWithID: docID]; 209 | if (!doc.currentRevisionID) 210 | return nil; 211 | return [UserProfile modelForDocument: doc]; 212 | } 213 | 214 | 215 | - (UIImage*) pictureForUsername: (NSString*)username { 216 | UserProfile* profile = [self profileWithUsername: username]; 217 | if (profile) { 218 | UIImage* picture = profile.picture; 219 | if (picture) 220 | return picture; 221 | } 222 | return [UserProfile loadGravatarForEmail: username]; 223 | } 224 | 225 | 226 | - (void) setMyProfileName: (NSString*)name nick: (NSString*)nick { 227 | UserProfile* myProfile = [self profileWithUsername: self.username]; 228 | [myProfile setName: name nick: nick]; 229 | } 230 | 231 | - (void) setMyProfilePicture:(UIImage *)picture { 232 | UserProfile* myProfile = [self profileWithUsername: self.username]; 233 | [myProfile setPicture: picture]; 234 | } 235 | 236 | 237 | - (CBLQuery*) allUsersQuery { 238 | return [_usersView createQuery]; 239 | } 240 | 241 | - (NSArray*) allOtherUsers { 242 | NSMutableArray* users = [NSMutableArray array]; 243 | for (CBLQueryRow* row in [self.allUsersQuery run: NULL].allObjects) { 244 | UserProfile* user = [UserProfile modelForDocument: row.document]; 245 | if (![user.username isEqualToString: _username]) 246 | [users addObject: user]; 247 | } 248 | return users; 249 | } 250 | 251 | 252 | 253 | @end 254 | -------------------------------------------------------------------------------- /CouchChat/CouchChat-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | ${PRODUCT_NAME} 9 | CFBundleExecutable 10 | ${EXECUTABLE_NAME} 11 | CFBundleIcons 12 | 13 | CFBundlePrimaryIcon 14 | 15 | CFBundleIconFiles 16 | 17 | iPhone icon.png 18 | iPhone icon@2x.png 19 | 20 | 21 | 22 | CFBundleIdentifier 23 | org.couchbase.${PRODUCT_NAME:rfc1034identifier} 24 | CFBundleInfoDictionaryVersion 25 | 6.0 26 | CFBundleName 27 | ${PRODUCT_NAME} 28 | CFBundlePackageType 29 | APPL 30 | CFBundleShortVersionString 31 | 1.0 32 | CFBundleSignature 33 | ???? 34 | CFBundleVersion 35 | 1.0 36 | LSRequiresIPhoneOS 37 | 38 | UIRequiredDeviceCapabilities 39 | 40 | armv7 41 | 42 | UIStatusBarTintParameters 43 | 44 | UINavigationBar 45 | 46 | Style 47 | UIBarStyleDefault 48 | Translucent 49 | 50 | 51 | 52 | UISupportedInterfaceOrientations 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationLandscapeLeft 56 | UIInterfaceOrientationLandscapeRight 57 | 58 | UISupportedInterfaceOrientations~ipad 59 | 60 | UIInterfaceOrientationPortrait 61 | UIInterfaceOrientationPortraitUpsideDown 62 | UIInterfaceOrientationLandscapeLeft 63 | UIInterfaceOrientationLandscapeRight 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /CouchChat/CouchChat-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header for all source files of the 'CouchChat' target in the 'CouchChat' project 3 | // 4 | 5 | #import 6 | 7 | #ifndef __IPHONE_4_0 8 | #warning "This project uses features only available in iOS SDK 4.0 and later." 9 | #endif 10 | 11 | #ifdef __OBJC__ 12 | #import 13 | #import 14 | #endif 15 | -------------------------------------------------------------------------------- /CouchChat/Default-568h@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/CouchChat-iOS/656704ed7661223cd2856c5600b3c3c178c6caf4/CouchChat/Default-568h@2x.png -------------------------------------------------------------------------------- /CouchChat/Default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/CouchChat-iOS/656704ed7661223cd2856c5600b3c3c178c6caf4/CouchChat/Default.png -------------------------------------------------------------------------------- /CouchChat/Default@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/CouchChat-iOS/656704ed7661223cd2856c5600b3c3c178c6caf4/CouchChat/Default@2x.png -------------------------------------------------------------------------------- /CouchChat/LoginController.h: -------------------------------------------------------------------------------- 1 | // 2 | // LoginController.h 3 | // TouchWiki 4 | // 5 | // Created by Jens Alfke on 1/3/13. 6 | // Copyright (c) 2013 Couchbase. All rights reserved. 7 | // 8 | 9 | #import 10 | @class SyncManager; 11 | 12 | 13 | /** Simple wrapper around a UIAlertView that prompts for a username and password. */ 14 | @interface LoginController : NSObject 15 | 16 | - (id) initWithURL: (NSURL*)url username: (NSString*)username; 17 | 18 | - (void) run; 19 | 20 | @property (weak) SyncManager* delegate; 21 | 22 | @end 23 | -------------------------------------------------------------------------------- /CouchChat/LoginController.m: -------------------------------------------------------------------------------- 1 | // 2 | // LoginController.m 3 | // TouchWiki 4 | // 5 | // Created by Jens Alfke on 1/3/13. 6 | // Copyright (c) 2013 Couchbase. All rights reserved. 7 | // 8 | 9 | #import "LoginController.h" 10 | #import "SyncManager.h" 11 | 12 | 13 | @implementation LoginController 14 | { 15 | NSString* _username; 16 | NSURL* _url; 17 | } 18 | 19 | 20 | - (id) initWithURL: (NSURL*)url username: (NSString*)username { 21 | self = [super init]; 22 | if (self) { 23 | _url = url; 24 | _username = username; 25 | } 26 | return self; 27 | } 28 | 29 | 30 | - (void) run { 31 | NSString* title = [NSString stringWithFormat: @"Log into %@", _url.host]; 32 | UIAlertView* alert = [[UIAlertView alloc] initWithTitle: title 33 | message: nil 34 | delegate: self 35 | cancelButtonTitle: @"Cancel" 36 | otherButtonTitles: @"OK", nil]; 37 | alert.alertViewStyle = UIAlertViewStyleLoginAndPasswordInput; 38 | if (_username) 39 | [alert textFieldAtIndex: 0].text = _username; 40 | [alert show]; 41 | } 42 | 43 | 44 | - (BOOL)alertViewShouldEnableFirstOtherButton:(UIAlertView *)alert { 45 | return [alert textFieldAtIndex: 0].text.length > 0 46 | && [alert textFieldAtIndex: 1].text.length > 0; 47 | } 48 | 49 | 50 | - (void)alertView:(UIAlertView *)alert didDismissWithButtonIndex:(NSInteger)buttonIndex { 51 | switch (buttonIndex) { 52 | case 0: 53 | [_delegate loginCanceled]; 54 | break; 55 | case 1: 56 | [_delegate setUsername: [alert textFieldAtIndex: 0].text 57 | password: [alert textFieldAtIndex: 1].text]; 58 | break; 59 | } 60 | } 61 | 62 | 63 | @end 64 | -------------------------------------------------------------------------------- /CouchChat/SyncManager.h: -------------------------------------------------------------------------------- 1 | // 2 | // SyncManager.h 3 | // CouchChat 4 | // 5 | // Created by Jens Alfke on 12/19/12. 6 | // Copyright (c) 2012 Couchbase. All rights reserved. 7 | // 8 | 9 | #import 10 | @protocol SyncManagerDelegate; 11 | 12 | 13 | @interface SyncManager : NSObject 14 | 15 | - (id) initWithDatabase: (CBLDatabase*)database; 16 | 17 | @property (readonly) CBLDatabase* database; 18 | @property (nonatomic, weak) id delegate; 19 | 20 | @property (nonatomic) NSURL* syncURL; 21 | @property (nonatomic) bool continuous; 22 | 23 | @property (nonatomic, readonly) NSArray* replications; 24 | 25 | // These are not KVO-observable; observe SyncManagerStateChangedNotification instead 26 | @property (nonatomic, readonly) unsigned completed, total; 27 | @property (nonatomic, readonly) float progress; 28 | @property (nonatomic, readonly) bool active; 29 | @property (nonatomic, readonly) CBLReplicationStatus status; 30 | @property (nonatomic, readonly) NSError* error; 31 | 32 | - (void) syncNow; 33 | 34 | - (void) setUsername: (NSString*)username password: (NSString*)password; 35 | - (void) loginCanceled; 36 | 37 | @end 38 | 39 | 40 | /** Posted by a SyncManager instance when its replication state properties change. */ 41 | extern NSString* const SyncManagerStateChangedNotification; 42 | 43 | 44 | @protocol SyncManagerDelegate 45 | @optional 46 | - (void) syncManagerProgressChanged: (SyncManager*)manager; 47 | - (void) syncManager: (SyncManager*)manager addedReplication: (CBLReplication*)replication; 48 | - (bool) syncManagerShouldPromptForLogin: (SyncManager*)manager; 49 | 50 | @end -------------------------------------------------------------------------------- /CouchChat/SyncManager.m: -------------------------------------------------------------------------------- 1 | // 2 | // SyncManager.m 3 | // CouchChat 4 | // 5 | // Created by Jens Alfke on 12/19/12. 6 | // Copyright (c) 2012 Couchbase. All rights reserved. 7 | // 8 | 9 | #import "SyncManager.h" 10 | #import "LoginController.h" 11 | 12 | 13 | NSString* const SyncManagerStateChangedNotification = @"SyncManagerStateChanged"; 14 | 15 | 16 | @implementation SyncManager 17 | { 18 | NSMutableArray* _replications; 19 | bool _showingSyncButton; 20 | __weak id _delegate; 21 | LoginController* _loginController; 22 | } 23 | 24 | 25 | - (id) initWithDatabase: (CBLDatabase*)db { 26 | NSParameterAssert(db); 27 | self = [super init]; 28 | if (self) { 29 | _database = db; 30 | _replications = [[NSMutableArray alloc] init]; 31 | [self addReplications: db.allReplications]; 32 | } 33 | return self; 34 | } 35 | 36 | 37 | @synthesize delegate=_delegate, replications=_replications, status=_status; 38 | 39 | 40 | - (void) addReplication: (CBLReplication*)repl { 41 | if (![_replications containsObject: repl]) { 42 | [_replications addObject: repl]; 43 | [[NSNotificationCenter defaultCenter] addObserver: self 44 | selector: @selector(replicationProgress:) 45 | name: kCBLReplicationChangeNotification 46 | object: repl]; 47 | if (!_syncURL) 48 | _syncURL = repl.remoteURL; 49 | if (repl.continuous) 50 | _continuous = true; 51 | if ([_delegate respondsToSelector: @selector(syncManager:addedReplication:)]) 52 | [_delegate syncManager: self addedReplication: repl]; 53 | [repl start]; 54 | } 55 | } 56 | 57 | 58 | - (void) addReplications: (NSArray*)replications { 59 | for (CBLReplication* repl in replications) { 60 | [self addReplication: repl]; 61 | } 62 | } 63 | 64 | 65 | - (void) forgetReplication: (CBLReplication*)repl { 66 | [_replications removeObject: repl]; 67 | [[NSNotificationCenter defaultCenter] removeObserver: self 68 | name: kCBLReplicationChangeNotification 69 | object: repl]; 70 | } 71 | 72 | 73 | - (void) forgetAll { 74 | for (CBLReplication* repl in _replications) { 75 | [[NSNotificationCenter defaultCenter] removeObserver: self 76 | name: kCBLReplicationChangeNotification 77 | object: repl]; 78 | } 79 | _replications = [[NSMutableArray alloc] init]; 80 | _syncURL = nil; 81 | } 82 | 83 | 84 | - (void) setSyncURL: (NSURL*)url { 85 | if (url == _syncURL || [url isEqual: _syncURL]) 86 | return; 87 | [self forgetAll]; 88 | if (url) { 89 | CBLReplication *pullReplication = [self.database createPullReplication:url]; 90 | [pullReplication setContinuous:YES]; 91 | [self addReplication: pullReplication]; 92 | 93 | CBLReplication *pushReplication = [self.database createPushReplication:url]; 94 | [pushReplication setContinuous:YES]; 95 | [self addReplication: pushReplication]; 96 | } 97 | } 98 | 99 | 100 | - (void) setContinuous:(bool)continuous { 101 | _continuous = continuous; 102 | for (CBLReplication* repl in _replications) 103 | repl.continuous = continuous; 104 | } 105 | 106 | 107 | - (void) setUsername: (NSString*)username password: (NSString*)password { 108 | _loginController = nil; 109 | NSURLCredential* cred = [NSURLCredential credentialWithUser: username 110 | password: password 111 | persistence: NSURLCredentialPersistencePermanent]; 112 | for (CBLReplication* repl in _replications) { 113 | repl.credential = cred; 114 | } 115 | } 116 | 117 | 118 | - (void) loginCanceled { 119 | _loginController = nil; 120 | } 121 | 122 | 123 | - (void) syncNow { 124 | for (CBLReplication* repl in _replications) { 125 | if (!repl.continuous) 126 | [repl start]; 127 | } 128 | } 129 | 130 | 131 | - (void) replicationProgress: (NSNotificationCenter*)n { 132 | bool active = false; 133 | unsigned completed = 0, total = 0; 134 | CBLReplicationStatus status = kCBLReplicationStopped; 135 | NSError* error = nil; 136 | for (CBLReplication* repl in _replications) { 137 | status = MAX(status, repl.status); 138 | if (!error) 139 | error = repl.lastError; 140 | if (repl.status == kCBLReplicationActive) { 141 | active = true; 142 | completed += repl.completedChangesCount; 143 | total += repl.changesCount; 144 | } 145 | } 146 | 147 | if (error != _error && error.code == 401) { 148 | // Auth needed (or auth is incorrect). See if the delegate wants to do its own auth; 149 | // otherwise put up a username/password login panel: 150 | if (![_delegate respondsToSelector: @selector(syncManagerShouldPromptForLogin:)] || 151 | [_delegate syncManagerShouldPromptForLogin: self]) { 152 | if (!_loginController) { 153 | NSString* user = [_replications[0] credential].user; 154 | _loginController = [[LoginController alloc] initWithURL: self.syncURL username: user]; 155 | _loginController.delegate = self; 156 | [_loginController run]; 157 | } 158 | error = nil; 159 | } 160 | } 161 | 162 | if (active != _active || completed != _completed || total != _total || status != _status 163 | || error != _error) { 164 | _active = active; 165 | _completed = completed; 166 | _total = total; 167 | _progress = (completed / (float)MAX(total, 1u)); 168 | _status = status; 169 | _error = error; 170 | NSLog(@"SYNCMGR: active=%d; status=%d; %u/%u; %@", 171 | active, status, completed, total, error.localizedDescription); //FIX: temporary logging 172 | if ([_delegate respondsToSelector: @selector(syncManagerProgressChanged:)]) 173 | [_delegate syncManagerProgressChanged: self]; 174 | [[NSNotificationCenter defaultCenter] 175 | postNotificationName: SyncManagerStateChangedNotification 176 | object: self]; 177 | } 178 | } 179 | 180 | 181 | @end 182 | -------------------------------------------------------------------------------- /CouchChat/UserPickerController.h: -------------------------------------------------------------------------------- 1 | // 2 | // UserPickerController.h 3 | // CouchChat 4 | // 5 | // Created by Jens Alfke on 3/4/13. 6 | // Copyright (c) 2013 Couchbase. All rights reserved. 7 | // 8 | 9 | #import "THContactPickerViewController.h" 10 | #import "UserProfile.h" 11 | @protocol UserPickerControllerDelegate; 12 | 13 | // Declare that UserProfile implements the THContact protocol. 14 | @interface UserProfile () 15 | @end 16 | 17 | 18 | @interface UserPickerController : THContactPickerViewController 19 | 20 | - (id) initWithUsers: (NSArray*)users 21 | delegate: (id)delegate; 22 | 23 | @end 24 | 25 | 26 | @protocol UserPickerControllerDelegate 27 | 28 | - (void) userPickerController: (UserPickerController*)controller 29 | pickedUsers: (NSArray*)users; 30 | 31 | @end 32 | -------------------------------------------------------------------------------- /CouchChat/UserPickerController.m: -------------------------------------------------------------------------------- 1 | // 2 | // UserPickerController.m 3 | // CouchChat 4 | // 5 | // Created by Jens Alfke on 3/4/13. 6 | // Copyright (c) 2013 Couchbase. All rights reserved. 7 | // 8 | 9 | #import "UserPickerController.h" 10 | #import "THContactPickerView.h" 11 | #import "UserProfile.h" 12 | 13 | @interface THContactPickerView (Private) 14 | @property (readonly) UITextView* textView; 15 | @end 16 | 17 | 18 | @interface UserPickerController () 19 | 20 | @end 21 | 22 | @implementation UserPickerController 23 | { 24 | __weak id _delegate; 25 | bool _started; 26 | } 27 | 28 | - (id) initWithUsers: (NSArray*)users 29 | delegate: (id)delegate 30 | { 31 | self = [super initWithNibName: @"THContactPickerViewController" bundle: nil]; 32 | if (self) { 33 | _delegate = delegate; 34 | 35 | self.contacts = [users sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) { 36 | return [[obj1 displayName] localizedCaseInsensitiveCompare: [obj2 displayName]]; 37 | }]; 38 | //self.tableView 39 | 40 | self.title = @"Invite To Chat"; 41 | } 42 | return self; 43 | } 44 | 45 | - (void)viewDidLoad { 46 | [super viewDidLoad]; 47 | 48 | [self.contactPickerView setPlaceholderString:@"Choose people…"]; 49 | UIBarButtonItem* startButton = [[UIBarButtonItem alloc] initWithTitle: @"Start" 50 | style: UIBarButtonItemStyleDone 51 | target: self 52 | action: @selector(start:)]; 53 | self.navigationItem.rightBarButtonItem = startButton; 54 | } 55 | 56 | - (void)viewDidDisappear:(BOOL)animated { 57 | if (!_started) 58 | [_delegate userPickerController: self pickedUsers: nil]; 59 | } 60 | 61 | - (IBAction) start: (id)sender { 62 | _started = true; 63 | [_delegate userPickerController: self pickedUsers: self.selectedContacts]; 64 | } 65 | 66 | @end 67 | -------------------------------------------------------------------------------- /CouchChat/UserProfile.h: -------------------------------------------------------------------------------- 1 | // 2 | // UserProfile.h 3 | // CouchChat 4 | // 5 | // Created by Jens Alfke on 2/15/13. 6 | // Copyright (c) 2013 Couchbase. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | 12 | /** Information about a user. */ 13 | @interface UserProfile : CBLModel 14 | 15 | /** The user's unique identifier, used in the "author" property of chat messages. */ 16 | @property (readonly) NSString* username; 17 | 18 | @property (readonly, copy) NSString* name; /**< Person's name. */ 19 | @property (readonly, copy) NSString* nick; /**< Nickname, aka "handle" or "screen name". */ 20 | @property (readonly, copy) NSString* email; /**< Primary email address. */ 21 | 22 | @property (readonly) NSString* displayName; /**< Best name to display (name, else username) */ 23 | 24 | /** Does this profile represent the logged-in user? */ 25 | @property (readonly) bool isMe; 26 | 27 | /** A small picture for use as an avatar/userpic. */ 28 | @property (readonly, weak) UIImage* picture; 29 | 30 | /** Maps a username to the document ID of the user's profile. */ 31 | + (NSString*) docIDForUsername: (NSString*)username; 32 | 33 | /** Creates a new UserProfile, presumably for the local logged-in user. */ 34 | + (UserProfile*) createInDatabase: (CBLDatabase*)database 35 | withUsername: (NSString*)username; 36 | 37 | /** Synchronously loads an image from gravatar.com for the given email address. */ 38 | + (UIImage*) loadGravatarForEmail: (NSString*)email; 39 | 40 | + (NSString*) usernameFromDocID: (NSString*)docID; 41 | 42 | + (NSString*) listOfNames: (id)userArrayOrSet; 43 | 44 | @end 45 | -------------------------------------------------------------------------------- /CouchChat/UserProfile.m: -------------------------------------------------------------------------------- 1 | // 2 | // UserProfile.m 3 | // CouchChat 4 | // 5 | // Created by Jens Alfke on 2/15/13. 6 | // Copyright (c) 2013 Couchbase. All rights reserved. 7 | // 8 | 9 | #import "UserProfile.h" 10 | #import "ChatStore.h" 11 | #import 12 | #import 13 | 14 | 15 | @implementation UserProfile 16 | { 17 | bool _checkedPicture; 18 | __weak UIImage* _picture; 19 | } 20 | 21 | 22 | + (NSString*) docIDForUsername: (NSString*)username { 23 | return [@"profile:" stringByAppendingString: username]; 24 | } 25 | 26 | + (NSString*) usernameFromDocID: (NSString*)docID { 27 | return [docID substringFromIndex: 8]; 28 | } 29 | 30 | 31 | @dynamic name, nick; 32 | 33 | 34 | - (NSString*) username { 35 | return [self.class usernameFromDocID: self.document.documentID]; 36 | } 37 | 38 | 39 | - (NSString*) email { 40 | NSString* email = [self getValueOfProperty: @"email"]; 41 | if (!email) { 42 | // If no explicit email, assume the username is a valid email if it contains an "@": 43 | NSString* username = self.username; 44 | if ([username rangeOfString: @"@"].length > 0) 45 | email = username; 46 | } 47 | return email; 48 | } 49 | 50 | 51 | - (NSString*) displayName { 52 | return self.name ?: (self.nick ?: self.username); 53 | } 54 | 55 | 56 | - (bool) isMe { 57 | return [self.username isEqualToString: [[ChatStore sharedInstance] username]]; 58 | } 59 | 60 | 61 | - (void) didLoadFromDocument { 62 | // Invalidate cached picture: 63 | _picture = nil; 64 | _checkedPicture = false; 65 | [super didLoadFromDocument]; 66 | } 67 | 68 | 69 | - (UIImage*) picture { 70 | UIImage* picture = _picture; // _picture is weak, so assign to local var first 71 | if (!_checkedPicture && picture == nil) { 72 | NSData* pictureData = [[self attachmentNamed: @"avatar"] content]; 73 | if (pictureData) 74 | picture = [[UIImage alloc] initWithData: pictureData]; 75 | else if (self.email) 76 | picture = [UserProfile loadGravatarForEmail: self.email]; 77 | _picture = picture; 78 | _checkedPicture = true; 79 | } 80 | return picture; 81 | } 82 | 83 | 84 | + (UserProfile*) createInDatabase: (CBLDatabase*)database 85 | withUsername: (NSString*)username 86 | { 87 | NSString* docID = [self docIDForUsername: username]; 88 | CBLDocument* doc = [database documentWithID: docID]; 89 | UserProfile* profile = [UserProfile modelForDocument: doc]; 90 | 91 | [profile setValue: @"profile" ofProperty: @"type"]; 92 | 93 | NSString* nick = username; 94 | NSRange at = [username rangeOfString: @"@"]; 95 | if (at.length > 0) { 96 | nick = [username substringToIndex: at.location]; 97 | [profile setValue: username ofProperty: @"email"]; 98 | } 99 | [profile setValue: nick ofProperty: @"nick"]; 100 | 101 | NSError* error; 102 | if (![profile save: &error]) 103 | return nil; 104 | return profile; 105 | } 106 | 107 | 108 | - (void) setName: (NSString*)name nick: (NSString*)nick { 109 | self.autosaves = true; 110 | [self setValue: name ofProperty: @"name"]; 111 | [self setValue: nick ofProperty: @"nick"]; 112 | } 113 | 114 | - (void) setPicture:(UIImage *)picture { 115 | self.autosaves = true; 116 | if (picture) { 117 | NSData* imageData = UIImageJPEGRepresentation(picture, 0.6); 118 | [self setAttachmentNamed: @"avatar" withContentType: @"image/jpeg" content: imageData]; 119 | } else { 120 | [self removeAttachmentNamed: @"avatar"]; 121 | } 122 | } 123 | 124 | 125 | + (UIImage*) loadGravatarForEmail: (NSString*)email { 126 | static NSMutableDictionary* sGravatars; 127 | 128 | if (!email || [email rangeOfString: @"@"].length == 0) 129 | return nil; // not an email address 130 | email = email.lowercaseString; 131 | 132 | UIImage* picture = sGravatars[email]; 133 | if (!picture) { 134 | NSData* data = [email dataUsingEncoding: NSUTF8StringEncoding]; 135 | uint8_t md5[16]; 136 | CC_MD5(data.bytes, (CC_LONG)data.length, md5); 137 | NSString *md5email = [NSString stringWithFormat: 138 | @"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", 139 | md5[0], md5[1], md5[2], md5[3], md5[4], md5[5], md5[6], md5[7], 140 | md5[8], md5[9], md5[10], md5[11], md5[12], md5[13], md5[14], md5[15] ]; 141 | NSString* urlStr = [NSString stringWithFormat:@"http://www.gravatar.com/avatar/%@?d=retro]", 142 | md5email]; 143 | NSURL* url = [NSURL URLWithString: urlStr]; 144 | 145 | NSData* pictureData = [NSData dataWithContentsOfURL: url]; 146 | NSLog(@"Gravatar for %@ <%@> -- %lu bytes", email, urlStr, (unsigned long)pictureData.length); 147 | if (pictureData) 148 | picture = [[UIImage alloc] initWithData: pictureData]; 149 | 150 | if (!sGravatars) 151 | sGravatars = [NSMutableDictionary dictionary]; 152 | sGravatars[email] = picture ?: [NSNull null]; 153 | } else if ((id)picture == [NSNull null]) { 154 | picture = nil; 155 | } 156 | return picture; 157 | } 158 | 159 | 160 | + (NSString*) listOfNames: (id)userArrayOrSet { 161 | NSMutableString* names = [NSMutableString string]; 162 | for (UserProfile* profile in userArrayOrSet) { 163 | if (!profile.isMe) { 164 | if (names.length > 0) 165 | [names appendString: @", "]; 166 | [names appendString: profile.displayName]; 167 | } 168 | } 169 | return names; 170 | } 171 | 172 | 173 | @end 174 | -------------------------------------------------------------------------------- /CouchChat/UserProfile_Private.h: -------------------------------------------------------------------------------- 1 | // 2 | // UserProfile_Private.h 3 | // CouchChat 4 | // 5 | // Created by Jens Alfke on 3/1/13. 6 | // Copyright (c) 2013 Couchbase. All rights reserved. 7 | // 8 | 9 | #import "UserProfile.h" 10 | 11 | @interface UserProfile () 12 | - (void) setName: (NSString*)name nick: (NSString*)nick; 13 | - (void) setPicture:(UIImage *)picture; 14 | @end 15 | -------------------------------------------------------------------------------- /CouchChat/en.lproj/ChatController_iPad.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1552 5 | 12C3006 6 | 3084 7 | 1187.34 8 | 625.00 9 | 10 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 11 | 2083 12 | 13 | 14 | IBNSLayoutConstraint 15 | IBProxyObject 16 | IBUILabel 17 | IBUIView 18 | 19 | 20 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 21 | 22 | 23 | PluginDependencyRecalculationVersion 24 | 25 | 26 | 27 | 28 | IBFilesOwner 29 | IBIPadFramework 30 | 31 | 32 | IBFirstResponder 33 | IBIPadFramework 34 | 35 | 36 | 37 | 274 38 | 39 | 40 | 41 | 298 42 | {{20, 495}, {728, 18}} 43 | 44 | 45 | 3 46 | MQA 47 | 48 | YES 49 | NO 50 | IBIPadFramework 51 | Detail view content goes here 52 | 53 | 1 54 | MCAwIDAAA 55 | darkTextColor 56 | 57 | 58 | 1 59 | 10 60 | 1 61 | 62 | 1 63 | 4 64 | 65 | 66 | Helvetica 67 | 14 68 | 16 69 | 70 | 71 | 72 | {{0, 20}, {768, 1004}} 73 | 74 | NO 75 | 76 | 2 77 | 78 | IBIPadFramework 79 | 80 | 81 | 82 | 83 | 84 | 85 | view 86 | 87 | 88 | 89 | 12 90 | 91 | 92 | 93 | 94 | 95 | 0 96 | 97 | 98 | 99 | 100 | 101 | -1 102 | 103 | 104 | File's Owner 105 | 106 | 107 | -2 108 | 109 | 110 | 111 | 112 | 8 113 | 114 | 115 | 116 | 117 | 10 118 | 0 119 | 120 | 10 121 | 1 122 | 123 | 0.0 124 | 125 | 1000 126 | 127 | 5 128 | 22 129 | 2 130 | 131 | 132 | 133 | 6 134 | 0 135 | 136 | 6 137 | 1 138 | 139 | 20 140 | 141 | 1000 142 | 143 | 8 144 | 29 145 | 3 146 | 147 | 148 | 149 | 5 150 | 0 151 | 152 | 5 153 | 1 154 | 155 | 20 156 | 157 | 1000 158 | 159 | 8 160 | 29 161 | 3 162 | 163 | 164 | 165 | 166 | 167 | 168 | 81 169 | 170 | 171 | 172 | 173 | 174 | 94 175 | 176 | 177 | 178 | 179 | 97 180 | 181 | 182 | 183 | 184 | 98 185 | 186 | 187 | 188 | 189 | 190 | 191 | ChatController 192 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 193 | UIResponder 194 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 195 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 196 | 197 | 198 | 199 | 200 | 201 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 202 | 203 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 204 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 205 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 206 | 207 | 208 | 209 | 210 | 211 | 98 212 | 213 | 214 | 0 215 | IBIPadFramework 216 | YES 217 | 3 218 | YES 219 | 2083 220 | 221 | 222 | -------------------------------------------------------------------------------- /CouchChat/en.lproj/ChatController_iPhone.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1552 5 | 12C3006 6 | 3084 7 | 1187.34 8 | 625.00 9 | 10 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 11 | 2083 12 | 13 | 14 | IBNSLayoutConstraint 15 | IBProxyObject 16 | IBUITableView 17 | IBUITextField 18 | IBUIView 19 | 20 | 21 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 22 | 23 | 24 | PluginDependencyRecalculationVersion 25 | 26 | 27 | 28 | 29 | IBFilesOwner 30 | IBCocoaTouchFramework 31 | 32 | 33 | IBFirstResponder 34 | IBCocoaTouchFramework 35 | 36 | 37 | 38 | 274 39 | 40 | 41 | 42 | 292 43 | {{10, 463}, {299, 31}} 44 | 45 | 46 | 47 | _NS:9 48 | NO 49 | YES 50 | IBCocoaTouchFramework 51 | 0 52 | 53 | 3 54 | Message 55 | 56 | 3 57 | MAA 58 | 59 | 2 60 | 61 | 62 | 14 63 | 64 | 2 65 | 7 66 | IBCocoaTouchFramework 67 | 68 | 69 | 1 70 | 14 71 | 72 | 73 | Helvetica 74 | 14 75 | 16 76 | 77 | 78 | 79 | 80 | 274 81 | {320, 455} 82 | 83 | 84 | 85 | _NS:9 86 | 87 | 3 88 | MQA 89 | 90 | YES 91 | IBCocoaTouchFramework 92 | YES 93 | 1 94 | 0 95 | YES 96 | 44 97 | 22 98 | 22 99 | 100 | 101 | {{0, 64}, {320, 504}} 102 | 103 | 104 | 105 | 106 | 3 107 | MQA 108 | 109 | 110 | 111 | 112 | NO 113 | 114 | 115 | IBUIScreenMetrics 116 | 117 | YES 118 | 119 | 120 | 121 | 122 | 123 | {320, 568} 124 | {568, 320} 125 | 126 | 127 | IBCocoaTouchFramework 128 | Retina 4 Full Screen 129 | 2 130 | 131 | IBCocoaTouchFramework 132 | 133 | 134 | 135 | 136 | 137 | 138 | view 139 | 140 | 141 | 142 | 3 143 | 144 | 145 | 146 | _bubbles 147 | 148 | 149 | 150 | 53 151 | 152 | 153 | 154 | _inputLine 155 | 156 | 157 | 158 | 54 159 | 160 | 161 | 162 | delegate 163 | 164 | 165 | 166 | 55 167 | 168 | 169 | 170 | bubbleDataSource 171 | 172 | 173 | 174 | 52 175 | 176 | 177 | 178 | 179 | 180 | 0 181 | 182 | 183 | 184 | 185 | 186 | 1 187 | 188 | 189 | 190 | 191 | 6 192 | 0 193 | 194 | 6 195 | 1 196 | 197 | 11 198 | 199 | 1000 200 | 201 | 9 202 | 40 203 | 3 204 | 205 | 206 | 207 | 3 208 | 0 209 | 210 | 4 211 | 1 212 | 213 | 8 214 | 215 | 1000 216 | 217 | 6 218 | 24 219 | 3 220 | 221 | 222 | 223 | 4 224 | 0 225 | 226 | 4 227 | 1 228 | 229 | 10 230 | 231 | 1000 232 | 233 | 9 234 | 40 235 | 3 236 | 237 | 238 | 239 | 5 240 | 0 241 | 242 | 5 243 | 1 244 | 245 | 10 246 | 247 | 1000 248 | 249 | 9 250 | 40 251 | 3 252 | 253 | 254 | 255 | 3 256 | 0 257 | 258 | 3 259 | 1 260 | 261 | 0.0 262 | 263 | 1000 264 | 265 | 9 266 | 40 267 | 3 268 | 269 | 270 | 271 | 6 272 | 0 273 | 274 | 6 275 | 1 276 | 277 | 0.0 278 | 279 | 1000 280 | 281 | 8 282 | 29 283 | 3 284 | 285 | 286 | 287 | 5 288 | 0 289 | 290 | 5 291 | 1 292 | 293 | 0.0 294 | 295 | 1000 296 | 297 | 8 298 | 29 299 | 3 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | -1 308 | 309 | 310 | File's Owner 311 | 312 | 313 | -2 314 | 315 | 316 | 317 | 318 | 12 319 | 320 | 321 | 322 | 323 | 8 324 | 0 325 | 326 | 0 327 | 1 328 | 329 | 31 330 | 331 | 1000 332 | 333 | 9 334 | 40 335 | 1 336 | 337 | 338 | 339 | 340 | 341 | 13 342 | 343 | 344 | 345 | 346 | 347 | 15 348 | 349 | 350 | 351 | 352 | 23 353 | 354 | 355 | 356 | 357 | 27 358 | 359 | 360 | 361 | 362 | 35 363 | 364 | 365 | 366 | 367 | 46 368 | 369 | 370 | 371 | 372 | 50 373 | 374 | 375 | 376 | 377 | 51 378 | 379 | 380 | 381 | 382 | 56 383 | 384 | 385 | 386 | 387 | 388 | 389 | ChatController 390 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 391 | UIResponder 392 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 393 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 404 | 405 | 406 | 407 | 408 | BTVBubbleTableView 409 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 410 | 411 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 412 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 413 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 414 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 415 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 416 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 417 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 418 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 419 | 420 | 421 | 422 | 423 | 424 | 56 425 | 426 | 427 | 428 | 429 | BTVBubbleTableView 430 | UITableView 431 | 432 | bubbleDataSource 433 | id 434 | 435 | 436 | bubbleDataSource 437 | 438 | bubbleDataSource 439 | id 440 | 441 | 442 | 443 | IBProjectSource 444 | ./Classes/BTVBubbleTableView.h 445 | 446 | 447 | 448 | ChatController 449 | UIViewController 450 | 451 | id 452 | id 453 | 454 | 455 | 456 | addPicture: 457 | id 458 | 459 | 460 | configureSync 461 | id 462 | 463 | 464 | 465 | BTVBubbleTableView 466 | UITextField 467 | UILabel 468 | 469 | 470 | 471 | _bubbles 472 | BTVBubbleTableView 473 | 474 | 475 | _inputLine 476 | UITextField 477 | 478 | 479 | detailDescriptionLabel 480 | UILabel 481 | 482 | 483 | 484 | IBProjectSource 485 | ./Classes/ChatController.h 486 | 487 | 488 | 489 | NSLayoutConstraint 490 | NSObject 491 | 492 | IBProjectSource 493 | ./Classes/NSLayoutConstraint.h 494 | 495 | 496 | 497 | 498 | 0 499 | IBCocoaTouchFramework 500 | YES 501 | 3 502 | YES 503 | 2083 504 | 505 | 506 | -------------------------------------------------------------------------------- /CouchChat/en.lproj/ChatListController_iPad.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1552 5 | 12C3006 6 | 3084 7 | 1187.34 8 | 625.00 9 | 10 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 11 | 2083 12 | 13 | 14 | IBProxyObject 15 | IBUITableView 16 | 17 | 18 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 19 | 20 | 21 | PluginDependencyRecalculationVersion 22 | 23 | 24 | 25 | 26 | IBFilesOwner 27 | IBIPadFramework 28 | 29 | 30 | IBFirstResponder 31 | IBIPadFramework 32 | 33 | 34 | 35 | 274 36 | {{0, 20}, {320, 832}} 37 | 38 | 3 39 | MQA 40 | 41 | YES 42 | 43 | 2 44 | 45 | 46 | IBUISplitViewMasterSimulatedSizeMetrics 47 | 48 | YES 49 | 50 | 51 | 52 | 53 | 54 | {320, 852} 55 | {320, 768} 56 | 57 | 58 | IBIPadFramework 59 | Master 60 | IBUISplitViewController 61 | 62 | IBUISplitViewControllerContentSizeLocation 63 | IBUISplitViewControllerContentSizeLocationMaster 64 | 65 | 66 | IBIPadFramework 67 | YES 68 | 1 69 | 0 70 | YES 71 | 44 72 | 22 73 | 22 74 | 75 | 76 | 77 | 78 | 79 | 80 | view 81 | 82 | 83 | 84 | 3 85 | 86 | 87 | 88 | dataSource 89 | 90 | 91 | 92 | 4 93 | 94 | 95 | 96 | delegate 97 | 98 | 99 | 100 | 5 101 | 102 | 103 | 104 | 105 | 106 | 0 107 | 108 | 109 | 110 | 111 | 112 | -1 113 | 114 | 115 | File's Owner 116 | 117 | 118 | -2 119 | 120 | 121 | 122 | 123 | 2 124 | 125 | 126 | 127 | 128 | 129 | 130 | ChatListController 131 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 132 | UIResponder 133 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 134 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 135 | 136 | 137 | 138 | 139 | 140 | 5 141 | 142 | 143 | 0 144 | IBIPadFramework 145 | YES 146 | 3 147 | YES 148 | 2083 149 | 150 | 151 | -------------------------------------------------------------------------------- /CouchChat/en.lproj/ChatListController_iPhone.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1552 5 | 12C3006 6 | 3084 7 | 1187.34 8 | 625.00 9 | 10 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 11 | 2083 12 | 13 | 14 | IBProxyObject 15 | IBUITableView 16 | 17 | 18 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 19 | 20 | 21 | PluginDependencyRecalculationVersion 22 | 23 | 24 | 25 | 26 | IBFilesOwner 27 | IBCocoaTouchFramework 28 | 29 | 30 | IBFirstResponder 31 | IBCocoaTouchFramework 32 | 33 | 34 | 35 | 274 36 | {{0, 20}, {320, 548}} 37 | 38 | 39 | 40 | 3 41 | MQA 42 | 43 | YES 44 | 45 | 46 | IBUIScreenMetrics 47 | 48 | YES 49 | 50 | 51 | 52 | 53 | 54 | {320, 568} 55 | {568, 320} 56 | 57 | 58 | IBCocoaTouchFramework 59 | Retina 4 Full Screen 60 | 2 61 | 62 | IBCocoaTouchFramework 63 | YES 64 | 1 65 | 0 66 | YES 67 | 44 68 | 22 69 | 22 70 | 71 | 72 | 73 | 74 | 75 | 76 | view 77 | 78 | 79 | 80 | 3 81 | 82 | 83 | 84 | _table 85 | 86 | 87 | 88 | 9 89 | 90 | 91 | 92 | delegate 93 | 94 | 95 | 96 | 5 97 | 98 | 99 | 100 | dataSource 101 | 102 | 103 | 104 | 11 105 | 106 | 107 | 108 | 109 | 110 | 0 111 | 112 | 113 | 114 | 115 | 116 | -1 117 | 118 | 119 | File's Owner 120 | 121 | 122 | -2 123 | 124 | 125 | 126 | 127 | 2 128 | 129 | 130 | 131 | 132 | 133 | 134 | ChatListController 135 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 136 | UIResponder 137 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 138 | com.apple.InterfaceBuilder.IBCocoaTouchPlugin 139 | 140 | 141 | 142 | 143 | 144 | 11 145 | 146 | 147 | 148 | 149 | ChatListController 150 | UIViewController 151 | 152 | newChat: 153 | id 154 | 155 | 156 | newChat: 157 | 158 | newChat: 159 | id 160 | 161 | 162 | 163 | _table 164 | UITableView 165 | 166 | 167 | _table 168 | 169 | _table 170 | UITableView 171 | 172 | 173 | 174 | IBProjectSource 175 | ./Classes/ChatListController.h 176 | 177 | 178 | 179 | 180 | 0 181 | IBCocoaTouchFramework 182 | YES 183 | 3 184 | YES 185 | 2083 186 | 187 | 188 | -------------------------------------------------------------------------------- /CouchChat/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /CouchChat/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // CouchChat 4 | // 5 | // Created by Jens Alfke on 2/13/13. 6 | // Copyright (c) 2013 Couchbase. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "AppDelegate.h" 12 | 13 | int main(int argc, char *argv[]) 14 | { 15 | @autoreleasepool { 16 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Frameworks/README.txt: -------------------------------------------------------------------------------- 1 | Put CouchbaseLite.framework in here 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CouchChat demo for Couchbase Lite 2 | 3 | **Warning: this example is very out of date, so expect to spend some effort wrangling with it. Check our [sample app list](http://developer.couchbase.com/mobile/develop/samples/samples/index.html) for an updated list.** 4 | 5 | CouchChat is a multi-user messaging app, essentially a primitive clone of the iOS Messages app. It illustrates how you can use Couchbase Lite in your mobile apps to share data across devices and among users. If you familiarize yourself with this code, you'll be ready to write your own multi-user interactive data driven applications on iOS. 6 | 7 | There is a [tour of the data model](https://github.com/couchbaselabs/CouchChat-iOS/wiki/Chat-App-Data-Model) on the wiki. 8 | 9 | ## Architecture 10 | 11 | There are three main components to the system: 12 | 13 | * This app, which embeds [Couchbase Lite](https://github.com/couchbase/couchbase-lite-ios) for iOS. 14 | * The Couchbase [Sync Gateway](https://github.com/couchbase/sync_gateway), which runs on a server and handles the synchronization connections from mobile devices. 15 | * [Couchbase Server 2](http://www.couchbase.com/download) for data storage. (For development this is optional; you can instead use a very simple built-in data store called Walrus.) 16 | 17 | Couchbase Server should be deployed behind your firewall (like databases normally are), and then the Sync Gateway should be deployed where it can be accessed by mobile devices from the public internet, and it can reach Couchbase Server. Mobile devices connect to the Sync Gateway, which enforces access control and update validation policies. 18 | 19 | ![Couchbase Mobile Architecture](http://jchris.ic.ht/files/slides/mobile-arch.png) 20 | 21 | ## Server setup 22 | 23 | Before you can run the app, you'll need to install the Couchbase Sync Gateway and configure it for chat. 24 | 25 | ### Install the Sync Gateway 26 | 27 | Download a copy of the Sync Gateway for your platform from [Couchbase's download page](http://www.couchbase.com/download#cb-mobile). (Or you can [build it from source](https://github.com/couchbaselabs/sync_gateway) if you want.) When you're done you'll be the proud owner of a `sync_gateway` command-line tool. 28 | 29 | ### Configure the gateway for chat 30 | 31 | The CouchChat repo contains a configuration file for the gateway, `sync-gateway-config.json`. Put the path to the config file on the command line when you launch the gateway. 32 | 33 | The Gateway needs to know the URL that clients connect to it at, in order for Persona authentication to work properly. It gets this URL from the `persona.origin` property in the configuration file. It should consist only of the root URL of the server, including the port but without any path or trailing slash. Edit line 4 of `sync-gateway-config.json` to contain the URL that your Sync Gateway can be reached at, for example: 34 | 35 | "origin": "http://myserver.example.com:4984/", 36 | 37 | The example config file uses an in-memory Walrus bucket, so all your data will be lost when the Sync Gateway exits. This is fine for experimenting, but if you want to store data persistently you can edit the `databases.chat.server` property of the config file to point to an existing directory where you'd like to store your data. In production you should replace the `walrus:` url with a URL to Couchbase Server's 8091 port. 38 | 39 | Now you can launch the Sync Gateway, passing it the path to the configuration file: 40 | 41 | sync_gateway ~/code/CouchChat-iOS/sync-gateway-config.json 42 | 43 | ## Running the iOS app 44 | 45 | ### Install the submodules: 46 | 47 | git submodule init 48 | git submodule update 49 | 50 | ### Build/Install the Couchbase Lite framework 51 | 52 | [Download Couchbase Lite for iOS](http://www.couchbase.com/download#cb-mobile) from Couchbase. Or if you want to build it from source, follow [the directions in its wiki](https://github.com/couchbase/couchbase-lite-ios/wiki/Building-Couchbase-Lite#building-the-framework). 53 | 54 | Copy the `CouchbaseLite.framework` into the the CouchChat repository's `Frameworks/` folder. 55 | 56 | ### Configure the server URL 57 | 58 | Open CouchChat.xcodeproj, and change the value of `kServerDBURLString` in `AppDelegate.m` to the public URL of your Sync Gateway's chat database. This should match the value of the `-personaOrigin` command-line flag given to the Sync Gateway, but with the database name (default is `chat`) appended. For example, you might change that line to: 59 | 60 | #define kServerDBURLString http://animal.local:4984/chat 61 | 62 | ### Build and run the app 63 | 64 | Now you can build and run your app in the simulator or on a connected iOS device. 65 | 66 | After startup it will prompt you to login with Mozilla Persona. Once you are logged in, you can: 67 | 68 | * Create a chat room 69 | * Invite other users 70 | * Send messages to users. 71 | * Attach pictures to a message (or take a picture if your device has a camera.) 72 | 73 | Any message in a chat room will show up on all devices that are subscribed to that room. 74 | 75 | ## Running the PhoneGap version 76 | 77 | For extra credit you can try running the HTML5 version of CouchChat against the same Sync Gateway database. The HTML5 version of the app is under development in our [Couchbase Lite PhoneGap Kit](https://github.com/couchbaselabs/Couchbase-Lite-PhoneGap-Kit) repository. 78 | 79 | ## How the data flows 80 | 81 | The JavaScript sync function in the server config file (`sync-gateway-config.json`) determines how data flows between mobile devices, or it can throw an error if a given update is not allowed to proceed. Read the Sync Gateway documentation for more details. 82 | 83 | Read the [tour of the data model](https://github.com/couchbaselabs/CouchChat-iOS/wiki/Chat-App-Data-Model) to learn how the sync function for CouchChat works. 84 | 85 | 86 | -------------------------------------------------------------------------------- /Resources/Camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/CouchChat-iOS/656704ed7661223cd2856c5600b3c3c178c6caf4/Resources/Camera.png -------------------------------------------------------------------------------- /Resources/ChatIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/CouchChat-iOS/656704ed7661223cd2856c5600b3c3c178c6caf4/Resources/ChatIcon.png -------------------------------------------------------------------------------- /Resources/ChatIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/CouchChat-iOS/656704ed7661223cd2856c5600b3c3c178c6caf4/Resources/ChatIcon@2x.png -------------------------------------------------------------------------------- /Resources/double_lined.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/CouchChat-iOS/656704ed7661223cd2856c5600b3c3c178c6caf4/Resources/double_lined.png -------------------------------------------------------------------------------- /iPhone icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/CouchChat-iOS/656704ed7661223cd2856c5600b3c3c178c6caf4/iPhone icon.png -------------------------------------------------------------------------------- /iPhone icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/CouchChat-iOS/656704ed7661223cd2856c5600b3c3c178c6caf4/iPhone icon@2x.png -------------------------------------------------------------------------------- /sync-gateway-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "log": ["CRUD", "REST+"], 3 | "persona": { 4 | "origin": "http://mineral.local:4984/", 5 | "register": true 6 | }, 7 | "databases": { 8 | "chat": { 9 | "server": "walrus:", 10 | "users": { 11 | "GUEST": {"disabled": true} 12 | }, 13 | "sync": ` 14 | 15 | function(doc, oldDoc) { 16 | if (doc.channel_id) { 17 | // doc belongs to a channel 18 | channel("ch-"+doc.channel_id); 19 | // this document describes a channel 20 | if (doc.channel_id == doc._id) { 21 | // magic document, treat it carefully 22 | if (oldDoc) { 23 | requireUser(oldDoc.owners) 24 | } 25 | if (!Array.isArray(doc.owners)) { 26 | throw({forbidden : "owners must be an array"}) 27 | } 28 | // grants access to the channel to all members and owners 29 | access(doc.owners, "ch-"+doc._id); 30 | if (Array.isArray(doc.members)) { 31 | access(doc.members, "ch-"+doc._id); 32 | } 33 | } 34 | } 35 | if (doc.type == "profile") { 36 | channel("profiles"); 37 | var user = doc._id.substring(doc._id.indexOf(":")+1); 38 | requireUser(user); 39 | access(user, "profiles"); 40 | } 41 | } 42 | 43 | ` 44 | } 45 | } 46 | } 47 | --------------------------------------------------------------------------------