├── 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 |
22 |
23 |
24 |
25 |
26 |
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 | 
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 | 
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 |
--------------------------------------------------------------------------------