├── RChatMinimalServer ├── .vscode │ ├── settings.json │ ├── tasks.json │ └── launch.json ├── .gitignore ├── resetRealm.sh ├── tsconfig.json ├── package.json └── src │ └── index.ts ├── Graphics ├── RChat-screen.png ├── RChat-DataModel.png ├── RChat-DataModel.graffle ├── RChat-Studio-View.png └── RealmStudio-admin-privs.png ├── RChat-iOS ├── RChat │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── camera-50.imageset │ │ │ ├── camera-50.pdf │ │ │ └── Contents.json │ │ ├── menu_icon.imageset │ │ │ ├── menu_icon@3x.png │ │ │ └── Contents.json │ │ ├── pen_icon.imageset │ │ │ ├── pen_icon@3x.png │ │ │ └── Contents.json │ │ ├── send_icon.imageset │ │ │ ├── send_icon@3x.png │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── AppIcon-20x20@1x.png │ │ │ ├── AppIcon-29x29@1x.png │ │ │ ├── AppIcon-29x29@2x.png │ │ │ ├── AppIcon-29x29@3x.png │ │ │ ├── AppIcon-40x40@1x.png │ │ │ ├── AppIcon-40x40@2x.png │ │ │ ├── AppIcon-60x60@1x.png │ │ │ ├── AppIcon-60x60@2x.png │ │ │ ├── AppIcon-60x60@3x.png │ │ │ ├── AppIcon-76x76@1x.png │ │ │ ├── AppIcon-76x76@2x.png │ │ │ ├── AppIcon-29x29@2x-1.png │ │ │ ├── AppIcon-40x40@1x-1.png │ │ │ ├── AppIcon-40x40@1x-3.png │ │ │ ├── AppIcon-40x40@2x-1.png │ │ │ ├── AppIcon-60x60@2x-1.png │ │ │ ├── AppIcon-83.5x83.5@2x.png │ │ │ └── Contents.json │ │ ├── attach_icon.imageset │ │ │ ├── attach_icon@3x.png │ │ │ └── Contents.json │ │ ├── launch_logo.imageset │ │ │ ├── launch_logo@3x.png │ │ │ └── Contents.json │ │ ├── profile_icon.imageset │ │ │ ├── profile_icon@3x.png │ │ │ └── Contents.json │ │ └── more_verticle_icon.imageset │ │ │ ├── more_verticle_icon@3x.png │ │ │ └── Contents.json │ ├── AppDelegate+RealmSetup.swift │ ├── RChatMessageViewModelProtocol.swift │ ├── RChatMessageModelProtocol.swift │ ├── String+Extensions.swift │ ├── SendingStatusCollectionViewCell.swift │ ├── UIViewController+Extensions.swift │ ├── MimeType.swift │ ├── RChatCollectionViewLayout.swift │ ├── RChatTextMessageModel.swift │ ├── Images │ │ ├── RChatImageMessageViewModel.swift │ │ ├── RChatImageMessageCollectionViewCellStyle.swift │ │ ├── RChatImageMessageViewModelBuilder.swift │ │ ├── RChatImageMessageModel.swift │ │ └── RChatImageMessageHandler.swift │ ├── RChatButton.swift │ ├── WelcomeViewModel.swift │ ├── SendingStatusModel.swift │ ├── CustomNavController.swift │ ├── RTextField.swift │ ├── TimeSeperatorPresenterBuilder.swift │ ├── RChatTextMessageCollectionViewCellStyle.swift │ ├── RChatBaseMessageHandler.swift │ ├── SendingStatusPresenterBuilder.swift │ ├── RChatMessageCollectionViewCellAvatarStyle.swift │ ├── RChatTextMessageViewModelBuilder.swift │ ├── UIColor+Hex.swift │ ├── TimeSeperatorModel.swift │ ├── ComposeUserTableViewCell.swift │ ├── RChatTextMessageHandler.swift │ ├── MembersTableViewCell.swift │ ├── ChatMessage+MessageModelProtocol.swift │ ├── ColorLabel.swift │ ├── ProfileImageRow.swift │ ├── SettingsViewController+ImagePicking.swift │ ├── TimeSeparatorPresenter.swift │ ├── TimeSeperatorCollectionViewCell.swift │ ├── SearchResultTableViewCell.swift │ ├── ChatViewController+ImageDelegate.swift │ ├── User.swift │ ├── RChatTextMessageViewModel.swift │ ├── RChatRecipientBar.swift │ ├── SendingStatusPresenter.swift │ ├── ChatMessage.swift │ ├── Info.plist │ ├── AppDelegate.swift │ ├── SettingsViewModel.swift │ ├── LoadingView.swift │ ├── SendingStatusCollectionViewCell.xib │ ├── ChatViewModel.swift │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ ├── ConversationTableViewCell .swift │ ├── LoginViewController.swift │ ├── Conversation.swift │ ├── RChatDecorator.swift │ ├── WelcomeViewController.swift │ ├── RChatConstants.swift │ ├── MembersViewController.swift │ ├── ComposeViewController.swift │ ├── SearchResultsViewController.swift │ ├── RChatInputView.swift │ ├── RLMLoginViewController.swift │ ├── SettingsViewController.swift │ ├── LoginViewModel.swift │ ├── ConversationSearchView.swift │ ├── ChatViewController.swift │ └── ConversationsViewController.swift ├── RChat.xcodeproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata ├── RChat.xcworkspace │ └── contents.xcworkspacedata ├── RChatTests │ ├── Info.plist │ └── RChatTests.swift ├── RChatUITests │ ├── Info.plist │ └── RChatUITests.swift ├── Podfile ├── Extensions │ ├── TextField+Utilities.swift │ ├── String+Utilities.swift │ ├── Bundle+Utilities.swift │ ├── View+Utilities.swift │ └── Color+Uilities.swift └── Podfile.lock ├── .gitignore ├── CONTRIBUTING.md └── Readme.md /RChatMinimalServer/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "git.ignoreLimitWarning": true 3 | } -------------------------------------------------------------------------------- /Graphics/RChat-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realm/roc-ios/HEAD/Graphics/RChat-screen.png -------------------------------------------------------------------------------- /Graphics/RChat-DataModel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realm/roc-ios/HEAD/Graphics/RChat-DataModel.png -------------------------------------------------------------------------------- /Graphics/RChat-DataModel.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realm/roc-ios/HEAD/Graphics/RChat-DataModel.graffle -------------------------------------------------------------------------------- /Graphics/RChat-Studio-View.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realm/roc-ios/HEAD/Graphics/RChat-Studio-View.png -------------------------------------------------------------------------------- /Graphics/RealmStudio-admin-privs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realm/roc-ios/HEAD/Graphics/RealmStudio-admin-privs.png -------------------------------------------------------------------------------- /RChat-iOS/RChat/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /RChatMinimalServer/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | DataLoaded.txt 3 | node_modules 4 | data 5 | realm-object-server 6 | *~ 7 | dist/* 8 | \#* 9 | 10 | *.xcuserdatad 11 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/Assets.xcassets/camera-50.imageset/camera-50.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realm/roc-ios/HEAD/RChat-iOS/RChat/Assets.xcassets/camera-50.imageset/camera-50.pdf -------------------------------------------------------------------------------- /RChat-iOS/RChat/Assets.xcassets/menu_icon.imageset/menu_icon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realm/roc-ios/HEAD/RChat-iOS/RChat/Assets.xcassets/menu_icon.imageset/menu_icon@3x.png -------------------------------------------------------------------------------- /RChat-iOS/RChat/Assets.xcassets/pen_icon.imageset/pen_icon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realm/roc-ios/HEAD/RChat-iOS/RChat/Assets.xcassets/pen_icon.imageset/pen_icon@3x.png -------------------------------------------------------------------------------- /RChat-iOS/RChat/Assets.xcassets/send_icon.imageset/send_icon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realm/roc-ios/HEAD/RChat-iOS/RChat/Assets.xcassets/send_icon.imageset/send_icon@3x.png -------------------------------------------------------------------------------- /RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realm/roc-ios/HEAD/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@1x.png -------------------------------------------------------------------------------- /RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realm/roc-ios/HEAD/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@1x.png -------------------------------------------------------------------------------- /RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realm/roc-ios/HEAD/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x.png -------------------------------------------------------------------------------- /RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realm/roc-ios/HEAD/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@3x.png -------------------------------------------------------------------------------- /RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realm/roc-ios/HEAD/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@1x.png -------------------------------------------------------------------------------- /RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realm/roc-ios/HEAD/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x.png -------------------------------------------------------------------------------- /RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realm/roc-ios/HEAD/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@1x.png -------------------------------------------------------------------------------- /RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realm/roc-ios/HEAD/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@2x.png -------------------------------------------------------------------------------- /RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realm/roc-ios/HEAD/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@3x.png -------------------------------------------------------------------------------- /RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realm/roc-ios/HEAD/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@1x.png -------------------------------------------------------------------------------- /RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realm/roc-ios/HEAD/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@2x.png -------------------------------------------------------------------------------- /RChat-iOS/RChat/Assets.xcassets/attach_icon.imageset/attach_icon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realm/roc-ios/HEAD/RChat-iOS/RChat/Assets.xcassets/attach_icon.imageset/attach_icon@3x.png -------------------------------------------------------------------------------- /RChat-iOS/RChat/Assets.xcassets/launch_logo.imageset/launch_logo@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realm/roc-ios/HEAD/RChat-iOS/RChat/Assets.xcassets/launch_logo.imageset/launch_logo@3x.png -------------------------------------------------------------------------------- /RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realm/roc-ios/HEAD/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x-1.png -------------------------------------------------------------------------------- /RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@1x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realm/roc-ios/HEAD/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@1x-1.png -------------------------------------------------------------------------------- /RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@1x-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realm/roc-ios/HEAD/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@1x-3.png -------------------------------------------------------------------------------- /RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realm/roc-ios/HEAD/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x-1.png -------------------------------------------------------------------------------- /RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realm/roc-ios/HEAD/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@2x-1.png -------------------------------------------------------------------------------- /RChat-iOS/RChat/Assets.xcassets/profile_icon.imageset/profile_icon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realm/roc-ios/HEAD/RChat-iOS/RChat/Assets.xcassets/profile_icon.imageset/profile_icon@3x.png -------------------------------------------------------------------------------- /RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realm/roc-ios/HEAD/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5x83.5@2x.png -------------------------------------------------------------------------------- /RChat-iOS/RChat/Assets.xcassets/more_verticle_icon.imageset/more_verticle_icon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realm/roc-ios/HEAD/RChat-iOS/RChat/Assets.xcassets/more_verticle_icon.imageset/more_verticle_icon@3x.png -------------------------------------------------------------------------------- /RChat-iOS/RChat.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/Assets.xcassets/camera-50.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "camera-50.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /RChat-iOS/RChat/AppDelegate+RealmSetup.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate+RealmSetup.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 1/21/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RealmSwift 11 | 12 | extension AppDelegate { 13 | 14 | func setupRealm(){ 15 | 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/RChatMessageViewModelProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RChatMessageViewModelProtocol.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 1/10/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol RChatMessageViewModelProtocol { 12 | var messageModel: RChatMessageModelProtocol { get } 13 | } 14 | -------------------------------------------------------------------------------- /RChat-iOS/RChat.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /RChatMinimalServer/resetRealm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | read -r -p "This will remove all Realms and data. Are you sure? [Y/n]" response 3 | 4 | response=$(echo "$response" | tr '[:upper:]' '[:lower:]') 5 | if [[ $response =~ ^(yes|y| ) ]] || [[ -z $response ]]; then 6 | echo "Removing Realm files..." 7 | rm -rvf realm-object-server data DataLoaded.txt 8 | echo "Done." 9 | fi 10 | 11 | 12 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/RChatMessageModelProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RChatMessageModelProtocol.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 1/10/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Chatto 11 | import ChattoAdditions 12 | 13 | protocol RChatMessageModelProtocol: MessageModelProtocol { 14 | var status: MessageStatus { get set } 15 | } 16 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/Assets.xcassets/pen_icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "filename" : "pen_icon@3x.png", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /RChat-iOS/RChat/Assets.xcassets/attach_icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "filename" : "attach_icon@3x.png", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /RChat-iOS/RChat/Assets.xcassets/launch_logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "filename" : "launch_logo@3x.png", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /RChat-iOS/RChat/Assets.xcassets/menu_icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "filename" : "menu_icon@3x.png", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /RChat-iOS/RChat/Assets.xcassets/send_icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "filename" : "send_icon@3x.png", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /RChat-iOS/RChat/Assets.xcassets/profile_icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "filename" : "profile_icon@3x.png", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /RChat-iOS/RChat/String+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Extensions.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 2/3/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension String { 12 | var isEmptyOrWhitespace : Bool { 13 | if(self.isEmpty) { 14 | return true 15 | } 16 | return (self.trimmingCharacters(in: .whitespaces).isEmpty) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/Assets.xcassets/more_verticle_icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "filename" : "more_verticle_icon@3x.png", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /RChat-iOS/RChat/SendingStatusCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SendingStatusCollectionViewCell.swift 3 | // Eden 4 | // 5 | // Created by Max Alexander on 12/31/16. 6 | // Copyright © 2016 Epoque. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SendingStatusCollectionViewCell: UICollectionViewCell { 12 | 13 | @IBOutlet private weak var label: UILabel! 14 | 15 | var text: NSAttributedString? { 16 | didSet { 17 | self.label.attributedText = self.text 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/UIViewController+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+Extensions.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 2/3/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIViewController { 12 | 13 | // This gets rid of the backBarButtonItem's title for FUTURE pushed viewcontrollers 14 | func removeBackButtonTitle() { 15 | navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil) 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /RChatMinimalServer/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "0.1.0", 5 | "command": "npm", 6 | "isShellCommand": true, 7 | "showOutput": "always", 8 | "suppressTaskName": true, 9 | "tasks": [ 10 | { 11 | "taskName": "test", 12 | "args": ["run", "test"], 13 | "isTestCommand": true 14 | }, 15 | { 16 | "taskName": "build", 17 | "args": ["run", "build"], 18 | "isBuildCommand": true 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /RChatMinimalServer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "noImplicitAny": false, 7 | "removeComments": true, 8 | "preserveConstEnums": true, 9 | "sourceMap": true, 10 | "outDir": "dist", 11 | "sourceRoot": "src", 12 | "declaration": true, 13 | "emitDecoratorMetadata": true, 14 | "experimentalDecorators": true 15 | }, 16 | "include": [ 17 | "src/**/*.ts" 18 | ], 19 | "exclude": [ 20 | "node_modules", 21 | "dist" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/MimeType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MimeType.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 1/10/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum MimeType : String { 12 | case textPlain = "text/plain" 13 | case textMarkdown = "text/markdown" 14 | case imageJPEG = "image/jpeg" 15 | case imageGIF = "image/gif" 16 | case imagePNG = "image/png" 17 | 18 | /// This is a helper to remind you to attempt to load a URL 19 | var isImage : Bool { 20 | return self == .imageGIF 21 | || self == .imageJPEG 22 | || self == .imagePNG 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/RChatCollectionViewLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RChatCollectionViewLayout.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 1/10/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Chatto 11 | import ChattoAdditions 12 | 13 | class RChatCollectionViewLayout: ChatCollectionViewLayout { 14 | 15 | 16 | override func initialLayoutAttributesForAppearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? { 17 | let attributes = super.initialLayoutAttributesForAppearingItem(at: itemIndexPath) 18 | 19 | attributes?.center = CGPoint(x: 0, y: 0) 20 | attributes?.alpha = 0 21 | 22 | 23 | return attributes 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/RChatTextMessageModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RChatTextMessageModel.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 1/10/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ChattoAdditions 11 | 12 | 13 | class RChatTextMessageModel: TextMessageModel, RChatMessageModelProtocol { 14 | init(messageModel: ChatMessage){ 15 | super.init(messageModel: messageModel, text: messageModel.text) 16 | } 17 | 18 | var status: MessageStatus { 19 | get { 20 | return self._messageModel.status 21 | } 22 | set { 23 | // self._messageModel.status = newValue 24 | // 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/Images/RChatImageMessageViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RChatTextMessageViewModel.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 1/10/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ChattoAdditions 11 | import SDWebImage 12 | 13 | 14 | class RChatImageMessageViewModel: PhotoMessageViewModel, RChatMessageViewModelProtocol { 15 | override init(photoMessage: RChatImageMessageModel, messageViewModel: MessageViewModelProtocol) { 16 | super.init(photoMessage: photoMessage, messageViewModel: messageViewModel) 17 | } 18 | 19 | var messageModel: RChatMessageModelProtocol { 20 | return self.messageModel 21 | } 22 | 23 | } 24 | 25 | 26 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/RChatButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RChatButton.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 1/20/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class RChatButton : UIButton { 12 | 13 | init(){ 14 | super.init(frame: .zero) 15 | layer.cornerRadius = 4.0 16 | layer.masksToBounds = true 17 | layer.backgroundColor = RChatConstants.Colors.primaryColor.cgColor 18 | setTitleColor(.white, for: .normal) 19 | setTitleColor(.lightGray, for: .disabled) 20 | titleLabel?.font = RChatConstants.Fonts.boldFont 21 | } 22 | 23 | required init?(coder aDecoder: NSCoder) { 24 | fatalError("init(coder:) has not been implemented") 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/WelcomeViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WelcomeViewModel.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 1/20/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RealmSwift 11 | 12 | class WelcomeViewModel { 13 | 14 | // FROM UI 15 | func loginDidTap(){ 16 | goToLogin?() 17 | } 18 | 19 | func registerDidTap(){ 20 | goToSignup?() 21 | } 22 | 23 | // TO UI 24 | var isAlreadyLoggedIn : ((_ isLoggedIn: Bool) -> Void)? { 25 | didSet { 26 | isAlreadyLoggedIn?(RChatConstants.isLoggedIn) 27 | } 28 | } 29 | 30 | var goToLogin : (() -> Void)? 31 | var goToSignup : (() -> Void)? 32 | 33 | init(){ 34 | 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /RChat-iOS/RChatTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /RChat-iOS/RChatUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/SendingStatusModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SendingStatusModel.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 1/10/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Chatto 11 | import ChattoAdditions 12 | 13 | // This is a dirty implementation that shows what's needed to add a new type of element 14 | // @see ChatItemsDemoDecorator 15 | 16 | class SendingStatusModel: ChatItemProtocol { 17 | let uid: String 18 | static var chatItemType: ChatItemType { 19 | return "decoration-status" 20 | } 21 | 22 | var type: String { return SendingStatusModel.chatItemType } 23 | let status: MessageStatus 24 | 25 | init (uid: String, status: MessageStatus) { 26 | self.uid = uid 27 | self.status = status 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /RChatMinimalServer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "minimal-rchat-server", 3 | "version": "0.0.1", 4 | "license": "MIT", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/realm/roc-ios.git" 8 | }, 9 | "keywords": [ 10 | "realm", 11 | "chat" 12 | ], 13 | "author": "David Spector, ds@realm.io", 14 | "description": "A minimal server for RChat under ROS2.0", 15 | "main": "src/index.js", 16 | "scripts": { 17 | "build": "rm -rf dist; ./node_modules/.bin/tsc", 18 | "clean": "rm -rf dist", 19 | "start": "npm run build && node dist/index.js" 20 | }, 21 | "devDependencies": { 22 | "typescript": "2.5.3" 23 | }, 24 | "dependencies": { 25 | "realm-object-server": "2.0.13", 26 | "ts-node": "*" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/CustomNavController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomNavController.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 2/1/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class CustomNavController : UINavigationController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | navigationBar.tintColor = UIColor.white 16 | navigationBar.barTintColor = RChatConstants.Colors.primaryColorDark 17 | navigationBar.isOpaque = true 18 | navigationBar.isTranslucent = false 19 | navigationBar.titleTextAttributes = [ 20 | NSForegroundColorAttributeName : UIColor.white 21 | ] 22 | } 23 | 24 | override var preferredStatusBarStyle: UIStatusBarStyle { 25 | return .lightContent 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/RTextField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RTextField.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 2/1/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | import UIKit 9 | 10 | class RTextField: UITextField { 11 | 12 | var insetX: CGFloat = 0 13 | var insetY: CGFloat = 0 14 | 15 | init(){ 16 | super.init(frame: .zero) 17 | } 18 | 19 | required init?(coder aDecoder: NSCoder) { 20 | super.init(coder: aDecoder) 21 | } 22 | 23 | func commonInit(){ 24 | font = RChatConstants.Fonts.regularFont 25 | } 26 | 27 | override func textRect(forBounds bounds: CGRect) -> CGRect { 28 | return bounds.insetBy(dx: insetX, dy: insetY) 29 | } 30 | 31 | override func editingRect(forBounds bounds: CGRect) -> CGRect { 32 | return textRect(forBounds: bounds) 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/TimeSeperatorPresenterBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimeSeperatorPresenterBuilder.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 1/10/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Chatto 11 | 12 | public class TimeSeparatorPresenterBuilder: ChatItemPresenterBuilderProtocol { 13 | 14 | public func canHandleChatItem(_ chatItem: ChatItemProtocol) -> Bool { 15 | return chatItem is TimeSeparatorModel 16 | } 17 | 18 | public func createPresenterWithChatItem(_ chatItem: ChatItemProtocol) -> ChatItemPresenterProtocol { 19 | assert(self.canHandleChatItem(chatItem)) 20 | return TimeSeparatorPresenter(timeSeparatorModel: chatItem as! TimeSeparatorModel) 21 | } 22 | 23 | public var presenterType: ChatItemPresenterProtocol.Type { 24 | return TimeSeparatorPresenter.self 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/RChatTextMessageCollectionViewCellStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EdenTextMessageCollectionViewStyle.swift 3 | // Eden 4 | // 5 | // Created by Max Alexander on 12/31/16. 6 | // Copyright © 2016 Epoque. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Chatto 11 | import ChattoAdditions 12 | 13 | class RChatTextMessageCollectionViewCellStyle : TextMessageCollectionViewCellDefaultStyle { 14 | 15 | init(){ 16 | let colors = BaseMessageCollectionViewCellDefaultStyle.Colors(incoming: RChatConstants.Colors.clouds, outgoing: RChatConstants.Colors.primaryColor) 17 | let style = BaseMessageCollectionViewCellDefaultStyle(colors: colors) 18 | super.init(baseStyle: style ) 19 | 20 | } 21 | 22 | override func textFont(viewModel: TextMessageViewModelProtocol, isSelected: Bool) -> UIFont { 23 | return RChatConstants.Fonts.regularFont 24 | } 25 | 26 | 27 | 28 | } 29 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/Images/RChatImageMessageCollectionViewCellStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EdenTextMessageCollectionViewStyle.swift 3 | // Eden 4 | // 5 | // Created by Max Alexander on 12/31/16. 6 | // Copyright © 2016 Epoque. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Chatto 11 | import ChattoAdditions 12 | 13 | class RChatImageMessageCollectionViewCellStyle : TextMessageCollectionViewCellDefaultStyle { 14 | 15 | init(){ 16 | let colors = BaseMessageCollectionViewCellDefaultStyle.Colors(incoming: RChatConstants.Colors.clouds, outgoing: RChatConstants.Colors.primaryColor) 17 | let style = BaseMessageCollectionViewCellDefaultStyle(colors: colors) 18 | super.init(baseStyle: style) 19 | 20 | } 21 | 22 | override func textFont(viewModel: TextMessageViewModelProtocol, isSelected: Bool) -> UIFont { 23 | return RChatConstants.Fonts.regularFont 24 | } 25 | 26 | 27 | 28 | } 29 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/RChatBaseMessageHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RChatBaseMessageHandler.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 1/10/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Chatto 11 | import ChattoAdditions 12 | 13 | class RChatBaseMessageHandler { 14 | 15 | func userDidTapOnFailIcon(viewModel: RChatMessageViewModelProtocol) { 16 | 17 | } 18 | 19 | func userDidTapOnAvatar(viewModel: RChatMessageViewModelProtocol) { 20 | } 21 | 22 | func userDidTapOnBubble(viewModel: RChatMessageViewModelProtocol) { 23 | print("userDidTapOnBubble") 24 | } 25 | 26 | func userDidBeginLongPressOnBubble(viewModel: RChatMessageViewModelProtocol) { 27 | print("userDidBeginLongPressOnBubble") 28 | } 29 | 30 | func userDidEndLongPressOnBubble(viewModel: RChatMessageViewModelProtocol) { 31 | print("userDidEndLongPressOnBubble") 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/SendingStatusPresenterBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SendingStatusPresenterBuilder.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 1/10/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Chatto 11 | import ChattoAdditions 12 | 13 | class SendingStatusPresenterBuilder: ChatItemPresenterBuilderProtocol { 14 | 15 | public func canHandleChatItem(_ chatItem: ChatItemProtocol) -> Bool { 16 | return chatItem is SendingStatusModel ? true : false 17 | } 18 | 19 | public func createPresenterWithChatItem(_ chatItem: ChatItemProtocol) -> ChatItemPresenterProtocol { 20 | assert(self.canHandleChatItem(chatItem)) 21 | return SendingStatusPresenter( 22 | statusModel: chatItem as! SendingStatusModel 23 | ) 24 | } 25 | 26 | public var presenterType: ChatItemPresenterProtocol.Type { 27 | return SendingStatusPresenter.self 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/RChatMessageCollectionViewCellAvatarStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RChatMessageCollectionViewCellAvatarStyle.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 1/10/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ChattoAdditions 11 | 12 | class RChatMessageCollectionViewCellAvatarStyle: BaseMessageCollectionViewCellDefaultStyle { 13 | 14 | init(){ 15 | let dateStyle = BaseMessageCollectionViewCellDefaultStyle.DateTextStyle(font: RChatConstants.Fonts.dateFont, color: UIColor.darkGray) 16 | super.init(dateTextStyle: dateStyle) 17 | baseColorOutgoing = RChatConstants.Colors.primaryColor 18 | } 19 | 20 | override func avatarSize(viewModel: MessageViewModelProtocol) -> CGSize { 21 | // Display avatar for both incoming and outgoing messages for demo purpose 22 | return viewModel.isIncoming ? CGSize(width: 35, height: 35) : CGSize.zero 23 | } 24 | 25 | 26 | 27 | } 28 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/RChatTextMessageViewModelBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RChatTextMessageViewModelBuilder.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 1/10/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ChattoAdditions 11 | import SDWebImage 12 | 13 | class RChatTextMessageViewModelBuilder: ViewModelBuilderProtocol { 14 | init() {} 15 | 16 | let messageViewModelBuilder = MessageViewModelDefaultBuilder() 17 | 18 | func createViewModel(_ textMessage: RChatTextMessageModel) -> RChatTextMessageViewModel { 19 | let messageViewModel = self.messageViewModelBuilder.createMessageViewModel(textMessage) 20 | let textMessageViewModel = RChatTextMessageViewModel(textMessage: textMessage, messageViewModel: messageViewModel) 21 | return textMessageViewModel 22 | } 23 | 24 | func canCreateViewModel(fromModel model: Any) -> Bool { 25 | return model is RChatTextMessageModel 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/Images/RChatImageMessageViewModelBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RChatImageMessageViewModelBuilder.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 1/10/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ChattoAdditions 11 | import SDWebImage 12 | 13 | class RChatImageMessageViewModelBuilder: ViewModelBuilderProtocol { 14 | init() {} 15 | 16 | let messageViewModelBuilder = MessageViewModelDefaultBuilder() 17 | 18 | func createViewModel(_ message: RChatImageMessageModel) -> RChatImageMessageViewModel { 19 | let messageViewModel = self.messageViewModelBuilder.createMessageViewModel(message) 20 | let imageMessageViewModel = RChatImageMessageViewModel(photoMessage: message, messageViewModel: messageViewModel) 21 | return imageMessageViewModel 22 | } 23 | 24 | func canCreateViewModel(fromModel model: Any) -> Bool { 25 | return model is RChatImageMessageModel 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/UIColor+Hex.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+Hex.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 1/10/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIColor { 12 | convenience init(hexString: String) { 13 | let hex = hexString.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) 14 | var int = UInt32() 15 | Scanner(string: hex).scanHexInt32(&int) 16 | let a, r, g, b: UInt32 17 | switch hex.characters.count { 18 | case 3: // RGB (12-bit) 19 | (a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17) 20 | case 6: // RGB (24-bit) 21 | (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF) 22 | case 8: // ARGB (32-bit) 23 | (a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF) 24 | default: 25 | (a, r, g, b) = (255, 0, 0, 0) 26 | } 27 | self.init(red: CGFloat(r) / 255, green: CGFloat(g) / 255, blue: CGFloat(b) / 255, alpha: CGFloat(a) / 255) 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /RChat-iOS/RChatTests/RChatTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RChatTests.swift 3 | // RChatTests 4 | // 5 | // Created by Max Alexander on 1/9/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import RChat 11 | 12 | class RChatTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | // Use XCTAssert and related functions to verify your tests produce the correct results. 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/TimeSeperatorModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimeSeperatorModel.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 1/10/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | import Chatto 12 | 13 | class TimeSeparatorModel: ChatItemProtocol { 14 | let uid: String 15 | let type: String = TimeSeparatorModel.chatItemType 16 | let date: String 17 | 18 | static var chatItemType: ChatItemType { 19 | return "TimeSeparatorModel" 20 | } 21 | 22 | init(uid: String, date: String) { 23 | self.date = date 24 | self.uid = uid 25 | } 26 | } 27 | 28 | extension Date { 29 | // Have a time stamp formatter to avoid keep creating new ones. This improves performance 30 | private static let weekdayAndDateStampDateFormatter: DateFormatter = { 31 | let dateFormatter = DateFormatter() 32 | dateFormatter.timeZone = TimeZone.autoupdatingCurrent 33 | dateFormatter.dateFormat = "EEE MMM dd" // "Monday, Mar 7 2016" 34 | return dateFormatter 35 | }() 36 | 37 | func toWeekDayAndDateString() -> String { 38 | return Date.weekdayAndDateStampDateFormatter.string(from: self) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /RChatMinimalServer/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Debug app.ts", 9 | "type": "node", 10 | "request": "launch", 11 | "program": "${workspaceRoot}/node_modules/ts-node/dist/_bin.js", 12 | "args": ["./src/index.ts"], 13 | "cwd": "${workspaceRoot}", 14 | "protocol": "inspector", 15 | "preLaunchTask": "build", 16 | "skipFiles": [ 17 | "/**" 18 | ] 19 | } 20 | //, 21 | // { 22 | // "name": "Debug current file", 23 | // "type": "node", 24 | // "request": "launch", 25 | // "program": "${workspaceRoot}/${relativeFile}", 26 | // "stopOnEntry": false, 27 | // "cwd": "${workspaceRoot}", 28 | // "protocol": "inspector", 29 | // "skipFiles": [ 30 | // "" 31 | // ] 32 | // } 33 | ] 34 | } -------------------------------------------------------------------------------- /RChat-iOS/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | 5 | post_install do |installer| 6 | # Your list of targets here. 7 | myTargets = ['Eureka'] 8 | 9 | installer.pods_project.targets.each do |target| 10 | if myTargets.include? target.name 11 | target.build_configurations.each do |config| 12 | config.build_settings['SWIFT_VERSION'] = '4.0' 13 | end 14 | end 15 | end 16 | end 17 | 18 | target 'RChat' do 19 | # Comment the next line if you're not using Swift and don't want to use dynamic frameworks 20 | use_frameworks! 21 | 22 | # Pods for RChat 23 | pod 'Chatto' 24 | pod 'ChattoAdditions' 25 | pod 'RealmSwift' 26 | pod 'SideMenu', '~> 2.1.3' 27 | pod 'SDWebImage', '~> 3.8.2' 28 | pod 'Eureka' 29 | pod 'Cartography', '1.0.1' 30 | pod 'TURecipientBar', '~> 2.0.4' 31 | pod 'NVActivityIndicatorView', '3.3' 32 | pod 'BRYXBanner', '~> 0.7.1' 33 | pod 'RealmLoginKit' 34 | 35 | 36 | target 'RChatTests' do 37 | inherit! :search_paths 38 | # Pods for testing 39 | end 40 | 41 | target 'RChatUITests' do 42 | inherit! :search_paths 43 | # Pods for testing 44 | end 45 | 46 | end 47 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/ComposeUserTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ComposeUserTableViewCell.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 2/1/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Cartography 11 | 12 | class ComposeUserTableViewCell : UITableViewCell { 13 | 14 | static let REUSE_ID = "ComposeUserTableViewCell" 15 | static let HEIGHT : CGFloat = 45 16 | 17 | lazy var nameLabel : UILabel = { 18 | let n = UILabel() 19 | return n 20 | }() 21 | 22 | override init(style: UITableViewCellStyle, reuseIdentifier: String?) { 23 | super.init(style: style, reuseIdentifier: reuseIdentifier) 24 | contentView.addSubview(nameLabel) 25 | constrain(nameLabel) { (nameLabel) in 26 | nameLabel.left == nameLabel.superview!.left + RChatConstants.Numbers.horizontalSpacing 27 | nameLabel.right == nameLabel.superview!.right - RChatConstants.Numbers.horizontalSpacing 28 | nameLabel.top == nameLabel.superview!.top 29 | nameLabel.bottom == nameLabel.superview!.bottom 30 | } 31 | 32 | } 33 | 34 | required init?(coder aDecoder: NSCoder) { 35 | fatalError("init(coder:) has not been implemented") 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/RChatTextMessageHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RChatTextMessageHandler.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 1/10/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ChattoAdditions 11 | 12 | class RChatTextMessageHandler: BaseMessageInteractionHandlerProtocol { 13 | private let baseHandler: RChatBaseMessageHandler 14 | init (baseHandler: RChatBaseMessageHandler) { 15 | self.baseHandler = baseHandler 16 | } 17 | 18 | func userDidTapOnFailIcon(viewModel: RChatTextMessageViewModel, failIconView: UIView) { 19 | self.baseHandler.userDidTapOnFailIcon(viewModel: viewModel) 20 | } 21 | 22 | func userDidTapOnAvatar(viewModel: RChatTextMessageViewModel) { 23 | self.baseHandler.userDidTapOnAvatar(viewModel: viewModel) 24 | } 25 | 26 | func userDidTapOnBubble(viewModel: RChatTextMessageViewModel) { 27 | self.baseHandler.userDidTapOnBubble(viewModel: viewModel) 28 | } 29 | 30 | func userDidBeginLongPressOnBubble(viewModel: RChatTextMessageViewModel) { 31 | self.baseHandler.userDidBeginLongPressOnBubble(viewModel: viewModel) 32 | } 33 | 34 | func userDidEndLongPressOnBubble(viewModel: RChatTextMessageViewModel) { 35 | self.baseHandler.userDidEndLongPressOnBubble(viewModel: viewModel) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/Images/RChatImageMessageModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RChatImageMessageModel.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 1/10/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ChattoAdditions 11 | 12 | class RChatImageMessageModel: PhotoMessageModel, RChatMessageModelProtocol { 13 | override init(messageModel: ChatMessage, imageSize: CGSize, image: UIImage) { 14 | super.init(messageModel: messageModel, imageSize:CGSize(width:256, height:256), image: UIImage(data: messageModel.extraInfo! as Data)!) 15 | } 16 | var status: MessageStatus { 17 | get { 18 | return self._messageModel.status 19 | } 20 | set { 21 | // self._messageModel.status = newValue 22 | // 23 | } 24 | } 25 | 26 | } 27 | 28 | 29 | //class RChatImageMessageModel: TextMessageModel, RChatMessageModelProtocol { 30 | // init(messageModel: ChatMessage){ 31 | // super.init(messageModel: messageModel, text: messageModel.text) 32 | // } 33 | // 34 | // var status: MessageStatus { 35 | // get { 36 | // return self._messageModel.status 37 | // } 38 | // set { 39 | // // self._messageModel.status = newValue 40 | // // 41 | // } 42 | // } 43 | //} 44 | 45 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/Images/RChatImageMessageHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RChatTextMessageHandler.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 1/10/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ChattoAdditions 11 | 12 | class RChatImageMessageHandler: BaseMessageInteractionHandlerProtocol { 13 | private let baseHandler: RChatBaseMessageHandler 14 | init (baseHandler: RChatBaseMessageHandler) { 15 | self.baseHandler = baseHandler 16 | } 17 | 18 | func userDidTapOnFailIcon(viewModel: RChatImageMessageViewModel, failIconView: UIView) { 19 | self.baseHandler.userDidTapOnFailIcon(viewModel: viewModel) 20 | } 21 | 22 | func userDidTapOnAvatar(viewModel: RChatImageMessageViewModel) { 23 | self.baseHandler.userDidTapOnAvatar(viewModel: viewModel) 24 | } 25 | 26 | func userDidTapOnBubble(viewModel: RChatImageMessageViewModel) { 27 | self.baseHandler.userDidTapOnBubble(viewModel: viewModel) 28 | } 29 | 30 | func userDidBeginLongPressOnBubble(viewModel: RChatImageMessageViewModel) { 31 | self.baseHandler.userDidBeginLongPressOnBubble(viewModel: viewModel) 32 | } 33 | 34 | func userDidEndLongPressOnBubble(viewModel: RChatImageMessageViewModel) { 35 | self.baseHandler.userDidEndLongPressOnBubble(viewModel: viewModel) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/MembersTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MembersTableViewCell.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 2/8/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Cartography 11 | 12 | class MemberTableViewCell: UITableViewCell { 13 | 14 | static let REUSE_ID = "MemberTableViewCell" 15 | static let HEIGHT: CGFloat = 44 16 | 17 | lazy var label: UILabel = { 18 | let label = UILabel() 19 | label.textColor = .black 20 | label.textAlignment = .right 21 | return label 22 | }() 23 | 24 | override init(style: UITableViewCellStyle, reuseIdentifier: String?) { 25 | super.init(style: style, reuseIdentifier: reuseIdentifier) 26 | backgroundColor = .clear 27 | contentView.addSubview(label) 28 | constrain(label) { (label) in 29 | label.left == label.superview!.left + RChatConstants.Numbers.horizontalSpacing 30 | label.right == label.superview!.right - RChatConstants.Numbers.horizontalSpacing 31 | label.top == label.superview!.top 32 | label.bottom == label.superview!.bottom 33 | } 34 | } 35 | 36 | required init?(coder aDecoder: NSCoder) { 37 | super.init(coder: aDecoder) 38 | } 39 | 40 | func setupWithUser(user: User){ 41 | label.text = user.defaultingName 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /RChat-iOS/RChatUITests/RChatUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RChatUITests.swift 3 | // RChatUITests 4 | // 5 | // Created by Max Alexander on 1/9/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class RChatUITests: XCTestCase { 12 | 13 | override func setUp() { 14 | super.setUp() 15 | 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | 18 | // In UI tests it is usually best to stop immediately when a failure occurs. 19 | continueAfterFailure = false 20 | // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. 21 | XCUIApplication().launch() 22 | 23 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 24 | } 25 | 26 | override func tearDown() { 27 | // Put teardown code here. This method is called after the invocation of each test method in the class. 28 | super.tearDown() 29 | } 30 | 31 | func testExample() { 32 | // Use recording to get started writing UI tests. 33 | // Use XCTAssert and related functions to verify your tests produce the correct results. 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/ChatMessage+MessageModelProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChatMessage+MessageModelProtocol.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 1/10/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ChattoAdditions 11 | import Chatto 12 | import RealmSwift 13 | /// This gives ChatMessage abilities to be consumed by Chatto's framework. They are just accessors. 14 | extension ChatMessage : MessageModelProtocol { 15 | 16 | var uid: String { 17 | return messageId 18 | } 19 | 20 | var type: ChatItemType { 21 | return mimeType 22 | } 23 | 24 | var date: Date { 25 | return timestamp 26 | } 27 | 28 | var senderId: String { 29 | //return user!.userId // <-- this is an unafe reference to the user object DHMS 30 | var idString = "" 31 | DispatchQueue.main.sync { // yikes -- not what we want to do - but have to find a place to put the safe thread reference code in the code that uses this getter... :\ 32 | idString = self.user!.userId 33 | } 34 | return idString 35 | } 36 | 37 | var isIncoming : Bool { 38 | return user!.isSameObject(as: User.getMe()) 39 | } 40 | 41 | // Realm will aggressively try to send it over, this is out of our control 42 | var status: MessageStatus { 43 | return .success 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/ColorLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorLabel.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 1/10/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ColorLabel: UILabel { 12 | @IBInspectable 13 | var cornerRadius: CGFloat { 14 | get { 15 | return layer.cornerRadius 16 | } 17 | set { 18 | layer.cornerRadius = newValue 19 | layer.masksToBounds = newValue > 0 20 | } 21 | } 22 | 23 | @IBInspectable 24 | var hPadding: CGFloat = 0 25 | 26 | @IBInspectable 27 | var vPadding: CGFloat = 0 28 | 29 | override func textRect(forBounds bounds: CGRect, limitedToNumberOfLines numberOfLines: Int) -> CGRect { 30 | let textInsets = UIEdgeInsets(top: vPadding, left: hPadding, bottom: vPadding, right: hPadding) 31 | var rect = textInsets.apply(rect: bounds) 32 | rect = super.textRect(forBounds: rect, limitedToNumberOfLines: numberOfLines) 33 | return textInsets.inverse.apply(rect: rect) 34 | } 35 | 36 | override func drawText(in rect: CGRect) { 37 | let textInsets = UIEdgeInsets(top: vPadding, left: hPadding, bottom: vPadding, right: hPadding) 38 | super.drawText(in: textInsets.apply(rect: rect)) 39 | } 40 | } 41 | private extension UIEdgeInsets { 42 | var inverse: UIEdgeInsets { 43 | return UIEdgeInsets(top: -top, left: -left, bottom: -bottom, right: -right) 44 | } 45 | 46 | func apply(rect: CGRect) -> CGRect { 47 | return UIEdgeInsetsInsetRect(rect, self) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/ProfileImageRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileImageRow.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 2/3/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Cartography 11 | import Eureka 12 | 13 | 14 | final class ProfileCell : Cell, CellType { 15 | 16 | private static let IMAGE_VIEW_LENGTH : CGFloat = 100 17 | 18 | lazy var profileImageView : UIImageView = { 19 | let i = UIImageView() 20 | i.layer.cornerRadius = IMAGE_VIEW_LENGTH / 2 21 | i.layer.borderColor = RChatConstants.Colors.silver.cgColor 22 | i.layer.borderWidth = 2 23 | i.backgroundColor = RChatConstants.Colors.asbestos 24 | i.layer.masksToBounds = true 25 | return i 26 | }() 27 | 28 | 29 | override func setup() { 30 | super.setup() 31 | height = { 120 } 32 | contentView.addSubview(profileImageView) 33 | constrain(profileImageView) { (profileImageView) in 34 | profileImageView.centerX == profileImageView.superview!.centerX 35 | profileImageView.centerY == profileImageView.superview!.centerY 36 | profileImageView.width == ProfileCell.IMAGE_VIEW_LENGTH 37 | profileImageView.height == ProfileCell.IMAGE_VIEW_LENGTH 38 | } 39 | } 40 | 41 | } 42 | 43 | final class ProfileRow : Row, RowType { 44 | 45 | required public init(tag: String?) { 46 | super.init(tag: tag) 47 | // We set the cellProvider to load the .xib corresponding to our cell 48 | cellProvider = CellProvider() 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/SettingsViewController+ImagePicking.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsViewController+ImagePicking.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 2/3/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension SettingsViewController : UIImagePickerControllerDelegate, UINavigationControllerDelegate { 12 | 13 | func presentCamera(){ 14 | let imagePickerController = UIImagePickerController() 15 | imagePickerController.sourceType = .camera 16 | imagePickerController.delegate = self 17 | imagePickerController.allowsEditing = true 18 | self.present(imagePickerController, animated: true, completion: nil) 19 | } 20 | 21 | func presentPhotoLibrary(){ 22 | let imagePickerController = UIImagePickerController() 23 | imagePickerController.sourceType = .photoLibrary 24 | imagePickerController.delegate = self 25 | imagePickerController.allowsEditing = true 26 | imagePickerController.view.backgroundColor = .white 27 | imagePickerController.topViewController?.view.backgroundColor = .white 28 | self.present(imagePickerController, animated: true, completion: nil) 29 | } 30 | 31 | func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) { 32 | picker.dismiss(animated: true, completion: nil) 33 | guard let image = info[UIImagePickerControllerEditedImage] as? UIImage else { return } 34 | self.viewModel.avatarImage = image 35 | self.profileRow.cell.profileImageView.image = self.viewModel.avatarImage 36 | } 37 | 38 | 39 | 40 | } 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xcuserstate 23 | 24 | ## Obj-C/Swift specific 25 | *.hmap 26 | *.ipa 27 | *.dSYM.zip 28 | *.dSYM 29 | 30 | ## Playgrounds 31 | timeline.xctimeline 32 | playground.xcworkspace 33 | 34 | # Swift Package Manager 35 | # 36 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 37 | # Packages/ 38 | .build/ 39 | 40 | # CocoaPods 41 | # 42 | # We recommend against adding the Pods directory to your .gitignore. However 43 | # you should judge for yourself, the pros and cons are mentioned at: 44 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 45 | # 46 | Pods/ 47 | 48 | # Carthage 49 | # 50 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 51 | # Carthage/Checkouts 52 | 53 | Carthage/Build 54 | 55 | # fastlane 56 | # 57 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 58 | # screenshots whenever they are needed. 59 | # For more information about the recommended setup visit: 60 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 61 | 62 | fastlane/report.xml 63 | fastlane/Preview.html 64 | fastlane/screenshots 65 | fastlane/test_output 66 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/TimeSeparatorPresenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimeSeparatorPresenter.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 1/10/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Chatto 11 | 12 | class TimeSeparatorPresenter: ChatItemPresenterProtocol { 13 | 14 | let timeSeparatorModel: TimeSeparatorModel 15 | init (timeSeparatorModel: TimeSeparatorModel) { 16 | self.timeSeparatorModel = timeSeparatorModel 17 | } 18 | 19 | private static let cellReuseIdentifier = TimeSeparatorCollectionViewCell.self.description() 20 | 21 | static func registerCells(_ collectionView: UICollectionView) { 22 | collectionView.register(TimeSeparatorCollectionViewCell.self, forCellWithReuseIdentifier: cellReuseIdentifier) 23 | } 24 | 25 | func dequeueCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell { 26 | return collectionView.dequeueReusableCell(withReuseIdentifier: TimeSeparatorPresenter.cellReuseIdentifier, for: indexPath) 27 | } 28 | 29 | func configureCell(_ cell: UICollectionViewCell, decorationAttributes: ChatItemDecorationAttributesProtocol?) { 30 | guard let timeSeparatorCell = cell as? TimeSeparatorCollectionViewCell else { 31 | assert(false, "expecting status cell") 32 | return 33 | } 34 | 35 | timeSeparatorCell.text = self.timeSeparatorModel.date 36 | } 37 | 38 | var canCalculateHeightInBackground: Bool { 39 | return true 40 | } 41 | 42 | func heightForCell(maximumWidth width: CGFloat, decorationAttributes: ChatItemDecorationAttributesProtocol?) -> CGFloat { 43 | return 36 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/TimeSeperatorCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimeSeperatorCollectionViewCell.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 1/10/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | import Chatto 12 | 13 | class TimeSeparatorCollectionViewCell: UICollectionViewCell { 14 | 15 | private let label: UILabel = { 16 | let label = ColorLabel() 17 | label.backgroundColor = UIColor(white: 0.2, alpha: 0.25) 18 | label.cornerRadius = 8 19 | label.hPadding = 10 20 | label.vPadding = 2 21 | label.font = UIFont.boldSystemFont(ofSize: 14) 22 | label.textColor = UIColor.white 23 | return label 24 | }() 25 | 26 | override init(frame: CGRect) { 27 | super.init(frame: frame) 28 | self.commonInit() 29 | } 30 | 31 | required init?(coder aDecoder: NSCoder) { 32 | super.init(coder: aDecoder) 33 | self.commonInit() 34 | } 35 | 36 | private func commonInit() { 37 | backgroundColor = UIColor.clear 38 | self.contentView.addSubview(label) 39 | } 40 | 41 | var text: String = "" { 42 | didSet { 43 | if oldValue != text { 44 | self.setTextOnLabel(text) 45 | } 46 | } 47 | } 48 | 49 | private func setTextOnLabel(_ text: String) { 50 | self.label.text = text 51 | self.setNeedsLayout() 52 | } 53 | 54 | override func layoutSubviews() { 55 | super.layoutSubviews() 56 | self.label.bounds.size = self.label.sizeThatFits(self.contentView.bounds.size) 57 | self.label.center = self.contentView.center 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/SearchResultTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchResultTableViewCell.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 2/8/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Cartography 11 | 12 | class SearchResultTableViewCell: UITableViewCell { 13 | 14 | static let REUSE_ID = "SearchResultTableViewCell" 15 | static let HEIGHT: CGFloat = 44 16 | 17 | lazy var label: UILabel = { 18 | let label = UILabel() 19 | label.textColor = .white 20 | return label 21 | }() 22 | 23 | override init(style: UITableViewCellStyle, reuseIdentifier: String?) { 24 | super.init(style: style, reuseIdentifier: reuseIdentifier) 25 | backgroundColor = .clear 26 | contentView.addSubview(label) 27 | constrain(label) { (label) in 28 | label.left == label.superview!.left + RChatConstants.Numbers.horizontalSpacing 29 | label.right == label.superview!.right - RChatConstants.Numbers.horizontalSpacing 30 | label.top == label.superview!.top 31 | label.bottom == label.superview!.bottom 32 | } 33 | } 34 | 35 | required init?(coder aDecoder: NSCoder) { 36 | super.init(coder: aDecoder) 37 | } 38 | 39 | func setupWithUser(user: User){ 40 | label.text = "👤| " + user.defaultingName 41 | } 42 | 43 | func setupWithConversation(conversation: Conversation){ 44 | label.text = "👥 | " + conversation.defaultingName 45 | } 46 | 47 | // Added support for searching inside chats 48 | func setupWithChat(chat: ChatMessage){ 49 | let conversation = chat.conversations.first 50 | label.text = "💬 | " + conversation!.defaultingName 51 | } 52 | 53 | 54 | } 55 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/ChatViewController+ImageDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChatViewController+ImageDelegate.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 1/10/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension ChatViewController : UINavigationControllerDelegate, UIImagePickerControllerDelegate { 12 | 13 | func presentCamera(){ 14 | let imagePickerController = UIImagePickerController() 15 | imagePickerController.delegate = self 16 | imagePickerController.sourceType = .camera 17 | present(imagePickerController, animated: true, completion: nil) 18 | } 19 | 20 | func presentPhotoLibrary(){ 21 | let imagePickerController = UIImagePickerController() 22 | imagePickerController.delegate = self 23 | imagePickerController.sourceType = .photoLibrary 24 | present(imagePickerController, animated: true, completion: nil) 25 | } 26 | 27 | func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { 28 | picker.dismiss(animated: true, completion: nil) 29 | } 30 | 31 | func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) { 32 | picker.dismiss(animated: true, completion: nil) 33 | guard let image = info[UIImagePickerControllerOriginalImage] as? UIImage else { return } 34 | print("you've selected an image. \(image)") 35 | 36 | // this really doesn't need the level of infirection that the text messages uses ... as long as we have the required data 37 | // this should just make a new message and get it into the message stream. 38 | ChatMessage.sendImageChatMessage(conversation: viewModel.conversation!, image: image) 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/User.swift: -------------------------------------------------------------------------------- 1 | // 2 | // User.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 2/1/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RealmSwift 11 | 12 | class User : Object { 13 | dynamic var userId: String = "" 14 | dynamic var username: String = "" 15 | dynamic var displayName : String = "" 16 | // New additions... 20-Sept-2017 17 | dynamic var avatarImage : Data? 18 | dynamic var latitude : Double = -999.0 19 | dynamic var longitude : Double = -999.0 20 | dynamic var shareLocation = false 21 | dynamic var sharePresence = false 22 | dynamic var lastSeenAt: Date? // will power a ring or glow around hte users avatar; grey=offline; yellow=seenc in last 10 mins; green = seen in last minute 23 | 24 | var defaultingName: String { 25 | if !displayName.isEmpty { 26 | return displayName 27 | } 28 | return "@\(username)" 29 | } 30 | 31 | override static func primaryKey() -> String? { 32 | return "userId" 33 | } 34 | override static func ignoredProperties() -> [String] { 35 | return ["defaultingName"] 36 | } 37 | } 38 | 39 | extension User { 40 | 41 | static func getMe() -> User! { 42 | let realm = RChatConstants.Realms.global 43 | return realm.object(ofType: User.self, forPrimaryKey: RChatConstants.myUserId) 44 | } 45 | 46 | static func searchForUsers(searchTerm: String) -> Results { 47 | let realm = RChatConstants.Realms.global 48 | let predicate = NSPredicate(format: "(username contains[c] %@ OR displayName contains[c] %@) AND (userId != %@)", searchTerm, searchTerm, RChatConstants.myUserId) 49 | return realm.objects(User.self).filter(predicate) 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /RChat-iOS/Extensions/TextField+Utilities.swift: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Copyright 2016 Realm Inc. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | //////////////////////////////////////////////////////////////////////////// 18 | 19 | import Foundation 20 | import UIKit 21 | 22 | extension UITextField { 23 | func innerShadowWithTint(tint: UIColor, backgroundColor: UIColor, radius: CGFloat, opacity: Float) { 24 | let shadowLayer = CALayer() 25 | shadowLayer.frame = CGRect(x: 0, y: self.frame.size.height + 2, width: self.frame.size.width, height: 2.0) // was: CGRectMake(0, self.frame.size.height + 2, self.frame.size.width, 2.0) 26 | shadowLayer.backgroundColor = backgroundColor.cgColor 27 | shadowLayer.shadowColor = tint.cgColor 28 | shadowLayer.shadowOffset = CGSize.zero 29 | shadowLayer.shadowRadius = radius 30 | shadowLayer.shadowOpacity = opacity 31 | self.layer.addSublayer(shadowLayer) 32 | } 33 | 34 | func setBorderWithColor(color: UIColor, width: CGFloat, cornerRadius: CGFloat) { 35 | self.layer.borderColor = color.cgColor 36 | self.layer.borderWidth = width 37 | self.layer.cornerRadius = cornerRadius 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /RChat-iOS/Extensions/String+Utilities.swift: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Copyright 2016 Realm Inc. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | //////////////////////////////////////////////////////////////////////////// 18 | 19 | import Foundation 20 | import UIKit 21 | 22 | extension String { 23 | func isEmail() -> Bool { 24 | let regex = try? NSRegularExpression(pattern: "^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,4}$", options: .caseInsensitive) 25 | return regex?.firstMatch(in: self, options: [], range: NSMakeRange(0, self.characters.count)) != nil 26 | } 27 | 28 | // Returns a scaled image of whatever string its given; mostly useful for getting images of emjoi 29 | func image(size: CGSize, fontColor: UIColor, backgroundColor: UIColor) -> UIImage? { 30 | UIGraphicsBeginImageContextWithOptions(size, false, 0) 31 | backgroundColor.set() 32 | let rect = CGRect(origin: CGPoint(), size: size) 33 | UIRectFill(CGRect(origin: CGPoint(), size: size)) 34 | (self as NSString).draw(in: rect, withAttributes: [NSFontAttributeName: UIFont.systemFont(ofSize: size.height), NSForegroundColorAttributeName: fontColor]) 35 | let image = UIGraphicsGetImageFromCurrentImageContext() 36 | UIGraphicsEndImageContext() 37 | return image 38 | } 39 | 40 | 41 | } 42 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/RChatTextMessageViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RChatTextMessageViewModel.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 1/10/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ChattoAdditions 11 | import SDWebImage 12 | 13 | class RChatTextMessageViewModel: TextMessageViewModel, RChatMessageViewModelProtocol { 14 | 15 | override init(textMessage: RChatTextMessageModel, messageViewModel: MessageViewModelProtocol) { 16 | super.init(textMessage: textMessage, messageViewModel: messageViewModel) 17 | 18 | /* This is some logic for loading the UserAvatar 19 | let userId = textMessage.senderId 20 | guard let spriteUrl = User.getByUserId(userId: userId)?.spriteUrl else { 21 | return 22 | } 23 | SDWebImageManager.shared().downloadImage(with: URL(string: spriteUrl)!, options: [], progress: nil) { [weak self] (image, err, _, _, _) in 24 | if let image = image { 25 | self?.avatarImage.value = image 26 | }else { 27 | self?.avatarImage.value = UIImage(named: "sample_sprite") 28 | } 29 | } 30 | */ 31 | } 32 | 33 | var messageModel: RChatMessageModelProtocol { 34 | return self.textMessage 35 | } 36 | 37 | } 38 | 39 | class EdenTextMessageViewModelBuilder: ViewModelBuilderProtocol { 40 | init() {} 41 | 42 | let messageViewModelBuilder = MessageViewModelDefaultBuilder() 43 | 44 | func createViewModel(_ textMessage: RChatTextMessageModel) -> RChatTextMessageViewModel { 45 | let messageViewModel = self.messageViewModelBuilder.createMessageViewModel(textMessage) 46 | let textMessageViewModel = RChatTextMessageViewModel(textMessage: textMessage, messageViewModel: messageViewModel) 47 | return textMessageViewModel 48 | } 49 | 50 | func canCreateViewModel(fromModel model: Any) -> Bool { 51 | return model is RChatTextMessageModel 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /RChat-iOS/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - AAPhotoCircleCrop (1.2.0) 3 | - BRYXBanner (0.7.3) 4 | - Cartography (1.0.1) 5 | - Chatto (3.2.0) 6 | - ChattoAdditions (3.2.0): 7 | - Chatto 8 | - Eureka (4.0.1) 9 | - NVActivityIndicatorView (3.3) 10 | - Realm (3.0.1): 11 | - Realm/Headers (= 3.0.1) 12 | - Realm/Headers (3.0.1) 13 | - RealmLoginKit (0.1.3): 14 | - Realm 15 | - RealmLoginKit/Core (= 0.1.3) 16 | - TORoundedTableView 17 | - RealmLoginKit/Core (0.1.3): 18 | - Realm 19 | - TORoundedTableView 20 | - RealmSwift (3.0.1): 21 | - Realm (= 3.0.1) 22 | - SDWebImage (3.8.2): 23 | - SDWebImage/Core (= 3.8.2) 24 | - SDWebImage/Core (3.8.2) 25 | - SideMenu (2.1.5) 26 | - TORoundedTableView (0.1.3) 27 | - TURecipientBar (2.0.4) 28 | 29 | DEPENDENCIES: 30 | - AAPhotoCircleCrop 31 | - BRYXBanner (~> 0.7.1) 32 | - Cartography (= 1.0.1) 33 | - Chatto 34 | - ChattoAdditions 35 | - Eureka 36 | - NVActivityIndicatorView (= 3.3) 37 | - RealmLoginKit 38 | - RealmSwift 39 | - SDWebImage (~> 3.8.2) 40 | - SideMenu (~> 2.1.3) 41 | - TURecipientBar (~> 2.0.4) 42 | 43 | SPEC CHECKSUMS: 44 | AAPhotoCircleCrop: 61371491e3db6d295f6402b40d4ab481129ddf43 45 | BRYXBanner: bd26a91cb8a5826a8bc7687891043d287dd3ee53 46 | Cartography: c1460e99395b824d9d75360b0382faeb0b33dcd7 47 | Chatto: c5d1d9bfea49618bd55946e903b54d6503f8883f 48 | ChattoAdditions: af8f01388821c0ceb6a45654ab6b38dbee6c8836 49 | Eureka: c8bd5cc07143b6f66268c208d28a246c99b41955 50 | NVActivityIndicatorView: 0a4cd7863d0e409e65b72e253c3e13f48d0260d7 51 | Realm: ba6144c74f835f7497791f6599d63a7755377b83 52 | RealmLoginKit: 646d412bb28b8319235fb48e54ed6d6e57b98e17 53 | RealmSwift: 7d13b1ac4b6022a13f841e3143e7789c9e40ad0d 54 | SDWebImage: 098e97e6176540799c27e804c96653ee0833d13c 55 | SideMenu: bcd2d7b7d4395af088e213cebd11c3ac7d958f5a 56 | TORoundedTableView: cd1437ef81ab3888dea8c06d2dfe4795bcaac768 57 | TURecipientBar: 4904645f476ad056e17c9458fa9894552339e63a 58 | 59 | PODFILE CHECKSUM: 9ce2f32f5f36b69346586933cb80eab4c90018a2 60 | 61 | COCOAPODS: 1.3.1 62 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/RChatRecipientBar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RChatRecipientBar.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 2/1/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import TURecipientBar 11 | 12 | 13 | class RChatRecipientBar : TURecipientsBar { 14 | 15 | var users : [User] { 16 | return userRecipients.map({ $0.user }) 17 | } 18 | 19 | var userRecipients : [UserRecipient] { 20 | var array = [UserRecipient]() 21 | for recipient in self.recipients { 22 | if let userRecipient = recipient as? UserRecipient { 23 | array.append(userRecipient) 24 | } 25 | } 26 | return array 27 | } 28 | 29 | init(){ 30 | super.init(frame: .zero) 31 | } 32 | 33 | required init?(coder aDecoder: NSCoder) { 34 | super.init(coder: aDecoder) 35 | } 36 | 37 | private func commonInit() { 38 | 39 | } 40 | 41 | func addUser(user: User){ 42 | if userRecipients.contains(where: { $0.user.userId == user.userId }) { 43 | return 44 | } 45 | addRecipient(UserRecipient(user: user)) 46 | } 47 | 48 | func removeUser(user: User){ 49 | removeByUserId(userId: user.userId) 50 | } 51 | 52 | func removeByUserId(userId: String){ 53 | guard let foundRecipient = userRecipients.filter({ $0.user.userId == userId }).first else { return } 54 | removeRecipient(foundRecipient) 55 | } 56 | 57 | func containsUser(user: User) -> Bool { 58 | return userRecipients.contains(where: { $0.user.userId == user.userId }) 59 | } 60 | 61 | } 62 | 63 | class UserRecipient : NSObject, NSCopying, TURecipientProtocol { 64 | 65 | public var recipientTitle: String { 66 | return user.displayName 67 | } 68 | 69 | let user: User 70 | 71 | init(user: User){ 72 | self.user = user 73 | super.init() 74 | } 75 | 76 | public func copy(with zone: NSZone? = nil) -> Any { 77 | return UserRecipient(user: self.user) 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/SendingStatusPresenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SendingStatusPresenter.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 1/10/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Chatto 11 | import ChattoAdditions 12 | 13 | class SendingStatusPresenter: ChatItemPresenterProtocol { 14 | 15 | let statusModel: SendingStatusModel 16 | init (statusModel: SendingStatusModel) { 17 | self.statusModel = statusModel 18 | } 19 | 20 | static func registerCells(_ collectionView: UICollectionView) { 21 | collectionView.register(UINib(nibName: "SendingStatusCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: "SendingStatusCollectionViewCell") 22 | } 23 | 24 | func dequeueCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell { 25 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "SendingStatusCollectionViewCell", for: indexPath) 26 | return cell 27 | } 28 | 29 | func configureCell(_ cell: UICollectionViewCell, decorationAttributes: ChatItemDecorationAttributesProtocol?) { 30 | guard let statusCell = cell as? SendingStatusCollectionViewCell else { 31 | assert(false, "expecting status cell") 32 | return 33 | } 34 | 35 | let attrs = [ 36 | NSFontAttributeName : UIFont.systemFont(ofSize: 10.0), 37 | NSForegroundColorAttributeName: self.statusModel.status == .failed ? UIColor.red : UIColor.black 38 | ] 39 | statusCell.text = NSAttributedString( 40 | string: self.statusText(), 41 | attributes: attrs) 42 | } 43 | 44 | func statusText() -> String { 45 | switch self.statusModel.status { 46 | case .failed: 47 | return NSLocalizedString("Sending failed", comment: "") 48 | case .sending: 49 | return NSLocalizedString("Sending...", comment: "") 50 | default: 51 | return "" 52 | } 53 | } 54 | 55 | var canCalculateHeightInBackground: Bool { 56 | return true 57 | } 58 | 59 | func heightForCell(maximumWidth width: CGFloat, decorationAttributes: ChatItemDecorationAttributesProtocol?) -> CGFloat { 60 | return 19 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/ChatMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Message.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 1/9/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import RealmSwift 10 | import Chatto 11 | import ChattoAdditions 12 | 13 | class ChatMessage : Object { 14 | 15 | dynamic var messageId: String = UUID().uuidString 16 | dynamic var user: User? 17 | 18 | // Payload: text or image depending on mimetype 19 | dynamic var mimeType: String = MimeType.textPlain.rawValue 20 | dynamic var text: String = "" 21 | dynamic var extraInfo: NSData? 22 | 23 | dynamic var timestamp: Date = Date() 24 | 25 | let conversations = LinkingObjects(fromType: Conversation.self, property: "chatMessages") 26 | 27 | override static func primaryKey() -> String? { 28 | return "messageId" 29 | } 30 | } 31 | 32 | 33 | extension ChatMessage { 34 | 35 | static func sendTextChatMessage(conversation: Conversation, text: String){ 36 | let chatMessage = ChatMessage() 37 | chatMessage.user = User.getMe() 38 | chatMessage.text = text 39 | let realm = conversation.realm! 40 | try! realm.write { 41 | conversation.chatMessages.append(chatMessage) 42 | } 43 | } 44 | 45 | 46 | // MARK: Insert image 47 | static func sendImageChatMessage(conversation: Conversation, image: UIImage?){ 48 | guard image != nil else { 49 | return 50 | } 51 | let chatMessage = ChatMessage() 52 | chatMessage.user = User.getMe() 53 | chatMessage.mimeType = MimeType.imagePNG.rawValue 54 | 55 | // @FIXME: this is a placeholder to deal with an image size support issue 56 | let resizedImage = image?.resizeImage(targetSize: CGSize(width:image!.size.width / 2, height: image!.size.height / 2)) 57 | 58 | chatMessage.extraInfo = (UIImagePNGRepresentation(resizedImage!)! as NSData) 59 | let realm = conversation.realm! 60 | try! realm.write { 61 | conversation.chatMessages.append(chatMessage) 62 | } 63 | } 64 | 65 | 66 | static func searchInChats(searchTerm: String) -> Results { 67 | let realm = RChatConstants.Realms.global 68 | let predicate = NSPredicate(format: "text contains[c] %@", searchTerm) 69 | return realm.objects(ChatMessage.self).filter(predicate) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | RChat2 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.2 21 | CFBundleVersion 22 | 4 23 | LSRequiresIPhoneOS 24 | 25 | NSAppTransportSecurity 26 | 27 | NSAllowsArbitraryLoads 28 | 29 | 30 | NSCameraUsageDescription 31 | RChat needs permissions to your camera to allow you to send image messages. 32 | NSLocationAlwaysAndWhenInUseUsageDescription 33 | RChat needs your location to show your locaiton on a map; you can opt-out at any time 34 | NSPhotoLibraryAddUsageDescription 35 | RChat need permission to import media items from chats to your photo library if you want to save them 36 | NSPhotoLibraryUsageDescription 37 | RChat needs permissions to your photo library to allow you to send image messages. 38 | UILaunchStoryboardName 39 | LaunchScreen 40 | UIRequiredDeviceCapabilities 41 | 42 | armv7 43 | 44 | UIStatusBarStyle 45 | UIStatusBarStyleLightContent 46 | UISupportedInterfaceOrientations 47 | 48 | UIInterfaceOrientationPortrait 49 | UIInterfaceOrientationLandscapeLeft 50 | UIInterfaceOrientationLandscapeRight 51 | 52 | UISupportedInterfaceOrientations~ipad 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationPortraitUpsideDown 56 | UIInterfaceOrientationLandscapeLeft 57 | UIInterfaceOrientationLandscapeRight 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 1/9/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | 20 | window = UIWindow(frame: UIScreen.main.bounds) 21 | window?.backgroundColor = UIColor.white 22 | window?.makeKeyAndVisible() 23 | //window?.rootViewController = CustomNavController(rootViewController: WelcomeViewController()) 24 | window?.rootViewController = CustomNavController(rootViewController: RLMLoginViewController()) 25 | 26 | return true 27 | } 28 | 29 | func applicationWillResignActive(_ application: UIApplication) { 30 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 31 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 32 | } 33 | 34 | func applicationDidEnterBackground(_ application: UIApplication) { 35 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 36 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 37 | } 38 | 39 | func applicationWillEnterForeground(_ application: UIApplication) { 40 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 41 | } 42 | 43 | func applicationDidBecomeActive(_ application: UIApplication) { 44 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 45 | } 46 | 47 | func applicationWillTerminate(_ application: UIApplication) { 48 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 49 | } 50 | 51 | 52 | } 53 | 54 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/SettingsViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsViewModel.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 2/3/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RealmSwift 11 | 12 | extension Data { 13 | func toImage() -> UIImage? { 14 | if let tmpImage = UIImage(data: self) { 15 | return tmpImage 16 | } 17 | return nil 18 | } 19 | } 20 | 21 | class SettingsViewModel { 22 | 23 | // FROM UI 24 | func profileRowDidTap(){ 25 | presentProfileImageChangeAlert?() 26 | } 27 | func logoutRowDidTap(){ 28 | presentLogoutAlert?() 29 | } 30 | func confirmLogoutDidTap(){ 31 | SyncUser.current?.logOut() 32 | returnToWelcomeViewController?() 33 | } 34 | func saveRowDidTap(){ 35 | let myUser = User.getMe() 36 | let realm = RChatConstants.Realms.global 37 | try! realm.write { 38 | myUser?.displayName = self.displayName ?? "" 39 | myUser?.sharePresence = self.sharePresence ?? false 40 | myUser?.shareLocation = self.shareLocation ?? false 41 | if self.avatarImage != nil { 42 | myUser?.avatarImage = UIImagePNGRepresentation(self.avatarImage!) as Data? 43 | } else { 44 | print("User avatar was nil.") 45 | } 46 | } 47 | showSaveSuccessBanner?() 48 | } 49 | 50 | func saveLocationEnableChoice() { 51 | let myUser = User.getMe() 52 | let realm = RChatConstants.Realms.global 53 | try! realm.write { 54 | myUser?.shareLocation = self.shareLocation ?? false 55 | } 56 | showSaveSuccessBanner?() 57 | } 58 | 59 | func savePresenceEnableChoice() { 60 | let myUser = User.getMe() 61 | let realm = RChatConstants.Realms.global 62 | try! realm.write { 63 | myUser?.sharePresence = self.sharePresence ?? false 64 | } 65 | showSaveSuccessBanner?() 66 | } 67 | 68 | 69 | // TO UI 70 | var presentProfileImageChangeAlert: ((Void) -> ())? 71 | var presentLogoutAlert: ((Void) -> ())? 72 | var returnToWelcomeViewController: ((Void) -> ())? 73 | var showSaveSuccessBanner: ((Void) -> ())? 74 | 75 | var username: String? 76 | var displayName: String? 77 | var shareLocation: Bool? 78 | var sharePresence: Bool? 79 | var avatarImage: UIImage? 80 | 81 | 82 | init(){ 83 | username = User.getMe().username 84 | displayName = User.getMe().displayName 85 | shareLocation = User.getMe().shareLocation 86 | sharePresence = User.getMe().sharePresence 87 | avatarImage = User.getMe().avatarImage?.toImage() ?? genericUserImage() 88 | } 89 | 90 | func genericUserImage() -> UIImage { 91 | return UIImage(named:"camera-50")! 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/LoadingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoadingView.swift 3 | // EChat 4 | // 5 | // Created by Max Alexander on 1/21/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Cartography 11 | import NVActivityIndicatorView 12 | 13 | class LoadingView : UIView { 14 | 15 | lazy var backgroundView : UIView = { 16 | let v = UIView() 17 | v.alpha = 0.5 18 | v.backgroundColor = UIColor.black 19 | return v 20 | }() 21 | 22 | lazy var activityIndicatorView : NVActivityIndicatorView = { 23 | let v = NVActivityIndicatorView(frame: CGRect(x: 0, y: 0, width: 80, height: 80), type: .orbit, color: UIColor.white) 24 | return v 25 | }() 26 | 27 | init(){ 28 | super.init(frame: .zero) 29 | commonInit() 30 | } 31 | 32 | required init?(coder aDecoder: NSCoder) { 33 | super.init(coder: aDecoder) 34 | commonInit() 35 | } 36 | 37 | func commonInit(){ 38 | addSubview(backgroundView) 39 | addSubview(activityIndicatorView) 40 | 41 | constrain(backgroundView, activityIndicatorView) { (backgroundView, activityIndicatorView) in 42 | backgroundView.left == backgroundView.superview!.left 43 | backgroundView.bottom == backgroundView.superview!.bottom 44 | backgroundView.top == backgroundView.superview!.top 45 | backgroundView.right == backgroundView.superview!.right 46 | 47 | activityIndicatorView.width == 80 48 | activityIndicatorView.height == 80 49 | activityIndicatorView.centerX == activityIndicatorView.superview!.centerX 50 | activityIndicatorView.centerY == activityIndicatorView.superview!.centerY 51 | 52 | } 53 | } 54 | 55 | static func show(superView: UIView){ 56 | superView.endEditing(false) 57 | hide(superView: superView) 58 | let loadingView = LoadingView() 59 | superView.addSubview(loadingView) 60 | constrain(loadingView) { (v) in 61 | v.left == v.superview!.left 62 | v.right == v.superview!.right 63 | v.top == v.superview!.top 64 | v.bottom == v.superview!.bottom 65 | } 66 | loadingView.activityIndicatorView.startAnimating() 67 | } 68 | 69 | static func hide(superView: UIView){ 70 | for subview in superView.subviews { 71 | if subview is LoadingView { 72 | UIView.animate(withDuration: 0.25, animations: { 73 | subview.removeFromSuperview() 74 | }) 75 | 76 | } 77 | for subview1 in subview.subviews { 78 | if subview1 is LoadingView { 79 | UIView.animate(withDuration: 0.25, animations: { 80 | subview1.removeFromSuperview() 81 | }) 82 | } 83 | } 84 | } 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "AppIcon-40x40@1x-3.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "AppIcon-60x60@1x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "AppIcon-29x29@2x.png", 19 | "scale" : "2x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "AppIcon-29x29@3x.png", 25 | "scale" : "3x" 26 | }, 27 | { 28 | "size" : "40x40", 29 | "idiom" : "iphone", 30 | "filename" : "AppIcon-40x40@2x.png", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "AppIcon-60x60@2x.png", 37 | "scale" : "3x" 38 | }, 39 | { 40 | "size" : "60x60", 41 | "idiom" : "iphone", 42 | "filename" : "AppIcon-60x60@2x-1.png", 43 | "scale" : "2x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "AppIcon-60x60@3x.png", 49 | "scale" : "3x" 50 | }, 51 | { 52 | "size" : "20x20", 53 | "idiom" : "ipad", 54 | "filename" : "AppIcon-20x20@1x.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "AppIcon-40x40@1x.png", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "size" : "29x29", 65 | "idiom" : "ipad", 66 | "filename" : "AppIcon-29x29@1x.png", 67 | "scale" : "1x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "AppIcon-29x29@2x-1.png", 73 | "scale" : "2x" 74 | }, 75 | { 76 | "size" : "40x40", 77 | "idiom" : "ipad", 78 | "filename" : "AppIcon-40x40@1x-1.png", 79 | "scale" : "1x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "AppIcon-40x40@2x-1.png", 85 | "scale" : "2x" 86 | }, 87 | { 88 | "size" : "76x76", 89 | "idiom" : "ipad", 90 | "filename" : "AppIcon-76x76@1x.png", 91 | "scale" : "1x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "AppIcon-76x76@2x.png", 97 | "scale" : "2x" 98 | }, 99 | { 100 | "size" : "83.5x83.5", 101 | "idiom" : "ipad", 102 | "filename" : "AppIcon-83.5x83.5@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "idiom" : "ios-marketing", 107 | "size" : "1024x1024", 108 | "scale" : "1x" 109 | } 110 | ], 111 | "info" : { 112 | "version" : 1, 113 | "author" : "xcode" 114 | } 115 | } -------------------------------------------------------------------------------- /RChat-iOS/RChat/SendingStatusCollectionViewCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/ChatViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChatViewModel.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 2/3/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RealmSwift 11 | import Chatto 12 | 13 | class ChatViewModel : ChatDataSourceProtocol { 14 | 15 | var delegate: ChatDataSourceDelegateProtocol? 16 | var chatItems: [ChatItemProtocol] = [] 17 | var notificationToken : NotificationToken? 18 | var observeConversationToken : NotificationToken? 19 | 20 | private var isFirst: Bool = true 21 | 22 | var defaultingNameCallback : ((String) -> Void)? { 23 | didSet { 24 | self.defaultingNameCallback?(conversation?.defaultingName ?? "") 25 | } 26 | } 27 | 28 | var conversation : Conversation? { 29 | didSet { 30 | observeConversationToken?.invalidate() 31 | notificationToken?.invalidate() 32 | isFirst = true 33 | guard let c = conversation else { return } 34 | let chatMessages = c.chatMessages 35 | 36 | notificationToken = chatMessages 37 | .observe({ [weak self] (changes) in 38 | guard let `self` = self else { return } 39 | self.isFirst = false 40 | var items = [ChatItemProtocol]() 41 | for m in Array(chatMessages).map({ ChatMessage(value: $0) }) { 42 | if m.mimeType == MimeType.textPlain.rawValue { 43 | items.append(RChatTextMessageModel(messageModel: m)) 44 | } 45 | if m.mimeType == MimeType.imagePNG.rawValue { 46 | items.append(RChatImageMessageModel(messageModel: m, imageSize: CGSize(width:256, height:256), image: UIImage(data:m.extraInfo! as Data)!)) 47 | } 48 | } 49 | self.chatItems = items 50 | self.delegate?.chatDataSourceDidUpdate(self, updateType: self.isFirst ? .reload : .normal) 51 | }) 52 | 53 | observeConversationToken = Conversation.observeConversationBy(conversationId: c.conversationId) { [weak self] conversation in 54 | guard let `self` = self else { return } 55 | self.defaultingNameCallback?(conversation?.defaultingName ?? "") 56 | } 57 | } 58 | } 59 | 60 | var hasMoreNext: Bool { 61 | return false 62 | } 63 | 64 | var hasMorePrevious: Bool { 65 | return false 66 | } 67 | 68 | func loadNext() { 69 | 70 | } 71 | 72 | func loadPrevious() { 73 | 74 | } 75 | 76 | func adjustNumberOfMessages(preferredMaxCount: Int?, focusPosition: Double, completion: ((Bool)) -> Void) { 77 | completion(false) 78 | } 79 | 80 | func sendMessage(text: String){ 81 | guard let conversation = self.conversation else { fatalError("We are not attached to a conversation. It is nil") } 82 | ChatMessage.sendTextChatMessage(conversation: conversation, text: text) 83 | } 84 | 85 | deinit { 86 | notificationToken?.invalidate() 87 | observeConversationToken?.invalidate() 88 | 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /RChat-iOS/Extensions/Bundle+Utilities.swift: -------------------------------------------------------------------------------- 1 | 2 | //////////////////////////////////////////////////////////////////////////// 3 | // 4 | // Copyright 2016 Realm Inc. 5 | // 6 | // Licensed under the Apache License, Version 2.0 (the "License"); 7 | // you may not use this file except in compliance with the License. 8 | // You may obtain a copy of the License at 9 | // 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // 12 | // Unless required by applicable law or agreed to in writing, software 13 | // distributed under the License is distributed on an "AS IS" BASIS, 14 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | // See the License for the specific language governing permissions and 16 | // limitations under the License. 17 | // 18 | //////////////////////////////////////////////////////////////////////////// 19 | // 20 | // Bundle+Utilities.swift 21 | // RealmBingo 22 | // 23 | // Created by David Spector on 4/8/17. 24 | // Copyright © 2017 Realm. All rights reserved. 25 | // 26 | 27 | import Foundation 28 | 29 | /* 30 | This is a little collection of convenience methods to get useful info out of an application property bundle. 31 | 32 | 33 | Apple document all of the official oners here: 34 | 35 | https://developer.apple.com/library/content/documentation/General/Reference/InfoPlistKeyReference/Articles/AboutInformationPropertyListFiles.html 36 | 37 | You can, of course define your own - most commonly people put things like the build date in their app bundles as a way of differentiating 38 | different buld on the same day, etc. You can also set any of these properties at com;ile or link time by using Apple's version tool 39 | which is part fo the Xcode chain. 40 | 41 | Apple's recommended version strategy can be found in TN2420: https://developer.apple.com/library/content/technotes/tn2420/_index.html 42 | 43 | */ 44 | extension Bundle { 45 | 46 | 47 | // returns the canonical appliocation name - this is the name default name of the app if CFBundleDisplayName is not set 48 | var appName: String? { 49 | return Bundle.main.infoDictionary!["CFBundleName"] as! String? 50 | } 51 | 52 | // returns the canonical application name - this is the name default name of the app if CFDisplayName is not specified 53 | var displayName: String? { 54 | return Bundle.main.infoDictionary!["CFBundleDisplayName"] as! String? 55 | } 56 | 57 | // returns the application bundle name - e.g., com.myorg.thisAppBundleName 58 | var bundleName: String? { 59 | return Bundle.main.infoDictionary!["CFBundleIdentifier"] as! String? 60 | } 61 | 62 | 63 | // returns the build number - this is not your applications version number but the internal build number 64 | // that should be incremented with every iTunesConnect submission 65 | var buildNumber: String? { 66 | return Bundle.main.infoDictionary!["CFBundleVersion"] as? String // was: CFBuildNumber 67 | } 68 | 69 | 70 | // returns the version number - known as the marketing version number 71 | var vesionNumber: String? { 72 | return Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String? 73 | } 74 | 75 | // returns the version number - known as the marketing version number 76 | var buildDate: Date? { 77 | return Bundle.main.infoDictionary!["CFBuildDate"] as! Date? 78 | } 79 | 80 | 81 | } 82 | -------------------------------------------------------------------------------- /RChat-iOS/Extensions/View+Utilities.swift: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Copyright 2016 Realm Inc. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | //////////////////////////////////////////////////////////////////////////// 18 | 19 | import Foundation 20 | import UIKit 21 | 22 | extension UIView { 23 | /** 24 | Shake the current view 25 | 26 | - parameter offset: offset in pixels 27 | - parameter count: number of times to shake 28 | */ 29 | func shakeWithOffset(offset:CGFloat, count: Float) { 30 | let animation = CABasicAnimation(keyPath: "position.x") 31 | animation.duration = 0.05 32 | animation.repeatCount = count 33 | animation.autoreverses = true 34 | animation.fromValue = self.center.x - offset 35 | animation.toValue = self.center.x + offset 36 | 37 | self.layer.add(animation, forKey: "position.x") 38 | } 39 | 40 | 41 | /** 42 | Ramp up (fade in) the alpha of the view over the user provided duration 43 | 44 | - parameter duration: duration in seconds, defaults to 1.0 45 | */ 46 | func fadeIn(duration: TimeInterval = 0.5) { 47 | UIView.animate(withDuration: duration, delay: 0.0, options: UIViewAnimationOptions.curveEaseIn, animations: { 48 | self.alpha = 1.0 // Instead of a specific instance of, say, birdTypeLabel, we simply set [thisInstance] (ie, self)'s alpha 49 | }, completion: nil) 50 | } 51 | 52 | /** 53 | Ramp down (fade out) the alpha of the view over the user provided duration 54 | 55 | - parameter duration: duration in seconds, defaults to 1.0 56 | */ 57 | func fadeOut(duration: TimeInterval = 1.0) { 58 | UIView.animate(withDuration: duration, delay: 0.0, options: UIViewAnimationOptions.curveEaseOut, animations: { 59 | self.alpha = 0.0 60 | }, completion: nil) 61 | } 62 | 63 | 64 | /** 65 | Apply a gradient of the supplied colors in even bands to a given view 66 | */ 67 | func gradientLayerForView(colors:[CGColor]) { 68 | let gradientLayer = CAGradientLayer() 69 | gradientLayer.frame = self.bounds 70 | gradientLayer.colors = colors 71 | let interval = Float(self.bounds.size.height) / Float(gradientLayer.colors!.count) 72 | var locations: [NSNumber]? 73 | 74 | locations = stride(from:0.0, through: Double(bounds.size.height), by: Double(interval)).map{NSNumber(value: $0)} // was CGFloat 75 | // Swift 2.3: 76 | //for index in Float(0).stride(through: Float(self.bounds.size.height) , by: interval) { 77 | // locations?.append(NSNumber(index)) 78 | //} 79 | gradientLayer.locations = locations 80 | self.layer.addSublayer(gradientLayer) 81 | 82 | } 83 | 84 | 85 | 86 | 87 | } 88 | -------------------------------------------------------------------------------- /RChat-iOS/Extensions/Color+Uilities.swift: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Copyright 2016 Realm Inc. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | //////////////////////////////////////////////////////////////////////////// 18 | 19 | import Foundation 20 | import UIKit 21 | 22 | extension UIColor { 23 | 24 | class func fromHex(hexString: String, alpha : Float = 1.0) -> UIColor { 25 | var newColor = UIColor.clear // this compensates for a bug in Swift2.x 26 | let scan = Scanner(string: hexString) 27 | var hexValue : UInt32 = 0 28 | if scan.scanHexInt32(&hexValue) { 29 | let r : CGFloat = CGFloat( hexValue >> 16 & 0x0ff) / 255.0 30 | let g : CGFloat = CGFloat( hexValue >> 8 & 0x0ff) / 255.0 31 | let b : CGFloat = CGFloat( hexValue & 0x0ff) / 255.0 32 | newColor = UIColor(red: r , green: g, blue: b , alpha: CGFloat(alpha)) 33 | } 34 | 35 | return newColor 36 | } 37 | 38 | func colorByAdjustingSaturation(factor : CGFloat) -> UIColor{ 39 | var h: CGFloat = 0 40 | var s: CGFloat = 0 41 | var b: CGFloat = 0 42 | var alpha:CGFloat = 0 43 | 44 | // get the old HSB 45 | self.getHue(&h, saturation: &s, brightness: &b, alpha: &alpha ) 46 | // apply the new S 47 | let newColor = UIColor(hue: h, saturation: s * factor, brightness: b, alpha: alpha) 48 | 49 | return newColor 50 | } 51 | 52 | func colorByAdjustingBrightness(factor : CGFloat) -> UIColor{ 53 | var h: CGFloat = 0 54 | var s: CGFloat = 0 55 | var b: CGFloat = 0 56 | var alpha:CGFloat = 0 57 | 58 | // get the old HSB 59 | self.getHue(&h, saturation: &s, brightness: &b, alpha: &alpha ) 60 | // apply the new B 61 | let newColor = UIColor(hue: h, saturation: s, brightness: b * factor, alpha: alpha) 62 | 63 | return newColor 64 | } 65 | 66 | func colorByAdjustingHue(factor : CGFloat) -> UIColor{ 67 | var h: CGFloat = 0 68 | var s: CGFloat = 0 69 | var b: CGFloat = 0 70 | var alpha:CGFloat = 0 71 | 72 | // get the old HSB 73 | self.getHue(&h, saturation: &s, brightness: &b, alpha: &alpha ) 74 | // apply the new H 75 | let newColor = UIColor(hue: h * factor, saturation: s, brightness: b , alpha: alpha) 76 | 77 | return newColor 78 | } 79 | 80 | func hexString() -> String { 81 | let components = self.cgColor.components 82 | 83 | let red = Float(components![0]) 84 | let green = Float(components![1]) 85 | let blue = Float(components![2]) 86 | return String(format: "#%02lX%02lX%02lX", lroundf(red * 255), lroundf(green * 255), lroundf(blue * 255)) 87 | } 88 | 89 | 90 | } 91 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/ConversationTableViewCell .swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConversationTableViewCell .swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 1/10/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Cartography 11 | 12 | class ConversationTableViewCell : UITableViewCell { 13 | 14 | lazy var unreadIndicatorLabel : UILabel = { 15 | let label = UILabel() 16 | label.backgroundColor = .clear 17 | label.layer.cornerRadius = RChatConstants.Numbers.cornerRadius 18 | label.layer.masksToBounds = true 19 | label.textColor = .white 20 | label.font = RChatConstants.Fonts.boldFont 21 | label.adjustsFontSizeToFitWidth = true 22 | label.textAlignment = .center 23 | return label 24 | }() 25 | 26 | static let REUSE_ID = "ConversationTableViewCell" 27 | static let HEIGHT : CGFloat = 45 28 | 29 | override init(style: UITableViewCellStyle, reuseIdentifier: String?) { 30 | super.init(style: style, reuseIdentifier: reuseIdentifier) 31 | backgroundColor = .clear 32 | textLabel?.textColor = UIColor.white 33 | textLabel?.translatesAutoresizingMaskIntoConstraints = false 34 | 35 | textLabel?.numberOfLines = 0 36 | textLabel?.textAlignment = .left 37 | textLabel?.lineBreakMode = .byWordWrapping 38 | 39 | contentView.addSubview(unreadIndicatorLabel) 40 | 41 | constrain(textLabel!, unreadIndicatorLabel) { (textLabel, unreadIndicatorLabel) in 42 | unreadIndicatorLabel.left == unreadIndicatorLabel.superview!.left + RChatConstants.Numbers.horizontalSpacing 43 | unreadIndicatorLabel.height == 28 44 | unreadIndicatorLabel.width == 28 45 | unreadIndicatorLabel.centerY == unreadIndicatorLabel.superview!.centerY 46 | 47 | textLabel.left == unreadIndicatorLabel.right + RChatConstants.Numbers.horizontalSpacing 48 | textLabel.top == textLabel.superview!.top 49 | textLabel.bottom == textLabel.superview!.bottom 50 | textLabel.right == unreadIndicatorLabel.superview!.right - RChatConstants.Numbers.minorHorizontalSpacing 51 | } 52 | } 53 | 54 | required init?(coder aDecoder: NSCoder) { 55 | fatalError("init(coder:) has not been implemented") 56 | } 57 | 58 | func setupWithConversation(conversation: Conversation){ 59 | textLabel?.text = conversation.defaultingName 60 | textLabel?.font = conversation.unreadCount > 0 ? RChatConstants.Fonts.boldFont : RChatConstants.Fonts.regularFont 61 | unreadIndicatorLabel.text = conversation.unreadCount == 0 ? "0" : "\(conversation.unreadCount)" 62 | unreadIndicatorLabel.layer.borderWidth = conversation.unreadCount == 0 ? 2.0 : 0 63 | 64 | // let's colorize the unread count depending on how far behind the user is on a conversation... 65 | switch conversation.unreadCount { 66 | case 0: 67 | unreadIndicatorLabel.layer.borderColor = UIColor.lightGray.cgColor 68 | unreadIndicatorLabel.backgroundColor = UIColor.clear 69 | break 70 | case 1...25: 71 | unreadIndicatorLabel.layer.borderColor = UIColor.green.cgColor 72 | unreadIndicatorLabel.backgroundColor = UIColor.clear 73 | break 74 | case 26...50: 75 | unreadIndicatorLabel.layer.borderColor = UIColor.yellow.cgColor 76 | unreadIndicatorLabel.backgroundColor = UIColor.darkGray 77 | break 78 | default: 79 | unreadIndicatorLabel.layer.borderColor = UIColor.black.cgColor 80 | unreadIndicatorLabel.backgroundColor = UIColor.red 81 | } 82 | } 83 | 84 | override func prepareForReuse() { 85 | super.prepareForReuse() 86 | textLabel?.text = "" 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/LoginViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginViewController.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 1/20/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Eureka 11 | 12 | class LoginViewControler : FormViewController { 13 | 14 | lazy var usernameRow : TextRow = { 15 | let row = TextRow() 16 | row.title = "Username" 17 | row.cellSetup({ (cell, row) in 18 | cell.textField.autocapitalizationType = .none 19 | }) 20 | return row 21 | }() 22 | 23 | lazy var passwordRow : PasswordRow = { 24 | let row = PasswordRow() 25 | row.title = "Password" 26 | return row 27 | }() 28 | 29 | lazy var buttonRow : ButtonRow = { 30 | let row = ButtonRow() 31 | row.title = "Login" 32 | return row 33 | }() 34 | 35 | 36 | let viewModel : LoginViewModel 37 | 38 | init(mode: LoginViewModel.Mode){ 39 | viewModel = LoginViewModel(mode: mode) 40 | super.init(nibName: nil, bundle: nil) 41 | } 42 | 43 | required init?(coder aDecoder: NSCoder) { 44 | fatalError("init(coder:) has not been implemented") 45 | } 46 | 47 | 48 | override func viewWillAppear(_ animated: Bool) { 49 | super.viewWillAppear(animated) 50 | navigationController?.setNavigationBarHidden(false, animated: true) 51 | } 52 | 53 | override func viewDidLoad() { 54 | super.viewDidLoad() 55 | title = "Login" 56 | 57 | form +++ Section() 58 | <<< usernameRow 59 | <<< passwordRow 60 | 61 | form +++ Section() 62 | <<< buttonRow 63 | 64 | 65 | // TO VIEWMODEL 66 | usernameRow.onChange { [weak self] (r) in 67 | self?.viewModel.username = r.value ?? "" 68 | } 69 | 70 | passwordRow.onChange { [weak self] (r) in 71 | self?.viewModel.password = r.value ?? "" 72 | } 73 | 74 | buttonRow.onCellSelection { [weak self] (_, _) in 75 | if self?.viewModel.mode == .login { 76 | self?.viewModel.attemptLogin() 77 | } 78 | if self?.viewModel.mode == .signup { 79 | self?.viewModel.attemptRegistration() 80 | } 81 | } 82 | 83 | // FROM VIEWMODEL 84 | viewModel.isProcessingCallback = { [weak self] isProcessing in 85 | guard let `self` = self else { return } 86 | self.buttonRow.disabled = Condition(booleanLiteral: isProcessing) 87 | self.buttonRow.reload() 88 | if isProcessing { 89 | LoadingView.show(superView: self.view) 90 | }else{ 91 | LoadingView.hide(superView: self.view) 92 | } 93 | } 94 | 95 | viewModel.authErrorCallback = { [weak self] errorMessage in 96 | guard let `self` = self else { return } 97 | let alertViewController = UIAlertController(title: errorMessage, message: nil, preferredStyle: .alert) 98 | alertViewController.addAction(UIAlertAction(title: "Okay", style: .cancel, handler: nil)) 99 | self.present(alertViewController, animated: true, completion: nil) 100 | } 101 | 102 | viewModel.authSuccessCallback = { [weak self] _ in 103 | guard let `self` = self else { return } 104 | self.navigationController?.setViewControllers([ChatViewController()], animated: true) 105 | } 106 | 107 | viewModel.modeCallback = { [weak self] mode in 108 | guard let `self` = self else { return } 109 | self.title = mode == .login ? "Login" : "Signup" 110 | self.buttonRow.title = mode == .login ? "Login" : "Signup" 111 | self.buttonRow.reload() 112 | } 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/Conversation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Conversation.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 1/9/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import RealmSwift 10 | 11 | class Conversation : Object { 12 | 13 | dynamic var conversationId: String = RChatConstants.genericConversationId 14 | dynamic var displayName: String = "" 15 | dynamic var unreadCount: Int = 0 16 | let users = List() 17 | let chatMessages = List() 18 | 19 | // This should show some sort of name even if the display name is empty 20 | var defaultingName: String { 21 | if !displayName.isEmptyOrWhitespace { 22 | return displayName 23 | } 24 | return users 25 | .filter("userId != %@", RChatConstants.myUserId) 26 | .map({ $0.defaultingName }).joined(separator: ",") 27 | } 28 | 29 | override static func primaryKey() -> String? { 30 | return "conversationId" 31 | } 32 | 33 | override static func ignoredProperties() -> [String] { 34 | return ["defaultingName"] 35 | } 36 | 37 | } 38 | 39 | extension Conversation { 40 | 41 | static func searchForConversations(searchTerm: String) -> Results { 42 | let realm = RChatConstants.Realms.global 43 | 44 | let predicate = NSPredicate(format: "displayName contains[c] %@", searchTerm) 45 | return realm.objects(Conversation.self).filter(predicate) 46 | } 47 | 48 | static func generateDirectMessage(userId1: String, userId2: String) -> String { 49 | let parts = [userId1, userId2].sorted(by: { $0 < $1 }) 50 | return "dm|\(parts[0])|\(parts[1])" 51 | } 52 | 53 | static func putConversation(users: [User]) -> Conversation { 54 | if users.count < 2 { 55 | fatalError("Cannot create a conversation with less than 2 users") 56 | } 57 | var conversationId : String = UUID().uuidString 58 | if users.count == 2 { 59 | conversationId = generateDirectMessage(userId1: users[0].userId, userId2: users[1].userId) 60 | } 61 | let realm = RChatConstants.Realms.global 62 | let conversation = Conversation() 63 | conversation.conversationId = conversationId 64 | conversation.users.append(objectsIn: users) 65 | try! realm.write { 66 | realm.add(conversation, update: true) 67 | } 68 | return conversation 69 | } 70 | 71 | @discardableResult 72 | static func generateDefaultConversation() -> Conversation { 73 | let realm = RChatConstants.Realms.global 74 | 75 | let createConversation = { () -> Conversation in 76 | let conversation = realm.create(Conversation.self, value: [ 77 | "conversationId": RChatConstants.genericConversationId, 78 | "displayName": "Welcome to RChat" 79 | ], update: true) 80 | // we don't add users here, as this is a public channel 81 | //conversation.users.append(User.getMe()) 82 | 83 | return conversation 84 | } 85 | 86 | // Just call block, already in transaction 87 | if realm.isInWriteTransaction { 88 | return createConversation() 89 | } 90 | 91 | realm.beginWrite() 92 | let conversation = createConversation() 93 | try! realm.commitWrite() 94 | 95 | return conversation 96 | } 97 | 98 | static func observeConversationBy(conversationId: String, callback: @escaping ((Conversation?) -> Void)) -> NotificationToken { 99 | let realm = RChatConstants.Realms.global 100 | let results = realm.objects(Conversation.self).filter("conversationId = %@", conversationId) 101 | return results.observe({ (_) in 102 | let conversation = results.first 103 | callback(conversation) 104 | }) 105 | } 106 | 107 | } 108 | 109 | -------------------------------------------------------------------------------- /RChatMinimalServer/src/index.ts: -------------------------------------------------------------------------------- 1 | import { BasicServer } from 'realm-object-server' 2 | import * as path from 'path' 3 | 4 | const server = new BasicServer() 5 | var theRealm: Realm = null; 6 | const RealmName = "RC-global"; 7 | 8 | console.log(`Directory is ${__dirname}`); 9 | server.start({ 10 | // This is the location where ROS will store its runtime data 11 | //httpsAddress: "0.0.0.0", 12 | dataPath: path.join(__dirname, '../data') 13 | 14 | // The address on which to listen for connections 15 | // Default: 0.0.0.0 16 | // address?: string 17 | 18 | // The port on which to listen for connections 19 | // Default: 9080 20 | // port?: number 21 | 22 | // Override the default list of authentication providers 23 | // authProviders?: IAuthProvider[] 24 | 25 | // Autogenerate public and private keys on startup 26 | // autoKeyGen?: boolean 27 | 28 | // Specify an alternative path to the private key. Otherwise, it is expected to be under the data path. 29 | // privateKeyPath?: string 30 | 31 | // Specify an alternative path to the public key. Otherwise, it is expected to be under the data path. 32 | // publicKeyPath?: string 33 | 34 | // The desired logging threshold. Can be one of: all, trace, debug, detail, info, warn, error, fatal, off) 35 | // Default: info 36 | // logLevel?: string 37 | 38 | // Enable the HTTPS Server. 39 | // https?: boolean 40 | 41 | // The port on which to listen for HTTPS connections. 42 | // Default: 0.0.0.0 43 | // httpsAddress?: string 44 | 45 | // The address on which to listen for HTTPS connections. 46 | // Default: 9443 47 | // httpsPort?: number 48 | 49 | // The path to your HTTPS private key in PEM format. Required if HTTPS is enabled. 50 | // httpsKeyPath?: string 51 | 52 | // The path to your HTTPS certificate chain in PEM format. Required if HTTPS is enabled. 53 | // httpsCertChainPath?: string 54 | 55 | // Specify the length of time (in seconds) in which access tokens are valid. 56 | // Default: 600 (ten minutes) 57 | // accessTokenTtl?: number 58 | 59 | // Specify the length of time (in seconds) in which refresh tokens are valid. 60 | // Default: 3153600000 (ten years) 61 | // refreshTokenTtl?: number 62 | }) 63 | .then(() => { 64 | console.log(`Your server is started `, server.address); 65 | return Realm.Sync.User.login('http://localhost:9080', 'realm-admin', ''); 66 | }) 67 | .then((user) => { 68 | return Realm.open({ 69 | sync: { 70 | user: user, 71 | url: `realm://localhost:9080/${RealmName}` 72 | }, 73 | schema: [], 74 | }); 75 | 76 | }) 77 | .then(realm => { 78 | theRealm = realm; 79 | // if your app has options - maybe to load sample data, or run in a 80 | // specialized mode, could proess them here: 81 | processCommandLineOptions(); 82 | }) 83 | .catch(err => { 84 | console.error(`There was an error starting your file`, err); 85 | }); 86 | 87 | 88 | 89 | function processCommandLineOptions() { 90 | // let param = process.argv[2]; // Gets the first token after the `node index.js` 91 | // if ((typeof param != 'undefined') && param == "--load-sample-data") { 92 | // if (theRealm.objects(PersonSchema.name).length > 0 || fs.existsSync(dataLoadedFilePath) == true) { 93 | // console.log("Data already loaded... skipping.") 94 | // return; 95 | // } else { 96 | // // do someting here - for example: loadSampleData(); 97 | // } 98 | // } // check command line params 99 | } // of processCommandLineOptions 100 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/RChatDecorator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RChatDecorator.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 1/10/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Chatto 11 | import ChattoAdditions 12 | 13 | final class RChatDecorator: ChatItemsDecoratorProtocol { 14 | struct Constants { 15 | static let shortSeparation: CGFloat = 3 16 | static let normalSeparation: CGFloat = 10 17 | static let timeIntervalThresholdToIncreaseSeparation: TimeInterval = 120 18 | } 19 | 20 | func decorateItems(_ chatItems: [ChatItemProtocol]) -> [DecoratedChatItem] { 21 | var decoratedChatItems = [DecoratedChatItem]() 22 | let calendar = Calendar.current 23 | 24 | for (index, chatItem) in chatItems.enumerated() { 25 | let next: ChatItemProtocol? = (index + 1 < chatItems.count) ? chatItems[index + 1] : nil 26 | let prev: ChatItemProtocol? = (index > 0) ? chatItems[index - 1] : nil 27 | 28 | let bottomMargin = self.separationAfterItem(chatItem, next: next) 29 | var showsTail = false 30 | var additionalItems = [DecoratedChatItem]() 31 | 32 | var addTimeSeparator = false 33 | if let currentMessage = chatItem as? MessageModelProtocol { 34 | if let nextMessage = next as? MessageModelProtocol { 35 | showsTail = currentMessage.senderId != nextMessage.senderId 36 | } else { 37 | showsTail = true 38 | } 39 | 40 | if let previousMessage = prev as? MessageModelProtocol { 41 | addTimeSeparator = !calendar.isDate(currentMessage.date, inSameDayAs: previousMessage.date) 42 | } else { 43 | addTimeSeparator = true 44 | } 45 | 46 | if self.showsStatusForMessage(currentMessage) { 47 | additionalItems.append( 48 | DecoratedChatItem( 49 | chatItem: SendingStatusModel(uid: "\(currentMessage.uid)-decoration-status", status: currentMessage.status), 50 | decorationAttributes: nil) 51 | ) 52 | } 53 | 54 | if addTimeSeparator { 55 | let dateTimeStamp = DecoratedChatItem(chatItem: TimeSeparatorModel(uid: "\(currentMessage.uid)-time-separator", date: currentMessage.date.toWeekDayAndDateString()), decorationAttributes: nil) 56 | decoratedChatItems.append(dateTimeStamp) 57 | } 58 | } 59 | 60 | decoratedChatItems.append(DecoratedChatItem( 61 | chatItem: chatItem, 62 | decorationAttributes: ChatItemDecorationAttributes(bottomMargin: bottomMargin, canShowTail: showsTail, canShowAvatar: showsTail, canShowFailedIcon: true)) 63 | ) 64 | decoratedChatItems.append(contentsOf: additionalItems) 65 | } 66 | 67 | return decoratedChatItems 68 | } 69 | 70 | func separationAfterItem(_ current: ChatItemProtocol?, next: ChatItemProtocol?) -> CGFloat { 71 | guard let nexItem = next else { return 0 } 72 | guard let currentMessage = current as? MessageModelProtocol else { return Constants.normalSeparation } 73 | guard let nextMessage = nexItem as? MessageModelProtocol else { return Constants.normalSeparation } 74 | 75 | if self.showsStatusForMessage(currentMessage) { 76 | return 0 77 | } else if currentMessage.senderId != nextMessage.senderId { 78 | return Constants.normalSeparation 79 | } else if nextMessage.date.timeIntervalSince(currentMessage.date) > Constants.timeIntervalThresholdToIncreaseSeparation { 80 | return Constants.normalSeparation 81 | } else { 82 | return Constants.shortSeparation 83 | } 84 | } 85 | 86 | func showsStatusForMessage(_ message: MessageModelProtocol) -> Bool { 87 | return message.status == .failed || message.status == .sending 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/WelcomeViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WelcomeViewController.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 1/20/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Cartography 11 | 12 | class WelcomeViewController : UIViewController { 13 | 14 | lazy var loginButton : RChatButton = { 15 | let b = RChatButton() 16 | b.setTitle("Login", for: .normal) 17 | b.translatesAutoresizingMaskIntoConstraints = false 18 | return b 19 | }() 20 | 21 | lazy var registerButton : RChatButton = { 22 | let b = RChatButton() 23 | b.setTitle("Register", for: .normal) 24 | b.translatesAutoresizingMaskIntoConstraints = false 25 | return b 26 | }() 27 | 28 | lazy var launchLogoImageView : UIImageView = { 29 | let i = UIImageView() 30 | i.image = RChatConstants.Images.launchLogo 31 | i.tintColor = .white 32 | return i 33 | }() 34 | 35 | let viewModel = WelcomeViewModel() 36 | 37 | override func viewWillAppear(_ animated: Bool) { 38 | super.viewWillAppear(animated) 39 | navigationController?.setNavigationBarHidden(true, animated: true) 40 | } 41 | 42 | override func viewDidLoad() { 43 | super.viewDidLoad() 44 | view.backgroundColor = RChatConstants.Colors.primaryColorDark 45 | setupSubviewsAndLayout() 46 | // FROM VIEWMODEL 47 | viewModel.isAlreadyLoggedIn = { [weak self] isAlreadyLogged in 48 | if(isAlreadyLogged){ 49 | self?.navigationController?.setViewControllers([ChatViewController()], animated: true) 50 | } 51 | } 52 | 53 | viewModel.goToLogin = { [weak self] in 54 | self?.navigationController?.pushViewController(LoginViewControler(mode:.login), animated: true) 55 | } 56 | 57 | viewModel.goToSignup = { [weak self] in 58 | self?.navigationController?.pushViewController(LoginViewControler(mode:.signup), animated: true) 59 | } 60 | } 61 | 62 | func loginButtonDidTap(button: UIButton){ 63 | viewModel.loginDidTap() 64 | } 65 | 66 | func registerButtonDidTap(button: UIButton){ 67 | viewModel.registerDidTap() 68 | } 69 | 70 | } 71 | 72 | extension WelcomeViewController { 73 | 74 | func setupSubviewsAndLayout(){ 75 | navigationItem.backBarButtonItem = nil 76 | 77 | view.addSubview(loginButton) 78 | view.addSubview(registerButton) 79 | view.addSubview(launchLogoImageView) 80 | 81 | loginButton.addTarget(self, action: #selector(loginButtonDidTap(button:)), for: .touchUpInside) 82 | registerButton.addTarget(self, action: #selector(registerButtonDidTap(button:)), for: .touchUpInside) 83 | 84 | constrain(loginButton, registerButton, launchLogoImageView) { (loginButton, registerButton, launchLogoImageView) in 85 | 86 | loginButton.left == loginButton.superview!.left + RChatConstants.Numbers.horizontalSpacing 87 | loginButton.height == 50 88 | loginButton.bottom == loginButton.superview!.bottom - RChatConstants.Numbers.majorVerticalSpacing 89 | loginButton.right == loginButton.superview!.centerX - RChatConstants.Numbers.minorHorizontalSpacing 90 | 91 | registerButton.left == registerButton.superview!.centerX + RChatConstants.Numbers.minorHorizontalSpacing 92 | registerButton.height == 50 93 | registerButton.bottom == registerButton.superview!.bottom - RChatConstants.Numbers.majorVerticalSpacing 94 | registerButton.right == registerButton.superview!.right - RChatConstants.Numbers.horizontalSpacing 95 | 96 | launchLogoImageView.height == 100 97 | launchLogoImageView.width == 100 98 | launchLogoImageView.centerX == launchLogoImageView.superview!.centerX 99 | launchLogoImageView.centerY == launchLogoImageView.superview!.centerY 100 | 101 | } 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/RChatConstants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RChatConstants.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 1/10/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Foundation 11 | import RealmSwift 12 | 13 | // NB: this is a Realm2.x app - MUST run against a 2.x server 14 | var realmServerAddress = "127.0.0.1"// "104.131.128.86" //"127.0.0.1" 15 | 16 | // Right now the right nav isn't useful for testing and just confuses users. 17 | let shouldDisplayRightNavItem = false 18 | 19 | 20 | struct RChatConstants { 21 | 22 | static var myUserId : String! { 23 | get { 24 | return SyncUser.current?.identity 25 | } 26 | } 27 | 28 | static var isLoggedIn : Bool { 29 | return SyncUser.current != nil 30 | } 31 | 32 | static var objectServerEndpoint : URL { 33 | return URL(string: "realm://\(realmServerAddress):9080" )! 34 | } 35 | 36 | static var authServerEndpoint : URL { 37 | return URL(string: "http://\(realmServerAddress):9080" )! 38 | } 39 | 40 | static var globalRealmURL : URL { 41 | return URL(string: "\(RChatConstants.objectServerEndpoint.absoluteString)/RC-global")! 42 | } 43 | 44 | static var myRealmURL : URL { 45 | return URL(string: "\(RChatConstants.objectServerEndpoint.absoluteString)/~/RC-userRealm")! 46 | } 47 | 48 | static var genericConversationId : String = "pub|generic" 49 | 50 | /// Referencing Flat Colors from here : https://flatuicolors.com/ 51 | struct Colors { 52 | //APP 53 | static var primaryColor = UIColor(hexString: "#38457c") 54 | static var primaryColorDark = UIColor(hexString: "#1C233F") 55 | 56 | //WHITES AND GRAY 57 | static var clouds = UIColor(hexString: "#ecf0f1") 58 | static var concrete = UIColor(hexString: "#95a5a6") 59 | static var silver = UIColor(hexString: "#bdc3c7") 60 | static var asbestos = UIColor(hexString: "#7f8c8d") 61 | //BLUES 62 | static var peterRiver = UIColor(hexString: "#3498db") 63 | static var belizeHole = UIColor(hexString: "#2980b9") 64 | static var wetAsphalt = UIColor(hexString: "#34495e") 65 | static var midnightBlue = UIColor(hexString: "#2c3e50") 66 | //GREENS 67 | static var nephritis = UIColor(hexString: "#27ae60") 68 | } 69 | 70 | struct Fonts { 71 | static var regularFont = UIFont.systemFont(ofSize: 16) 72 | static var boldFont = UIFont.boldSystemFont(ofSize: 16) 73 | static var dateFont = UIFont.systemFont(ofSize: 14) 74 | } 75 | 76 | struct Images { 77 | static var attachIcon = UIImage(named: "attach_icon")?.withRenderingMode(.alwaysTemplate) 78 | static var sendIcon = UIImage(named: "send_icon")?.withRenderingMode(.alwaysTemplate) 79 | static var menuIcon = UIImage(named: "menu_icon")?.withRenderingMode(.alwaysTemplate) 80 | static var profileIcon = UIImage(named: "profile_icon")?.withRenderingMode(.alwaysTemplate) 81 | static var penIcon = UIImage(named: "pen_icon")?.withRenderingMode(.alwaysTemplate) 82 | static var launchLogo = UIImage(named: "launch_logo")?.withRenderingMode(.alwaysTemplate) 83 | static var verticalMoreIcon = UIImage(named: "more_verticle_icon")?.withRenderingMode(.alwaysTemplate) 84 | } 85 | 86 | struct Numbers { 87 | static var horizontalSpacing : CGFloat = 16 88 | static var minorHorizontalSpacing : CGFloat = 8 89 | static var verticalSpacing : CGFloat = 8 90 | static var majorVerticalSpacing : CGFloat = 16 91 | static var cornerRadius : CGFloat = 4 92 | } 93 | 94 | struct Realms { 95 | static var global : Realm { 96 | let syncServerURL = RChatConstants.globalRealmURL 97 | let config = Realm.Configuration(syncConfiguration: SyncConfiguration(user: SyncUser.current!, realmURL: syncServerURL)) 98 | return try! Realm(configuration: config) 99 | } 100 | static var myRealm : Realm { 101 | let syncServerURL = RChatConstants.myRealmURL 102 | let config = Realm.Configuration(syncConfiguration: SyncConfiguration(user: SyncUser.current!, realmURL: syncServerURL)) 103 | return try! Realm(configuration: config) 104 | } 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Filing Issues 4 | 5 | Whether you find a bug, typo or have a feature request please [file an issue](https://github.com/realm-demos/realm-puzzle/issues) on our GitHub repository. 6 | 7 | When filing an issue, please provide as much of the following information as possible in order to help others fix it: 8 | 9 | 1. **Goals** 10 | 2. **Expected results** 11 | 3. **Actual results** 12 | 4. **Steps to reproduce** 13 | 5. **Code sample that highlights the issue** (full Xcode projects that we can compile ourselves are ideal) 14 | 6. **Version of Realm / Xcode / OSX** 15 | 7. **Version of involved dependency manager (CocoaPods / Carthage)** 16 | 17 | ### Speeding things up :runner: 18 | 19 | You may just copy this little script below and run it directly in your project directory in **Terminal.app**. It will take of compiling a list of relevant data as described in points 6. and 7. in the list above. It copies the list directly to your pasteboard for your convenience, so you can attach it easily when filing a new issue without having to worry about formatting and we may help you faster because we don't have to ask for particular details of your local setup first. 20 | 21 | ```shell 22 | echo "\`\`\` 23 | $(sw_vers) 24 | 25 | $(xcode-select -p) 26 | $(xcodebuild -version) 27 | 28 | $(which pod && pod --version) 29 | $(test -e Podfile.lock && cat Podfile.lock | sed -nE 's/^ - (Realm(Swift)? [^:]*):?/\1/p' || echo "(not in use here)") 30 | 31 | $(which bash && bash -version | head -n1) 32 | 33 | $(which carthage && carthage version) 34 | $(test -e Cartfile.resolved && cat Cartfile.resolved | grep --color=no realm || echo "(not in use here)") 35 | 36 | $(which git && git --version) 37 | \`\`\`" | tee /dev/tty | pbcopy 38 | ``` 39 | 40 | ## Contributing Enhancements 41 | 42 | We love contributions to our apps! If you'd like to contribute code, documentation, or any other improvements, please [file a Pull Request](https://github.com/realm-demos/realm-puzzle/pulls) on our GitHub repository. Make sure to accept our [CLA](#cla) and to follow our [style guide](https://github.com/realm/realm-cocoa/wiki/Objective-C-Style-Guide). 43 | 44 | ### Commit Messages 45 | 46 | Although we don’t enforce a strict format for commit messages, we prefer that you follow the guidelines below, which are common among open source projects. Following these guidelines helps with the review process, searching commit logs and documentation of implementation details. At a high level, the contents of the commit message should convey the rationale of the change, without delving into much detail. For example, `setter names were not set right` leaves the reviewer wondering about which bits and why they weren’t “right”. In contrast, `[RLMProperty] Correctly capitalize setterName` conveys almost all there is to the change. 47 | 48 | Below are some guidelines about the format of the commit message itself: 49 | 50 | * Separate the commit message into a single-line title and a separate body that describes the change. 51 | * Make the title concise to be easily read within a commit log. 52 | * Make the body concise, while including the complete reasoning. Unless required to understand the change, additional code examples or other details should be left to the pull request. 53 | * If the commit fixes a bug, include the number of the issue in the message. 54 | * Use the first person present tense - for example "Fix …" instead of "Fixes …" or "Fixed …". 55 | * For text formatting and spelling, follow the same rules as documentation and in-code comments — for example, the use of capitalization and periods. 56 | * If the commit is a bug fix on top of another recently committed change, or a revert or reapply of a patch, include the Git revision number of the prior related commit, e.g. `Revert abcd3fg because it caused #1234`. 57 | 58 | ### CLA 59 | 60 | Realm welcomes all contributions! The only requirement we have is that, like many other projects, we need to have a [Contributor License Agreement](https://en.wikipedia.org/wiki/Contributor_License_Agreement) (CLA) in place before we can accept any external code. Our own CLA is a modified version of the Apache Software Foundation’s CLA. 61 | 62 | [Please submit your CLA electronically using our Google form](https://docs.google.com/forms/d/1bVp-Wp5nmNFz9Nx-ngTmYBVWVdwTyKj4T0WtfVm0Ozs/viewform?fbzx=4154977190905366979) so we can accept your submissions. The GitHub username you file there will need to match that of your Pull Requests. If you have any questions or cannot file the CLA electronically, you can email . 63 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/MembersViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MembersViewController.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 2/8/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SideMenu 11 | import RealmSwift 12 | import Cartography 13 | 14 | protocol MembersViewControllerDelegate: class { 15 | func memberSelected(user: User) 16 | } 17 | 18 | class MembersViewController: 19 | UISideMenuNavigationController, 20 | UITableViewDataSource, 21 | UITableViewDelegate 22 | { 23 | 24 | weak var membersViewControllerDelegate: MembersViewControllerDelegate? 25 | 26 | lazy var tableView: UITableView = { 27 | let tableView = UITableView() 28 | tableView.separatorColor = .clear 29 | tableView.separatorInset = .zero 30 | tableView.contentInset = UIEdgeInsetsMake(80, 0, 300, 0) 31 | tableView.register(MemberTableViewCell.self, forCellReuseIdentifier: MemberTableViewCell.REUSE_ID) 32 | tableView.rowHeight = MemberTableViewCell.HEIGHT 33 | return tableView 34 | }() 35 | 36 | var members: List? 37 | var token : NotificationToken? 38 | 39 | override func viewDidLoad() { 40 | super.viewDidLoad() 41 | title = "Members" 42 | view.backgroundColor = UIColor.white 43 | view.addSubview(tableView) 44 | constrain(tableView) { (tableView) in 45 | tableView.left == tableView.superview!.left 46 | tableView.right == tableView.superview!.right 47 | tableView.top == tableView.superview!.top 48 | tableView.bottom == tableView.superview!.bottom 49 | } 50 | tableView.delegate = self 51 | tableView.dataSource = self 52 | } 53 | 54 | override func didReceiveMemoryWarning() { 55 | super.didReceiveMemoryWarning() 56 | // Dispose of any resources that can be recreated. 57 | } 58 | 59 | override var preferredStatusBarStyle: UIStatusBarStyle { 60 | return .default 61 | } 62 | 63 | func numberOfSections(in tableView: UITableView) -> Int { 64 | return 1 65 | } 66 | 67 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 68 | return members?.count ?? 0 69 | } 70 | 71 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 72 | let cell = tableView.dequeueReusableCell(withIdentifier: MemberTableViewCell.REUSE_ID, for: indexPath) as! MemberTableViewCell 73 | if let user = members?[indexPath.row] { 74 | cell.setupWithUser(user: user) 75 | } 76 | return cell 77 | } 78 | 79 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 80 | tableView.deselectRow(at: indexPath, animated: true) 81 | guard let user = members?[indexPath.row] else { return } 82 | membersViewControllerDelegate?.memberSelected(user: user) 83 | dismiss(animated: true, completion: nil) 84 | } 85 | 86 | func setupWithConversation(conversation: Conversation) { 87 | token?.invalidate() 88 | members = conversation.users 89 | token = conversation.users.observe({ [weak self] (changes) in 90 | guard let `self` = self else { return } 91 | switch changes { 92 | case .initial: 93 | // Results are now populated and can be accessed without blocking the UI 94 | self.tableView.reloadData() 95 | break 96 | case .update(_, let deletions, let insertions, let modifications): 97 | // Query results have changed, so apply them to the UITableView 98 | self.tableView.beginUpdates() 99 | self.tableView.insertRows(at: insertions.map({ IndexPath(row: $0, section: 0) }), 100 | with: .automatic) 101 | self.tableView.deleteRows(at: deletions.map({ IndexPath(row: $0, section: 0)}), 102 | with: .automatic) 103 | self.tableView.reloadRows(at: modifications.map({ IndexPath(row: $0, section: 0) }), 104 | with: .automatic) 105 | self.tableView.endUpdates() 106 | break 107 | case .error(let error): 108 | // An error occurred while opening the Realm file on the background worker thread 109 | fatalError("\(error)") 110 | break 111 | } 112 | }) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/ComposeViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ComposeViewController.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 2/1/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import TURecipientBar 11 | import Cartography 12 | 13 | protocol ComposeViewControllerDelegate : class { 14 | func composeWithUsers(users: [User]) 15 | } 16 | 17 | class ComposeViewController: UIViewController, TURecipientsBarDelegate, UITableViewDataSource, UITableViewDelegate { 18 | 19 | weak var delegate: ComposeViewControllerDelegate? 20 | 21 | lazy var recipientBar : RChatRecipientBar = { 22 | let recipientBar = RChatRecipientBar() 23 | return recipientBar 24 | }() 25 | 26 | lazy var tableView : UITableView = { 27 | let tableView = UITableView() 28 | tableView.register(ComposeUserTableViewCell.self, forCellReuseIdentifier: ComposeUserTableViewCell.REUSE_ID) 29 | tableView.rowHeight = ComposeUserTableViewCell.HEIGHT 30 | return tableView 31 | }() 32 | 33 | lazy var doneButton : UIBarButtonItem = { 34 | let button = UIBarButtonItem(title: "Done", style: .plain, target: self, action: #selector(ComposeViewController.doneButtonDidTap(button:))) 35 | return button 36 | }() 37 | 38 | var users = [User]() 39 | 40 | override func viewDidAppear(_ animated: Bool) { 41 | super.viewDidAppear(animated) 42 | recipientBar.becomeFirstResponder() 43 | } 44 | 45 | override func viewWillDisappear(_ animated: Bool) { 46 | super.viewWillDisappear(animated) 47 | recipientBar.resignFirstResponder() 48 | } 49 | 50 | override func viewDidLoad() { 51 | super.viewDidLoad() 52 | view.addSubview(recipientBar) 53 | view.addSubview(tableView) 54 | 55 | navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Cancel", style: .plain, target: self, action: #selector(ComposeViewController.cancelDidTap(button:))) 56 | recipientBar.recipientsBarDelegate = self 57 | title = "New Message" 58 | 59 | tableView.delegate = self 60 | tableView.dataSource = self 61 | 62 | constrain(recipientBar, tableView) { (recipientBar, tableView) in 63 | recipientBar.left == recipientBar.superview!.left 64 | recipientBar.right == recipientBar.superview!.right 65 | recipientBar.top == recipientBar.superview!.top 66 | recipientBar.height >= 45 67 | 68 | tableView.left == tableView.superview!.left 69 | tableView.right == tableView.superview!.right 70 | tableView.bottom == tableView.superview!.bottom 71 | tableView.top == recipientBar.bottom 72 | } 73 | } 74 | 75 | func cancelDidTap(button: UIBarButtonItem){ 76 | dismiss(animated: true, completion: nil) 77 | } 78 | 79 | func doneButtonDidTap(button: UIBarButtonItem){ 80 | let users = recipientBar.users 81 | var mutableUsers = [User]() 82 | mutableUsers.append(contentsOf: users) 83 | mutableUsers.append(User.getMe()) // lets add me 84 | dismiss(animated: true) { 85 | self.delegate?.composeWithUsers(users: mutableUsers) 86 | } 87 | } 88 | 89 | func recipientsBar(_ recipientsBar: TURecipientsBar, textDidChange searchText: String?) { 90 | let searchTextNonOptional = searchText ?? "" 91 | let users = User.searchForUsers(searchTerm: searchTextNonOptional) 92 | self.users = Array(users) 93 | self.tableView.reloadData() 94 | } 95 | 96 | func recipientsBar(_ recipientsBar: TURecipientsBar, didAdd recipient: TURecipientProtocol) { 97 | evaluateDoneButton() 98 | recipientsBar.text = "" 99 | } 100 | 101 | func recipientsBar(_ recipientsBar: TURecipientsBar, didRemove recipient: TURecipientProtocol) { 102 | evaluateDoneButton() 103 | } 104 | 105 | func evaluateDoneButton(){ 106 | navigationItem.rightBarButtonItem = recipientBar.users.count == 0 ? nil : doneButton 107 | } 108 | 109 | override func didReceiveMemoryWarning() { 110 | super.didReceiveMemoryWarning() 111 | // Dispose of any resources that can be recreated. 112 | } 113 | 114 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 115 | return users.count 116 | } 117 | 118 | func numberOfSections(in tableView: UITableView) -> Int { 119 | return 1 120 | } 121 | 122 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 123 | let cell = tableView.dequeueReusableCell(withIdentifier: ComposeUserTableViewCell.REUSE_ID, for: indexPath) as! ComposeUserTableViewCell 124 | let user = users[indexPath.row] 125 | cell.nameLabel.text = user.displayName 126 | return cell 127 | } 128 | 129 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 130 | tableView.deselectRow(at: indexPath, animated: true) 131 | tableView.reloadRows(at: [indexPath], with: .automatic) 132 | let user = users[indexPath.row] 133 | if recipientBar.containsUser(user: user) { 134 | recipientBar.removeUser(user: user) 135 | } else { 136 | recipientBar.addUser(user: user) 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/SearchResultsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchResultsViewController.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 2/8/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Cartography 11 | import RealmSwift 12 | 13 | protocol SearchResultsViewControllerDelegate : class { 14 | func selectedSearchedConversation(conversation: Conversation) 15 | } 16 | 17 | class SearchResultsViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { 18 | 19 | weak var delegate: SearchResultsViewControllerDelegate? 20 | 21 | var conversations : Results? 22 | var users: Results? 23 | var chats: Results? 24 | 25 | lazy var tableView : UITableView = { 26 | let t = UITableView() 27 | t.separatorInset = .zero 28 | t.separatorColor = .clear 29 | t.backgroundColor = RChatConstants.Colors.primaryColorDark 30 | t.keyboardDismissMode = .interactive 31 | t.register(SearchResultTableViewCell.self, forCellReuseIdentifier: SearchResultTableViewCell.REUSE_ID) 32 | return t 33 | }() 34 | 35 | override func viewDidLoad() { 36 | super.viewDidLoad() 37 | view.addSubview(tableView) 38 | constrain(tableView) { (tableView) in 39 | tableView.left == tableView.superview!.left 40 | tableView.right == tableView.superview!.right 41 | tableView.top == tableView.superview!.top 42 | tableView.bottom == tableView.superview!.bottom 43 | } 44 | 45 | tableView.dataSource = self 46 | tableView.delegate = self 47 | } 48 | 49 | func numberOfSections(in tableView: UITableView) -> Int { 50 | return 3 // Was 2 51 | } 52 | 53 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 54 | switch section { 55 | case 0: 56 | return conversations?.count ?? 0 57 | case 1: 58 | return users?.count ?? 0 59 | 60 | // Added support for searching inside chats 61 | case 2: 62 | return chats?.count ?? 0 63 | default: 64 | return 0 65 | } 66 | } 67 | 68 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 69 | let cell = tableView.dequeueReusableCell(withIdentifier: SearchResultTableViewCell.REUSE_ID, for: indexPath) as! SearchResultTableViewCell 70 | 71 | switch indexPath.section { 72 | case 0: 73 | if let conversation = conversations?[indexPath.row] { 74 | cell.setupWithConversation(conversation: conversation) 75 | } 76 | case 1: 77 | if let user = users?[indexPath.row] { 78 | cell.setupWithUser(user: user) 79 | } 80 | 81 | // Added support for searching inside chats 82 | case 2: 83 | if let chat = chats?[indexPath.row] { 84 | cell.setupWithChat(chat: chat) 85 | } 86 | default: 87 | fatalError("No such section exists") 88 | } 89 | return cell 90 | } 91 | 92 | func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 93 | var result: String? 94 | switch section { 95 | case 0: 96 | result = "Conversations" 97 | case 1: // users 98 | result = "Users" 99 | case 2: 100 | result = "Chats" 101 | default: 102 | result = "Unknown Section!" 103 | } 104 | return result 105 | } 106 | 107 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 108 | tableView.deselectRow(at: indexPath, animated: true) 109 | switch indexPath.section { 110 | case 0: 111 | if let conversation = conversations?[indexPath.row] { 112 | delegate?.selectedSearchedConversation(conversation: conversation) 113 | } 114 | return; 115 | case 1: 116 | if let user = users?[indexPath.row] { 117 | var mutableUsers = [User]() 118 | mutableUsers.append(user) 119 | mutableUsers.append(User.getMe()) // lets add me 120 | let conversation = Conversation.putConversation(users: mutableUsers) 121 | delegate?.selectedSearchedConversation(conversation: conversation) 122 | } 123 | return; 124 | 125 | // Added support for searching inside chats 126 | case 2: 127 | if let chat = chats?[indexPath.row] { 128 | let conversation = chat.conversations.first 129 | delegate?.selectedSearchedConversation(conversation: conversation!) 130 | } 131 | return; 132 | 133 | default: 134 | fatalError("No such section exists") 135 | } 136 | } 137 | 138 | func searchConversationsAndUsers(searchTerm: String){ 139 | conversations = Conversation.searchForConversations(searchTerm: searchTerm) 140 | users = User.searchForUsers(searchTerm: searchTerm) 141 | 142 | // Added support for searching inside chats 143 | chats = ChatMessage.searchInChats(searchTerm: searchTerm) 144 | 145 | tableView.reloadData() 146 | } 147 | 148 | } 149 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/RChatInputView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RChatInputView.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 1/10/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol RChatInputViewDelegate : class { 12 | func sendMessage(text: String) 13 | func attachmentButtonDidTapped() 14 | } 15 | 16 | class RChatInputView : UIView, UITextViewDelegate { 17 | 18 | weak var delegate : RChatInputViewDelegate? 19 | 20 | let textView : UITextView = { 21 | let textView = UITextView() 22 | textView.textContainerInset = UIEdgeInsets(top: 8, left: 10, bottom: 8, right: 40) 23 | textView.isScrollEnabled = false 24 | textView.layer.borderColor = RChatConstants.Colors.silver.cgColor 25 | textView.layer.borderWidth = 1.0 26 | textView.layer.cornerRadius = 36 / 2 27 | textView.layer.masksToBounds = true 28 | textView.translatesAutoresizingMaskIntoConstraints = false 29 | textView.font = RChatConstants.Fonts.regularFont 30 | return textView 31 | }() 32 | 33 | lazy var topBorder : UIView = { 34 | let view = UIView() 35 | view.backgroundColor = RChatConstants.Colors.silver 36 | view.translatesAutoresizingMaskIntoConstraints = false 37 | return view 38 | }() 39 | 40 | lazy var attachmentButton : UIButton = { 41 | let button = UIButton() 42 | button.tintColor = RChatConstants.Colors.primaryColor 43 | let image = RChatConstants.Images.attachIcon 44 | button.setImage(image, for: .normal) 45 | button.translatesAutoresizingMaskIntoConstraints = false 46 | return button 47 | }() 48 | 49 | lazy var sendButton : UIButton = { 50 | let button = UIButton() 51 | button.layer.cornerRadius = 28 / 2 52 | button.layer.masksToBounds = true 53 | button.backgroundColor = RChatConstants.Colors.primaryColor 54 | let image = RChatConstants.Images.sendIcon 55 | button.setImage(image, for: .normal) 56 | button.tintColor = .white 57 | button.imageView?.contentMode = .scaleAspectFit 58 | button.imageEdgeInsets = UIEdgeInsetsMake(5, 5, 5, 5) 59 | button.alpha = 0 60 | button.translatesAutoresizingMaskIntoConstraints = false 61 | return button 62 | }() 63 | 64 | init(){ 65 | super.init(frame: CGRect.zero) 66 | translatesAutoresizingMaskIntoConstraints = false 67 | backgroundColor = RChatConstants.Colors.clouds 68 | 69 | addSubview(textView) 70 | addSubview(attachmentButton) 71 | addSubview(topBorder) 72 | addSubview(sendButton) 73 | 74 | textView.delegate = self 75 | sendButton.addTarget(self, action: #selector(sendButtonDidTap), for: .touchUpInside) 76 | attachmentButton.addTarget(self, action: #selector(attachmentButtonDidTap), for: .touchUpInside) 77 | 78 | addConstraints( 79 | NSLayoutConstraint.constraints(withVisualFormat: "H:|-0-[topBorder]-0-|", options: [], metrics: nil, views: ["topBorder": topBorder]) 80 | ) 81 | addConstraints( 82 | NSLayoutConstraint.constraints(withVisualFormat: "V:|-0-[topBorder(1)]", options: [], metrics: nil, views: ["topBorder": topBorder]) 83 | ) 84 | 85 | addConstraints( 86 | NSLayoutConstraint.constraints(withVisualFormat: 87 | "H:|-8-[attachmentButton(33)]-8-[textView]-16-|", options: [], metrics: nil, views: 88 | ["attachmentButton": attachmentButton, "textView": textView]) 89 | ) 90 | addConstraints( 91 | NSLayoutConstraint.constraints(withVisualFormat: 92 | "V:[attachmentButton(33)]-8-|", options: [], metrics: nil, views: ["attachmentButton": attachmentButton]) 93 | ) 94 | addConstraints( 95 | NSLayoutConstraint.constraints(withVisualFormat: 96 | "V:|-8-[textView(>=37)]-8-|", options: [], metrics: nil, views: ["textView": textView]) 97 | ) 98 | addConstraints( 99 | NSLayoutConstraint.constraints(withVisualFormat: 100 | "H:[sendButton(28)]-22-|", options: [], metrics: nil, views: ["sendButton": sendButton]) 101 | ) 102 | addConstraints( 103 | NSLayoutConstraint.constraints(withVisualFormat: 104 | "V:[sendButton(28)]-13-|", options: [], metrics: nil, views: ["sendButton": sendButton]) 105 | ) 106 | 107 | } 108 | 109 | func textViewDidChange(_ textView: UITextView) { 110 | evaluateSendButtonAlpha() 111 | } 112 | 113 | func evaluateSendButtonAlpha(){ 114 | let text = textView.text ?? "" 115 | let alpha : CGFloat = text.characters.count == 0 ? 0 : 1 116 | UIView.animate(withDuration: 0.25) { 117 | self.sendButton.alpha = alpha 118 | } 119 | } 120 | 121 | func sendButtonDidTap(){ 122 | delegate?.sendMessage(text: textView.text) 123 | textView.text = "" 124 | evaluateSendButtonAlpha() 125 | } 126 | 127 | func attachmentButtonDidTap(){ 128 | delegate?.attachmentButtonDidTapped() 129 | } 130 | 131 | override func layoutSubviews() { 132 | self.updateConstraints() // Interface rotation or size class changes will reset constraints as defined in interface builder -> constraintsForVisibleTextView will be activated 133 | super.layoutSubviews() 134 | } 135 | 136 | required init?(coder aDecoder: NSCoder) { 137 | fatalError("init(coder:) has not been implemented") 138 | } 139 | 140 | } 141 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/RLMLoginViewController.swift: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Copyright 2017 Realm Inc. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | //////////////////////////////////////////////////////////////////////////// 18 | // 19 | // RLMLoginViewController.swift 20 | // RChat 21 | // 22 | // Created by David Spector on 6/1/17. 23 | // Copyright © 2017 Realm. All rights reserved. 24 | // 25 | 26 | import UIKit 27 | import RealmSwift 28 | import RealmLoginKit 29 | 30 | class RLMLoginViewController: UIViewController { 31 | var loginViewController: LoginViewController! 32 | var token: NotificationToken! 33 | var myIdentity = SyncUser.current?.identity! 34 | 35 | let useAsyncOpen = false 36 | 37 | override func viewDidLoad() { 38 | super.viewDidLoad() 39 | 40 | // Do any additional setup after loading the view. 41 | } 42 | 43 | 44 | override func viewDidAppear(_ animated: Bool) { 45 | loginViewController = LoginViewController(style: .lightOpaque) 46 | loginViewController.isServerURLFieldHidden = false 47 | loginViewController.isRegistering = true 48 | 49 | if (SyncUser.current != nil) { 50 | // yup - we've got a stored session, so just go right to the UITabView 51 | self.navigationController?.setViewControllers([ChatViewController()], animated: true) 52 | } else { 53 | // show the RealmLoginKit controller 54 | if loginViewController!.serverURL == nil { 55 | loginViewController!.serverURL = RChatConstants.authServerEndpoint.absoluteString //Constants.syncAuthURL.absoluteString 56 | } 57 | 58 | // Set a closure that will be called on successful login 59 | loginViewController.loginSuccessfulHandler = { user in 60 | DispatchQueue.main.async { 61 | 62 | self.setup(user: user) 63 | 64 | if (self.loginViewController!.serverURL != RChatConstants.authServerEndpoint.absoluteString) { 65 | realmServerAddress = self.loginViewController!.serverURL! 66 | } 67 | self.loginViewController!.dismiss(animated: true, completion: nil) 68 | self.navigationController?.setViewControllers([ChatViewController()], animated: true) 69 | 70 | // Realm.asyncOpen(configuration: commonRealmConfig(user:SyncUser.current!)) { realm, error in 71 | // if let realm = realm { 72 | // } else if let error = error { 73 | // print("Error on return from AsyncOpen(): \(error)") 74 | // } 75 | // } // of asyncOpen() 76 | 77 | } // of main queue dispatch 78 | }// of login controller 79 | 80 | present(loginViewController, animated: true, completion: nil) 81 | } 82 | } 83 | 84 | override func didReceiveMemoryWarning() { 85 | super.didReceiveMemoryWarning() 86 | // Dispose of any resources that can be recreated. 87 | } 88 | 89 | 90 | 91 | func setPermissionForRealm(_ realm: Realm?, accessLevel: SyncAccessLevel, personID: String) { 92 | if let realm = realm { 93 | let permission = SyncPermission(realmPath: realm.configuration.syncConfiguration!.realmURL.path, 94 | identity: personID, 95 | accessLevel: accessLevel) 96 | SyncUser.current?.apply(permission) { error in 97 | if let error = error { 98 | print("Error when attempting to set permissions: \(error.localizedDescription)") 99 | return 100 | } else { 101 | print("Permissions successfully set") 102 | } 103 | } 104 | } 105 | } 106 | 107 | 108 | private func setup(user: SyncUser){ 109 | let realm = RChatConstants.Realms.global 110 | 111 | if user.isAdmin == true { 112 | self.setPermissionForRealm(realm, accessLevel: SyncAccessLevel 113 | .write, personID: "*") 114 | } 115 | 116 | try! realm.write { 117 | let defaultValues = ["userId": user.identity!, 118 | "username": loginViewController.username, 119 | "displayName" : loginViewController.username] 120 | let newUser = realm.create(User.self, value: defaultValues, update: true) 121 | 122 | let defaultConversation = Conversation.generateDefaultConversation() 123 | defaultConversation.users.append(newUser) 124 | } 125 | } 126 | /* 127 | // MARK: - Navigation 128 | 129 | // In a storyboard-based application, you will often want to do a little preparation before navigation 130 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 131 | // Get the new view controller using segue.destinationViewController. 132 | // Pass the selected object to the new view controller. 133 | } 134 | */ 135 | 136 | } 137 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/SettingsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsViewController.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 2/3/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Eureka 11 | import BRYXBanner 12 | 13 | struct Platform { 14 | static var isSimulator: Bool { 15 | return TARGET_OS_SIMULATOR != 0 16 | } 17 | 18 | } 19 | 20 | 21 | class SettingsViewController : FormViewController { 22 | private var tmpImage: UIImage? 23 | 24 | 25 | lazy var profileRow : ProfileRow = { 26 | let row = ProfileRow() 27 | return row 28 | }() 29 | 30 | lazy var usernameRow : TextRow = { 31 | let row = TextRow() { row in 32 | row.title = "Username: " 33 | } 34 | row.disabled = Condition(booleanLiteral: true) 35 | return row 36 | }() 37 | 38 | lazy var displayNameRow : TextRow = { 39 | let row = TextRow() { row in 40 | row.title = "Display Name:" 41 | } 42 | return row 43 | }() 44 | 45 | lazy var shareLocationRow : SwitchRow = { 46 | let row = SwitchRow() { row in 47 | row.title = "Share Location:" 48 | } 49 | return row 50 | }() 51 | 52 | lazy var sharePresenceRow : SwitchRow = { 53 | let row = SwitchRow() { row in 54 | row.title = "Share Online/Offline Presence:" 55 | } 56 | return row 57 | }() 58 | 59 | 60 | lazy var saveButtonRow : ButtonRow = { 61 | let row = ButtonRow() { row in 62 | row.title = "Save Changes" 63 | } 64 | return row 65 | }() 66 | 67 | lazy var logoutButtonRow : ButtonRow = { 68 | let row = ButtonRow() { row in 69 | row.title = "Logout" 70 | } 71 | return row 72 | }() 73 | 74 | var viewModel = SettingsViewModel() 75 | 76 | override func viewDidLoad() { 77 | super.viewDidLoad() 78 | 79 | title = "Profile" 80 | 81 | form +++ Section() 82 | <<< profileRow.onCellSelection({ [weak self] (cell, row) in 83 | guard let `self` = self else { return } 84 | row.deselect(animated: true) 85 | self.viewModel.profileRowDidTap() 86 | }) 87 | <<< usernameRow 88 | <<< displayNameRow.onChange({ [weak self] (r) in 89 | guard let `self` = self else { return } 90 | self.viewModel.displayName = r.value 91 | }) 92 | 93 | <<< shareLocationRow.onChange({ [weak self] (r) in 94 | guard let `self` = self else { return } 95 | self.viewModel.shareLocation = r.value 96 | }) 97 | <<< sharePresenceRow.onChange({ [weak self] (r) in 98 | guard let `self` = self else { return } 99 | self.viewModel.sharePresence = r.value 100 | }) 101 | 102 | 103 | form +++ Section() 104 | <<< saveButtonRow.onCellSelection({ [weak self] (_, _) in 105 | guard let `self` = self else { return } 106 | self.viewModel.saveRowDidTap() 107 | }) 108 | 109 | form +++ Section() 110 | <<< logoutButtonRow.onCellSelection({ [weak self] (_, _) in 111 | guard let `self` = self else { return } 112 | self.viewModel.logoutRowDidTap() 113 | }) 114 | 115 | 116 | usernameRow.value = viewModel.username 117 | displayNameRow.value = viewModel.displayName 118 | sharePresenceRow.value = viewModel.sharePresence 119 | shareLocationRow.value = viewModel.shareLocation 120 | profileRow.cell.profileImageView.image = viewModel.avatarImage 121 | 122 | viewModel.presentProfileImageChangeAlert = { [weak self] in 123 | guard let `self` = self else { return } 124 | let alertController = UIAlertController(title: "Change Profile Image", message: nil, preferredStyle: .actionSheet) 125 | 126 | if !Platform.isSimulator { // Only allow the user to select the camera on a real device, else we'll crash 😱 127 | alertController.addAction(UIAlertAction(title: "From Camera", style: .default, handler: { (_) in 128 | self.presentCamera() 129 | })) 130 | } 131 | 132 | alertController.addAction(UIAlertAction(title: "From Photo Library", style: .default, handler: { (_) in 133 | self.presentPhotoLibrary() 134 | })) 135 | alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) 136 | self.present(alertController, animated: true, completion: nil) 137 | } 138 | 139 | viewModel.presentLogoutAlert = { [weak self] in 140 | guard let `self` = self else { return } 141 | let alertController = UIAlertController(title: "Are you sure?", message: nil, preferredStyle: .actionSheet) 142 | alertController.addAction(UIAlertAction(title: "Yes, Log me out", style: .destructive, handler: { (_) in 143 | self.viewModel.confirmLogoutDidTap() 144 | })) 145 | alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) 146 | self.present(alertController, animated: true, completion: nil) 147 | } 148 | 149 | viewModel.returnToWelcomeViewController = { [weak self] in 150 | guard let `self` = self else { return } 151 | self.navigationController?.setViewControllers([RLMLoginViewController()], animated: true) 152 | } 153 | 154 | viewModel.showSaveSuccessBanner = { 155 | let banner = Banner(title: "Settings Saved") 156 | banner.backgroundColor = RChatConstants.Colors.nephritis 157 | banner.dismissesOnTap = true 158 | banner.show(duration: 1.5) 159 | } 160 | } 161 | 162 | 163 | 164 | } 165 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # RChat - A Realm Platform Demo 2 | ### Authors: Max Alexander, max.alexander@realm.io & David Spector, ds@realm.io 3 | 4 | 5 | # Overview 6 | 7 | RChat is a general purpose chat client that can be used either as a stand-alone chat system (i.e., an app unto itself) or as an embedded chat view that can be quickly and easily added to any exiting application to provide an off-line first chat capability. 8 | 9 | 10 | # Data Model 11 | 12 | ![RChat Data Model](Graphics/RChat-DataModel.png) 13 | 14 | 15 | ## Prerequisites 16 | 17 | - Xcode 8.33 or higher 18 | - Realm Object Server 2.0.11 or higher 19 | - Cocoapods 20 | - Nodejs v8.6 or higher 21 | - Node Package Manager (npm) 22 | 23 | The RChat iOS app uses [Cocoapods](https://www.cocoapods.org) to set up the project's 3rd party dependencies. Installation can be directly (from instructions at the Cocapods site) or alternatively through a package management system like [Homebrew](brew.sh/). 24 | 25 | ### Realm Platform 26 | 27 | This application demonstrates features of the [Realm Platform](https://realm.io/products/realm-platform/) and needs to have a working instance of the Realm Object Server version 2.x to make data available between instances of the RChat app. The Realm Object Server can be installed via npm as a node application for macOS or Linux. Please see the [installation instructions](https://realm.io/docs/get-started/installation/developer-edition/). 28 | 29 | This repo also has a minimal server suitable for testing whose only requirement for pre-existing software is nodejs and npm. Installation/setup/operation of this server is convered in the installation section below. 30 | 31 | ### Realm Studio 32 | 33 | Another useful tool is [Realm Studio](https://realm.io/products/realm-studio/) which is available for macOS, Linux and Windows and allows developers to inspect and manage Realms and the Realm Object Server. Realm Studio is recommended for all developers and can be downloaded from the [Realm web site](https://realm.io/products/realm-studio/). 34 | 35 | 36 | ### 3rd Party Modules 37 | 38 | The RChat iOS app makes use of a number of 3rd party modules: 39 | 40 | [Chatto and ChattoAdditions](https://github.com/badoo/Chatto.git) 41 | 42 | [RealmSwift](https://github.com/realm/realm-cocoa.git) 43 | 44 | [SideMenu](https://github.com/jonkykong/SideMenu.git) 45 | 46 | [SDWebImage](https://github.com/rs/SDWebImage.git) 47 | 48 | [Eureka](https://github.com/xmartlabs/Eureka.git) 49 | 50 | [Cartography](https://github.com/robb/Cartography.git) 51 | 52 | [TURecipientBar](https://github.com/davbeck/TURecipientBar.git) 53 | 54 | [NVActivityIndicatorView](https://github.com/ninjaprox/NVActivityIndicatorView.git) 55 | 56 | [BRYXBanner](https://github.com/bryx-inc/BRYXBanner.git) 57 | 58 | [RealmLoginKit](https://github.com/realm-demos/realm-loginkit.git) 59 | 60 | # Installation 61 | 62 | Clone this repository `git clone https://github.com/realm/roc-ios` to a convenient location your machine. 63 | 64 | 65 | ## Preparing the ROS Server 66 | 67 | This application comes with a demo server against which you can run the RChat client. The ROS platform requires [NodeJS](https://nodejs.org) version 8.5 or higher and the [npm node package manager](https://www.npmjs.com) installed in order to be able to run. If these are already installed, continue with the instructions below, if you need further info on nodejs or NPM installaton, please see their repsective web sites for installation instructions. 68 | 69 | 1. Open a new terminal window 70 | 0. Change directory to the download location where you downloaded the RChat repository 71 | 0. Change directory to the `RChatMinimalServer` directory 72 | 0. Install the required node server modules by running `npm install` 73 | 0. Run the node server with the command `node .` in the same same director 74 | 75 | ### Initial User Setup 76 | 77 | Since this is _client focued demo_, there is not a back-end server that sets up the RChat Realm or its permissions. In order to ensure the Realm permissions are correctly set the first user that logs in using the RChat service needs to be a Realm Server Administrator user. 78 | 79 | To accomplish this, launch Realm Studio and create one user that will be "user #1" for RChat and grant that user Server Administrator permission. This is done by creating a new user or editing an existing user and then setting the administrator permission in the User Panel as shown here: 80 | 81 |
82 | 83 | ## Preparing the iOS Client 84 | 85 | 1. Change directory to the `RChart-iOS` directory 86 | 3. Run `pod update` 87 | 4. Open the workspace `open RChat.xcworkspace` with Xcode 88 | 89 | ### Setting the Server Address 90 | 91 | By default the iOS client points to the local machine as its ROS server. if you have set up a ROS server on a different machine, you will need to edit the server IP address in the file `RChatConstants.swift` (which is in the `RChat/Data` directory) and replace the IP address for the Realm Server address with you server IP. 92 | 93 | 5. Run the app by selecting a simulator from Xcode's menu, and the press _Build & Run_; the simulator will start and you can log in to the chat server using the username and password you created above using Realm Studio. 94 | 95 | 96 | ## Running RChat 97 | 98 |

RChat Data Model - Realm Studio View
99 |

100 |

101 |

102 |
103 | RChat Screen Shot
104 | 105 | ## Contributing 106 | 107 | See [CONTRIBUTING.md](CONTRIBUTING.md) for more details! 108 | 109 | This project adheres to the [Contributor Covenant Code of Conduct](https://realm.io/conduct/). By participating, you are expected to uphold this code. Please report unacceptable behavior to [info@realm.io](mailto:info@realm.io). 110 | 111 | ## License 112 | 113 | Distributed under the Apache 2.0 license. See [LICENSE](LICENSE) for more information. 114 | 115 | ![analytics](https://ga-beacon.appspot.com/UA-50247013-2/realm/roc-ios/README?pixel) 116 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/LoginViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginViewModel.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 1/20/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RealmSwift 11 | 12 | class LoginViewModel { 13 | 14 | enum Mode { 15 | case login, signup 16 | } 17 | 18 | let mode : Mode 19 | 20 | init(mode: Mode){ 21 | self.mode = mode 22 | } 23 | 24 | var username: String = "" 25 | var password: String = "" 26 | 27 | var permissionToken: NotificationToken? 28 | var permissionChange: SyncPermissionChange? 29 | 30 | deinit { 31 | if let permissionToken = self.permissionToken { 32 | permissionToken.invalidate() 33 | } 34 | } 35 | 36 | // FROM UI 37 | func attemptLogin(){ 38 | let usernameCredentials = SyncCredentials.usernamePassword(username: username, password: password) 39 | isProcessingCallback?(true) 40 | SyncUser.logIn(with: usernameCredentials, server: RChatConstants.authServerEndpoint) { [weak self] (user, error) in 41 | guard let `self` = self else { return } 42 | DispatchQueue.main.sync { 43 | self.isProcessingCallback?(false) 44 | if let user = user { 45 | self.setup(user: user) 46 | 47 | // Should only be called by an admin user 48 | //self.updateGlobalRealmPermission(user: user) 49 | 50 | self.authSuccessCallback?(user) 51 | return 52 | } else if let error = error { 53 | let description = error.localizedDescription 54 | self.authErrorCallback?(description) 55 | return 56 | } 57 | let errorMessage = "No user was found!" 58 | self.authErrorCallback?(errorMessage) 59 | } 60 | } 61 | } 62 | 63 | func attemptRegistration(){ 64 | let usernameCredentials = SyncCredentials.usernamePassword(username: username, password: password, register: true) 65 | isProcessingCallback?(true) 66 | SyncUser.logIn(with: usernameCredentials, server: RChatConstants.authServerEndpoint) { [weak self] (user, error) in 67 | guard let `self` = self else { return } 68 | DispatchQueue.main.sync { 69 | self.isProcessingCallback?(false) 70 | if let user = user { 71 | self.setup(user: user) 72 | 73 | // Should only be called by an admin user 74 | //self.updateGlobalRealmPermission(user: user) 75 | 76 | self.authSuccessCallback?(user) 77 | return 78 | } else if let error = error { 79 | let description = error.localizedDescription 80 | self.authErrorCallback?(description) 81 | return 82 | } 83 | let errorMessage = "No user was found!" 84 | self.authErrorCallback?(errorMessage) 85 | } 86 | } 87 | } 88 | 89 | // TO UI 90 | var isProcessingCallback: ((_ isProcessing: Bool) -> Void)? { 91 | didSet { 92 | isProcessingCallback?(false) 93 | } 94 | } 95 | var authSuccessCallback : ((_ user: SyncUser) -> Void)? 96 | var authErrorCallback: ((_ errorMessage: String) -> Void)? 97 | var modeCallback : ((_ mode: Mode) -> Void)? { 98 | didSet { 99 | modeCallback?(self.mode) 100 | } 101 | } 102 | 103 | // THIS SHOULD ONLY BE CALLED WITH AN ADMIN USER! 104 | private func updateGlobalRealmPermission(user: SyncUser) { 105 | // set permissions 106 | let managementRealm = try! user.managementRealm() 107 | 108 | self.permissionChange = permissionChange(realmURL: RChatConstants.globalRealmURL.absoluteString, 109 | // The remote Realm URL on which to apply the changes 110 | userID: "*", // The user ID for which these permission changes should be applied 111 | mayRead: true, // Grant read access 112 | mayWrite: true, // Grant write access 113 | mayManage: false) // Grant management access 114 | 115 | try! managementRealm.write { 116 | managementRealm.add(self.permissionChange!) 117 | } 118 | 119 | // Listen for update to permission change 120 | self.permissionToken = self.permissionChange!.addNotificationBlock({ [weak self] (objectChange) in 121 | switch objectChange { 122 | case .change(let properties): 123 | if let statusChange = properties.first(where: { $0.name == "status" }) { 124 | let status = statusChange.newValue as! SyncManagementObjectStatus 125 | switch status { 126 | case .success: 127 | print("Successfully set global Realm permission") 128 | self?.permissionToken?.stop() 129 | case .notProcessed: 130 | print("Did not process global Realm permission") 131 | self?.permissionToken?.stop() 132 | case .error: 133 | print("Error in setting global Realm permission") 134 | self?.permissionToken?.stop() 135 | } 136 | } 137 | case .deleted: 138 | break 139 | case .error(let error): 140 | print(error) 141 | } 142 | }) 143 | } 144 | 145 | private func setup(user: SyncUser){ 146 | let realm = RChatConstants.Realms.global 147 | 148 | try! realm.write { 149 | let defaultValues = ["userId": user.identity!, 150 | "username": self.username, 151 | "displayName" : self.username] 152 | let newUser = realm.create(User.self, value: defaultValues, update: true) 153 | 154 | let defaultConversation = Conversation.generateDefaultConversation() 155 | defaultConversation.users.append(newUser) 156 | } 157 | } 158 | 159 | } 160 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/ConversationSearchView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConversationSearchView.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 1/10/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Cartography 11 | 12 | protocol ConversationSearchViewDelegate : class { 13 | func fireChatSearch(searchTerm: String) 14 | func searchStateChanged(isFirstResponder: Bool) 15 | } 16 | 17 | class ConversationSearchView : UIView, UITextFieldDelegate { 18 | 19 | weak var delegate: ConversationSearchViewDelegate? 20 | 21 | lazy var iconButton : UIButton = { 22 | let i = UIButton() 23 | i.layer.cornerRadius = RChatConstants.Numbers.cornerRadius 24 | i.setImage(RChatConstants.Images.profileIcon, for: .normal) 25 | i.contentMode = .scaleAspectFit 26 | i.tintColor = UIColor.white 27 | i.imageEdgeInsets = UIEdgeInsets(top: 4, left: 4, bottom: 4, right: 4) 28 | return i 29 | }() 30 | 31 | lazy var searchTextField : RTextField = { 32 | let t = RTextField() 33 | t.backgroundColor = RChatConstants.Colors.primaryColorDark 34 | t.layer.cornerRadius = RChatConstants.Numbers.cornerRadius 35 | t.layer.masksToBounds = true 36 | t.insetX = RChatConstants.Numbers.minorHorizontalSpacing 37 | t.textColor = .white 38 | t.attributedPlaceholder = NSAttributedString(string: "Search", attributes: [ 39 | NSForegroundColorAttributeName : UIColor.lightGray, 40 | NSFontAttributeName: RChatConstants.Fonts.regularFont 41 | ]) 42 | t.keyboardAppearance = .dark 43 | return t 44 | }() 45 | 46 | lazy var cancelButton : UIButton = { 47 | let b = UIButton() 48 | b.setTitle("Cancel", for: .normal) 49 | b.setTitleColor(.white, for: .normal) 50 | b.titleLabel?.adjustsFontSizeToFitWidth = true 51 | b.titleLabel?.font = RChatConstants.Fonts.regularFont 52 | return b 53 | }() 54 | 55 | let constraintGroup = ConstraintGroup() 56 | 57 | init(){ 58 | super.init(frame: CGRect.zero) 59 | backgroundColor = RChatConstants.Colors.primaryColor 60 | addSubview(iconButton) 61 | addSubview(searchTextField) 62 | addSubview(cancelButton) 63 | 64 | searchTextField.delegate = self 65 | searchTextField.addTarget(self, action: #selector(ConversationSearchView.textFieldDidChange(textField:)), for: .editingChanged) 66 | cancelButton.addTarget(self, action: #selector(cancelButtonDidTap(button:)), for: .touchUpInside) 67 | 68 | toggle(isEditing: false) 69 | } 70 | 71 | required init?(coder aDecoder: NSCoder) { 72 | fatalError("init(coder:) has not been implemented") 73 | } 74 | 75 | func textFieldDidChange(textField: UITextField){ 76 | delegate?.fireChatSearch(searchTerm: textField.text ?? "") 77 | } 78 | 79 | func textFieldDidBeginEditing(_ textField: UITextField) { 80 | toggle(isEditing: true, animated: true) 81 | delegate?.searchStateChanged(isFirstResponder: true) 82 | } 83 | 84 | func textFieldDidEndEditing(_ textField: UITextField) { 85 | toggle(isEditing: false, animated: true) 86 | delegate?.searchStateChanged(isFirstResponder: false) 87 | } 88 | 89 | func cancelButtonDidTap(button: UIButton){ 90 | searchTextField.resignFirstResponder() 91 | searchTextField.text = "" 92 | } 93 | 94 | func toggle(isEditing: Bool, animated: Bool = false){ 95 | if isEditing { 96 | constrain(iconButton, searchTextField, cancelButton, replace: constraintGroup, block: { (iconButton, searchTextField, cancelButton) in 97 | iconButton.width == 33 98 | iconButton.height == 33 99 | iconButton.left == iconButton.superview!.left + RChatConstants.Numbers.horizontalSpacing 100 | iconButton.bottom == iconButton.superview!.bottom - RChatConstants.Numbers.verticalSpacing 101 | 102 | cancelButton.right == cancelButton.superview!.right - RChatConstants.Numbers.horizontalSpacing 103 | cancelButton.height == 33 104 | cancelButton.width == 50 105 | cancelButton.bottom == cancelButton.superview!.bottom - RChatConstants.Numbers.verticalSpacing 106 | 107 | searchTextField.left == searchTextField.superview!.left + RChatConstants.Numbers.horizontalSpacing 108 | searchTextField.right == cancelButton.left - RChatConstants.Numbers.horizontalSpacing 109 | searchTextField.bottom == cancelButton.superview!.bottom - RChatConstants.Numbers.verticalSpacing 110 | searchTextField.height == 33 111 | }) 112 | }else{ 113 | constrain(iconButton, searchTextField, cancelButton, replace: constraintGroup, block: { (iconButton, searchTextField, cancelButton) in 114 | iconButton.width == 33 115 | iconButton.height == 33 116 | iconButton.left == iconButton.superview!.left + RChatConstants.Numbers.horizontalSpacing 117 | iconButton.bottom == iconButton.superview!.bottom - RChatConstants.Numbers.verticalSpacing 118 | 119 | cancelButton.right == cancelButton.superview!.right - RChatConstants.Numbers.horizontalSpacing 120 | cancelButton.height == 33 121 | cancelButton.width == 50 122 | cancelButton.bottom == cancelButton.superview!.bottom - RChatConstants.Numbers.verticalSpacing 123 | 124 | searchTextField.left == iconButton.right + RChatConstants.Numbers.minorHorizontalSpacing 125 | searchTextField.right == searchTextField.superview!.right - RChatConstants.Numbers.minorHorizontalSpacing 126 | searchTextField.bottom == searchTextField.superview!.bottom - RChatConstants.Numbers.verticalSpacing 127 | searchTextField.height == 33 128 | }) 129 | } 130 | let imageViewNewAlpha: CGFloat = isEditing ? 0 : 1 131 | let cancelButtonNewAlpha: CGFloat = isEditing ? 1 : 0 132 | 133 | if animated { 134 | UIView.animate(withDuration: 0.25, animations: layoutIfNeeded) 135 | UIView.animate(withDuration: 0.25, animations: { 136 | self.cancelButton.alpha = cancelButtonNewAlpha 137 | self.iconButton.alpha = imageViewNewAlpha 138 | }) 139 | }else{ 140 | self.cancelButton.alpha = cancelButtonNewAlpha 141 | self.iconButton.alpha = imageViewNewAlpha 142 | layoutIfNeeded() 143 | } 144 | } 145 | 146 | } 147 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/ChatViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChatViewController.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 1/9/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Chatto 11 | import ChattoAdditions 12 | import SideMenu 13 | 14 | class ChatViewController : BaseChatViewController, 15 | RChatInputViewDelegate, 16 | ConversationsViewControllerDelegate, 17 | MembersViewControllerDelegate 18 | { 19 | 20 | let chatInputView = RChatInputView() 21 | let messageHandler = RChatBaseMessageHandler() 22 | var conversation: Conversation 23 | var viewModel : ChatViewModel { 24 | return self.chatDataSource as! ChatViewModel 25 | } 26 | 27 | init(conversation : Conversation = Conversation.generateDefaultConversation()) { 28 | self.conversation = conversation 29 | super.init(nibName: nil, bundle: nil) 30 | self.chatDataSource = ChatViewModel() 31 | self.chatItemsDecorator = RChatDecorator() 32 | } 33 | 34 | required init?(coder aDecoder: NSCoder) { 35 | fatalError("init(coder:) has not been implemented") 36 | } 37 | 38 | override func viewWillAppear(_ animated: Bool) { 39 | super.viewWillAppear(animated) 40 | navigationController?.setNavigationBarHidden(false, animated: true) 41 | } 42 | 43 | override func viewDidLoad() { 44 | super.viewDidLoad() 45 | view.backgroundColor = UIColor.white 46 | navigationItem.leftBarButtonItem = { 47 | let barButtonItem = UIBarButtonItem(image: RChatConstants.Images.menuIcon, style: .plain, target: self, action: #selector(ChatViewController.menuBarButtonTapped)) 48 | barButtonItem.tintColor = .white 49 | return barButtonItem 50 | }() 51 | 52 | // @FIXME right now this shows a ist of users who recently chatted? Confusing to internal users 53 | if shouldDisplayRightNavItem { 54 | navigationItem.rightBarButtonItem = { 55 | let barButtonItem = UIBarButtonItem(image: RChatConstants.Images.verticalMoreIcon, style: .plain, target: self, action: #selector(ChatViewController.membersBarButtonTapped)) 56 | barButtonItem.tintColor = .white 57 | return barButtonItem 58 | }() 59 | } 60 | 61 | chatInputView.delegate = self 62 | viewModel.conversation = conversation 63 | 64 | SideMenuManager.menuLeftNavigationController = { 65 | let conversationsViewController = ConversationsViewController() 66 | conversationsViewController.leftSide = true 67 | conversationsViewController.conversationsViewControllerDelegate = self 68 | return conversationsViewController 69 | }() 70 | SideMenuManager.menuRightNavigationController = { 71 | let membersViewController = MembersViewController() 72 | membersViewController.leftSide = false 73 | membersViewController.membersViewControllerDelegate = self 74 | return membersViewController 75 | }() 76 | 77 | SideMenuManager.menuPresentMode = .viewSlideInOut 78 | SideMenuManager.menuFadeStatusBar = false 79 | SideMenuManager.menuWidth = max(round(min(UIScreen.main.bounds.width, UIScreen.main.bounds.height) * 0.85), 240) 80 | SideMenuManager.menuShadowOpacity = 0 81 | SideMenuManager.menuAnimationPresentDuration = 0.25 82 | SideMenuManager.menuAnimationDismissDuration = 0.25 83 | SideMenuManager.menuAddScreenEdgePanGesturesToPresent(toView: self.view) 84 | 85 | viewModel.defaultingNameCallback = { [weak self] defaultingName in 86 | guard let `self` = self else { return } 87 | self.title = defaultingName 88 | } 89 | 90 | } 91 | 92 | override func createChatInputView() -> UIView { 93 | return chatInputView 94 | } 95 | 96 | override func createCollectionViewLayout() -> UICollectionViewLayout { 97 | let layout = ChatCollectionViewLayout() 98 | layout.delegate = self 99 | return layout 100 | } 101 | 102 | override func createPresenterBuilders() -> [ChatItemType : [ChatItemPresenterBuilderProtocol]] { 103 | 104 | // regular text messages 105 | let textMessagePresenter = TextMessagePresenterBuilder( 106 | viewModelBuilder: RChatTextMessageViewModelBuilder(), 107 | interactionHandler: RChatTextMessageHandler(baseHandler: self.messageHandler) 108 | ) 109 | textMessagePresenter.baseMessageStyle = { 110 | let style = RChatMessageCollectionViewCellAvatarStyle() 111 | return style 112 | }() 113 | textMessagePresenter.textCellStyle = RChatTextMessageCollectionViewCellStyle() 114 | 115 | // image/photo messages 116 | let imageMessagePresenter = PhotoMessagePresenterBuilder( 117 | viewModelBuilder: RChatImageMessageViewModelBuilder(), 118 | interactionHandler: RChatImageMessageHandler(baseHandler: self.messageHandler) 119 | ) 120 | 121 | return [ 122 | MimeType.textPlain.rawValue: [textMessagePresenter], 123 | MimeType.imagePNG.rawValue: [imageMessagePresenter], 124 | TimeSeparatorModel.chatItemType: [TimeSeparatorPresenterBuilder()] 125 | ] 126 | } 127 | 128 | func menuBarButtonTapped(){ 129 | present(SideMenuManager.menuLeftNavigationController!, animated: true, completion: nil) 130 | } 131 | 132 | func membersBarButtonTapped(){ 133 | let membersViewController = SideMenuManager.menuRightNavigationController as! MembersViewController 134 | membersViewController.setupWithConversation(conversation: self.conversation) 135 | present(membersViewController, animated: true, completion: nil) 136 | } 137 | 138 | func attachmentButtonDidTapped() { 139 | let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) 140 | if !Platform.isSimulator { // Only allow the user to select the camera on a real device, else we'll crash 😱 141 | alertController.addAction(UIAlertAction(title: "Camera", style: .default, handler: { [weak self] (_) in 142 | self?.presentCamera() 143 | })) 144 | } 145 | alertController.addAction(UIAlertAction(title: "Library", style: .default, handler: { [weak self] (_) in 146 | self?.presentPhotoLibrary() 147 | })) 148 | alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: { (_) in 149 | 150 | })) 151 | present(alertController, animated: true, completion: nil) 152 | } 153 | 154 | func sendMessage(text: String) { 155 | viewModel.sendMessage(text: text) 156 | } 157 | 158 | // ConversationsViewControllerDelegate 159 | func changeConversation(conversation: Conversation) { 160 | viewModel.conversation = conversation 161 | } 162 | 163 | func goToProfile() { 164 | removeBackButtonTitle() 165 | navigationController?.pushViewController(SettingsViewController(), animated: true) 166 | } 167 | 168 | // MembersViewControllerDelegate 169 | func memberSelected(user: User) { 170 | 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /RChat-iOS/RChat/ConversationsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConversationsViewController.swift 3 | // RChat 4 | // 5 | // Created by Max Alexander on 1/9/17. 6 | // Copyright © 2017 Max Alexander. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SideMenu 11 | import RealmSwift 12 | import Cartography 13 | 14 | 15 | protocol ConversationsViewControllerDelegate: class { 16 | func changeConversation(conversation: Conversation) 17 | func goToProfile() 18 | } 19 | 20 | class ConversationsViewController : UISideMenuNavigationController, 21 | UITableViewDataSource, 22 | UITableViewDelegate, 23 | ComposeViewControllerDelegate, 24 | ConversationSearchViewDelegate, 25 | SearchResultsViewControllerDelegate 26 | { 27 | 28 | weak var conversationsViewControllerDelegate: ConversationsViewControllerDelegate? 29 | 30 | lazy var tableView : UITableView = { 31 | let t = UITableView() 32 | t.backgroundColor = RChatConstants.Colors.midnightBlue 33 | t.translatesAutoresizingMaskIntoConstraints = false 34 | t.separatorColor = .clear 35 | t.register(ConversationTableViewCell.self, forCellReuseIdentifier: ConversationTableViewCell.REUSE_ID) 36 | t.rowHeight = ConversationTableViewCell.HEIGHT 37 | t.contentInset = UIEdgeInsetsMake(0, 0, 200, 0) 38 | return t 39 | }() 40 | 41 | lazy var searchView : ConversationSearchView = { 42 | let c = ConversationSearchView() 43 | c.translatesAutoresizingMaskIntoConstraints = false 44 | return c 45 | }() 46 | 47 | lazy var penButton : UIButton = { 48 | let b = UIButton() 49 | b.imageEdgeInsets = UIEdgeInsetsMake(4, 4, 4, 4) 50 | b.imageView?.contentMode = .scaleAspectFit 51 | b.layer.cornerRadius = 44 / 2 52 | b.layer.masksToBounds = true 53 | b.tintColor = .white 54 | b.setImage(RChatConstants.Images.penIcon, for: .normal) 55 | b.backgroundColor = RChatConstants.Colors.primaryColor 56 | return b 57 | }() 58 | 59 | lazy var searchResultsController : SearchResultsViewController = { 60 | let s = SearchResultsViewController() 61 | s.view.alpha = 0 62 | return s 63 | }() 64 | 65 | var conversations : Results! 66 | var notificationToken : NotificationToken? 67 | 68 | override func viewWillAppear(_ animated: Bool) { 69 | super.viewWillAppear(animated) 70 | tableView.reloadData() 71 | } 72 | 73 | override func viewDidLoad() { 74 | super.viewDidLoad() 75 | view.backgroundColor = RChatConstants.Colors.primaryColorDark 76 | view.addSubview(tableView) 77 | 78 | addChildViewController(searchResultsController) 79 | view.addSubview(searchResultsController.view) 80 | view.addSubview(searchView) 81 | view.addSubview(penButton) 82 | 83 | penButton.addTarget(self, action: #selector(ConversationsViewController.penButtonDidTap(button:)), for: .touchUpInside) 84 | searchView.iconButton.addTarget(self, action: #selector(ConversationsViewController.profileIconButtonDidTap(button:)), for: .touchUpInside) 85 | 86 | tableView.dataSource = self 87 | tableView.delegate = self 88 | 89 | searchView.delegate = self 90 | 91 | searchResultsController.delegate = self 92 | 93 | constrain(searchView, tableView, penButton, searchResultsController.view) { (searchView, tableView, penButton, searchResultsView) in 94 | searchView.left == searchView.superview!.left 95 | searchView.right == searchView.superview!.right 96 | searchView.height == 65 97 | searchView.top == searchView.superview!.top 98 | 99 | tableView.top == searchView.bottom 100 | tableView.left == tableView.superview!.left 101 | tableView.right == tableView.superview!.right 102 | tableView.bottom == tableView.superview!.bottom 103 | 104 | searchResultsView.top == searchResultsView.superview!.top 105 | searchResultsView.bottom == tableView.bottom 106 | searchResultsView.left == tableView.left 107 | searchResultsView.right == tableView.right 108 | 109 | penButton.width == 44 110 | penButton.height == 44 111 | penButton.right == penButton.superview!.right - RChatConstants.Numbers.horizontalSpacing 112 | penButton.bottom == penButton.superview!.bottom - RChatConstants.Numbers.verticalSpacing 113 | } 114 | 115 | let realm = RChatConstants.Realms.global 116 | let predicate = NSPredicate(format: "ANY users.userId = %@", RChatConstants.myUserId) 117 | conversations = realm.objects(Conversation.self).filter(predicate) 118 | 119 | notificationToken = conversations 120 | .observe { [weak self] (changes) in 121 | guard let `self` = self else { return } 122 | switch changes { 123 | case .initial: 124 | print(self.conversations.count) 125 | self.tableView.reloadData() 126 | break 127 | case .update(_, let deletions, let insertions, let modifications): 128 | // Query results have changed, so apply them to the UITableView 129 | self.tableView.beginUpdates() 130 | self.tableView.insertRows(at: insertions.map({ IndexPath(row: $0, section: 0) }), 131 | with: .automatic) 132 | self.tableView.deleteRows(at: deletions.map({ IndexPath(row: $0, section: 0)}), 133 | with: .automatic) 134 | self.tableView.reloadRows(at: modifications.map({ IndexPath(row: $0, section: 0) }), 135 | with: .automatic) 136 | self.tableView.endUpdates() 137 | break 138 | case .error(let error): 139 | fatalError(error.localizedDescription) 140 | break 141 | } 142 | } 143 | 144 | } 145 | 146 | override var preferredStatusBarStyle: UIStatusBarStyle { 147 | return UIStatusBarStyle.lightContent 148 | } 149 | 150 | func penButtonDidTap(button: UIButton){ 151 | let composeViewController = ComposeViewController() 152 | composeViewController.delegate = self 153 | let controller = CustomNavController(rootViewController: composeViewController) 154 | present(controller, animated: true, completion: nil) 155 | } 156 | 157 | func profileIconButtonDidTap(button: UIButton) { 158 | dismiss(animated: true, completion: nil) 159 | conversationsViewControllerDelegate?.goToProfile() 160 | } 161 | 162 | func numberOfSections(in tableView: UITableView) -> Int { 163 | return 1 164 | } 165 | 166 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 167 | return conversations.count 168 | } 169 | 170 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 171 | let cell = tableView.dequeueReusableCell(withIdentifier: ConversationTableViewCell.REUSE_ID, for: indexPath) as! ConversationTableViewCell 172 | let conversation = conversations[indexPath.row] 173 | cell.setupWithConversation(conversation: conversation) 174 | return cell 175 | } 176 | 177 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 178 | tableView.deselectRow(at: indexPath, animated: true) 179 | let conversation = conversations[indexPath.row] 180 | conversationsViewControllerDelegate?.changeConversation(conversation: conversation) 181 | dismiss(animated: true, completion: nil) 182 | } 183 | 184 | func selectedSearchedConversation(conversation: Conversation) { 185 | searchView.searchTextField.text = "" 186 | conversationsViewControllerDelegate?.changeConversation(conversation: conversation) 187 | dismiss(animated: true, completion: nil) 188 | } 189 | 190 | func composeWithUsers(users: [User]) { 191 | let conversation = Conversation.putConversation(users: users) 192 | conversationsViewControllerDelegate?.changeConversation(conversation: conversation) 193 | dismiss(animated: true, completion: nil) 194 | } 195 | 196 | func fireChatSearch(searchTerm: String) { 197 | let selector = #selector(SearchResultsViewController.searchConversationsAndUsers(searchTerm:)) 198 | NSObject.cancelPreviousPerformRequests(withTarget: searchResultsController, selector: selector, object: nil) 199 | searchResultsController.perform(selector, with: searchTerm, afterDelay: 0.5) 200 | } 201 | 202 | func searchStateChanged(isFirstResponder: Bool) { 203 | UIView.animate(withDuration: 0.25) { 204 | self.searchResultsController.view.alpha = isFirstResponder ? 1 : 0 205 | } 206 | } 207 | 208 | deinit { 209 | notificationToken?.invalidate() 210 | } 211 | } 212 | --------------------------------------------------------------------------------