├── 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 app icon](assets/RChatIcon80.png) 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 | ![Screenshot of a chatroom with messages](assets/ChatRoom.png) 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 | ![Realm application Id](assets/realm-app-id.png) 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 | --------------------------------------------------------------------------------