├── dump
└── RChat
│ ├── User.bson
│ ├── ChatMessage.bson
│ ├── Chatster.bson
│ ├── Chatster.metadata.json
│ ├── ChatMessage.metadata.json
│ └── User.metadata.json
├── RChat-iOS
├── README.md
├── RChat
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── Leaf.imageset
│ │ │ ├── Leaf.png
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ ├── RChat Icon - 20.png
│ │ │ ├── RChat Icon - 29.png
│ │ │ ├── RChat Icon - 40.png
│ │ │ ├── RChat Icon - 58.png
│ │ │ ├── RChat Icon - 60.png
│ │ │ ├── RChat Icon - 76.png
│ │ │ ├── RChat Icon - 80.png
│ │ │ ├── RChat Icon - 87.png
│ │ │ ├── RChat Icon - 1024.png
│ │ │ ├── RChat Icon - 120-1.png
│ │ │ ├── RChat Icon - 120.png
│ │ │ ├── RChat Icon - 152.png
│ │ │ ├── RChat Icon - 167.png
│ │ │ ├── RChat Icon - 180.png
│ │ │ ├── RChat Icon - 40-1.png
│ │ │ ├── RChat Icon - 40-2.png
│ │ │ ├── RChat Icon - 58-1.png
│ │ │ ├── RChat Icon - 80-1.png
│ │ │ └── Contents.json
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ ├── Active.colorset
│ │ │ └── Contents.json
│ │ ├── Clear.colorset
│ │ │ └── Contents.json
│ │ ├── Inactive.colorset
│ │ │ └── Contents.json
│ │ ├── MyBubble.colorset
│ │ │ └── Contents.json
│ │ ├── OtherBubble.colorset
│ │ │ └── Contents.json
│ │ └── GreenBackground.colorset
│ │ │ └── Contents.json
│ ├── Preview Content
│ │ ├── Preview Assets.xcassets
│ │ │ ├── Contents.json
│ │ │ ├── jane.imageset
│ │ │ │ ├── jane.jpg
│ │ │ │ └── Contents.json
│ │ │ ├── rod.imageset
│ │ │ │ ├── rod.jpg
│ │ │ │ └── Contents.json
│ │ │ ├── spud1.imageset
│ │ │ │ ├── spud1.jpg
│ │ │ │ └── Contents.json
│ │ │ ├── spud2.imageset
│ │ │ │ ├── spud2.jpg
│ │ │ │ └── Contents.json
│ │ │ ├── spud3.imageset
│ │ │ │ ├── spud3.jpg
│ │ │ │ └── Contents.json
│ │ │ ├── spud4.imageset
│ │ │ │ ├── spud4.jpg
│ │ │ │ └── Contents.json
│ │ │ ├── spud5.imageset
│ │ │ │ ├── spud5.jpg
│ │ │ │ └── Contents.json
│ │ │ ├── spud6.imageset
│ │ │ │ ├── spud6.jpg
│ │ │ │ └── Contents.json
│ │ │ ├── spud7.imageset
│ │ │ │ ├── spud7.jpg
│ │ │ │ └── Contents.json
│ │ │ ├── spud8.imageset
│ │ │ │ ├── spud8.jpg
│ │ │ │ └── Contents.json
│ │ │ ├── freddy.imageset
│ │ │ │ ├── freddy.jpg
│ │ │ │ └── Contents.json
│ │ │ ├── mugShot.imageset
│ │ │ │ ├── Andrew_Morgan_Mug_2017.jpg
│ │ │ │ └── Contents.json
│ │ │ └── mugShotThumb.imageset
│ │ │ │ ├── Andrew_Morgan_250x211.jpg
│ │ │ │ └── Contents.json
│ │ ├── PreviewHelpers.swift
│ │ └── SampleData.swift
│ ├── Model
│ │ ├── Photo.swift
│ │ ├── UserPreferences.swift
│ │ ├── Conversation.swift
│ │ ├── Chatster.swift
│ │ ├── ChatMessage.swift
│ │ ├── Member.swift
│ │ └── User.swift
│ ├── RChatApp.swift
│ ├── Custom Previews
│ │ ├── PreviewColorScheme.swift
│ │ ├── PreviewNoDevice.swift
│ │ ├── PreviewOrientation.swift
│ │ └── PreviewDevices.swift
│ ├── RChat.entitlements
│ ├── Views
│ │ ├── Components
│ │ │ ├── Pictures
│ │ │ │ ├── BlankPersonIconView.swift
│ │ │ │ ├── Thumbnail.swift
│ │ │ │ ├── PhotoFullSizeView.swift
│ │ │ │ ├── ThumbnailPhotoView.swift
│ │ │ │ ├── AvatarThumbNailView.swift
│ │ │ │ ├── AvatarButton.swift
│ │ │ │ ├── UIImage+Thumbnail.swift
│ │ │ │ ├── ThumbnailWithExpand.swift
│ │ │ │ ├── ThumbNailView.swift
│ │ │ │ └── ThumbnailWithDelete.swift
│ │ │ ├── LabeledButton.swift
│ │ │ ├── BackButton.swift
│ │ │ ├── Buttons
│ │ │ │ ├── SendButton.swift
│ │ │ │ ├── AttachButton.swift
│ │ │ │ ├── CameraButton.swift
│ │ │ │ ├── LocationButton.swift
│ │ │ │ ├── DeleteButton.swift
│ │ │ │ └── ButtonTemplate.swift
│ │ │ ├── CaptionLabel.swift
│ │ │ ├── MarkDown.swift
│ │ │ ├── LabeledText.swift
│ │ │ ├── CheckBox.swift
│ │ │ ├── OnOffCircleView.swift
│ │ │ ├── InputField.swift
│ │ │ ├── CallToActionButton.swift
│ │ │ ├── Maps
│ │ │ │ ├── MapView.swift
│ │ │ │ ├── MapThumbnailWithExpand.swift
│ │ │ │ └── MapThumbnailWithDelete.swift
│ │ │ ├── OpaqueProgressView.swift
│ │ │ ├── TextDate.swift
│ │ │ └── SearchBox.swift
│ │ ├── User Accounts & Profile
│ │ │ ├── UserProfileButton.swift
│ │ │ ├── UserAvatarView.swift
│ │ │ ├── OnlineAlertSettings.swift
│ │ │ ├── LogoutButton.swift
│ │ │ ├── LoginView.swift
│ │ │ └── SetUserProfileView.swift
│ │ ├── Conversations
│ │ │ ├── ConversationCardView.swift
│ │ │ ├── MugShotGridView.swift
│ │ │ ├── SaveConversationButton.swift
│ │ │ ├── ConversationListView.swift
│ │ │ ├── ConversationCardContentsView.swift
│ │ │ └── NewConversationView.swift
│ │ ├── Chat Messages
│ │ │ ├── ChatRoomView.swift
│ │ │ ├── ChatInputBox copy.swift
│ │ │ ├── AuthorView.swift
│ │ │ ├── ChatBubbleView.swift
│ │ │ ├── ChatRoomBubblesView.swift
│ │ │ └── ChatInputBox.swift
│ │ ├── LoggedInView.swift
│ │ └── ContentView.swift
│ ├── AppState.swift
│ ├── Helpers
│ │ ├── LocationHelper.swift
│ │ └── PhotoCaptureController.swift
│ └── Info.plist
├── RChat.xcodeproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ ├── IDEWorkspaceChecks.plist
│ │ │ └── swiftpm
│ │ │ └── Package.resolved
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── RChat.xcscheme
├── RChatTests
│ ├── Info.plist
│ └── RChatTests.swift
├── RChatUITests
│ ├── Info.plist
│ └── RChatUITests.swift
└── .gitignore
├── RChat-Realm
└── RChat
│ ├── http_endpoints
│ └── config.json
│ ├── environments
│ ├── qa.json
│ ├── testing.json
│ ├── development.json
│ ├── no-environment.json
│ └── production.json
│ ├── data_sources
│ └── mongodb-atlas
│ │ ├── RealmLog
│ │ └── Log
│ │ │ ├── schema.json
│ │ │ ├── relationships.json
│ │ │ └── rules.json
│ │ ├── RChatFlex
│ │ ├── User
│ │ │ ├── relationships.json
│ │ │ ├── rules.json
│ │ │ └── schema.json
│ │ ├── ChatMessage
│ │ │ ├── relationships.json
│ │ │ ├── rules.json
│ │ │ └── schema.json
│ │ └── Chatster
│ │ │ ├── relationships.json
│ │ │ ├── rules.json
│ │ │ └── schema.json
│ │ └── config.json
│ ├── auth
│ ├── custom_user_data.json
│ └── providers.json
│ ├── graphql
│ └── config.json
│ ├── values
│ ├── dbName.json
│ └── defaultLocation.json
│ ├── realm_config.json
│ ├── functions
│ ├── countChats.js
│ ├── config.json
│ ├── createNewUserDocument.js
│ ├── chatMessageChange.js
│ ├── resetFunc.js
│ └── userDocWrittenTo.js
│ ├── triggers
│ ├── userRegistered.json
│ ├── ChatMessageChange.json
│ └── UserCollectionWrite.json
│ └── sync
│ └── config.json
├── assets
├── .DS_Store
├── ChatRoom.png
├── RChat Icon.png
├── RChatIcon80.png
├── realm-app-id.png
├── RChat Icon.afphoto
├── RChat Icon - 1024.png
├── RChat Icon - 120.png
├── RChat Icon - 152.png
├── RChat Icon - 167.png
├── RChat Icon - 180.png
├── RChat Icon - 20.png
├── RChat Icon - 29.png
├── RChat Icon - 40.png
├── RChat Icon - 58.png
├── RChat Icon - 60.png
├── RChat Icon - 76.png
├── RChat Icon - 80.png
├── RChat Icon - 87.png
├── RChat Icon_1024x1024.png
└── RChat Icon_1024x1024.svg
├── Permissions.json
├── README.md
└── .gitignore
/dump/RChat/User.bson:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/dump/RChat/ChatMessage.bson:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/dump/RChat/Chatster.bson:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/RChat-iOS/README.md:
--------------------------------------------------------------------------------
1 | # RChat - iOS App
2 |
--------------------------------------------------------------------------------
/RChat-Realm/RChat/http_endpoints/config.json:
--------------------------------------------------------------------------------
1 | []
2 |
--------------------------------------------------------------------------------
/RChat-Realm/RChat/environments/qa.json:
--------------------------------------------------------------------------------
1 | {
2 | "values": {}
3 | }
4 |
--------------------------------------------------------------------------------
/RChat-Realm/RChat/data_sources/mongodb-atlas/RealmLog/Log/schema.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/RChat-Realm/RChat/environments/testing.json:
--------------------------------------------------------------------------------
1 | {
2 | "values": {}
3 | }
4 |
--------------------------------------------------------------------------------
/RChat-Realm/RChat/auth/custom_user_data.json:
--------------------------------------------------------------------------------
1 | {
2 | "enabled": false
3 | }
4 |
--------------------------------------------------------------------------------
/RChat-Realm/RChat/data_sources/mongodb-atlas/RealmLog/Log/relationships.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/RChat-Realm/RChat/environments/development.json:
--------------------------------------------------------------------------------
1 | {
2 | "values": {}
3 | }
4 |
--------------------------------------------------------------------------------
/RChat-Realm/RChat/environments/no-environment.json:
--------------------------------------------------------------------------------
1 | {
2 | "values": {}
3 | }
4 |
--------------------------------------------------------------------------------
/RChat-Realm/RChat/environments/production.json:
--------------------------------------------------------------------------------
1 | {
2 | "values": {}
3 | }
4 |
--------------------------------------------------------------------------------
/assets/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/assets/.DS_Store
--------------------------------------------------------------------------------
/RChat-Realm/RChat/data_sources/mongodb-atlas/RChatFlex/User/relationships.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/assets/ChatRoom.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/assets/ChatRoom.png
--------------------------------------------------------------------------------
/RChat-Realm/RChat/data_sources/mongodb-atlas/RChatFlex/ChatMessage/relationships.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/RChat-Realm/RChat/data_sources/mongodb-atlas/RChatFlex/Chatster/relationships.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/RChat-Realm/RChat/graphql/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "use_natural_pluralization": true
3 | }
4 |
--------------------------------------------------------------------------------
/assets/RChat Icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/assets/RChat Icon.png
--------------------------------------------------------------------------------
/assets/RChatIcon80.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/assets/RChatIcon80.png
--------------------------------------------------------------------------------
/assets/realm-app-id.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/assets/realm-app-id.png
--------------------------------------------------------------------------------
/assets/RChat Icon.afphoto:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/assets/RChat Icon.afphoto
--------------------------------------------------------------------------------
/assets/RChat Icon - 1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/assets/RChat Icon - 1024.png
--------------------------------------------------------------------------------
/assets/RChat Icon - 120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/assets/RChat Icon - 120.png
--------------------------------------------------------------------------------
/assets/RChat Icon - 152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/assets/RChat Icon - 152.png
--------------------------------------------------------------------------------
/assets/RChat Icon - 167.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/assets/RChat Icon - 167.png
--------------------------------------------------------------------------------
/assets/RChat Icon - 180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/assets/RChat Icon - 180.png
--------------------------------------------------------------------------------
/assets/RChat Icon - 20.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/assets/RChat Icon - 20.png
--------------------------------------------------------------------------------
/assets/RChat Icon - 29.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/assets/RChat Icon - 29.png
--------------------------------------------------------------------------------
/assets/RChat Icon - 40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/assets/RChat Icon - 40.png
--------------------------------------------------------------------------------
/assets/RChat Icon - 58.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/assets/RChat Icon - 58.png
--------------------------------------------------------------------------------
/assets/RChat Icon - 60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/assets/RChat Icon - 60.png
--------------------------------------------------------------------------------
/assets/RChat Icon - 76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/assets/RChat Icon - 76.png
--------------------------------------------------------------------------------
/assets/RChat Icon - 80.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/assets/RChat Icon - 80.png
--------------------------------------------------------------------------------
/assets/RChat Icon - 87.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/assets/RChat Icon - 87.png
--------------------------------------------------------------------------------
/assets/RChat Icon_1024x1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/assets/RChat Icon_1024x1024.png
--------------------------------------------------------------------------------
/RChat-Realm/RChat/values/dbName.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dbName",
3 | "value": "RChatFlex",
4 | "from_secret": false
5 | }
6 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Assets.xcassets/Leaf.imageset/Leaf.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/RChat-iOS/RChat/Assets.xcassets/Leaf.imageset/Leaf.png
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/RChat Icon - 20.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/RChat Icon - 20.png
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/RChat Icon - 29.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/RChat Icon - 29.png
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/RChat Icon - 40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/RChat Icon - 40.png
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/RChat Icon - 58.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/RChat Icon - 58.png
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/RChat Icon - 60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/RChat Icon - 60.png
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/RChat Icon - 76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/RChat Icon - 76.png
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/RChat Icon - 80.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/RChat Icon - 80.png
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/RChat Icon - 87.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/RChat Icon - 87.png
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/RChat Icon - 1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/RChat Icon - 1024.png
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/RChat Icon - 120-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/RChat Icon - 120-1.png
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/RChat Icon - 120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/RChat Icon - 120.png
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/RChat Icon - 152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/RChat Icon - 152.png
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/RChat Icon - 167.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/RChat Icon - 167.png
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/RChat Icon - 180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/RChat Icon - 180.png
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/RChat Icon - 40-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/RChat Icon - 40-1.png
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/RChat Icon - 40-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/RChat Icon - 40-2.png
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/RChat Icon - 58-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/RChat Icon - 58-1.png
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/RChat Icon - 80-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/RChat Icon - 80-1.png
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Preview Content/Preview Assets.xcassets/jane.imageset/jane.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/RChat-iOS/RChat/Preview Content/Preview Assets.xcassets/jane.imageset/jane.jpg
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Preview Content/Preview Assets.xcassets/rod.imageset/rod.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/RChat-iOS/RChat/Preview Content/Preview Assets.xcassets/rod.imageset/rod.jpg
--------------------------------------------------------------------------------
/RChat-Realm/RChat/realm_config.json:
--------------------------------------------------------------------------------
1 | {
2 | "app_id": "rchat-xxxxx",
3 | "config_version": 20210101,
4 | "name": "RChat",
5 | "location": "US-VA",
6 | "deployment_model": "LOCAL"
7 | }
8 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Preview Content/Preview Assets.xcassets/spud1.imageset/spud1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/RChat-iOS/RChat/Preview Content/Preview Assets.xcassets/spud1.imageset/spud1.jpg
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Preview Content/Preview Assets.xcassets/spud2.imageset/spud2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/RChat-iOS/RChat/Preview Content/Preview Assets.xcassets/spud2.imageset/spud2.jpg
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Preview Content/Preview Assets.xcassets/spud3.imageset/spud3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/RChat-iOS/RChat/Preview Content/Preview Assets.xcassets/spud3.imageset/spud3.jpg
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Preview Content/Preview Assets.xcassets/spud4.imageset/spud4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/RChat-iOS/RChat/Preview Content/Preview Assets.xcassets/spud4.imageset/spud4.jpg
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Preview Content/Preview Assets.xcassets/spud5.imageset/spud5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/RChat-iOS/RChat/Preview Content/Preview Assets.xcassets/spud5.imageset/spud5.jpg
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Preview Content/Preview Assets.xcassets/spud6.imageset/spud6.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/RChat-iOS/RChat/Preview Content/Preview Assets.xcassets/spud6.imageset/spud6.jpg
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Preview Content/Preview Assets.xcassets/spud7.imageset/spud7.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/RChat-iOS/RChat/Preview Content/Preview Assets.xcassets/spud7.imageset/spud7.jpg
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Preview Content/Preview Assets.xcassets/spud8.imageset/spud8.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/RChat-iOS/RChat/Preview Content/Preview Assets.xcassets/spud8.imageset/spud8.jpg
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Preview Content/Preview Assets.xcassets/freddy.imageset/freddy.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/RChat-iOS/RChat/Preview Content/Preview Assets.xcassets/freddy.imageset/freddy.jpg
--------------------------------------------------------------------------------
/RChat-iOS/RChat.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Preview Content/Preview Assets.xcassets/mugShot.imageset/Andrew_Morgan_Mug_2017.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/RChat-iOS/RChat/Preview Content/Preview Assets.xcassets/mugShot.imageset/Andrew_Morgan_Mug_2017.jpg
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Preview Content/Preview Assets.xcassets/mugShotThumb.imageset/Andrew_Morgan_250x211.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realm/RChat/HEAD/RChat-iOS/RChat/Preview Content/Preview Assets.xcassets/mugShotThumb.imageset/Andrew_Morgan_250x211.jpg
--------------------------------------------------------------------------------
/RChat-Realm/RChat/data_sources/mongodb-atlas/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mongodb-atlas",
3 | "type": "mongodb-atlas",
4 | "config": {
5 | "clusterName": "Cluster0",
6 | "readPreference": "primary",
7 | "wireProtocolEnabled": false
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/RChat-Realm/RChat/values/defaultLocation.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "defaultLocation",
3 | "value": [
4 | {
5 | "$numberDouble": "-0.10689139236939127"
6 | },
7 | {
8 | "$numberDouble": "51.506520923981554"
9 | }
10 | ],
11 | "from_secret": false
12 | }
13 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/dump/RChat/Chatster.metadata.json:
--------------------------------------------------------------------------------
1 | {"options":{"recordPreImages":true},"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"},{"v":{"$numberInt":"2"},"unique":true,"key":{"userName":{"$numberInt":"1"}},"name":"userName_1"},{"v":{"$numberInt":"2"},"key":{"partition":{"$numberInt":"1"}},"name":"partition_1"}],"uuid":"3a21e183ea0c4941b74815b28619c4f0","collectionName":"Chatster"}
--------------------------------------------------------------------------------
/RChat-Realm/RChat/data_sources/mongodb-atlas/RealmLog/Log/rules.json:
--------------------------------------------------------------------------------
1 | {
2 | "collection": "Log",
3 | "database": "RealmLog",
4 | "roles": [
5 | {
6 | "name": "default",
7 | "apply_when": {},
8 | "insert": false,
9 | "delete": false,
10 | "search": false,
11 | "additional_fields": {}
12 | }
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/RChat-Realm/RChat/data_sources/mongodb-atlas/RChatFlex/User/rules.json:
--------------------------------------------------------------------------------
1 | {
2 | "collection": "User",
3 | "database": "RChatFlex",
4 | "roles": [
5 | {
6 | "name": "default",
7 | "apply_when": {},
8 | "insert": false,
9 | "delete": false,
10 | "search": false,
11 | "additional_fields": {}
12 | }
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/RChat-Realm/RChat/data_sources/mongodb-atlas/RChatFlex/Chatster/rules.json:
--------------------------------------------------------------------------------
1 | {
2 | "collection": "Chatster",
3 | "database": "RChatFlex",
4 | "roles": [
5 | {
6 | "name": "default",
7 | "apply_when": {},
8 | "insert": false,
9 | "delete": false,
10 | "search": false,
11 | "additional_fields": {}
12 | }
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Model/Photo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Photo.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 24/11/2020.
6 | //
7 |
8 | import RealmSwift
9 | import SwiftUI
10 |
11 | class Photo: EmbeddedObject, ObjectKeyIdentifiable {
12 | @Persisted var _id = UUID().uuidString
13 | @Persisted var thumbNail: Data?
14 | @Persisted var picture: Data?
15 | @Persisted var date = Date()
16 | }
17 |
--------------------------------------------------------------------------------
/dump/RChat/ChatMessage.metadata.json:
--------------------------------------------------------------------------------
1 | {"options":{"recordPreImages":true},"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"},{"v":{"$numberInt":"2"},"key":{"location":"2dsphere"},"name":"location_2dsphere","2dsphereIndexVersion":{"$numberInt":"3"}},{"v":{"$numberInt":"2"},"key":{"partition":{"$numberInt":"1"}},"name":"partition_1"}],"uuid":"fac96d2cd3214383a7ef66ccc11a3e91","collectionName":"ChatMessage"}
--------------------------------------------------------------------------------
/RChat-Realm/RChat/data_sources/mongodb-atlas/RChatFlex/ChatMessage/rules.json:
--------------------------------------------------------------------------------
1 | {
2 | "collection": "ChatMessage",
3 | "database": "RChatFlex",
4 | "roles": [
5 | {
6 | "name": "default",
7 | "apply_when": {},
8 | "insert": false,
9 | "delete": false,
10 | "search": false,
11 | "additional_fields": {}
12 | }
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Model/UserPreferences.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserPreferences.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 23/11/2020.
6 | //
7 |
8 | import RealmSwift
9 |
10 | class UserPreferences: EmbeddedObject, ObjectKeyIdentifiable {
11 | @Persisted var displayName: String?
12 | @Persisted var avatarImage: Photo?
13 |
14 | var isEmpty: Bool { displayName == nil || displayName == "" }
15 | }
16 |
--------------------------------------------------------------------------------
/RChat-Realm/RChat/functions/countChats.js:
--------------------------------------------------------------------------------
1 | exports = function() {
2 | const dbName = context.values.get("dbName");
3 | const db = context.services.get("mongodb-atlas").db(dbName);
4 | const chatCollection = db.collection("ChatMessage");
5 |
6 | return chatCollection.count()
7 | .then(result => {
8 | return result
9 | }, error => {
10 | console.log(`Failed to count ChatMessage documents: ${error}`);
11 | });
12 | };
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Assets.xcassets/Leaf.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Leaf.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Model/Conversation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Conversation.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 23/11/2020.
6 | //
7 |
8 | import Foundation
9 | import RealmSwift
10 |
11 | class Conversation: EmbeddedObject, ObjectKeyIdentifiable {
12 | @Persisted var id = UUID().uuidString
13 | @Persisted var displayName = ""
14 | @Persisted var unreadCount = 0
15 | @Persisted var members = List()
16 | }
17 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Preview Content/Preview Assets.xcassets/jane.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "jane.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Preview Content/Preview Assets.xcassets/rod.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "rod.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Preview Content/Preview Assets.xcassets/spud1.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "spud1.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Preview Content/Preview Assets.xcassets/spud2.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "spud2.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Preview Content/Preview Assets.xcassets/spud3.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "spud3.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Preview Content/Preview Assets.xcassets/spud4.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "spud4.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Preview Content/Preview Assets.xcassets/spud5.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "spud5.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Preview Content/Preview Assets.xcassets/spud6.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "spud6.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Preview Content/Preview Assets.xcassets/spud7.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "spud7.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Preview Content/Preview Assets.xcassets/spud8.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "spud8.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Preview Content/Preview Assets.xcassets/freddy.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "freddy.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Preview Content/Preview Assets.xcassets/mugShot.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Andrew_Morgan_Mug_2017.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Preview Content/Preview Assets.xcassets/mugShotThumb.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Andrew_Morgan_250x211.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/dump/RChat/User.metadata.json:
--------------------------------------------------------------------------------
1 | {"options":{"recordPreImages":true},"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"},{"v":{"$numberInt":"2"},"key":{"conversation.id":{"$numberInt":"1"}},"name":"conversation.id_1"},{"v":{"$numberInt":"2"},"key":{"loction":"2dsphere"},"name":"loction_2dsphere","2dsphereIndexVersion":{"$numberInt":"3"}},{"v":{"$numberInt":"2"},"unique":true,"key":{"userName":{"$numberInt":"1"}},"name":"userName_1"}],"uuid":"ae01efe8eff549beb2d24049d7af1120","collectionName":"User"}
--------------------------------------------------------------------------------
/RChat-Realm/RChat/triggers/userRegistered.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "61eebbccef295f71984edeef",
3 | "name": "userRegistered",
4 | "type": "AUTHENTICATION",
5 | "config": {
6 | "operation_type": "CREATE",
7 | "providers": [
8 | "local-userpass"
9 | ]
10 | },
11 | "disabled": false,
12 | "event_processors": {
13 | "FUNCTION": {
14 | "config": {
15 | "function_name": "createNewUserDocument"
16 | }
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/RChat-Realm/RChat/auth/providers.json:
--------------------------------------------------------------------------------
1 | {
2 | "api-key": {
3 | "name": "api-key",
4 | "type": "api-key",
5 | "disabled": true
6 | },
7 | "local-userpass": {
8 | "name": "local-userpass",
9 | "type": "local-userpass",
10 | "config": {
11 | "autoConfirm": true,
12 | "resetFunctionName": "resetFunc",
13 | "runConfirmationFunction": false,
14 | "runResetFunction": true
15 | },
16 | "disabled": false
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/RChatApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RChatApp.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 23/11/2020.
6 | //
7 |
8 | import SwiftUI
9 | import RealmSwift
10 |
11 | let app = RealmSwift.App(id: "rchatflex-xxxxx") // TODO: Set the Realm application ID
12 |
13 | @main
14 | struct RChatApp: SwiftUI.App {
15 | @StateObject var state = AppState()
16 |
17 | var body: some Scene {
18 | WindowGroup {
19 | ContentView()
20 | .environmentObject(state)
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/RChat-Realm/RChat/functions/config.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "resetFunc",
4 | "private": true
5 | },
6 | {
7 | "name": "chatMessageChange",
8 | "private": true,
9 | "run_as_system": true
10 | },
11 | {
12 | "name": "countChats",
13 | "private": false
14 | },
15 | {
16 | "name": "createNewUserDocument",
17 | "private": true
18 | },
19 | {
20 | "name": "userDocWrittenTo",
21 | "private": true,
22 | "run_as_system": true
23 | }
24 | ]
25 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Custom Previews/PreviewColorScheme.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PreviewColorScheme.swift
3 | // Black Jack Trainer
4 | //
5 | // Created by Andrew Morgan on 14/10/2021.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct PreviewColorScheme: View {
11 | private let viewToPreview: Value
12 |
13 | init(_ viewToPreview: Value) {
14 | self.viewToPreview = viewToPreview
15 | }
16 |
17 | var body: some View {
18 | Group {
19 | viewToPreview
20 | viewToPreview.preferredColorScheme(.dark)
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Custom Previews/PreviewNoDevice.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PreviewNoDevice.swift
3 | // Black Jack Trainer
4 | //
5 | // Created by Andrew Morgan on 15/10/2021.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct PreviewNoDevice: View {
11 | private let viewToPreview: Value
12 |
13 | init(_ viewToPreview: Value) {
14 | self.viewToPreview = viewToPreview
15 | }
16 |
17 | var body: some View {
18 | Group {
19 | viewToPreview
20 | .previewLayout(.sizeThatFits)
21 | // .padding()
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Custom Previews/PreviewOrientation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PreviewOrientation.swift
3 | // Black Jack Trainer
4 | //
5 | // Created by Andrew Morgan on 14/10/2021.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct PreviewOrientation: View {
11 | private let viewToPreview: Value
12 |
13 | init(_ viewToPreview: Value) {
14 | self.viewToPreview = viewToPreview
15 | }
16 |
17 | var body: some View {
18 | Group {
19 | viewToPreview
20 | viewToPreview.previewInterfaceOrientation(.landscapeRight)
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/RChat.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.device.camera
8 |
9 | com.apple.security.network.client
10 |
11 | com.apple.security.personal-information.location
12 |
13 | com.apple.security.personal-information.photos-library
14 |
15 | keychain-access-groups
16 |
17 | $(AppIdentifierPrefix)com.clusterdb.RChat
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "realm-cocoa",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/realm/realm-cocoa.git",
7 | "state" : {
8 | "revision" : "933abaa8076966e237e66497def74df84c6adbb4",
9 | "version" : "10.42.0"
10 | }
11 | },
12 | {
13 | "identity" : "realm-core",
14 | "kind" : "remoteSourceControl",
15 | "location" : "https://github.com/realm/realm-core",
16 | "state" : {
17 | "revision" : "c04f5e401a1ec682e6b08b1ee157e19a0f834a5f",
18 | "version" : "13.17.1"
19 | }
20 | }
21 | ],
22 | "version" : 2
23 | }
24 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/Components/Pictures/BlankPersonIconView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BlankPersonIconView.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 24/11/2020.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct BlankPersonIconView: View {
11 | var body: some View {
12 | Image(systemName: "person.crop.circle.fill")
13 | .resizable()
14 | .foregroundColor(.gray)
15 | }
16 | }
17 |
18 | struct PersonIconView_Previews: PreviewProvider {
19 | static var previews: some View {
20 | AppearancePreviews(
21 | BlankPersonIconView()
22 | .frame(width: 50, height: 50)
23 | )
24 | .padding()
25 | .previewLayout(.sizeThatFits)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Model/Chatster.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Chatsters.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 25/11/2020.
6 | //
7 |
8 | import Foundation
9 | import RealmSwift
10 |
11 | class Chatster: Object, ObjectKeyIdentifiable {
12 | @Persisted(primaryKey: true) var _id = UUID().uuidString // This will match the _id of the associated User
13 | @Persisted var userName = ""
14 | @Persisted var displayName: String?
15 | @Persisted var avatarImage: Photo?
16 | @Persisted var lastSeenAt: Date?
17 | @Persisted var presence = "Off-Line"
18 |
19 | var presenceState: Presence {
20 | get { return Presence(rawValue: presence) ?? .hidden }
21 | set { presence = newValue.asString }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/RChat-Realm/RChat/triggers/ChatMessageChange.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "61eebc26ef295f71984f48b0",
3 | "name": "ChatMessageChange",
4 | "type": "DATABASE",
5 | "config": {
6 | "operation_types": [
7 | "INSERT"
8 | ],
9 | "database": "RChatFlex",
10 | "collection": "ChatMessage",
11 | "service_name": "mongodb-atlas",
12 | "match": {},
13 | "project": {},
14 | "full_document": true,
15 | "full_document_before_change": false,
16 | "unordered": false
17 | },
18 | "disabled": false,
19 | "event_processors": {
20 | "FUNCTION": {
21 | "config": {
22 | "function_name": "chatMessageChange"
23 | }
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/Components/Pictures/Thumbnail.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Thumbnail.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 26/11/2020.
6 | //
7 |
8 | import UIKit
9 | import SwiftUI
10 |
11 | struct Thumbnail: View {
12 | let imageData: Data
13 |
14 | var body: some View {
15 | Image(uiImage: (UIImage(data: imageData) ?? UIImage()))
16 | .resizable()
17 | .aspectRatio(contentMode: .fit)
18 | }
19 | }
20 |
21 | struct Thumbnail_Previews: PreviewProvider {
22 | static var previews: some View {
23 | AppearancePreviews(
24 | Thumbnail(imageData: (UIImage(named: "mugShotThumb") ?? UIImage()).jpegData(compressionQuality: 0.8)!)
25 | )
26 | .padding()
27 | .previewLayout(.sizeThatFits)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/Components/LabeledButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LabeledButton.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 23/11/2020.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct LabeledButton: View {
11 | let label: String
12 | let text: String
13 | let action: () -> Void
14 |
15 | var body: some View {
16 | Button(action: action) {
17 | LabeledText(label: label, text: text)
18 | }
19 | .buttonStyle(PlainButtonStyle())
20 | }
21 | }
22 |
23 | struct LabelledButton_Previews: PreviewProvider {
24 | static var previews: some View {
25 | AppearancePreviews(
26 | LabeledButton(label: "My label", text: "My Text") {}
27 | )
28 | .previewLayout(.sizeThatFits)
29 | .padding()
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/User Accounts & Profile/UserProfileButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserProfileButton.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 23/11/2020.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct UserProfileButton: View {
11 | @EnvironmentObject var state: AppState
12 |
13 | let action: () -> Void
14 |
15 | var body: some View {
16 | Button("Profile", action: action)
17 | .disabled(state.shouldIndicateActivity)
18 | }
19 | }
20 |
21 | struct UserProfileButton_Previews: PreviewProvider {
22 | static var previews: some View {
23 | return AppearancePreviews(
24 | UserProfileButton(action: { })
25 | )
26 | .padding()
27 | .previewLayout(.sizeThatFits)
28 | .environmentObject(AppState.sample)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/Components/BackButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BackButton.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 24/11/2020.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct BackButton: View {
11 | var label: String = "Back"
12 |
13 | private let spacing: CGFloat = 8
14 |
15 | var body: some View {
16 | HStack(spacing: spacing) {
17 | Image(systemName: "chevron.left")
18 | .aspectRatio(contentMode: .fit)
19 | Text(label)
20 | }
21 | }
22 | }
23 |
24 | struct BackButton_Previews: PreviewProvider {
25 | static var previews: some View {
26 | AppearancePreviews(
27 | BackButton(label: "Fish")
28 | .padding()
29 | .previewLayout(.sizeThatFits)
30 | )
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/Components/Buttons/SendButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SendButton.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 03/12/2020.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct SendButton: View {
11 | let action: () -> Void
12 | var active = true
13 |
14 | var body: some View {
15 | ButtonTemplate(action: action, active: active, activeImage: "paperplane.fill", inactiveImage: "paperplane")
16 | }
17 | }
18 |
19 | struct SendButton_Previews: PreviewProvider {
20 | static var previews: some View {
21 | AppearancePreviews(
22 | Group {
23 | SendButton(action: {}, active: false)
24 | SendButton(action: {})
25 | }
26 | )
27 | .previewLayout(.sizeThatFits)
28 | .padding()
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/Components/Buttons/AttachButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AttachButton.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 03/12/2020.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct AttachButton: View {
11 | let action: () -> Void
12 | var active = true
13 |
14 | var body: some View {
15 | ButtonTemplate(action: action, active: active, activeImage: "paperclip", inactiveImage: "paperclip")
16 | }
17 | }
18 |
19 | struct AttachButton_Previews: PreviewProvider {
20 | static var previews: some View {
21 | AppearancePreviews(
22 | Group {
23 | AttachButton(action: {}, active: false)
24 | AttachButton(action: {})
25 | }
26 | )
27 | .previewLayout(.sizeThatFits)
28 | .padding()
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/Components/Buttons/CameraButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CameraButton.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 03/12/2020.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct CameraButton: View {
11 | let action: () -> Void
12 | var active = true
13 |
14 | var body: some View {
15 | ButtonTemplate(action: action, active: active, activeImage: "camera.fill", inactiveImage: "camera")
16 | }
17 | }
18 |
19 | struct CameraButton_Previews: PreviewProvider {
20 | static var previews: some View {
21 | AppearancePreviews(
22 | Group {
23 | CameraButton(action: {}, active: false)
24 | CameraButton(action: {})
25 | }
26 | )
27 | .previewLayout(.sizeThatFits)
28 | .padding()
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/Components/Buttons/LocationButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LocationButton.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 09/12/2020.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct LocationButton: View {
11 | let action: () -> Void
12 | var active = true
13 |
14 | var body: some View {
15 | ButtonTemplate(action: action, active: active, activeImage: "location.fill", inactiveImage: "location")
16 | }
17 | }
18 |
19 | struct LocationButton_Previews: PreviewProvider {
20 | static var previews: some View {
21 | AppearancePreviews(
22 | Group {
23 | LocationButton(action: {}, active: false)
24 | LocationButton(action: {})
25 | }
26 | )
27 | .previewLayout(.sizeThatFits)
28 | .padding()
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/RChat-iOS/RChatTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/RChat-iOS/RChatUITests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/RChat-Realm/RChat/functions/createNewUserDocument.js:
--------------------------------------------------------------------------------
1 | exports = async function({user}) {
2 | const dbName = context.values.get("dbName");
3 | const db = context.services.get("mongodb-atlas").db(dbName);
4 | const userCollection = db.collection("User");
5 |
6 | const partition = `user=${user.id}`;
7 | const defaultLocation = context.values.get("defaultLocation");
8 | const userPreferences = {
9 | displayName: user.data.email
10 | };
11 |
12 | console.log(`user: ${JSON.stringify(user)}`);
13 |
14 | const userDoc = {
15 | _id: user.id,
16 | userName: user.data.email,
17 | userPreferences: userPreferences,
18 | location: context.values.get("defaultLocation"),
19 | lastSeenAt: null,
20 | presence:"Off-Line",
21 | conversations: []
22 | };
23 |
24 | await userCollection.insertOne(userDoc);
25 | };
--------------------------------------------------------------------------------
/RChat-Realm/RChat/triggers/UserCollectionWrite.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "61eebc75ef295f71984fa707",
3 | "name": "UserCollectionWrite",
4 | "type": "DATABASE",
5 | "config": {
6 | "operation_types": [
7 | "INSERT",
8 | "UPDATE",
9 | "DELETE",
10 | "REPLACE"
11 | ],
12 | "database": "RChatFlex",
13 | "collection": "User",
14 | "service_name": "mongodb-atlas",
15 | "match": {},
16 | "project": {},
17 | "full_document": true,
18 | "full_document_before_change": false,
19 | "unordered": false
20 | },
21 | "disabled": false,
22 | "event_processors": {
23 | "FUNCTION": {
24 | "config": {
25 | "function_name": "userDocWrittenTo"
26 | }
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Custom Previews/PreviewDevices.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PreviewDevices.swift
3 | // Black Jack Trainer
4 | //
5 | // Created by Andrew Morgan on 14/10/2021.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct PreviewDevices: View {
11 | let devices = [
12 | "iPhone 13 Pro Max",
13 | "iPhone 13 mini",
14 | "iPad (9th generation)"
15 | ]
16 |
17 | private let viewToPreview: Value
18 |
19 | init(_ viewToPreview: Value) {
20 | self.viewToPreview = viewToPreview
21 | }
22 |
23 | var body: some View {
24 | Group {
25 | ForEach(devices, id: \.self) { device in
26 | viewToPreview
27 | .previewDevice(PreviewDevice(rawValue: device))
28 | .previewDisplayName(device)
29 | }
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Assets.xcassets/Active.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0.261",
9 | "green" : "0.261",
10 | "red" : "0.261"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0.837",
27 | "green" : "0.837",
28 | "red" : "0.837"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Assets.xcassets/Clear.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "0.900",
8 | "blue" : "1.000",
9 | "green" : "1.000",
10 | "red" : "1.000"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "0.900",
26 | "blue" : "0.000",
27 | "green" : "0.000",
28 | "red" : "0.000"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Assets.xcassets/Inactive.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0.754",
9 | "green" : "0.754",
10 | "red" : "0.754"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0.261",
27 | "green" : "0.261",
28 | "red" : "0.261"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Assets.xcassets/MyBubble.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "0.300",
8 | "blue" : "0.319",
9 | "green" : "0.563",
10 | "red" : "0.000"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "0.500",
26 | "blue" : "0.319",
27 | "green" : "0.563",
28 | "red" : "0.000"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Assets.xcassets/OtherBubble.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "0.300",
8 | "blue" : "0.575",
9 | "green" : "0.329",
10 | "red" : "0.000"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "0.500",
26 | "blue" : "0.575",
27 | "green" : "0.329",
28 | "red" : "0.000"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/Components/CaptionLabel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CaptionLabel.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 23/11/2020.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct CaptionLabel: View {
11 | let title: String
12 |
13 | private let lineLimit = 5
14 |
15 | var body: some View {
16 | HStack {
17 | Text(LocalizedStringKey(title))
18 | .font(.caption)
19 | .lineLimit(lineLimit)
20 | .multilineTextAlignment(.leading)
21 | .foregroundColor(.secondary)
22 | Spacer()
23 | }
24 | }
25 | }
26 |
27 | struct CaptionLabel_Previews: PreviewProvider {
28 | static var previews: some View {
29 | AppearancePreviews(
30 | CaptionLabel(title: "Title")
31 | .previewLayout(.sizeThatFits)
32 | .padding()
33 | )
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Assets.xcassets/GreenBackground.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "0.335",
8 | "blue" : "0.000",
9 | "green" : "0.560",
10 | "red" : "0.000"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "0.663",
26 | "blue" : "0.000",
27 | "green" : "0.560",
28 | "red" : "0.000"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/Components/Buttons/DeleteButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DeleteButton.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 03/12/2020.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct DeleteButton: View {
11 | let action: () -> Void
12 | var active = true
13 | var padding: CGFloat = 8
14 |
15 | var body: some View {
16 | ButtonTemplate(action: action, active: active, activeImage: "minus.square.fill", inactiveImage: "minus.square", padding: padding)
17 | }
18 | }
19 |
20 | struct DeleteButton_Previews: PreviewProvider {
21 | static var previews: some View {
22 | AppearancePreviews(
23 | Group {
24 | DeleteButton(action: {}, active: false)
25 | DeleteButton(action: {})
26 | DeleteButton(action: {}, padding: 4)
27 | }
28 | )
29 | .previewLayout(.sizeThatFits)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/Components/MarkDown.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MarkDown.swift
3 | // MarkDown
4 | //
5 | // Created by Andrew Morgan on 13/09/2021.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct MarkDown: View {
11 | let text: String
12 |
13 | var body: some View {
14 | Text(safeAttributedString(text))
15 | }
16 | }
17 |
18 | private func safeAttributedString(_ sourceString: String) -> AttributedString {
19 | do {
20 | return try AttributedString(markdown: sourceString)
21 | } catch {
22 | print("Failed to convert Markdown to AttributedString: \(error.localizedDescription)")
23 | return try! AttributedString(markdown: "Text could not be rendered")
24 | }
25 | }
26 |
27 | struct MarkDown_Previews: PreviewProvider {
28 | static var previews: some View {
29 | MarkDown(text: "Sample of *italics*, **bold**, ~~strikethrough~~, [link](https://realm.io)")
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/Components/Pictures/PhotoFullSizeView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PhotoFullSizeView.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 24/11/2020.
6 | //
7 |
8 | import UIKit
9 | import SwiftUI
10 |
11 | struct PhotoFullSizeView: View {
12 | let photo: Photo
13 |
14 | var body: some View {
15 | VStack {
16 | if let picture = photo.picture {
17 | if let image = UIImage(data: picture) {
18 | Image(uiImage: image)
19 | .resizable()
20 | .aspectRatio(contentMode: .fit)
21 | }
22 | }
23 | }
24 | }
25 | }
26 |
27 | struct PhotoFullSizeView_Previews: PreviewProvider {
28 | static var previews: some View {
29 | AppearancePreviews(
30 | NavigationView {
31 | PhotoFullSizeView(photo: .sample)
32 | }
33 | )
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/Components/Pictures/ThumbnailPhotoView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ThumbnailPhotoView.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 24/11/2020.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ThumbnailPhotoView: View {
11 | let photo: Photo
12 | var imageSize: CGFloat = 64
13 |
14 | var body: some View {
15 | if let photo = photo.thumbNail {
16 | let mugShot = UIImage(data: photo)
17 | Image(uiImage: mugShot ?? UIImage())
18 | .renderingMode(.original)
19 | .resizable()
20 | .aspectRatio(contentMode: .fill)
21 | .frame(width: imageSize, height: imageSize)
22 | }
23 | }
24 | }
25 |
26 | struct ThumbnailPhotoView_Previews: PreviewProvider {
27 | static var previews: some View {
28 | AppearancePreviews(ThumbnailPhotoView(photo: .sample))
29 | .previewLayout(.sizeThatFits)
30 | .padding()
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Preview Content/PreviewHelpers.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PreviewHelpers.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 23/11/2020.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct AppearancePreviews: View {
11 | private let viewToPreview: Value
12 |
13 | init(_ viewToPreview: Value) {
14 | self.viewToPreview = viewToPreview
15 | }
16 |
17 | var body: some View {
18 | Group {
19 | viewToPreview
20 | viewToPreview.preferredColorScheme(.dark)
21 | }
22 | }
23 | }
24 |
25 | struct Landscape: View {
26 | private let viewToPreview: Value
27 |
28 | init(_ viewToPreview: Value) {
29 | self.viewToPreview = viewToPreview
30 | }
31 | let height = UIScreen.main.bounds.width
32 | let width = UIScreen.main.bounds.height
33 | var body: some View {
34 | viewToPreview
35 | .previewLayout(.fixed(width: width, height: height))
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/Components/LabeledText.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LabeledText.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 23/11/2020.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct LabeledText: View {
11 | let label: String
12 | let text: String
13 |
14 | private let lineLimit = 5
15 |
16 | var body: some View {
17 | VStack(alignment: .leading, spacing: .zero) {
18 | CaptionLabel(title: label)
19 | Text("\(text)")
20 | .font(.body)
21 | .lineLimit(lineLimit)
22 | }
23 | }
24 | }
25 |
26 | struct LabeledText_Previews: PreviewProvider {
27 | static var previews: some View {
28 | AppearancePreviews(
29 | HStack(alignment: .top) {
30 | LabeledText(label: "Label", text: "0.72367628765")
31 | LabeledText(label: "Date", text: "")
32 | }
33 | .previewLayout(.sizeThatFits)
34 | .padding()
35 | )
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/Conversations/ConversationCardView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ConversationCardView.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 26/11/2020.
6 | //
7 |
8 | import SwiftUI
9 | import RealmSwift
10 |
11 | struct ConversationCardView: View {
12 |
13 | let conversation: Conversation
14 | var isPreview = false
15 |
16 | var body: some View {
17 | VStack {
18 | if isPreview {
19 | ConversationCardContentsView(conversation: conversation)
20 | } else {
21 | ConversationCardContentsView(conversation: conversation)
22 | }
23 | }
24 | }
25 | }
26 |
27 | struct ConversationCardView_Previews: PreviewProvider {
28 | static var previews: some View {
29 | Realm.bootstrap()
30 |
31 | return AppearancePreviews(
32 | ConversationCardView(conversation: .sample, isPreview: true)
33 | )
34 | .padding()
35 | .previewLayout(.sizeThatFits)
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/Components/CheckBox.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CheckBox.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 18/12/2020.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct CheckBox: View {
11 | var title: String
12 | @Binding var isChecked: Bool
13 |
14 | var body: some View {
15 | Button(action: { self.isChecked.toggle() }) {
16 | HStack {
17 | Image(systemName: isChecked ? "checkmark.square": "square")
18 | Text(title)
19 | }
20 | .foregroundColor(isChecked ? .primary : .secondary)
21 | }
22 | }
23 | }
24 |
25 | struct CheckBox_Previews: PreviewProvider {
26 | static var previews: some View {
27 | AppearancePreviews(
28 | VStack {
29 | CheckBox(title: "Test checkbox", isChecked: .constant(true))
30 | CheckBox(title: "Test checkbox", isChecked: .constant(false))
31 | }
32 | )
33 | .padding()
34 | .previewLayout(.sizeThatFits)
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/RChat-iOS/RChatTests/RChatTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RChatTests.swift
3 | // RChatTests
4 | //
5 | // Created by Andrew Morgan on 23/11/2020.
6 | //
7 |
8 | import XCTest
9 | @testable import RChat
10 |
11 | class RChatTests: XCTestCase {
12 |
13 | override func setUpWithError() throws {
14 | // Put setup code here. This method is called before the invocation of each test method in the class.
15 | }
16 |
17 | override func tearDownWithError() throws {
18 | // Put teardown code here. This method is called after the invocation of each test method in the class.
19 | }
20 |
21 | func testExample() throws {
22 | // This is an example of a functional test case.
23 | // Use XCTAssert and related functions to verify your tests produce the correct results.
24 | }
25 |
26 | func testPerformanceExample() throws {
27 | // This is an example of a performance test case.
28 | self.measure {
29 | // Put the code you want to measure the time of here.
30 | }
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/AppState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppState.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 23/11/2020.
6 | //
7 |
8 | import RealmSwift
9 | import SwiftUI
10 | import Combine
11 |
12 | class AppState: ObservableObject {
13 |
14 | @Published var error: String?
15 | @Published var busyCount = 0
16 |
17 | var cancellables = Set()
18 |
19 | var shouldIndicateActivity: Bool {
20 | get {
21 | return busyCount > 0
22 | }
23 | set (newState) {
24 | if newState {
25 | busyCount += 1
26 | } else {
27 | if busyCount > 0 {
28 | busyCount -= 1
29 | } else {
30 | print("Attempted to decrement busyCount below 1")
31 | }
32 | }
33 | }
34 | }
35 |
36 | var loggedIn: Bool {
37 | app.currentUser != nil && app.currentUser?.state == .loggedIn
38 | }
39 |
40 | init() {
41 | app.currentUser?.logOut { _ in
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Model/ChatMessage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ChatMessage.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 23/11/2020.
6 | //
7 |
8 | import Foundation
9 | import RealmSwift
10 |
11 | class ChatMessage: Object, ObjectKeyIdentifiable {
12 | @Persisted(primaryKey: true) var _id = UUID().uuidString
13 | @Persisted var conversationID = ""
14 | @Persisted var author: String? // username
15 | @Persisted var authorID: String
16 | @Persisted var text = ""
17 | @Persisted var image: Photo?
18 | @Persisted var location = List()
19 | @Persisted var timestamp = Date()
20 |
21 | override static func primaryKey() -> String? {
22 | return "_id"
23 | }
24 |
25 | convenience init(author: String, authorID: String, text: String, image: Photo?, location: [Double] = []) {
26 | self.init()
27 | self.author = author
28 | self.authorID = authorID
29 | self.text = text
30 | self.image = image ?? nil
31 | location.forEach { coord in
32 | self.location.append(coord)
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/Components/OnOffCircleView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OnOffCircleView.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 24/11/2020.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct OnOffCircleView: View {
11 | let online: Bool
12 |
13 | private enum Dimensions {
14 | static let frameSize: CGFloat = 14.0
15 | static let innerCircleSize: CGFloat = 10
16 | }
17 |
18 | var body: some View {
19 | ZStack {
20 | Circle()
21 | .fill(Color.gray)
22 | .frame(width: Dimensions.frameSize, height: Dimensions.frameSize)
23 | Circle()
24 | .fill(online ? Color.green : Color.red)
25 | .frame(width: Dimensions.innerCircleSize, height: Dimensions.innerCircleSize)
26 | }
27 | }
28 | }
29 |
30 | struct OnOffCircleView_Previews: PreviewProvider {
31 | static var previews: some View {
32 | AppearancePreviews(
33 | Group {
34 | OnOffCircleView(online: true)
35 | }
36 | )
37 | .padding()
38 | .previewLayout(.sizeThatFits)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/Components/Pictures/AvatarThumbNailView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AvatarThumbNailView.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 24/11/2020.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct AvatarThumbNailView: View {
11 | let photo: Photo
12 | var imageSize: CGFloat = 102
13 |
14 | private enum Dimensions {
15 | static let radius: CGFloat = 4
16 | static let iconPadding: CGFloat = 8
17 | static let compressionQuality: CGFloat = 0.8
18 | }
19 |
20 | var body: some View {
21 | VStack {
22 | ThumbNailView(photo: photo)
23 | }
24 | .frame(width: imageSize, height: imageSize)
25 | .background(Color.gray)
26 | .cornerRadius(Dimensions.radius)
27 | .clipShape(/*@START_MENU_TOKEN@*/Circle()/*@END_MENU_TOKEN@*/)
28 | }
29 | }
30 |
31 | struct AvatarThumbNailView_Previews: PreviewProvider {
32 | static var previews: some View {
33 | AppearancePreviews(
34 | AvatarThumbNailView(photo: .sample)
35 | .padding()
36 | .previewLayout(.sizeThatFits)
37 | )
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/Components/Pictures/AvatarButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AvatarButton.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 24/11/2020.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct AvatarButton: View {
11 | let photo: Photo
12 | let action: () -> Void
13 |
14 | private enum Dimensions {
15 | static let frameWidth: CGFloat = 40
16 | static let frameHeight: CGFloat = 30
17 | static let opacity = 0.9
18 | }
19 |
20 | var body: some View {
21 | ZStack {
22 | Button(action: action) {
23 | AvatarThumbNailView(photo: photo)
24 | }
25 | Image(systemName: "camera.fill")
26 | .resizable()
27 | .frame(width: Dimensions.frameWidth, height: Dimensions.frameHeight)
28 | .foregroundColor(.gray)
29 | .opacity(Dimensions.opacity)
30 | }
31 | }
32 | }
33 |
34 | struct AvatarButton_Previews: PreviewProvider {
35 | static var previews: some View {
36 | AppearancePreviews(
37 | AvatarButton(photo: .sample, action: {})
38 | )
39 | .padding()
40 | .previewLayout(.sizeThatFits)
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/Components/Pictures/UIImage+Thumbnail.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIImage+Thumbnail.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 24/11/2020.
6 | //
7 |
8 | import UIKit
9 |
10 | extension UIImage {
11 | func thumbnail(size: CGFloat) -> UIImage? {
12 | var thumbnail: UIImage?
13 | guard let imageData = self.pngData() else {
14 | return nil
15 | }
16 | let options = [
17 | kCGImageSourceCreateThumbnailWithTransform: true,
18 | kCGImageSourceCreateThumbnailFromImageAlways: true,
19 | kCGImageSourceThumbnailMaxPixelSize: size] as [CFString : Any] as CFDictionary
20 |
21 | imageData.withUnsafeBytes { ptr in
22 | if let bytes = ptr.baseAddress?.assumingMemoryBound(to: UInt8.self),
23 | let cfData = CFDataCreate(kCFAllocatorDefault, bytes, imageData.count),
24 | let source = CGImageSourceCreateWithData(cfData, nil),
25 | let imageReference = CGImageSourceCreateThumbnailAtIndex(source, 0, options) {
26 | thumbnail = UIImage(cgImage: imageReference)
27 | }
28 | }
29 |
30 | return thumbnail
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Model/Member.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Member.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 01/12/2020.
6 | //
7 |
8 | import Foundation
9 | import RealmSwift
10 |
11 | class Member: EmbeddedObject, ObjectKeyIdentifiable {
12 | @Persisted var userName = ""
13 | @Persisted var membershipStatus = "User added, but invite pending"
14 |
15 | convenience init(_ userName: String) {
16 | self.init()
17 | self.userName = userName
18 | membershipState = .pending
19 | }
20 |
21 | convenience init(userName: String, state: MembershipStatus) {
22 | self.init()
23 | self.userName = userName
24 | membershipState = state
25 | }
26 |
27 | var membershipState: MembershipStatus {
28 | get { return MembershipStatus(rawValue: membershipStatus) ?? .left }
29 | set { membershipStatus = newValue.asString }
30 | }
31 | }
32 |
33 | enum MembershipStatus: String {
34 | case pending = "User added, but invite pending"
35 | case invited = "User has been invited to join"
36 | case active = "Membership active"
37 | case left = "User has left"
38 |
39 | var asString: String {
40 | self.rawValue
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Model/User.swift:
--------------------------------------------------------------------------------
1 | //
2 | // User.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 23/11/2020.
6 | //
7 |
8 | import Foundation
9 | import RealmSwift
10 |
11 | class User: Object, ObjectKeyIdentifiable {
12 | @Persisted(primaryKey: true) var _id = UUID().uuidString
13 | @Persisted var userName = ""
14 | @Persisted var userPreferences: UserPreferences?
15 | @Persisted var lastSeenAt: Date?
16 | @Persisted var conversations = List()
17 | @Persisted var presence = "On-Line"
18 |
19 | var isProfileSet: Bool { !(userPreferences?.isEmpty ?? true) }
20 | var presenceState: Presence {
21 | get { return Presence(rawValue: presence) ?? .hidden }
22 | set { presence = newValue.asString }
23 | }
24 |
25 | convenience init(userName: String, id: String) {
26 | self.init()
27 | self.userName = userName
28 | _id = id
29 | userPreferences = UserPreferences()
30 | userPreferences?.displayName = userName
31 | presence = "On-Line"
32 | }
33 | }
34 |
35 | enum Presence: String {
36 | case onLine = "On-Line"
37 | case offLine = "Off-Line"
38 | case hidden = "Hidden"
39 |
40 | var asString: String {
41 | self.rawValue
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/Components/Pictures/ThumbnailWithExpand.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ThumbnailWithExpand.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 07/12/2020.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ThumbnailWithExpand: View {
11 | let photo: Photo
12 |
13 | private enum Dimensions {
14 | static let frameSize: CGFloat = 100
15 | static let imageSize: CGFloat = 70
16 | static let buttonSize: CGFloat = 30
17 | static let radius: CGFloat = 8
18 | static let buttonPadding: CGFloat = 4
19 | }
20 |
21 | var body: some View {
22 | VStack {
23 | NavigationLink(destination: {
24 | PhotoFullSizeView(photo: photo)
25 | }, label: {
26 | ThumbNailView(photo: photo)
27 | .frame(width: Dimensions.imageSize, height: Dimensions.imageSize, alignment: .center)
28 | .clipShape(RoundedRectangle(cornerRadius: Dimensions.radius))
29 | })
30 | }
31 | }
32 | }
33 |
34 | struct ThumbnailWithExpand_Previews: PreviewProvider {
35 | static var previews: some View {
36 | AppearancePreviews(
37 | NavigationView {
38 | ThumbnailWithExpand(photo: .sample)
39 | }
40 | )
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/RChat-Realm/RChat/data_sources/mongodb-atlas/RChatFlex/Chatster/schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "bsonType": "object",
3 | "properties": {
4 | "_id": {
5 | "bsonType": "string"
6 | },
7 | "avatarImage": {
8 | "bsonType": "object",
9 | "properties": {
10 | "_id": {
11 | "bsonType": "string"
12 | },
13 | "date": {
14 | "bsonType": "date"
15 | },
16 | "picture": {
17 | "bsonType": "binData"
18 | },
19 | "thumbNail": {
20 | "bsonType": "binData"
21 | }
22 | },
23 | "required": [
24 | "_id",
25 | "date"
26 | ],
27 | "title": "Photo"
28 | },
29 | "displayName": {
30 | "bsonType": "string"
31 | },
32 | "lastSeenAt": {
33 | "bsonType": "date"
34 | },
35 | "presence": {
36 | "bsonType": "string"
37 | },
38 | "userName": {
39 | "bsonType": "string"
40 | }
41 | },
42 | "required": [
43 | "_id",
44 | "presence",
45 | "userName"
46 | ],
47 | "title": "Chatster"
48 | }
49 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/Components/Pictures/ThumbNailView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ThumbNailView.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 02/12/2020.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ThumbNailView: View {
11 | let photo: Photo?
12 | private let compressionQuality: CGFloat = 0.8
13 |
14 | var body: some View {
15 | VStack {
16 | if let photo = photo {
17 | if photo.thumbNail != nil || photo.picture != nil {
18 | if let photo = photo.thumbNail {
19 | Thumbnail(imageData: photo)
20 | } else {
21 | if let photo = photo.picture {
22 | Thumbnail(imageData: photo)
23 | } else {
24 | Thumbnail(imageData: UIImage().jpegData(compressionQuality: compressionQuality)!)
25 | }
26 | }
27 | }
28 | }
29 | }
30 | }
31 | }
32 |
33 | struct ThumbNailView_Previews: PreviewProvider {
34 | static var previews: some View {
35 | AppearancePreviews(
36 | Group {
37 | ThumbNailView(photo: .sample)
38 | ThumbNailView(photo: nil)
39 | }
40 | )
41 | .padding()
42 | .previewLayout(.sizeThatFits)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/RChat-Realm/RChat/functions/chatMessageChange.js:
--------------------------------------------------------------------------------
1 | exports = async function(changeEvent) {
2 | if (changeEvent.operationType != "insert") {
3 | console.log(`ChatMessage ${changeEvent.operationType} event – currently ignored.`);
4 | return;
5 | }
6 |
7 | console.log(`ChatMessage Insert event being processed`);
8 | console.log(`context.user: ${JSON.stringify(context.user)}`);
9 | console.log(`context.user.id: ${context.user.id}`);
10 | const dbName = context.values.get("dbName");
11 | const db = context.services.get("mongodb-atlas").db(dbName);
12 | let userCollection = db.collection("User");
13 | let eventCollection = db.collection("Event");
14 | let chatMessage = changeEvent.fullDocument;
15 | let conversation = chatMessage.conversationID;
16 | console.log(`Message: ${JSON.stringify(chatMessage)}`);
17 |
18 | const matchingUserQuery = {
19 | conversations: {
20 | $elemMatch: {
21 | id: conversation
22 | }
23 | }
24 | };
25 |
26 | const updateOperator = {
27 | $inc: {
28 | "conversations.$[element].unreadCount": 1
29 | }
30 | };
31 |
32 | const arrayFilter = {
33 | arrayFilters:[
34 | {
35 | "element.id": conversation
36 | }
37 | ]
38 | };
39 |
40 | await eventCollection.insertOne(changeEvent);
41 | await userCollection.updateMany(matchingUserQuery, updateOperator, arrayFilter);
42 | };
43 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/Conversations/MugShotGridView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MugShotGridView.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 26/11/2020.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct MugShotGridView: View {
11 | let members: [Chatster]
12 |
13 | private let rows = [
14 | GridItem(.flexible())
15 | ]
16 |
17 | private enum Dimensions {
18 | static let spacing: CGFloat = 0
19 | static let height: CGFloat = 50.0
20 | }
21 |
22 | var body: some View {
23 | ScrollView(.horizontal, showsIndicators: true) {
24 | LazyHGrid(rows: rows, alignment: .center, spacing: Dimensions.spacing) {
25 | ForEach(members) { member in
26 | UserAvatarView(
27 | photo: member.avatarImage,
28 | online: member.presenceState == .onLine ? true : false)
29 | }
30 | }
31 | .frame(height: Dimensions.height)
32 | }
33 | }
34 | }
35 |
36 | struct MugShotGridView_Previews: PreviewProvider {
37 | static var previews: some View {
38 | AppearancePreviews(
39 | MugShotGridView(members: [.sample, .sample2, .sample3, .sample, .sample2,
40 | .sample3, .sample, .sample2, .sample3, .sample, .sample2, .sample3])
41 | )
42 | .padding()
43 | .previewLayout(.sizeThatFits)
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/Conversations/SaveConversationButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SaveConversationButton.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 10/02/2021.
6 | //
7 |
8 | import SwiftUI
9 | import RealmSwift
10 |
11 | struct SaveConversationButton: View {
12 | @EnvironmentObject var state: AppState
13 |
14 | @ObservedRealmObject var user: User
15 |
16 | let name: String
17 | let members: [String]
18 | var done: () -> Void = { }
19 |
20 | var body: some View {
21 | Button(action: saveConversation) {
22 | Text("Save")
23 | }
24 | }
25 |
26 | private func saveConversation() {
27 | state.error = nil
28 | let conversation = Conversation()
29 | conversation.displayName = name
30 | conversation.members.append(Member(userName: user.userName, state: .active))
31 | conversation.members.append(objectsIn: members.map { Member($0) })
32 | $user.conversations.append(conversation)
33 | done()
34 | }
35 | }
36 |
37 | struct SaveConversationButton_Previews: PreviewProvider {
38 | static var previews: some View {
39 | return AppearancePreviews(
40 | SaveConversationButton(
41 | user: .sample, name: "Example Conversation",
42 | members: ["rod@contoso.com", "jane@contoso.com", "freddy@contoso.com"])
43 | )
44 | .previewLayout(.sizeThatFits)
45 | .padding()
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/RChat-iOS/RChatUITests/RChatUITests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RChatUITests.swift
3 | // RChatUITests
4 | //
5 | // Created by Andrew Morgan on 23/11/2020.
6 | //
7 |
8 | import XCTest
9 |
10 | class RChatUITests: XCTestCase {
11 |
12 | override func setUpWithError() throws {
13 | // Put setup code here. This method is called before the invocation of each test method in the class.
14 |
15 | // In UI tests it is usually best to stop immediately when a failure occurs.
16 | continueAfterFailure = false
17 |
18 | // 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.
19 | }
20 |
21 | override func tearDownWithError() throws {
22 | // Put teardown code here. This method is called after the invocation of each test method in the class.
23 | }
24 |
25 | func testExample() throws {
26 | // UI tests must launch the application that they test.
27 | let app = XCUIApplication()
28 | app.launch()
29 |
30 | // Use recording to get started writing UI tests.
31 | // Use XCTAssert and related functions to verify your tests produce the correct results.
32 | }
33 |
34 | func testLaunchPerformance() throws {
35 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) {
36 | // This measures how long it takes to launch your application.
37 | measure(metrics: [XCTApplicationLaunchMetric()]) {
38 | XCUIApplication().launch()
39 | }
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/RChat-Realm/RChat/data_sources/mongodb-atlas/RChatFlex/ChatMessage/schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "bsonType": "object",
3 | "properties": {
4 | "_id": {
5 | "bsonType": "string"
6 | },
7 | "author": {
8 | "bsonType": "string"
9 | },
10 | "authorID": {
11 | "bsonType": "string"
12 | },
13 | "conversationID": {
14 | "bsonType": "string"
15 | },
16 | "image": {
17 | "bsonType": "object",
18 | "properties": {
19 | "_id": {
20 | "bsonType": "string"
21 | },
22 | "date": {
23 | "bsonType": "date"
24 | },
25 | "picture": {
26 | "bsonType": "binData"
27 | },
28 | "thumbNail": {
29 | "bsonType": "binData"
30 | }
31 | },
32 | "required": [
33 | "_id",
34 | "date"
35 | ],
36 | "title": "Photo"
37 | },
38 | "location": {
39 | "bsonType": "array",
40 | "items": {
41 | "bsonType": "double"
42 | }
43 | },
44 | "text": {
45 | "bsonType": "string"
46 | },
47 | "timestamp": {
48 | "bsonType": "date"
49 | }
50 | },
51 | "required": [
52 | "_id",
53 | "authorID",
54 | "conversationID",
55 | "text",
56 | "timestamp"
57 | ],
58 | "title": "ChatMessage"
59 | }
60 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/Components/Buttons/ButtonTemplate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ButtonTemplate.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 03/12/2020.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ButtonTemplate: View {
11 | let action: () -> Void
12 | var active = true
13 | var activeImage = "paperplane.fill"
14 | var inactiveImage = "paperplane"
15 | var padding: CGFloat = 4
16 |
17 | private enum Dimensions {
18 | static let buttonSize: CGFloat = 60
19 | static let activeOpactity = 0.8
20 | static let disabledOpactity = 0.2
21 | }
22 |
23 | var body: some View {
24 | Button(action: { if active { action() } }) {
25 | Image(systemName: active ? activeImage : inactiveImage)
26 | .resizable()
27 | .aspectRatio(contentMode: .fit)
28 | .foregroundColor(.primary)
29 | .opacity(active ? Dimensions.activeOpactity : Dimensions.disabledOpactity)
30 | .padding(padding)
31 | }
32 | }
33 | }
34 |
35 | struct ButtonTemplate_Previews: PreviewProvider {
36 | static var previews: some View {
37 | AppearancePreviews(
38 | Group {
39 | ButtonTemplate(action: {})
40 | ButtonTemplate(action: {}, active: false)
41 | ButtonTemplate(action: {}, active: false, activeImage: "camera.fill", inactiveImage: "camera")
42 | ButtonTemplate(action: {}, active: true, activeImage: "camera.fill", inactiveImage: "camera")
43 | }
44 | )
45 | .previewLayout(.sizeThatFits)
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/RChat-Realm/RChat/sync/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "flexible",
3 | "state": "enabled",
4 | "development_mode_enabled": false,
5 | "service_name": "mongodb-atlas",
6 | "last_disabled": 1643384411,
7 | "permissions": {
8 | "rules": {
9 | "ChatMessage": [
10 | {
11 | "name": "anyone",
12 | "applyWhen": {},
13 | "read": {},
14 | "write": {
15 | "authorID": "%%user.id"
16 | }
17 | }
18 | ],
19 | "Chatster": [
20 | {
21 | "name": "anyone",
22 | "applyWhen": {},
23 | "read": true,
24 | "write": false
25 | }
26 | ],
27 | "User": [
28 | {
29 | "name": "anyone",
30 | "applyWhen": {},
31 | "read": {
32 | "_id": "%%user.id"
33 | },
34 | "write": {
35 | "_id": "%%user.id"
36 | }
37 | }
38 | ]
39 | },
40 | "defaultRoles": [
41 | {
42 | "name": "all",
43 | "applyWhen": {},
44 | "read": false,
45 | "write": false
46 | }
47 | ]
48 | },
49 | "queryable_fields_names": [
50 | "_id",
51 | "authorID",
52 | "conversationID",
53 | "timestamp",
54 | "userName"
55 | ]
56 | }
57 |
--------------------------------------------------------------------------------
/Permissions.json:
--------------------------------------------------------------------------------
1 | {
2 | "flexible_sync": {
3 | "state": "enabled",
4 | "permissions": {
5 | "rules": {},
6 | "User": {
7 | "roles": [
8 | {
9 | "name": "anyone",
10 | "applyWhen": {},
11 | "read": {
12 | "_id": "%%user.id"
13 | },
14 | "write": {
15 | "_id": "%%user.id"
16 | }
17 | }
18 | ]
19 | },
20 | "Chatster": {
21 | "roles": [
22 | {
23 | "name": "anyone",
24 | "applyWhen": {},
25 | "read": true,
26 | "write": false
27 | }
28 | ]
29 | },
30 | "ChatMessage": {
31 | "roles": [
32 | {
33 | "name": "anyone",
34 | "applyWhen": {},
35 | "read": {},
36 | "write": {
37 | "authorID": "%%user.id"
38 | }
39 | }
40 | ]
41 | },
42 | "defaultRoles": [
43 | {
44 | "name": "all",
45 | "applyWhen": {},
46 | "read": {},
47 | "write": {}
48 | }
49 | ]
50 | },
51 | "queryable_fields_names": ["_id", "userName", "conversationID", "authorID"]
52 | }
53 | }
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/Components/InputField.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InputField.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 23/11/2020.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct InputField: View {
11 |
12 | let title: String
13 | @Binding private(set) var text: String
14 | var showingSecureField = false
15 |
16 | private enum Dimensions {
17 | static let noSpacing: CGFloat = 0
18 | static let bottomPadding: CGFloat = 16
19 | static let iconSize: CGFloat = 20
20 | }
21 |
22 | var body: some View {
23 | VStack(spacing: Dimensions.noSpacing) {
24 | CaptionLabel(title: title)
25 | HStack(spacing: Dimensions.noSpacing) {
26 | if showingSecureField {
27 | SecureField("", text: $text)
28 | .padding(.bottom, Dimensions.bottomPadding)
29 | .foregroundColor(.primary)
30 | .font(.body)
31 | } else {
32 | TextField("", text: $text)
33 | .padding(.bottom, Dimensions.bottomPadding)
34 | .foregroundColor(.primary)
35 | .font(.body)
36 | }
37 | }
38 | }
39 | }
40 | }
41 |
42 | struct InputField_Previews: PreviewProvider {
43 | static var previews: some View {
44 | AppearancePreviews(
45 | Group {
46 | InputField(title: "Input", text: .constant("Data"))
47 | InputField(title: "Input secure", text: .constant("Data"), showingSecureField: true)
48 | }
49 | .previewLayout(.sizeThatFits)
50 | .padding()
51 | )
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Helpers/LocationHelper.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LocationHelper.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 24/11/2020.
6 | //
7 |
8 | import CoreLocation
9 |
10 | struct MyAnnotationItem: Identifiable {
11 | var coordinate: CLLocationCoordinate2D
12 | let id = UUID()
13 | }
14 |
15 | class LocationHelper: NSObject, ObservableObject {
16 |
17 | static let shared = LocationHelper()
18 | static let DefaultLocation = CLLocationCoordinate2D(latitude: 51.506520923981554, longitude: -0.10689139236939127)
19 |
20 | static var currentLocation: CLLocationCoordinate2D {
21 | guard let location = shared.locationManager.location else {
22 | return DefaultLocation
23 | }
24 | return location.coordinate
25 | }
26 |
27 | private let locationManager = CLLocationManager()
28 |
29 | private override init() {
30 | super.init()
31 | locationManager.delegate = self
32 | locationManager.desiredAccuracy = kCLLocationAccuracyBest
33 | locationManager.requestWhenInUseAuthorization()
34 | locationManager.startUpdatingLocation()
35 | }
36 | }
37 |
38 | extension LocationHelper: CLLocationManagerDelegate {
39 | func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { }
40 |
41 | public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
42 | print("Location manager failed with error: \(error.localizedDescription)")
43 | }
44 |
45 | public func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
46 | print("Location manager changed the status: \(status)")
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/Components/CallToActionButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CallToActionButton.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 23/11/2020.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct CallToActionButton: View {
11 | let title: String
12 | var showingArrow = false
13 | let action: () -> Void
14 |
15 | private enum Dimensions {
16 | static let labelSpacing: CGFloat = 14
17 | static let lineLimit = 1
18 | static let radius: CGFloat = 50.0
19 | }
20 |
21 | var body: some View {
22 | Button(action: action) {
23 | HStack {
24 | Spacer(minLength: Dimensions.labelSpacing)
25 | Text(LocalizedStringKey(title))
26 | .padding(.vertical, Dimensions.labelSpacing)
27 | .lineLimit(Dimensions.lineLimit)
28 | .font(Font.body.weight(.semibold))
29 | if showingArrow {
30 | Image(systemName: "arrow.right")
31 | .font(Font.caption2.weight(.bold))
32 | }
33 | Spacer(minLength: Dimensions.labelSpacing)
34 | }
35 | .foregroundColor(.white)
36 | .background(Color.blue)
37 | .cornerRadius(Dimensions.radius)
38 | }
39 | }
40 | }
41 |
42 | struct CallToActionButton_Previews: PreviewProvider {
43 | static var previews: some View {
44 | AppearancePreviews(
45 | Group {
46 | CallToActionButton(title: "Button", showingArrow: true, action: {})
47 | CallToActionButton(title: "Button", showingArrow: false, action: {})
48 | }
49 | )
50 | .previewLayout(.sizeThatFits)
51 | .padding()
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/User Accounts & Profile/UserAvatarView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserAvatarView.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 24/11/2020.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct UserAvatarView: View {
11 | let photo: Photo?
12 | let online: Bool
13 | var action: () -> Void = {}
14 |
15 | private enum Dimensions {
16 | static let imageSize: CGFloat = 30
17 | static let buttonSize: CGFloat = 36
18 | static let cornerRadius: CGFloat = 50.0
19 | }
20 |
21 | var body: some View {
22 | Button(action: action) {
23 | ZStack {
24 | image
25 | .cornerRadius(Dimensions.cornerRadius)
26 | HStack {
27 | Spacer()
28 | VStack {
29 | Spacer()
30 | OnOffCircleView(online: online)
31 | }
32 | }
33 | }
34 | }
35 | .frame(width: Dimensions.buttonSize, height: Dimensions.buttonSize)
36 | }
37 |
38 | var image: some View {
39 | Group {
40 | if let image = photo {
41 | return AnyView(ThumbnailPhotoView(photo: image, imageSize: Dimensions.imageSize))
42 | } else {
43 | return AnyView(BlankPersonIconView().frame(width: Dimensions.imageSize, height: Dimensions.imageSize))
44 | }
45 | }
46 | }
47 | }
48 |
49 | struct UserAvatarView_Previews: PreviewProvider {
50 | static var previews: some View {
51 | AppearancePreviews(
52 | UserAvatarView(photo: .sample, online: true, action: {})
53 | )
54 | .padding()
55 | .previewLayout(.sizeThatFits)
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/Components/Pictures/ThumbnailWithDelete.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ThumbnailWithDelete.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 03/12/2020.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ThumbnailWithDelete: View {
11 | let photo: Photo?
12 | var action: (() -> Void)?
13 |
14 | private enum Dimensions {
15 | static let frameSize: CGFloat = 100
16 | static let imageSize: CGFloat = 70
17 | static let buttonSize: CGFloat = 30
18 | static let radius: CGFloat = 8
19 | static let buttonPadding: CGFloat = 4
20 | }
21 |
22 | var body: some View {
23 | ZStack {
24 | ThumbNailView(photo: photo)
25 | .frame(width: Dimensions.imageSize, height: Dimensions.imageSize, alignment: .center)
26 | .clipShape(RoundedRectangle(cornerRadius: Dimensions.radius))
27 | if let action = action {
28 | VStack {
29 | HStack {
30 | Spacer()
31 | DeleteButton(action: action, padding: Dimensions.buttonPadding)
32 | .frame(width: Dimensions.buttonSize, height: Dimensions.buttonSize, alignment: .center)
33 | }
34 | Spacer()
35 | }
36 | .frame(width: Dimensions.frameSize, height: Dimensions.frameSize)
37 | }
38 | }
39 | }
40 | }
41 |
42 | struct ThumbnailWithDelete_Previews: PreviewProvider {
43 | static var previews: some View {
44 | AppearancePreviews(
45 | Group {
46 | ThumbnailWithDelete(photo: .sample, action: {})
47 | ThumbnailWithDelete(photo: .sample)
48 | }
49 | )
50 | .previewLayout(.sizeThatFits)
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/User Accounts & Profile/OnlineAlertSettings.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OnlineAlertSettings.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 25/11/2020.
6 | //
7 |
8 | import SwiftUI
9 | import UserNotifications
10 |
11 | struct OnlineAlertSettings: View {
12 | @EnvironmentObject var state: AppState
13 |
14 | @AppStorage("shouldRemindOnlineUser") var shouldRemindOnlineUser = false
15 | @AppStorage("onlineUserReminderHours") var onlineUserReminderHours = 8.0
16 |
17 | var body: some View {
18 | VStack {
19 | Toggle(isOn: $shouldRemindOnlineUser, label: {
20 | Text("On-line reminder")
21 | })
22 | .onChange(of: shouldRemindOnlineUser, perform: { value in
23 | if value {
24 | let notificationCenter = UNUserNotificationCenter.current()
25 | notificationCenter.requestAuthorization(options: [.alert, .badge, .sound]) { success, error in
26 | if !success {
27 | shouldRemindOnlineUser = false
28 | }
29 | if let error = error {
30 | state.error = "Failed to enable notifications: \(error.localizedDescription)"
31 | }
32 | }
33 | }
34 | })
35 | if shouldRemindOnlineUser {
36 | Slider(value: $onlineUserReminderHours, in: 1...24, step: 1)
37 | Text("Minimized alert in "
38 | + "\(Int(onlineUserReminderHours)) \(onlineUserReminderHours == 1 ? "hour" : "hours")")
39 | }
40 | }
41 | }
42 | }
43 |
44 | struct OnlineAlertSettings_Previews: PreviewProvider {
45 | static var previews: some View {
46 | AppearancePreviews(
47 | OnlineAlertSettings()
48 | )
49 | .padding()
50 | .previewLayout(.sizeThatFits)
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/assets/RChat Icon_1024x1024.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/Components/Maps/MapView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MapView.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 10/12/2020.
6 | //
7 |
8 | import MapKit
9 | import SwiftUI
10 |
11 | struct MapView: View {
12 | let location: CLLocationCoordinate2D
13 | let annotationItems: [MyAnnotationItem]
14 |
15 | @State private var region: MKCoordinateRegion = MKCoordinateRegion(
16 | center: CLLocationCoordinate2D(latitude: MapDefaults.latitude, longitude: MapDefaults.longitude),
17 | span: MKCoordinateSpan(latitudeDelta: MapDefaults.zoomedOut, longitudeDelta: MapDefaults.zoomedOut))
18 |
19 | private enum MapDefaults {
20 | static let latitude = 51.507222
21 | static let longitude = -0.1275
22 | static let zoomedOut = 2.0
23 | static let zoomedIn = 0.01
24 | }
25 |
26 | var body: some View {
27 | Map(coordinateRegion: $region,
28 | interactionModes: .all,
29 | showsUserLocation: true,
30 | annotationItems: annotationItems) { item in
31 | MapMarker(coordinate: item.coordinate)
32 | }
33 | .onAppear(perform: setupLocation)
34 | }
35 |
36 | func setupLocation() {
37 | region = MKCoordinateRegion(
38 | center: location,
39 | span: MKCoordinateSpan(latitudeDelta: MapDefaults.zoomedIn, longitudeDelta: MapDefaults.zoomedIn))
40 | }
41 | }
42 |
43 | struct MapView_Previews: PreviewProvider {
44 | static var previews: some View {
45 | let position = CLLocationCoordinate2D(latitude: 51.007222, longitude: -0.11)
46 | AppearancePreviews(
47 | Group {
48 | NavigationView {
49 | MapView(location: position, annotationItems: [])
50 | }
51 | NavigationView {
52 | MapView(location: position, annotationItems: [MyAnnotationItem(coordinate: position)])
53 | }
54 | }
55 | )
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/Components/OpaqueProgressView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OpaqueProgressView.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 23/11/2020.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct OpaqueProgressView: View {
11 | var message: String?
12 |
13 | private enum Dimensions {
14 | static let padding: CGFloat = 100
15 | static let bgColor = Color("Clear")
16 | static let cornerRadius: CGFloat = 16
17 | }
18 |
19 | init() {
20 | message = nil
21 | }
22 |
23 | init(_ message: String?) {
24 | self.message = message
25 | }
26 |
27 | var body: some View {
28 | VStack {
29 | if let message = message {
30 | ProgressView(message)
31 | } else {
32 | ProgressView()
33 | }
34 | }
35 | .padding(Dimensions.padding)
36 | .background(.ultraThinMaterial,
37 | in: RoundedRectangle(cornerRadius: Dimensions.cornerRadius))
38 | }
39 | }
40 |
41 | struct OpaquePreviewView_Previews: PreviewProvider {
42 | static var previews: some View {
43 | Group {
44 | AppearancePreviews(
45 | Group {
46 | ZStack {
47 | VStack {
48 | Text("Background Text")
49 | .padding(150)
50 | .background(Color.blue)
51 | }
52 | OpaqueProgressView()
53 | }
54 | ZStack {
55 | VStack {
56 | Text("Background Text")
57 | .padding(150)
58 | .background(Color.blue)
59 | }
60 | OpaqueProgressView("Some Text")
61 | }
62 | }
63 | )
64 | }
65 | .previewLayout(.sizeThatFits)
66 | .padding()
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/Chat Messages/ChatRoomView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ChatRoomView.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 03/12/2020.
6 | //
7 |
8 | import RealmSwift
9 | import SwiftUI
10 |
11 | struct ChatRoomView: View {
12 | @EnvironmentObject var state: AppState
13 |
14 | @ObservedRealmObject var user: User
15 | var conversation: Conversation?
16 | var isPreview = false
17 |
18 | let padding: CGFloat = 8
19 |
20 | var body: some View {
21 | VStack {
22 | if let conversation = conversation {
23 | if isPreview {
24 | ChatRoomBubblesView(user: user, conversation: conversation, isPreview: isPreview)
25 | } else {
26 | ChatRoomBubblesView(user: user, conversation: conversation)
27 | .environment(\.realmConfiguration, app.currentUser!.flexibleSyncConfiguration())
28 | }
29 | }
30 | Spacer()
31 | }
32 | .navigationBarTitle(conversation?.displayName ?? "Chat", displayMode: .inline)
33 | .padding(.horizontal, padding)
34 | .onAppear(perform: clearUnreadCount)
35 | .onDisappear(perform: clearUnreadCount)
36 | }
37 |
38 | private func clearUnreadCount() {
39 | if let conversationId = conversation?.id {
40 | if let conversationIndex = user.conversations.firstIndex(where: { $0.id == conversationId }) {
41 | $user.conversations[conversationIndex].unreadCount.wrappedValue = 0
42 | }
43 | }
44 | }
45 | }
46 |
47 | struct ChatRoom_Previews: PreviewProvider {
48 | static var previews: some View {
49 | Realm.bootstrap()
50 |
51 | return AppearancePreviews(
52 | Group {
53 | NavigationView {
54 | ChatRoomView(user: .sample, conversation: .sample, isPreview: true)
55 | }
56 | }
57 | )
58 | .environmentObject(AppState.sample)
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/Components/TextDate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TextDate.swift
3 | // TextDate
4 | //
5 | // Created by Andrew Morgan on 14/09/2021.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct TextDate: View {
11 | let date: Date
12 |
13 | private var isLessThanOneMinute: Bool { date.timeIntervalSinceNow > -60 }
14 | private var isLessThanOneDay: Bool { date.timeIntervalSinceNow > -60 * 60 * 24 }
15 | private var isLessThanOneWeek: Bool { date.timeIntervalSinceNow > -60 * 60 * 24 * 7}
16 | private var isLessThanOneYear: Bool { date.timeIntervalSinceNow > -60 * 60 * 24 * 365}
17 |
18 | var body: some View {
19 | if isLessThanOneMinute {
20 | Text(date.formatted(.dateTime.hour().minute().second()))
21 | } else {
22 | if isLessThanOneDay {
23 | Text(date.formatted(.dateTime.hour().minute()))
24 | } else {
25 | if isLessThanOneWeek {
26 | Text(date.formatted(.dateTime.weekday(.wide).hour().minute()))
27 | } else {
28 | if isLessThanOneYear {
29 | Text(date.formatted(.dateTime.month().day()))
30 | } else {
31 | Text(date.formatted(.dateTime.year().month().day()))
32 | }
33 | }
34 | }
35 | }
36 | }
37 | }
38 |
39 | struct TextDate_Previews: PreviewProvider {
40 | static var previews: some View {
41 | VStack {
42 | TextDate(date: Date(timeIntervalSinceNow: -60 * 60 * 24 * 365)) // 1 year ago
43 | TextDate(date: Date(timeIntervalSinceNow: -60 * 60 * 24 * 7)) // 1 week ago
44 | TextDate(date: Date(timeIntervalSinceNow: -60 * 60 * 24)) // 1 day ago
45 | TextDate(date: Date(timeIntervalSinceNow: -60 * 60)) // 1 hour ago
46 | TextDate(date: Date(timeIntervalSinceNow: -60)) // 1 minute ago
47 | TextDate(date: Date()) // Now
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/User Accounts & Profile/LogoutButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LogoutButton.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 23/11/2020.
6 | //
7 |
8 | import RealmSwift
9 | import SwiftUI
10 |
11 | struct LogoutButton: View {
12 | @EnvironmentObject var state: AppState
13 | @Environment(\.realm) var realm
14 |
15 | @ObservedRealmObject var user: User
16 | @Binding var userID: String?
17 | var action: () -> Void = {}
18 |
19 | @State private var isConfirming = false
20 |
21 | var body: some View {
22 | Button("Log Out") { isConfirming = true }
23 | .confirmationDialog("Are you that you want to logout",
24 | isPresented: $isConfirming) {
25 | Button("Confirm Logout", role: .destructive, action: logout)
26 | Button("Cancel", role: .cancel) {}
27 | }
28 | .disabled(state.shouldIndicateActivity)
29 | }
30 |
31 | private func logout() {
32 | state.shouldIndicateActivity = true
33 | action()
34 | // TODO: Find a way that this gets synced to backend
35 | $user.presenceState.wrappedValue = .offLine
36 | // TODO: Is there a way to do this without causing issues when users log out back in on the same devoce?
37 | // clearSubscriptions()
38 | app.currentUser?.logOut { _ in
39 | DispatchQueue.main.async {
40 | state.shouldIndicateActivity = false
41 | }
42 | }
43 | }
44 |
45 | private func clearSubscriptions() {
46 | let subscriptions = realm.subscriptions
47 | subscriptions.update {
48 | subscriptions.removeAll()
49 | }
50 | }
51 |
52 | }
53 |
54 | struct LogoutButton_Previews: PreviewProvider {
55 | static var previews: some View {
56 | AppearancePreviews(
57 | LogoutButton(user: User(), userID: .constant("Andrew"))
58 | .environmentObject(AppState())
59 | .previewLayout(.sizeThatFits)
60 | .padding()
61 | )
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/Chat Messages/ChatInputBox copy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ChatInputBox.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 02/12/2020.
6 | //
7 |
8 | import SwiftUI
9 | import UIKit
10 |
11 | struct ChatInputBox: View {
12 | var clicked: (String, Photo) -> Void
13 |
14 | private enum Dimensions {
15 | static let maxHeight: CGFloat = 100
16 | static let minHeight: CGFloat = 100
17 | static let radius: CGFloat = 20
18 | static let imageSize: CGFloat = 100
19 | }
20 |
21 | @State var photo: Photo?
22 | @State var chatText = "Type your message"
23 |
24 | var body: some View {
25 | HStack {
26 | AddPhotoButton(action: takePhoto, photo: photo)
27 | .frame(
28 | width: photo != nil ? Dimensions.imageSize : Dimensions.imageSize / 2,
29 | height: Dimensions.imageSize,
30 | alignment: .center)
31 | .clipShape(RoundedRectangle(cornerRadius: Dimensions.radius))
32 | TextEditor(text: $chatText)
33 | .padding()
34 | .frame(minWidth: 0, maxWidth: .infinity, minHeight: Dimensions.minHeight, maxHeight: Dimensions.maxHeight)
35 | .background(Color("GreenBackground"))
36 | .clipShape(RoundedRectangle(cornerRadius: Dimensions.radius))
37 | }
38 | .onAppear(perform: { clearBackground() })
39 | }
40 |
41 | func takePhoto() {
42 | // TODO: Implement
43 | }
44 |
45 | func clearBackground() {
46 | UITextView.appearance().backgroundColor = .clear
47 | }
48 | }
49 |
50 | struct ChatInputBox_Previews: PreviewProvider {
51 | static var previews: some View {
52 | AppearancePreviews(
53 | Group {
54 | NavigationView {
55 | ChatInputBox { (_, _) in }
56 | }
57 | NavigationView {
58 | ChatInputBox(clicked: { (_, _) in }, photo: .sample)
59 | }
60 | }
61 | )
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/Components/SearchBox.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SearchBox.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 08/12/2020.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct SearchBox: View {
11 | var placeholder: String = "Search"
12 | @Binding var searchText: String
13 |
14 | private enum Dimensions {
15 | static let inset: CGFloat = 7.0
16 | static let bottomInset: CGFloat = 4.0
17 | static let heightTextField: CGFloat = 36.0
18 | static let cornerRadius: CGFloat = 10.0
19 | static let padding: CGFloat = 16.0
20 | static let topPadding: CGFloat = 15.0
21 | static let glassSize: CGFloat = 24.0
22 | static let dividerHeight: CGFloat = 1.0
23 | }
24 |
25 | var body: some View {
26 | VStack {
27 | HStack {
28 | Image(systemName: "magnifyingglass")
29 | .frame(width: Dimensions.glassSize, height: Dimensions.glassSize)
30 | TextField(placeholder,
31 | text: $searchText
32 | )
33 | .disableAutocorrection(true)
34 | .autocapitalization(/*@START_MENU_TOKEN@*/.none/*@END_MENU_TOKEN@*/)
35 | .font(.body)
36 | }
37 | .padding(EdgeInsets(top: Dimensions.inset, leading: Dimensions.bottomInset, bottom: Dimensions.inset, trailing: Dimensions.inset))
38 | .frame(height: Dimensions.heightTextField)
39 | .foregroundColor(.secondary)
40 | .background(Color(.secondarySystemBackground))
41 | .cornerRadius(Dimensions.cornerRadius)
42 | .padding([.horizontal, .top], Dimensions.padding)
43 | Divider()
44 | .padding(.top, Dimensions.topPadding)
45 | .frame(height: Dimensions.dividerHeight)
46 | }
47 | }
48 | }
49 |
50 | struct SearchBox_Previews: PreviewProvider {
51 | static var previews: some View {
52 | AppearancePreviews(
53 | SearchBox(
54 | searchText: .constant("")
55 | )
56 | )
57 | .padding()
58 | .previewLayout(.sizeThatFits)
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/Conversations/ConversationListView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ConversationListView
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 25/11/2020.
6 | //
7 |
8 | import SwiftUI
9 | import RealmSwift
10 |
11 | struct ConversationListView: View {
12 |
13 | @EnvironmentObject var state: AppState
14 | @ObservedRealmObject var user: User
15 |
16 | var isPreview = false
17 |
18 | @State private var conversation: Conversation?
19 | @State private var showingAddChat = false
20 |
21 | private let sortDescriptors = [
22 | SortDescriptor(keyPath: "unreadCount", ascending: false),
23 | SortDescriptor(keyPath: "displayName", ascending: true)
24 | ]
25 |
26 | var body: some View {
27 | ZStack {
28 | VStack {
29 | let conversations = user.conversations.sorted(by: sortDescriptors)
30 | List(conversations) { conversation in
31 | NavigationLink {
32 | ChatRoomView(user: user, conversation: conversation)
33 | } label: {
34 | ConversationCardView(conversation: conversation, isPreview: isPreview)
35 | .listRowSeparator(.hidden)
36 | }
37 | }
38 | Button(action: { showingAddChat.toggle() }) {
39 | Text("New Chat Room")
40 | }
41 | .disabled(showingAddChat)
42 | Spacer()
43 | }
44 | }
45 | .onAppear {
46 | $user.presenceState.wrappedValue = .onLine
47 | }
48 | .sheet(isPresented: $showingAddChat) {
49 | NewConversationView(user: user)
50 | .environmentObject(state)
51 | .environment(\.realmConfiguration, app.currentUser!.flexibleSyncConfiguration())
52 | }
53 | }
54 | }
55 |
56 | struct ConversationListViewPreviews: PreviewProvider {
57 |
58 | static var previews: some View {
59 | Realm.bootstrap()
60 |
61 | return ConversationListView(user: .sample, isPreview: true)
62 | .environmentObject(AppState.sample)
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSLocationWhenInUseUsageDescription
6 | Your location will only be shared when you explicitly add it to a chat message
7 | NSCameraUsageDescription
8 | You have the chance to include photos in your messages
9 | NSPhotoLibraryUsageDescription
10 | RChat would like to give you the option to include photos from your library in your messages
11 | CFBundleDevelopmentRegion
12 | $(DEVELOPMENT_LANGUAGE)
13 | CFBundleExecutable
14 | $(EXECUTABLE_NAME)
15 | CFBundleIdentifier
16 | $(PRODUCT_BUNDLE_IDENTIFIER)
17 | CFBundleInfoDictionaryVersion
18 | 6.0
19 | CFBundleName
20 | $(PRODUCT_NAME)
21 | CFBundlePackageType
22 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
23 | CFBundleShortVersionString
24 | 1.0
25 | CFBundleVersion
26 | 1
27 | LSRequiresIPhoneOS
28 |
29 | UIApplicationSceneManifest
30 |
31 | UIApplicationSupportsMultipleScenes
32 |
33 |
34 | UIApplicationSupportsIndirectInputEvents
35 |
36 | UILaunchScreen
37 |
38 | UIRequiredDeviceCapabilities
39 |
40 | armv7
41 |
42 | UISupportedInterfaceOrientations
43 |
44 | UIInterfaceOrientationPortrait
45 | UIInterfaceOrientationLandscapeLeft
46 | UIInterfaceOrientationLandscapeRight
47 |
48 | UISupportedInterfaceOrientations~ipad
49 |
50 | UIInterfaceOrientationPortrait
51 | UIInterfaceOrientationPortraitUpsideDown
52 | UIInterfaceOrientationLandscapeLeft
53 | UIInterfaceOrientationLandscapeRight
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/Components/Maps/MapThumbnailWithExpand.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MapThumbnailWithExpand.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 10/12/2020.
6 | //
7 |
8 | import MapKit
9 | import SwiftUI
10 |
11 | struct MapThumbnailWithExpand: View {
12 | let location: [Double]
13 |
14 | @State private var region: MKCoordinateRegion = MKCoordinateRegion(
15 | center: CLLocationCoordinate2D(latitude: 51.507222, longitude: -0.1275),
16 | span: MKCoordinateSpan(latitudeDelta: 2.0, longitudeDelta: 2.0))
17 | @State private var annotationItems = [MyAnnotationItem]()
18 | @State private var position = CLLocationCoordinate2D(latitude: 51.507222, longitude: -0.1275)
19 |
20 | private enum Dimensions {
21 | static let frameSize: CGFloat = 100
22 | static let imageSize: CGFloat = 70
23 | static let buttonSize: CGFloat = 30
24 | static let radius: CGFloat = 8
25 | static let buttonPadding: CGFloat = 4
26 | }
27 |
28 | var body: some View {
29 | VStack {
30 | NavigationLink {
31 | MapView(location: position, annotationItems: annotationItems)
32 | } label: {
33 | Map(coordinateRegion: $region, annotationItems: annotationItems) { item in
34 | MapMarker(coordinate: item.coordinate)
35 | }
36 | .frame(width: Dimensions.imageSize, height: Dimensions.imageSize, alignment: .center)
37 | .clipShape(RoundedRectangle(cornerRadius: Dimensions.radius))
38 | }
39 | }
40 | .onAppear(perform: setupLocation)
41 | }
42 |
43 | func setupLocation() {
44 | position = CLLocationCoordinate2D(latitude: location[1], longitude: location[0])
45 | region = MKCoordinateRegion(
46 | center: position,
47 | span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1))
48 | annotationItems.append(MyAnnotationItem(coordinate: position))
49 | }
50 | }
51 |
52 | struct MapThumbnailWithExpand_Previews: PreviewProvider {
53 | static var previews: some View {
54 | AppearancePreviews(
55 | MapThumbnailWithExpand(location: [-0.10689139236939127, 51.506520923981554])
56 | )
57 | .previewLayout(.sizeThatFits)
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/Chat Messages/AuthorView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AuthorView.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 09/02/2021.
6 | //
7 |
8 | import SwiftUI
9 | import RealmSwift
10 |
11 | struct AuthorView: View {
12 | @EnvironmentObject var state: AppState
13 | @Environment(\.realm) var realm
14 | @ObservedResults(Chatster.self) var chatsters
15 |
16 | let userName: String
17 |
18 | var chatster: Chatster? {
19 | chatsters.filter("userName = %@", userName).first
20 | }
21 |
22 | private enum Dimensions {
23 | static let authorHeight: CGFloat = 25
24 | static let padding: CGFloat = 4
25 | }
26 |
27 | var body: some View {
28 | if let author = chatster {
29 | HStack {
30 | if let photo = author.avatarImage {
31 | AvatarThumbNailView(photo: photo, imageSize: Dimensions.authorHeight)
32 | }
33 | if let name = author.displayName {
34 | Text(name)
35 | .font(.caption)
36 | } else {
37 | Text(author.userName)
38 | .font(.caption)
39 | }
40 | Spacer()
41 | }
42 | .onAppear(perform: setSubscription)
43 | .frame(maxHeight: Dimensions.authorHeight)
44 | .padding(Dimensions.padding)
45 | }
46 | }
47 |
48 | private func setSubscription() {
49 | let subscriptions = realm.subscriptions
50 | subscriptions.update {
51 | if let currentSubscription = subscriptions.first(named: "all_chatsters") {
52 | currentSubscription.updateQuery(toType: Chatster.self) { chatster in
53 | chatster.userName != ""
54 | }
55 |
56 | } else {
57 | subscriptions.append(QuerySubscription(name: "all_chatsters") { chatster in
58 | chatster.userName != ""
59 | })
60 | }
61 | }
62 | }
63 | }
64 |
65 | struct AuthorView_Previews: PreviewProvider {
66 | static var previews: some View {
67 | Realm.bootstrap()
68 |
69 | return AppearancePreviews(AuthorView(userName: "rod@contoso.com"))
70 | .previewLayout(.sizeThatFits)
71 | .padding()
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/LoggedInView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LoggedInView.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 05/01/2022.
6 | //
7 |
8 | import SwiftUI
9 | import RealmSwift
10 |
11 | struct LoggedInView: View {
12 | @EnvironmentObject var state: AppState
13 | @Environment(\.realm) var realm
14 |
15 | @ObservedResults(User.self) var users
16 | @Binding var userID: String?
17 |
18 | @State private var showingProfileView = false
19 |
20 | var body: some View {
21 | ZStack {
22 | if let user = users.first {
23 | VStack {
24 | Text("Found \(users.count) users")
25 | Text("User = \(user.userName)")
26 | }
27 | if showingProfileView {
28 | SetUserProfileView(user: user, isPresented: $showingProfileView, userID: $userID)
29 | } else {
30 | ConversationListView(user: user)
31 | .navigationBarItems(
32 | trailing: state.loggedIn && !state.shouldIndicateActivity ? UserAvatarView(
33 | photo: user.userPreferences?.avatarImage,
34 | online: true) { showingProfileView.toggle() } : nil
35 | )
36 | }
37 | }
38 | }
39 | .navigationBarTitle("Chats", displayMode: .inline)
40 | .onAppear(perform: setSubscription)
41 | }
42 |
43 | private func setSubscription() {
44 | let subscriptions = realm.subscriptions
45 | subscriptions.update {
46 | if let currentSubscription = subscriptions.first(named: "user_id") {
47 | print("Replacing subscription for user_id")
48 | currentSubscription.updateQuery(toType: User.self) { user in
49 | user._id == userID!
50 | }
51 | } else {
52 | print("Appending subscription for user_id")
53 | subscriptions.append(QuerySubscription(name: "user_id") { user in
54 | user._id == userID!
55 | })
56 | }
57 | }
58 | }
59 | }
60 |
61 | struct LoggedInView_Previews: PreviewProvider {
62 | static var previews: some View {
63 | LoggedInView(userID: .constant("Andrew"))
64 | .environmentObject(AppState.sample)
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/RChat-Realm/RChat/functions/resetFunc.js:
--------------------------------------------------------------------------------
1 |
2 | /*
3 | This function will be run when the client SDK 'callResetPasswordFunction' and is called with an object parameter
4 | which contains four keys: 'token', 'tokenId', 'username', and 'password', and additional parameters
5 | for each parameter passed in as part of the argument list from the SDK.
6 |
7 | The return object must contain a 'status' key which can be empty or one of three string values:
8 | 'success', 'pending', or 'fail'
9 |
10 | 'success': the user's password is set to the passed in 'password' parameter.
11 |
12 | 'pending': the user's password is not reset and the UserPasswordAuthProviderClient 'resetPassword' function would
13 | need to be called with the token, tokenId, and new password via an SDK. (see below)
14 |
15 | const Realm = require("realm");
16 | const appConfig = {
17 | id: "my-app-id",
18 | timeout: 1000,
19 | app: {
20 | name: "my-app-name",
21 | version: "1"
22 | }
23 | };
24 | let app = new Realm.App(appConfig);
25 | let client = app.auth.emailPassword;
26 | await client.resetPassword(token, tokenId, newPassword);
27 |
28 | 'fail': the user's password is not reset and will not be able to log in with that password.
29 |
30 | If an error is thrown within the function the result is the same as 'fail'.
31 |
32 | Example below:
33 |
34 | exports = ({ token, tokenId, username, password }, sendEmail, securityQuestionAnswer) => {
35 | // process the reset token, tokenId, username and password
36 | if (sendEmail) {
37 | context.functions.execute('sendResetPasswordEmail', username, token, tokenId);
38 | // will wait for SDK resetPassword to be called with the token and tokenId
39 | return { status: 'pending' };
40 | } else if (context.functions.execute('validateSecurityQuestionAnswer', username, securityQuestionAnswer)) {
41 | // will set the users password to the password parameter
42 | return { status: 'success' };
43 | }
44 |
45 | // will not reset the password
46 | return { status: 'fail' };
47 | };
48 |
49 | The uncommented function below is just a placeholder and will result in failure.
50 | */
51 |
52 | exports = ({ token, tokenId, username, password }) => {
53 | // will not reset the password
54 | return { status: 'fail' };
55 | };
56 |
--------------------------------------------------------------------------------
/RChat-iOS/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## User settings
6 | xcuserdata/
7 |
8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
9 | *.xcscmblueprint
10 | *.xccheckout
11 |
12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
13 | build/
14 | DerivedData/
15 | *.moved-aside
16 | *.pbxuser
17 | !default.pbxuser
18 | *.mode1v3
19 | !default.mode1v3
20 | *.mode2v3
21 | !default.mode2v3
22 | *.perspectivev3
23 | !default.perspectivev3
24 |
25 | ## Obj-C/Swift specific
26 | *.hmap
27 |
28 | ## App packaging
29 | *.ipa
30 | *.dSYM.zip
31 | *.dSYM
32 |
33 | ## Playgrounds
34 | timeline.xctimeline
35 | playground.xcworkspace
36 |
37 | # Swift Package Manager
38 | #
39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
40 | # Packages/
41 | # Package.pins
42 | # Package.resolved
43 | # *.xcodeproj
44 | #
45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
46 | # hence it is not needed unless you have added a package configuration file to your project
47 | # .swiftpm
48 |
49 | .build/
50 |
51 | # CocoaPods
52 | #
53 | # We recommend against adding the Pods directory to your .gitignore. However
54 | # you should judge for yourself, the pros and cons are mentioned at:
55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
56 | #
57 | Pods/
58 | #
59 | # Add this line if you want to avoid checking in source code from the Xcode workspace
60 | # *.xcworkspace
61 |
62 | # Carthage
63 | #
64 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
65 | # Carthage/Checkouts
66 |
67 | Carthage/Build/
68 |
69 | # Accio dependency management
70 | Dependencies/
71 | .accio/
72 |
73 | # fastlane
74 | #
75 | # It is recommended to not store the screenshots in the git repo.
76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
77 | # For more information about the recommended setup visit:
78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
79 |
80 | fastlane/report.xml
81 | fastlane/Preview.html
82 | fastlane/screenshots/**/*.png
83 | fastlane/test_output
84 |
85 | # Code Injection
86 | #
87 | # After new code Injection tools there's a generated folder /iOSInjectionProject
88 | # https://github.com/johnno1962/injectionforxcode
89 |
90 | iOSInjectionProject/
91 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #  RChat – A Chat app built with SwiftUI and Realm
2 |
3 | RChat is a chat application. Members of a chat room share messages, photos, location, and presence information with each other. The initial version is an iOS (Swift & SwiftUI) app, but we will use the same data model and backend Realm application to build an Android version in the future.
4 |
5 | Read about the [Realm data architecture here](https://developer.mongodb.com/how-to/realm-swiftui-ios-chat-app) and [how the app was built here](https://developer.mongodb.com/how-to/building-a-mobile-chat-app-using-realm-new-way/).
6 |
7 | 
8 |
9 | ## Building and running the app
10 |
11 | 1. If you don't already have one, [create a MongoDB Atlas Cluster](https://cloud.mongodb.com/), keeping the default name of `Cluster0`.
12 | 1. Install the [Realm CLI](https://docs.mongodb.com/realm/deploy/realm-cli-reference) and [create an API key pair](https://docs.atlas.mongodb.com/configure-api-access#programmatic-api-keys).
13 | 1. Download the repo and install the Realm app:
14 | ```
15 | git clone https://github.com/ClusterDB/RChat.git
16 | cd RChat/RChat-Realm/RChat
17 | realm-cli login --api-key --private-api-key
18 | realm-cli import # Then answer prompts, naming the app RChat
19 | ```
20 | 4. From the Atlas UI, click on the Realm logo and you will see the RChat app. Open it and copy the App Id
21 |
22 | 
23 |
24 | 5. (Optional) Use `mongoimport` to import the empty database from the `dump` folder to create database indexes
25 | 1. Open the iOS project
26 | ```
27 | cd ../../RChat-iOS
28 | open RChat.xcodeproj
29 | ```
30 | 7. Update `RChatApp.swift` with your Realm App Id and then build
31 |
32 | > The `new-schema` branch contains all of the iOS and backend Realm app code needed to add a new feature to tag chat message as high priority. This includes schema and code changes. You can find all of the steps to safely make such a schema change in a production app in [Migrating Your iOS App's Synced Realm Schema in Production](https://www.mongodb.com/developer/how-to/realm-sync-migration/).
33 |
34 | > The `V2-schema` branch contains all of the iOS and backend Realm app code needed to make the `ChatMessage.author` field non-optional. You can find all of the steps to safely make such a schema change in a production app in [Migrating Your iOS App's Synced Realm Schema in Production](https://www.mongodb.com/developer/how-to/realm-sync-migration/).
35 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## Mac
6 | .DS_Store
7 |
8 | ## User settings
9 | xcuserdata/
10 |
11 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
12 | *.xcscmblueprint
13 | *.xccheckout
14 |
15 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
16 | build/
17 | DerivedData/
18 | *.moved-aside
19 | *.pbxuser
20 | !default.pbxuser
21 | *.mode1v3
22 | !default.mode1v3
23 | *.mode2v3
24 | !default.mode2v3
25 | *.perspectivev3
26 | !default.perspectivev3
27 |
28 | ## Obj-C/Swift specific
29 | *.hmap
30 |
31 | ## App packaging
32 | *.ipa
33 | *.dSYM.zip
34 | *.dSYM
35 |
36 | ## Playgrounds
37 | timeline.xctimeline
38 | playground.xcworkspace
39 |
40 | # Swift Package Manager
41 | #
42 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
43 | # Packages/
44 | # Package.pins
45 | # Package.resolved
46 | # *.xcodeproj
47 | #
48 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
49 | # hence it is not needed unless you have added a package configuration file to your project
50 | # .swiftpm
51 |
52 | .build/
53 |
54 | # CocoaPods
55 | #
56 | # We recommend against adding the Pods directory to your .gitignore. However
57 | # you should judge for yourself, the pros and cons are mentioned at:
58 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
59 | #
60 | Pods/
61 | #
62 | # Add this line if you want to avoid checking in source code from the Xcode workspace
63 | # *.xcworkspace
64 |
65 | # Carthage
66 | #
67 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
68 | # Carthage/Checkouts
69 |
70 | Carthage/Build/
71 |
72 | # Accio dependency management
73 | Dependencies/
74 | .accio/
75 |
76 | # fastlane
77 | #
78 | # It is recommended to not store the screenshots in the git repo.
79 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
80 | # For more information about the recommended setup visit:
81 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
82 |
83 | fastlane/report.xml
84 | fastlane/Preview.html
85 | fastlane/screenshots/**/*.png
86 | fastlane/test_output
87 |
88 | # Code Injection
89 | #
90 | # After new code Injection tools there's a generated folder /iOSInjectionProject
91 | # https://github.com/johnno1962/injectionforxcode
92 |
93 | iOSInjectionProject/
94 |
--------------------------------------------------------------------------------
/RChat-Realm/RChat/functions/userDocWrittenTo.js:
--------------------------------------------------------------------------------
1 | exports = async function(changeEvent) {
2 | const dbName = context.values.get("dbName");
3 | const db = context.services.get("mongodb-atlas").db(dbName);
4 | const chatster = db.collection("Chatster");
5 | const userCollection = db.collection("User");
6 | let eventCollection = context.services.get("mongodb-atlas").db("RChat").collection("Event");
7 | const docId = changeEvent.documentKey._id;
8 | const user = changeEvent.fullDocument;
9 | let conversationsChanged = false;
10 |
11 | console.log(`Mirroring user for docId=${docId}. operationType = ${changeEvent.operationType}`);
12 | switch (changeEvent.operationType) {
13 | case "insert":
14 | case "replace":
15 | case "update":
16 | console.log(`Writing data for ${user.userName}`);
17 | let chatsterDoc = {
18 | _id: user._id,
19 | userName: user.userName,
20 | lastSeenAt: user.lastSeenAt,
21 | presence: user.presence
22 | };
23 | if (user.userPreferences) {
24 | const prefs = user.userPreferences;
25 | chatsterDoc.displayName = prefs.displayName;
26 | if (prefs.avatarImage && prefs.avatarImage._id) {
27 | console.log(`Copying avatarImage`);
28 | chatsterDoc.avatarImage = prefs.avatarImage;
29 | console.log(`id of avatarImage = ${prefs.avatarImage._id}`);
30 | }
31 | }
32 | console.log('About to replaceOne Chatster doc');
33 | await chatster.replaceOne({ _id: user._id }, chatsterDoc, { upsert: true });
34 | if (user.conversations && user.conversations.length > 0) {
35 | for (i = 0; i < user.conversations.length; i++) {
36 | let membersToAdd = [];
37 | if (user.conversations[i].members.length > 0) {
38 | for (j = 0; j < user.conversations[i].members.length; j++) {
39 | if (user.conversations[i].members[j].membershipStatus == "User added, but invite pending") {
40 | membersToAdd.push(user.conversations[i].members[j].userName);
41 | user.conversations[i].members[j].membershipStatus = "Membership active";
42 | conversationsChanged = true;
43 | }
44 | }
45 | }
46 | if (membersToAdd.length > 0) {
47 | await userCollection.updateMany({userName: {$in: membersToAdd}}, {$push: {conversations: user.conversations[i]}});
48 | }
49 | }
50 | }
51 | if (conversationsChanged) {
52 | userCollection.updateOne({_id: user._id}, {$set: {conversations: user.conversations}});
53 | }
54 | break;
55 | case "delete":
56 | await chatster.deleteOne({_id: docId});
57 | break;
58 | }
59 | };
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/Components/Maps/MapThumbnailWithDelete.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MapThumbnailWithDelete.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 09/12/2020.
6 | //
7 |
8 | import MapKit
9 | import SwiftUI
10 |
11 | struct MapThumbnailWithDelete: View {
12 | let location: [Double]
13 | var action: (() -> Void)?
14 |
15 | @State private var region: MKCoordinateRegion = MKCoordinateRegion(
16 | center: CLLocationCoordinate2D(latitude: 51.507222, longitude: -0.1275),
17 | span: MKCoordinateSpan(latitudeDelta: 2.0, longitudeDelta: 2.0))
18 | @State private var annotationItems = [MyAnnotationItem]()
19 |
20 | private enum Dimensions {
21 | static let frameSize: CGFloat = 100
22 | static let imageSize: CGFloat = 70
23 | static let buttonSize: CGFloat = 30
24 | static let radius: CGFloat = 8
25 | static let buttonPadding: CGFloat = 4
26 | }
27 |
28 | var body: some View {
29 | ZStack {
30 | Map(coordinateRegion: $region, annotationItems: annotationItems) { item in
31 | MapMarker(coordinate: item.coordinate)
32 | }
33 | .frame(width: Dimensions.imageSize, height: Dimensions.imageSize, alignment: .center)
34 | .clipShape(RoundedRectangle(cornerRadius: Dimensions.radius))
35 | if let action = action {
36 | VStack {
37 | HStack {
38 | Spacer()
39 | DeleteButton(action: action, padding: Dimensions.buttonPadding)
40 | .frame(width: Dimensions.buttonSize, height: Dimensions.buttonSize, alignment: .center)
41 | }
42 | Spacer()
43 | }
44 | .onAppear(perform: setupLocation)
45 | .frame(width: Dimensions.frameSize, height: Dimensions.frameSize)
46 | }
47 | }
48 | }
49 |
50 | func setupLocation() {
51 | let position = CLLocationCoordinate2D(latitude: location[1], longitude: location[0])
52 | region = MKCoordinateRegion(
53 | center: position,
54 | span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1))
55 | annotationItems.append(MyAnnotationItem(coordinate: position))
56 | }
57 | }
58 |
59 | struct MapThumbnailWithDelete_Previews: PreviewProvider {
60 | static var previews: some View {
61 | AppearancePreviews(
62 | Group {
63 | MapThumbnailWithDelete(location: [-0.10689139236939127, 51.506520923981554], action: {})
64 | MapThumbnailWithDelete(location: [-0.10689139236939127, 51.506520923981554])
65 | }
66 | )
67 | .previewLayout(.sizeThatFits)
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/Chat Messages/ChatBubbleView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ChatBubbleView.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 04/12/2020.
6 | //
7 |
8 | import SwiftUI
9 | import RealmSwift
10 |
11 | struct ChatBubbleView: View {
12 | @ObservedRealmObject var chatMessage: ChatMessage
13 | let authorName: String?
14 | var isPreview = false
15 |
16 | private var isMyMessage: Bool { authorName == nil }
17 |
18 | private enum Dimensions {
19 | static let padding: CGFloat = 4
20 | static let horizontalOffset: CGFloat = 100
21 | static let cornerRadius: CGFloat = 15
22 | }
23 |
24 | var body: some View {
25 | HStack {
26 | if isMyMessage { Spacer().frame(width: Dimensions.horizontalOffset) }
27 | VStack {
28 | HStack {
29 | if let authorName = authorName {
30 | AuthorView(userName: authorName)
31 | }
32 | Spacer()
33 | TextDate(date: chatMessage.timestamp)
34 | .font(.caption)
35 | }
36 | HStack {
37 | if let photo = chatMessage.image {
38 | ThumbnailWithExpand(photo: photo)
39 | .padding(Dimensions.padding)
40 | }
41 | let location = chatMessage.location
42 | if location.count == 2 {
43 | MapThumbnailWithExpand(location: location.map { $0 })
44 | .padding(Dimensions.padding)
45 | }
46 | if chatMessage.text != "" {
47 | MarkDown(text: chatMessage.text)
48 | .padding(Dimensions.padding)
49 | }
50 | Spacer()
51 | }
52 | }
53 | .padding(Dimensions.padding)
54 | .background(Color(isMyMessage ? "MyBubble" : "OtherBubble"))
55 | .clipShape(RoundedRectangle(cornerRadius: Dimensions.cornerRadius))
56 | if !isMyMessage { Spacer().frame(width: Dimensions.horizontalOffset) }
57 | }
58 | }
59 | }
60 |
61 | struct ChatBubbleView_Previews: PreviewProvider {
62 | static var previews: some View {
63 | Realm.bootstrap()
64 |
65 | return Group {
66 | ChatBubbleView(chatMessage: .sample, authorName: "jane@contoso.com", isPreview: true)
67 | ChatBubbleView(chatMessage: .sample2, authorName: "freddy@contoso.com", isPreview: true)
68 | ChatBubbleView(chatMessage: .sample3, authorName: nil, isPreview: true)
69 | ChatBubbleView(chatMessage: .sample33, authorName: "jane@contoso.com", isPreview: true)
70 | }
71 | .padding()
72 | .previewLayout(.sizeThatFits)
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/Conversations/ConversationCardContentsView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ConversationCardContentsView.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 26/11/2020.
6 | //
7 |
8 | import SwiftUI
9 | import RealmSwift
10 |
11 | struct ConversationCardContentsView: View {
12 | @EnvironmentObject var state: AppState
13 | @ObservedResults(Chatster.self) var chatsters
14 | @Environment(\.realm) var realm
15 |
16 | let conversation: Conversation
17 |
18 | private struct Dimensions {
19 | static let mugWidth: CGFloat = 110
20 | static let cornerRadius: CGFloat = 5
21 | static let lineWidth: CGFloat = 1
22 | static let padding: CGFloat = 5
23 | }
24 |
25 | var chatMembers: [Chatster] {
26 | var chatsterList = [Chatster]()
27 | for member in conversation.members {
28 | chatsterList.append(contentsOf: chatsters.filter("userName = %@", member.userName))
29 | }
30 | return chatsterList
31 | }
32 |
33 | var body: some View {
34 | HStack {
35 | MugShotGridView(members: chatMembers)
36 | .frame(width: Dimensions.mugWidth)
37 | .padding(.trailing)
38 | VStack(alignment: .leading) {
39 | Text(conversation.displayName)
40 | .fontWeight(conversation.unreadCount > 0 ? .bold : .regular)
41 | CaptionLabel(title: conversation.unreadCount == 0 ? "" :
42 | "\(conversation.unreadCount) new \(conversation.unreadCount == 1 ? "message" : "messages")")
43 | }
44 | Spacer()
45 | }
46 | .padding(Dimensions.padding)
47 | .overlay(
48 | RoundedRectangle(cornerRadius: Dimensions.cornerRadius)
49 | .stroke(Color.gray, lineWidth: Dimensions.lineWidth)
50 | )
51 | .onAppear(perform: setSubscription)
52 | }
53 |
54 | private func setSubscription() {
55 | let subscriptions = realm.subscriptions
56 | subscriptions.update {
57 | if let currentSubscription = subscriptions.first(named: "all_chatsters") {
58 | currentSubscription.updateQuery(toType: Chatster.self) { chatster in
59 | chatster.userName != ""
60 | }
61 |
62 | } else {
63 | subscriptions.append(QuerySubscription(name: "all_chatsters") { chatster in
64 | chatster.userName != ""
65 | })
66 | }
67 | }
68 | }
69 | }
70 |
71 | struct ConversationCardContentsView_Previews: PreviewProvider {
72 | static var previews: some View {
73 | Realm.bootstrap()
74 |
75 | return AppearancePreviews(
76 | ForEach(Conversation.samples) { conversation in
77 | ConversationCardContentsView(conversation: conversation)
78 | }
79 | )
80 | .previewLayout(.sizeThatFits)
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "RChat Icon - 40-2.png",
5 | "idiom" : "iphone",
6 | "scale" : "2x",
7 | "size" : "20x20"
8 | },
9 | {
10 | "filename" : "RChat Icon - 60.png",
11 | "idiom" : "iphone",
12 | "scale" : "3x",
13 | "size" : "20x20"
14 | },
15 | {
16 | "filename" : "RChat Icon - 58-1.png",
17 | "idiom" : "iphone",
18 | "scale" : "2x",
19 | "size" : "29x29"
20 | },
21 | {
22 | "filename" : "RChat Icon - 87.png",
23 | "idiom" : "iphone",
24 | "scale" : "3x",
25 | "size" : "29x29"
26 | },
27 | {
28 | "filename" : "RChat Icon - 80-1.png",
29 | "idiom" : "iphone",
30 | "scale" : "2x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "filename" : "RChat Icon - 120-1.png",
35 | "idiom" : "iphone",
36 | "scale" : "3x",
37 | "size" : "40x40"
38 | },
39 | {
40 | "filename" : "RChat Icon - 120.png",
41 | "idiom" : "iphone",
42 | "scale" : "2x",
43 | "size" : "60x60"
44 | },
45 | {
46 | "filename" : "RChat Icon - 180.png",
47 | "idiom" : "iphone",
48 | "scale" : "3x",
49 | "size" : "60x60"
50 | },
51 | {
52 | "filename" : "RChat Icon - 20.png",
53 | "idiom" : "ipad",
54 | "scale" : "1x",
55 | "size" : "20x20"
56 | },
57 | {
58 | "filename" : "RChat Icon - 40-1.png",
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "20x20"
62 | },
63 | {
64 | "filename" : "RChat Icon - 29.png",
65 | "idiom" : "ipad",
66 | "scale" : "1x",
67 | "size" : "29x29"
68 | },
69 | {
70 | "filename" : "RChat Icon - 58.png",
71 | "idiom" : "ipad",
72 | "scale" : "2x",
73 | "size" : "29x29"
74 | },
75 | {
76 | "filename" : "RChat Icon - 40.png",
77 | "idiom" : "ipad",
78 | "scale" : "1x",
79 | "size" : "40x40"
80 | },
81 | {
82 | "filename" : "RChat Icon - 80.png",
83 | "idiom" : "ipad",
84 | "scale" : "2x",
85 | "size" : "40x40"
86 | },
87 | {
88 | "filename" : "RChat Icon - 76.png",
89 | "idiom" : "ipad",
90 | "scale" : "1x",
91 | "size" : "76x76"
92 | },
93 | {
94 | "filename" : "RChat Icon - 152.png",
95 | "idiom" : "ipad",
96 | "scale" : "2x",
97 | "size" : "76x76"
98 | },
99 | {
100 | "filename" : "RChat Icon - 167.png",
101 | "idiom" : "ipad",
102 | "scale" : "2x",
103 | "size" : "83.5x83.5"
104 | },
105 | {
106 | "filename" : "RChat Icon - 1024.png",
107 | "idiom" : "ios-marketing",
108 | "scale" : "1x",
109 | "size" : "1024x1024"
110 | }
111 | ],
112 | "info" : {
113 | "author" : "xcode",
114 | "version" : 1
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/User Accounts & Profile/LoginView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LoginView.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 23/11/2020.
6 | //
7 |
8 | import SwiftUI
9 | import RealmSwift
10 |
11 | struct LoginView: View {
12 | @EnvironmentObject var state: AppState
13 |
14 | @Binding var userID: String?
15 |
16 | enum Field: Hashable {
17 | case username
18 | case password
19 | }
20 |
21 | @State private var email = ""
22 | @State private var password = ""
23 | @State private var newUser = false
24 |
25 | @FocusState private var focussedField: Field?
26 |
27 | var body: some View {
28 | ZStack {
29 | VStack(spacing: 16) {
30 | Spacer()
31 | TextField("username", text: $email)
32 | .focused($focussedField, equals: .username)
33 | .submitLabel(.next)
34 | .onSubmit { focussedField = .password }
35 | SecureField("password", text: $password)
36 | .focused($focussedField, equals: .password)
37 | .onSubmit(userAction)
38 | .submitLabel(.go)
39 | Button(action: { newUser.toggle() }) {
40 | HStack {
41 | Image(systemName: newUser ? "checkmark.square" : "square")
42 | Text("Register new user")
43 | Spacer()
44 | }
45 | }
46 | Button(action: userAction) {
47 | Text(newUser ? "Register new user" : "Log in")
48 | }
49 | .buttonStyle(.borderedProminent)
50 | .controlSize(.large)
51 | Spacer()
52 | }
53 | }
54 | .onAppear {
55 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
56 | focussedField = .username
57 | }
58 | }
59 | .padding()
60 | }
61 |
62 | func userAction() {
63 | state.error = nil
64 | state.shouldIndicateActivity = true
65 | Task {
66 | if newUser {
67 | do {
68 | try await app.emailPasswordAuth.registerUser(email: email, password: password)
69 | } catch {
70 | state.error = error.localizedDescription
71 | state.shouldIndicateActivity = false
72 | }
73 | }
74 | do {
75 | let user = try await app.login(credentials: .emailPassword(email: email, password: password))
76 | userID = user.id
77 | state.shouldIndicateActivity = false
78 | } catch {
79 | state.error = error.localizedDescription
80 | state.shouldIndicateActivity = false
81 | }
82 | }
83 | }
84 | }
85 |
86 | struct LoginView_Previews: PreviewProvider {
87 | static var previews: some View {
88 | PreviewColorScheme(PreviewOrientation(
89 | LoginView(userID: .constant("1234554321"))
90 | .environmentObject(AppState())
91 | ))
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/RChat-Realm/RChat/data_sources/mongodb-atlas/RChatFlex/User/schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "bsonType": "object",
3 | "properties": {
4 | "_id": {
5 | "bsonType": "string"
6 | },
7 | "conversations": {
8 | "bsonType": "array",
9 | "items": {
10 | "bsonType": "object",
11 | "properties": {
12 | "displayName": {
13 | "bsonType": "string"
14 | },
15 | "id": {
16 | "bsonType": "string"
17 | },
18 | "members": {
19 | "bsonType": "array",
20 | "items": {
21 | "bsonType": "object",
22 | "properties": {
23 | "membershipStatus": {
24 | "bsonType": "string"
25 | },
26 | "userName": {
27 | "bsonType": "string"
28 | }
29 | },
30 | "required": [
31 | "membershipStatus",
32 | "userName"
33 | ],
34 | "title": "Member"
35 | }
36 | },
37 | "unreadCount": {
38 | "bsonType": "long"
39 | }
40 | },
41 | "required": [
42 | "unreadCount",
43 | "id",
44 | "displayName"
45 | ],
46 | "title": "Conversation"
47 | }
48 | },
49 | "lastSeenAt": {
50 | "bsonType": "date"
51 | },
52 | "presence": {
53 | "bsonType": "string"
54 | },
55 | "userName": {
56 | "bsonType": "string"
57 | },
58 | "userPreferences": {
59 | "bsonType": "object",
60 | "properties": {
61 | "avatarImage": {
62 | "bsonType": "object",
63 | "properties": {
64 | "_id": {
65 | "bsonType": "string"
66 | },
67 | "date": {
68 | "bsonType": "date"
69 | },
70 | "picture": {
71 | "bsonType": "binData"
72 | },
73 | "thumbNail": {
74 | "bsonType": "binData"
75 | }
76 | },
77 | "required": [
78 | "_id",
79 | "date"
80 | ],
81 | "title": "Photo"
82 | },
83 | "displayName": {
84 | "bsonType": "string"
85 | }
86 | },
87 | "required": [],
88 | "title": "UserPreferences"
89 | }
90 | },
91 | "required": [
92 | "_id",
93 | "userName",
94 | "presence"
95 | ],
96 | "title": "User"
97 | }
98 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/User Accounts & Profile/SetUserProfileView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SetUserProfileView.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 05/01/2022.
6 | //
7 |
8 | import SwiftUI
9 | import RealmSwift
10 |
11 | struct SetUserProfileView: View {
12 | @AppStorage("shouldShareLocation") var shouldShareLocation = false
13 |
14 | @ObservedRealmObject var user: User
15 | @Binding var isPresented: Bool
16 | @Binding var userID: String?
17 |
18 | @State private var displayName = ""
19 | @State private var photo: Photo?
20 | @State private var photoAdded = false
21 |
22 | var body: some View {
23 | Form {
24 | Section(header: Text("User Profile")) {
25 | if let photo = photo {
26 | AvatarButton(photo: photo) {
27 | self.showPhotoTaker()
28 | }
29 | }
30 | if photo == nil {
31 | Button(action: { self.showPhotoTaker() }) {
32 | Text("Add Photo")
33 | }
34 | }
35 | InputField(title: "Display Name", text: $displayName)
36 | CallToActionButton(title: "Save User Profile", action: saveProfile)
37 | }
38 | Section(header: Text("Device Settings")) {
39 | Toggle(isOn: $shouldShareLocation, label: {
40 | Text("Share Location")
41 | })
42 | .onChange(of: shouldShareLocation) { value in
43 | if value {
44 | _ = LocationHelper.currentLocation
45 | }
46 | }
47 | OnlineAlertSettings()
48 | }
49 | }
50 | .onAppear(perform: initData)
51 | .navigationBarItems(
52 | leading: Button(action: { isPresented = false }) { BackButton() },
53 | trailing: LogoutButton(user: user, userID: $userID, action: { isPresented = false }))
54 | .padding()
55 | .navigationBarTitle("Edit Profile", displayMode: .inline)
56 | }
57 |
58 | private func initData() {
59 | displayName = user.userPreferences?.displayName ?? "Unknown"
60 | photo = user.userPreferences?.avatarImage
61 | }
62 |
63 | private func saveProfile() {
64 | let userPreferences = UserPreferences()
65 | userPreferences.displayName = displayName
66 | if photoAdded {
67 | guard let newPhoto = photo else {
68 | print("Missing photo")
69 | return
70 | }
71 | userPreferences.avatarImage = newPhoto
72 | } else {
73 | userPreferences.avatarImage = Photo(photo)
74 | }
75 | $user.userPreferences.wrappedValue = userPreferences
76 | $user.presenceState.wrappedValue = .onLine
77 | isPresented.toggle()
78 | }
79 |
80 | private func showPhotoTaker() {
81 | PhotoCaptureController.show(source: .camera) { controller, photo in
82 | self.photo = photo
83 | photoAdded = true
84 | controller.hide()
85 | }
86 | }
87 | }
88 |
89 | struct SetUserProfileView_Previews: PreviewProvider {
90 | static var previews: some View {
91 | SetUserProfileView(user: User(), isPresented: .constant(true), userID: .constant("Andrew"))
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat.xcodeproj/xcshareddata/xcschemes/RChat.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
43 |
49 |
50 |
51 |
52 |
53 |
63 |
65 |
71 |
72 |
73 |
74 |
80 |
82 |
88 |
89 |
90 |
91 |
93 |
94 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Helpers/PhotoCaptureController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PhotoCaptureController.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 24/11/2020.
6 | //
7 |
8 | import UIKit
9 | import SwiftUI
10 | import RealmSwift
11 |
12 | class PhotoCaptureController: UIImagePickerController {
13 |
14 | @EnvironmentObject var state: AppState
15 |
16 | private var photoTaken: ((PhotoCaptureController, Photo) -> Void)?
17 | private var photo = Photo()
18 | private let imageSizeThumbnails: CGFloat = 102
19 | private let maximumImageSize = 1024 * 1024 // 1 MB
20 |
21 | override open var supportedInterfaceOrientations: UIInterfaceOrientationMask {
22 | .portrait
23 | }
24 |
25 | override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
26 | .portrait
27 | }
28 |
29 | static func show(source: UIImagePickerController.SourceType,
30 | photoToEdit: Photo = Photo(),
31 | photoTaken: ((PhotoCaptureController, Photo) -> Void)? = nil) {
32 |
33 | let picker = PhotoCaptureController()
34 | picker.photo = photoToEdit
35 | picker.setup(source)
36 | picker.photoTaken = photoTaken
37 | picker.present()
38 | }
39 |
40 | func setup(_ requestedSource: UIImagePickerController.SourceType) {
41 | if PhotoCaptureController.isSourceTypeAvailable(.camera) && requestedSource == .camera {
42 | sourceType = .camera
43 | } else {
44 | print("No camera found - using photo library instead")
45 | sourceType = .photoLibrary
46 | }
47 | allowsEditing = true
48 | delegate = self
49 | }
50 |
51 | func present() {
52 | UIViewController.keyWindow?.rootViewController?.present(self, animated: true)
53 | }
54 |
55 | func hide() {
56 | photoTaken = nil
57 | dismiss(animated: true)
58 | }
59 |
60 | private func compressImageIfNeeded(image: UIImage) -> UIImage? {
61 | let resultImage = image
62 |
63 | if let data = resultImage.jpegData(compressionQuality: 1) {
64 | if data.count > maximumImageSize {
65 |
66 | let neededQuality = CGFloat(maximumImageSize) / CGFloat(data.count)
67 | if let resized = resultImage.jpegData(compressionQuality: neededQuality),
68 | let resultImage = UIImage(data: resized) {
69 |
70 | return resultImage
71 | } else {
72 | print("Fail to resize image")
73 | }
74 | }
75 | }
76 | return resultImage
77 | }
78 | }
79 |
80 | extension PhotoCaptureController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
81 | func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
82 | guard let editedImage = info[.editedImage] as? UIImage,
83 | let result = compressImageIfNeeded(image: editedImage) else {
84 | print("Could't get the camera/library image")
85 | return
86 | }
87 |
88 | photo.date = Date()
89 | photo.picture = result.jpegData(compressionQuality: 0.8)
90 | photo.thumbNail = result.thumbnail(size: imageSizeThumbnails)?.jpegData(compressionQuality: 0.8)
91 | photoTaken?(self, photo)
92 | }
93 |
94 | func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
95 | hide()
96 | }
97 | }
98 |
99 | extension UIViewController {
100 |
101 | static var keyWindow: UIWindow? {
102 | // Get connected scenes
103 | return UIApplication.shared.connectedScenes
104 | // Keep only active scenes, onscreen and visible to the user
105 | .filter { $0.activationState == .foregroundActive }
106 | // Keep only the first `UIWindowScene`
107 | .first(where: { $0 is UIWindowScene })
108 | // Get its associated windows
109 | .flatMap({ $0 as? UIWindowScene })?.windows
110 | // Finally, keep only the key window
111 | .first(where: \.isKeyWindow)
112 | }
113 |
114 | }
115 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 23/11/2020.
6 | //
7 |
8 | import SwiftUI
9 | import UserNotifications
10 | import RealmSwift
11 |
12 | struct ContentView: View {
13 | @EnvironmentObject var state: AppState
14 |
15 | @AppStorage("shouldRemindOnlineUser") var shouldRemindOnlineUser = false
16 | @AppStorage("onlineUserReminderHours") var onlineUserReminderHours = 8.0
17 |
18 | @State private var userID: String?
19 |
20 | var body: some View {
21 | NavigationStack {
22 | ZStack {
23 | VStack {
24 | if state.loggedIn && userID != nil {
25 | LoggedInView(userID: $userID)
26 | .environment(\.realmConfiguration,
27 | app.currentUser!.flexibleSyncConfiguration())
28 | } else {
29 | LoginView(userID: $userID)
30 | }
31 | Spacer()
32 | if let error = state.error {
33 | Text("Error: \(error)")
34 | .foregroundColor(Color.red)
35 | }
36 | }
37 | if state.busyCount > 0 {
38 | OpaqueProgressView("Working With Realm")
39 | }
40 | }
41 | }
42 | .currentDeviceNavigationViewStyle(alwaysStacked: !state.loggedIn)
43 | .onReceive(NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification)) { _ in
44 | if shouldRemindOnlineUser {
45 | addNotification(timeInHours: Int(onlineUserReminderHours))
46 | }
47 | }
48 | .onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
49 | clearNotifications()
50 | }
51 | }
52 |
53 | func addNotification(timeInHours: Int) {
54 | let center = UNUserNotificationCenter.current()
55 |
56 | let addRequest = {
57 | let content = UNMutableNotificationContent()
58 | content.title = "Still logged in"
59 | content.subtitle = "You've been offline in the background for " +
60 | "\(onlineUserReminderHours) \(onlineUserReminderHours == 1 ? "hour" : "hours")"
61 | content.sound = UNNotificationSound.default
62 |
63 | let trigger = UNTimeIntervalNotificationTrigger(
64 | timeInterval: onlineUserReminderHours * 3600,
65 | repeats: false)
66 | let request = UNNotificationRequest(identifier: UUID().uuidString,
67 | content: content,
68 | trigger: trigger)
69 | center.add(request)
70 | }
71 |
72 | center.getNotificationSettings { settings in
73 | if settings.authorizationStatus == .authorized {
74 | addRequest()
75 | } else {
76 | center.requestAuthorization(options: [.alert, .badge, .sound]) { success, _ in
77 | if success {
78 | addRequest()
79 | }
80 | }
81 | }
82 | }
83 | }
84 |
85 | func clearNotifications() {
86 | let center = UNUserNotificationCenter.current()
87 | center.removeAllDeliveredNotifications()
88 | center.removeAllPendingNotificationRequests()
89 | }
90 | }
91 |
92 | extension View {
93 | public func currentDeviceNavigationViewStyle(alwaysStacked: Bool) -> AnyView {
94 | if UIDevice.current.userInterfaceIdiom == .pad && !alwaysStacked {
95 | return AnyView(self.navigationViewStyle(DefaultNavigationViewStyle()))
96 | } else {
97 | return AnyView(self.navigationViewStyle(StackNavigationViewStyle()))
98 | }
99 | }
100 | }
101 |
102 | struct ContentView_Previews: PreviewProvider {
103 | static var previews: some View {
104 | AppearancePreviews(
105 | Group {
106 | ContentView()
107 | .environmentObject(AppState())
108 | Landscape(ContentView()
109 | .environmentObject(AppState()))
110 | }
111 | )
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/Chat Messages/ChatRoomBubblesView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ChatRoomBubblesView.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 02/02/2021.
6 | //
7 |
8 | import SwiftUI
9 | import RealmSwift
10 |
11 | struct ChatRoomBubblesView: View {
12 | @EnvironmentObject var state: AppState
13 | @ObservedResults(ChatMessage.self, sortDescriptor: SortDescriptor(keyPath: "timestamp", ascending: true)) var chats
14 | @Environment(\.realm) var realm
15 |
16 | @ObservedRealmObject var user: User
17 | var conversation: Conversation?
18 | var isPreview = false
19 |
20 | @State private var realmChatsNotificationToken: NotificationToken?
21 | @State private var latestChatId = ""
22 |
23 | @State private var counter = 0
24 |
25 | private enum Dimensions {
26 | static let padding: CGFloat = 8
27 | }
28 |
29 | var body: some View {
30 | VStack {
31 | ScrollView(.vertical) {
32 | ScrollViewReader { (proxy: ScrollViewProxy) in
33 | VStack {
34 | ForEach(chats) { chatMessage in
35 | ChatBubbleView(chatMessage: chatMessage,
36 | authorName: chatMessage.author != user.userName ? chatMessage.author : nil,
37 | isPreview: isPreview)
38 | .id(chatMessage._id)
39 | }
40 | }
41 | .onAppear {
42 | scrollToBottom()
43 | }
44 | .onChange(of: latestChatId) { target in
45 | withAnimation {
46 | proxy.scrollTo(target, anchor: .bottom)
47 | }
48 | }
49 | }
50 | }
51 | Spacer()
52 | ChatInputBox(user: user, send: sendMessage, focusAction: scrollToBottom)
53 | }
54 | .navigationBarTitle(conversation?.displayName ?? "Chat", displayMode: .inline)
55 | .padding(.horizontal, Dimensions.padding)
56 | .onAppear { loadChatRoom() }
57 | .onDisappear { closeChatRoom() }
58 | }
59 |
60 | private func loadChatRoom() {
61 | scrollToBottom()
62 | setSubscription()
63 | realmChatsNotificationToken = chats.thaw()?.observe { _ in
64 | scrollToBottom()
65 | }
66 | }
67 |
68 | private func closeChatRoom() {
69 | clearSunscription()
70 | if let token = realmChatsNotificationToken {
71 | token.invalidate()
72 | }
73 | }
74 |
75 | private func sendMessage(chatMessage: ChatMessage) {
76 | guard let conversation = conversation else {
77 | print("comversation not set")
78 | return
79 | }
80 | chatMessage.conversationID = conversation.id
81 | $chats.append(chatMessage)
82 | }
83 |
84 | private func scrollToBottom() {
85 | latestChatId = chats.last?._id ?? ""
86 | }
87 |
88 | private func setSubscription() {
89 | let subscriptions = realm.subscriptions
90 | subscriptions.update {
91 | if let conversation = conversation {
92 | if let currentSubscription = subscriptions.first(named: "conversation") {
93 | currentSubscription.updateQuery(toType: ChatMessage.self) { chatMessage in
94 | chatMessage.conversationID == conversation.id
95 | }
96 | } else {
97 | subscriptions.append(QuerySubscription(name: "conversation") { chatMessage in
98 | chatMessage.conversationID == conversation.id
99 | })
100 | }
101 | }
102 | }
103 | }
104 |
105 | private func clearSunscription() {
106 | print("Leaving room, clearing subscription")
107 | let subscriptions = realm.subscriptions
108 | subscriptions.update {
109 | subscriptions.remove(named: "conversation")
110 | }
111 | }
112 | }
113 |
114 | struct ChatRoomBubblesView_Previews: PreviewProvider {
115 | static var previews: some View {
116 | Realm.bootstrap()
117 |
118 | return AppearancePreviews(ChatRoomBubblesView(user: .sample, isPreview: true))
119 | .environmentObject(AppState.sample)
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/Chat Messages/ChatInputBox.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ChatInputBox.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 02/12/2020.
6 | //
7 |
8 | import SwiftUI
9 | import RealmSwift
10 | import AuthenticationServices
11 |
12 | struct ChatInputBox: View {
13 | @AppStorage("shouldShareLocation") var shouldShareLocation = false
14 |
15 | @ObservedRealmObject var user: User
16 | var send: (_: ChatMessage) -> Void = { _ in }
17 | var focusAction: () -> Void = {}
18 |
19 | @FocusState var isTextFocussed: Bool
20 |
21 | private enum Dimensions {
22 | static let maxHeight: CGFloat = 100
23 | static let minHeight: CGFloat = 100
24 | static let radius: CGFloat = 10
25 | static let imageSize: CGFloat = 70
26 | static let padding: CGFloat = 15
27 | static let toolStripHeight: CGFloat = 35
28 | }
29 |
30 | @State var photo: Photo?
31 | @State var chatText = ""
32 | @State var location = [Double]()
33 |
34 | private var isEmpty: Bool { photo == nil && location == [] && chatText == "" }
35 |
36 | var body: some View {
37 | VStack {
38 | HStack {
39 | if let photo = photo {
40 | ThumbnailWithDelete(photo: photo, action: deletePhoto)
41 | }
42 | if location.count == 2 {
43 | MapThumbnailWithDelete(location: location, action: deleteMap)
44 | }
45 | TextEditor(text: $chatText)
46 | .onTapGesture(perform: focusAction)
47 | .focused($isTextFocussed)
48 | .padding(Dimensions.padding)
49 | .frame(minWidth: 0, maxWidth: .infinity, minHeight: Dimensions.minHeight, maxHeight: Dimensions.maxHeight)
50 | .scrollContentBackground(.hidden)
51 | .background(Color("GreenBackground"))
52 | .clipShape(RoundedRectangle(cornerRadius: Dimensions.radius))
53 | }
54 | HStack {
55 | Spacer()
56 | LocationButton(action: addLocation, active: shouldShareLocation && location.count == 0)
57 | AttachButton(action: addAttachment, active: photo == nil)
58 | CameraButton(action: takePhoto, active: photo == nil)
59 | SendButton(action: sendChat, active: !isEmpty)
60 | }
61 | .frame(height: Dimensions.toolStripHeight)
62 | }
63 | .padding(Dimensions.padding)
64 | .onAppear(perform: onAppear)
65 | }
66 |
67 | private func onAppear() {
68 | DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
69 | isTextFocussed = true
70 | }
71 | }
72 |
73 | private func addLocation() {
74 | let location = LocationHelper.currentLocation
75 | self.location = [location.longitude, location.latitude]
76 | }
77 |
78 | private func takePhoto() {
79 | PhotoCaptureController.show(source: .camera) { controller, photo in
80 | self.photo = photo
81 | controller.hide()
82 | }
83 | }
84 |
85 | private func addAttachment() {
86 | PhotoCaptureController.show(source: .photoLibrary) { controller, photo in
87 | self.photo = photo
88 | controller.hide()
89 | }
90 | }
91 |
92 | private func deletePhoto() {
93 | photo = nil
94 | }
95 |
96 | private func deleteMap() {
97 | location = []
98 | }
99 |
100 | private func sendChat() {
101 | sendMessage(text: chatText, photo: photo, location: location)
102 | photo = nil
103 | chatText = ""
104 | location = []
105 | isTextFocussed = true
106 | }
107 |
108 | private func sendMessage(text: String, photo: Photo?, location: [Double]) {
109 | let chatMessage = ChatMessage(
110 | author: user.userName,
111 | authorID: user._id,
112 | text: text,
113 | image: photo,
114 | location: location)
115 | send(chatMessage)
116 | }
117 | }
118 |
119 | struct ChatInputBox_Previews: PreviewProvider {
120 | static var previews: some View {
121 | AppearancePreviews(
122 | Group {
123 | ChatInputBox(user: .sample)
124 | ChatInputBox(user: .sample, photo: .sample, location: [])
125 | ChatInputBox(user: .sample, photo: .sample, location: [-0.10689139236939127, 51.506520923981554])
126 | }
127 | )
128 | .previewLayout(.sizeThatFits)
129 | .padding()
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Views/Conversations/NewConversationView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NewConversationView.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 27/11/2020.
6 | //
7 |
8 | import SwiftUI
9 | import RealmSwift
10 |
11 | struct NewConversationView: View {
12 | @EnvironmentObject var state: AppState
13 | @Environment(\.presentationMode) var presentationMode
14 | @ObservedResults(Chatster.self) var chatsters
15 | @Environment(\.realm) var realm
16 |
17 | @ObservedRealmObject var user: User
18 |
19 | var isPreview = false
20 |
21 | @State private var name = ""
22 | @State private var members = [String]()
23 | @State private var candidateMember = ""
24 | @State private var candidateMembers = [String]()
25 |
26 | private var isEmpty: Bool {
27 | !(name != "" && members.count > 0)
28 | }
29 |
30 | private var memberList: [String] {
31 | candidateMember == ""
32 | ? chatsters.compactMap {
33 | user.userName != $0.userName && !members.contains($0.userName)
34 | ? $0.userName
35 | : nil }
36 | : candidateMembers
37 | }
38 |
39 | var body: some View {
40 | let searchBinding = Binding(
41 | get: { candidateMember },
42 | set: {
43 | candidateMember = $0
44 | searchUsers()
45 | }
46 | )
47 |
48 | return NavigationView {
49 | ZStack {
50 | VStack {
51 | InputField(title: "Chat Name", text: $name)
52 | CaptionLabel(title: "Add Members")
53 | SearchBox(searchText: searchBinding)
54 | List {
55 | ForEach(memberList, id: \.self) { candidateMember in
56 | Button(action: { addMember(candidateMember) }) {
57 | HStack {
58 | Text(candidateMember)
59 | Spacer()
60 | Image(systemName: "plus.circle.fill")
61 | .renderingMode(.original)
62 | }
63 | }
64 | }
65 | }
66 | Divider()
67 | CaptionLabel(title: "Members")
68 | List {
69 | ForEach(members, id: \.self) { member in
70 | Text(member)
71 | }
72 | .onDelete(perform: deleteMember)
73 | }
74 | Spacer()
75 | }
76 | Spacer()
77 | if let error = state.error {
78 | Text("Error: \(error)")
79 | .foregroundColor(Color.red)
80 | }
81 | }
82 | .padding()
83 | .navigationBarTitle("New Chat", displayMode: .inline)
84 | .navigationBarItems(
85 | leading: Button("Dismiss") { presentationMode.wrappedValue.dismiss() },
86 | trailing: VStack {
87 | if isPreview {
88 | SaveConversationButton(user: user, name: name, members: members, done: { presentationMode.wrappedValue.dismiss() })
89 | } else {
90 | SaveConversationButton(user: user, name: name, members: members, done: { presentationMode.wrappedValue.dismiss() })
91 | }
92 | }
93 | .disabled(isEmpty)
94 | .padding()
95 | )
96 | }
97 | .onAppear {
98 | setSubscription()
99 | searchUsers()
100 | }
101 | }
102 |
103 | private func searchUsers() {
104 | var candidateChatsters: Results
105 | if candidateMember == "" {
106 | candidateChatsters = chatsters
107 | } else {
108 | let predicate = NSPredicate(format: "userName CONTAINS[cd] %@", candidateMember)
109 | candidateChatsters = chatsters.filter(predicate)
110 | }
111 | candidateMembers = []
112 | candidateChatsters.forEach { chatster in
113 | if !members.contains(chatster.userName) && chatster.userName != user.userName {
114 | candidateMembers.append(chatster.userName)
115 | }
116 | }
117 | }
118 |
119 | private func addMember(_ newMember: String) {
120 | state.error = nil
121 | if members.contains(newMember) {
122 | state.error = "\(newMember) is already part of this chat"
123 | } else {
124 | members.append(newMember)
125 | candidateMember = ""
126 | searchUsers()
127 | }
128 | }
129 |
130 | private func deleteMember(at offsets: IndexSet) {
131 | members.remove(atOffsets: offsets)
132 | }
133 |
134 | private func setSubscription() {
135 | let subscriptions = realm.subscriptions
136 | subscriptions.update {
137 | if let currentSubscription = subscriptions.first(named: "all_chatsters") {
138 | currentSubscription.updateQuery(toType: Chatster.self) { chatster in
139 | chatster.userName != ""
140 | }
141 |
142 | } else {
143 | subscriptions.append(QuerySubscription(name: "all_chatsters") { chatster in
144 | chatster.userName != ""
145 | })
146 | }
147 | }
148 | }
149 | }
150 |
151 | struct NewConversationView_Previews: PreviewProvider {
152 | static var previews: some View {
153 | Realm.bootstrap()
154 |
155 | return AppearancePreviews(
156 | NewConversationView(user: .sample, isPreview: true)
157 | .environmentObject(AppState.sample)
158 | )
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/RChat-iOS/RChat/Preview Content/SampleData.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SampleData.swift
3 | // RChat
4 | //
5 | // Created by Andrew Morgan on 23/11/2020.
6 | //
7 |
8 | // swiftlint:disable line_length
9 | // swiftlint:disable force_try
10 |
11 | import RealmSwift
12 | import UIKit
13 |
14 | protocol Samplable {
15 | associatedtype Item
16 | static var sample: Item { get }
17 | static var samples: [Item] { get }
18 | }
19 |
20 | extension Date {
21 | static var random: Date {
22 | Date(timeIntervalSince1970: (50 * 365 * 24 * 3600 + Double.random(in: 0..<(3600 * 24 * 365))))
23 | }
24 | }
25 |
26 | extension User {
27 | convenience init(username: String, presence: Presence, userPreferences: UserPreferences, conversations: [Conversation]) {
28 | self.init()
29 | self.userName = username
30 | self.presence = presence.asString
31 | self.userPreferences = userPreferences
32 | self.lastSeenAt = Date.random
33 | conversations.forEach { conversation in
34 | self.conversations.append(conversation)
35 | }
36 | }
37 |
38 | convenience init(_ user: User) {
39 | self.init()
40 | userName = user.userName
41 | userPreferences = UserPreferences(user.userPreferences)
42 | lastSeenAt = user.lastSeenAt
43 | conversations.append(objectsIn: user.conversations.map { Conversation($0) })
44 | presence = user.presence
45 | }
46 | }
47 |
48 | extension User: Samplable {
49 | static var samples: [User] { [sample, sample2, sample3] }
50 | static var sample: User {
51 | User(username: "rod@contoso.com", presence: .onLine, userPreferences: .sample, conversations: [.sample, .sample2, .sample3])
52 | }
53 | static var sample2: User {
54 | User(username: "jane@contoso.com", presence: .offLine, userPreferences: .sample2, conversations: [.sample, .sample2])
55 | }
56 | static var sample3: User {
57 | User(username: "freddy@contoso.com", presence: .hidden, userPreferences: .sample3, conversations: [.sample, .sample3])
58 | }
59 | }
60 |
61 | extension UserPreferences {
62 | convenience init(displayName: String, photo: Photo) {
63 | self.init()
64 | self.displayName = displayName
65 | self.avatarImage = photo
66 | }
67 |
68 | convenience init(_ userPreferences: UserPreferences?) {
69 | self.init()
70 | if let userPreferences = userPreferences {
71 | displayName = userPreferences.displayName
72 | avatarImage = Photo(userPreferences.avatarImage)
73 | }
74 | }
75 | }
76 |
77 | extension UserPreferences: Samplable {
78 | static var samples: [UserPreferences] { [sample, sample2, sample3] }
79 | static var sample = UserPreferences(displayName: "Rod Burton", photo: .sample)
80 | static var sample2 = UserPreferences(displayName: "Jane Tucker", photo: .sample2)
81 | static var sample3 = UserPreferences(displayName: "Freddy Marks", photo: .sample3)
82 | }
83 |
84 | extension Conversation {
85 | convenience init(displayName: String, unreadCount: Int, members: [Member]) {
86 | self.init()
87 | self.displayName = displayName
88 | self.unreadCount = unreadCount
89 | self.members.append(objectsIn: members.map { Member($0) })
90 |
91 | // forEach { username in
92 | // self.members.append(Member(username))
93 | // }
94 | }
95 |
96 | convenience init(_ conversation: Conversation) {
97 | self.init()
98 | displayName = conversation.displayName
99 | unreadCount = conversation.unreadCount
100 | members.append(objectsIn: conversation.members.map { Member($0) })
101 | }
102 | }
103 |
104 | extension Conversation: Samplable {
105 | static var samples: [Conversation] { [sample, sample2, sample3] }
106 | static var sample: Conversation {
107 | Conversation(displayName: "Sample chat", unreadCount: 2, members: Member.samples)
108 | }
109 | static var sample2: Conversation {
110 | Conversation(displayName: "Fishy chat", unreadCount: 0, members: Member.samples)
111 | }
112 | static var sample3: Conversation {
113 | Conversation(displayName: "Third chat", unreadCount: 1, members: Member.samples)
114 | }
115 | }
116 |
117 | extension Member {
118 | convenience init(_ member: Member) {
119 | self.init()
120 | userName = member.userName
121 | membershipStatus = member.membershipStatus
122 | }
123 | }
124 |
125 | extension Member: Samplable {
126 | static var samples: [Member] { [sample, sample2, sample3] }
127 | static var sample: Member {
128 | Member(userName: "rod@contoso.com", state: .active)
129 | }
130 | static var sample2: Member {
131 | Member(userName: "jane@contoso.com", state: .active)
132 | }
133 | static var sample3: Member {
134 | Member(userName: "freddy@contoso.com", state: .pending)
135 | }
136 | }
137 |
138 | extension Chatster {
139 | convenience init(user: User) {
140 | self.init()
141 | self._id = user._id
142 | self.userName = user.userName
143 | self.displayName = user.userPreferences!.displayName
144 | self.avatarImage = Photo(user.userPreferences?.avatarImage)
145 | lastSeenAt = Date.random
146 | self.presence = user.presence
147 | }
148 |
149 | convenience init(_ chatster: Chatster) {
150 | self.init()
151 | userName = chatster.userName
152 | displayName = chatster.displayName
153 | avatarImage = Photo(chatster.avatarImage)
154 | lastSeenAt = chatster.lastSeenAt
155 | presence = chatster.presence
156 | }
157 | }
158 |
159 | extension Chatster: Samplable {
160 | static var samples: [Chatster] { [sample, sample2, sample3] }
161 | static var sample: Chatster { Chatster(user: User(.sample)) }
162 | static var sample2: Chatster { Chatster(user: User(.sample2)) }
163 | static var sample3: Chatster { Chatster(user: User(.sample3)) }
164 | }
165 |
166 | extension AppState: Samplable {
167 | static var samples: [AppState] { [sample, sample2, sample3] }
168 | static var sample: AppState { AppState() }
169 | static var sample2: AppState { AppState() }
170 | static var sample3: AppState { AppState() }
171 | }
172 |
173 | extension Photo {
174 | convenience init(photoName: String) {
175 | self.init()
176 | self.thumbNail = (UIImage(named: photoName) ?? UIImage()).jpegData(compressionQuality: 0.8)
177 | self.picture = (UIImage(named: photoName) ?? UIImage()).jpegData(compressionQuality: 0.8)
178 | self.date = Date.random
179 | }
180 | convenience init(_ photo: Photo?) {
181 | self.init()
182 | if let photo = photo {
183 | self.thumbNail = photo.thumbNail
184 | self.picture = photo.picture
185 | self.date = photo.date
186 | }
187 | }
188 | }
189 |
190 | extension Photo: Samplable {
191 | static var samples: [Photo] { [sample, sample2, sample3]}
192 | static var sample: Photo { Photo(photoName: "rod") }
193 | static var sample2: Photo { Photo(photoName: "jane") }
194 | static var sample3: Photo { Photo(photoName: "freddy") }
195 | static var spud: Photo { Photo(photoName: "spud\(Int.random(in: 1...8))") }
196 | }
197 |
198 | extension ChatMessage {
199 | convenience init(conversation: Conversation,
200 | author: User,
201 | text: String = "This is the text for the message",
202 | includePhoto: Bool = false,
203 | includeLocation: Bool = false) {
204 | self.init()
205 | conversationID = conversation.id
206 | self.author = author.userName
207 | self.text = text
208 | if includePhoto { self.image = Photo.spud }
209 | self.timestamp = Date.random
210 | if includeLocation {
211 | self.location.append(-0.10689139236939127 + Double.random(in: -10..<10))
212 | self.location.append(51.506520923981554 + Double.random(in: -10..<10))
213 | }
214 | }
215 |
216 | convenience init(_ chatMessage: ChatMessage) {
217 | self.init()
218 | conversationID = chatMessage.conversationID
219 | author = chatMessage.author
220 | text = chatMessage.text
221 | image = Photo(chatMessage.image)
222 | location.append(objectsIn: chatMessage.location)
223 | timestamp = chatMessage.timestamp
224 | }
225 | }
226 |
227 | extension ChatMessage: Samplable {
228 | static var samples: [ChatMessage] { [sample, sample2, sample3, sample20, sample22, sample23, sample30, sample32, sample33] }
229 | static var sample: ChatMessage { ChatMessage(conversation: .sample, author: .sample) }
230 | static var sample2: ChatMessage { ChatMessage(conversation: .sample, author: .sample2, includePhoto: true) }
231 | static var sample3: ChatMessage { ChatMessage(conversation: .sample, author: .sample3, text: "Thoughts on this **spud**?", includePhoto: true, includeLocation: true)}
232 | static var sample20: ChatMessage { ChatMessage(conversation: .sample2, author: .sample) }
233 | static var sample22: ChatMessage { ChatMessage(conversation: .sample2, author: .sample2, includePhoto: true) }
234 | static var sample23: ChatMessage { ChatMessage(conversation: .sample2, author: .sample3, text: "Fancy trying this?", includePhoto: true, includeLocation: true)}
235 | static var sample30: ChatMessage { ChatMessage(conversation: .sample3, author: .sample) }
236 | static var sample32: ChatMessage { ChatMessage(conversation: .sample3, author: .sample2, includePhoto: true) }
237 | static var sample33: ChatMessage { ChatMessage(conversation: .sample3, author: .sample3, text: "Is this a bit controversial? If nothing else, this is a very long, tedious post - I just hope that there's space for it all to fit in", includePhoto: true, includeLocation: true)}
238 | }
239 |
240 | extension Realm: Samplable {
241 | static var samples: [Realm] { [sample] }
242 | static var sample: Realm {
243 | let realm = try! Realm()
244 | try! realm.write {
245 | realm.deleteAll()
246 | User.samples.forEach { user in
247 | realm.add(user)
248 | }
249 | Chatster.samples.forEach { chatster in
250 | realm.add(chatster)
251 | }
252 | ChatMessage.samples.forEach { message in
253 | realm.add(message)
254 | }
255 | }
256 | return realm
257 | }
258 |
259 | static func bootstrap() {
260 | do {
261 | let realm = try Realm()
262 | try realm.write {
263 | realm.deleteAll()
264 | realm.add(Chatster.samples)
265 | realm.add(User(User.sample))
266 | realm.add(ChatMessage.samples)
267 | }
268 | } catch {
269 | print("Failed to bootstrap the default realm")
270 | }
271 | }
272 | }
273 |
--------------------------------------------------------------------------------