├── .gitattributes ├── .gitignore ├── .idea ├── .name ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── jarRepositories.xml ├── misc.xml ├── render.experimental.xml └── runConfigurations.xml ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── google-services.json ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── fredrikbogg │ │ └── android_chat_app │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ │ └── com │ │ │ └── fredrikbogg │ │ │ └── android_chat_app │ │ │ ├── App.kt │ │ │ ├── data │ │ │ ├── Event.kt │ │ │ ├── Result.kt │ │ │ ├── db │ │ │ │ ├── entity │ │ │ │ │ ├── Chat.kt │ │ │ │ │ ├── Message.kt │ │ │ │ │ └── User.kt │ │ │ │ ├── remote │ │ │ │ │ ├── FirebaseAuthSource.kt │ │ │ │ │ ├── FirebaseDatabaseSource.kt │ │ │ │ │ └── FirebaseStorageSource.kt │ │ │ │ └── repository │ │ │ │ │ ├── AuthRepository.kt │ │ │ │ │ ├── DatabaseRepository.kt │ │ │ │ │ └── StorageRepository.kt │ │ │ └── model │ │ │ │ ├── ChatWithUserInfo.kt │ │ │ │ ├── CreateUser.kt │ │ │ │ └── Login.kt │ │ │ ├── ui │ │ │ ├── DefaultBindings.kt │ │ │ ├── DefaultViewModel.kt │ │ │ ├── chat │ │ │ │ ├── ChatFragment.kt │ │ │ │ ├── ChatViewModel.kt │ │ │ │ ├── MessagesBindings.kt │ │ │ │ └── MessagesListAdapter.kt │ │ │ ├── chats │ │ │ │ ├── ChatsBindings.kt │ │ │ │ ├── ChatsFragment.kt │ │ │ │ ├── ChatsListAdapter.kt │ │ │ │ └── ChatsViewModel.kt │ │ │ ├── main │ │ │ │ ├── MainActivity.kt │ │ │ │ └── MainViewModel.kt │ │ │ ├── notifications │ │ │ │ ├── NotificationsBindings.kt │ │ │ │ ├── NotificationsFragment.kt │ │ │ │ ├── NotificationsListAdapter.kt │ │ │ │ └── NotificationsViewModel.kt │ │ │ ├── profile │ │ │ │ ├── ProfileFragment.kt │ │ │ │ └── ProfileViewModel.kt │ │ │ ├── settings │ │ │ │ ├── SettingsFragment.kt │ │ │ │ └── SettingsViewModel.kt │ │ │ ├── start │ │ │ │ ├── StartFragment.kt │ │ │ │ ├── StartViewModel.kt │ │ │ │ ├── createAccount │ │ │ │ │ ├── CreateAccountFragment.kt │ │ │ │ │ └── CreateAccountViewModel.kt │ │ │ │ └── login │ │ │ │ │ ├── LoginFragment.kt │ │ │ │ │ └── LoginViewModel.kt │ │ │ └── users │ │ │ │ ├── UsersBindings.kt │ │ │ │ ├── UsersFragment.kt │ │ │ │ ├── UsersListAdapter.kt │ │ │ │ └── UsersViewModel.kt │ │ │ └── util │ │ │ ├── FileConverterUtil.kt │ │ │ ├── FirebaseUtil.kt │ │ │ ├── LiveDataExt.kt │ │ │ ├── SharedPreferencesUtil.kt │ │ │ ├── TextUtil.kt │ │ │ └── ViewExt.kt │ └── res │ │ ├── drawable-v24 │ │ ├── chat_box.png │ │ ├── rounded_rectangle_primary.xml │ │ └── rounded_rectangle_secondary.xml │ │ ├── drawable │ │ ├── ic_baseline_chat_bubble_24.xml │ │ ├── ic_baseline_error_24.xml │ │ ├── ic_baseline_notifications_24.xml │ │ ├── ic_baseline_people_24.xml │ │ ├── ic_baseline_person_24.xml │ │ ├── ic_baseline_settings_24.xml │ │ ├── round_circle_online_green.xml │ │ └── round_circle_primary.xml │ │ ├── font │ │ ├── nunito.xml │ │ ├── nunito_bold.xml │ │ ├── nunito_extrabold.xml │ │ └── nunito_semibold.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── fragment_chat.xml │ │ ├── fragment_chats.xml │ │ ├── fragment_create_account.xml │ │ ├── fragment_login.xml │ │ ├── fragment_notifications.xml │ │ ├── fragment_profile.xml │ │ ├── fragment_settings.xml │ │ ├── fragment_start.xml │ │ ├── fragment_users.xml │ │ ├── list_item_chat.xml │ │ ├── list_item_message_received.xml │ │ ├── list_item_message_sent.xml │ │ ├── list_item_notification.xml │ │ ├── list_item_user.xml │ │ ├── toolbar_addon_chat.xml │ │ └── toolbar_main.xml │ │ ├── menu │ │ └── bottom_nav_menu.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── navigation │ │ └── mobile_navigation.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── font_certs.xml │ │ ├── ic_launcher_background.xml │ │ ├── preloaded_fonts.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── fredrikbogg │ └── android_chat_app │ └── ExampleUnitTest.kt ├── build.gradle ├── github_images ├── chat.png ├── chats.png ├── create.png ├── db.png ├── header.png ├── login.png ├── notifications.png ├── profile.png ├── settings.png ├── start.png └── users.png ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.aar 4 | *.ap_ 5 | *.aab 6 | 7 | # Files for the ART/Dalvik VM 8 | *.dex 9 | 10 | # Java class files 11 | *.class 12 | 13 | # Generated files 14 | bin/ 15 | gen/ 16 | out/ 17 | # Uncomment the following line in case you need and you don't have the release build type files in your app 18 | # release/ 19 | 20 | # Gradle files 21 | .gradle/ 22 | build/ 23 | 24 | # Local configuration file (sdk path, etc) 25 | local.properties 26 | 27 | # Proguard folder generated by Eclipse 28 | proguard/ 29 | 30 | # Log Files 31 | *.log 32 | 33 | # Android Studio Navigation editor temp files 34 | .navigation/ 35 | 36 | # Android Studio captures folder 37 | captures/ 38 | 39 | # IntelliJ 40 | *.iml 41 | .idea/workspace.xml 42 | .idea/tasks.xml 43 | .idea/gradle.xml 44 | .idea/assetWizardSettings.xml 45 | .idea/dictionaries 46 | .idea/libraries 47 | # Android Studio 3 in .gitignore file. 48 | .idea/caches 49 | .idea/modules.xml 50 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 51 | .idea/navEditor.xml 52 | 53 | # Keystore files 54 | # Uncomment the following lines if you do not want to check your keystore files in. 55 | #*.jks 56 | #*.keystore 57 | 58 | # External native build folder generated in Android Studio 2.2 and later 59 | .externalNativeBuild 60 | .cxx/ 61 | 62 | # Google Services (e.g. APIs or Firebase) 63 | # google-services.json 64 | 65 | # Freeline 66 | freeline.py 67 | freeline/ 68 | freeline_project_description.json 69 | 70 | # fastlane 71 | fastlane/report.xml 72 | fastlane/Preview.html 73 | fastlane/screenshots 74 | fastlane/test_output 75 | fastlane/readme.md 76 | 77 | # Version control 78 | vcs.xml 79 | 80 | # lint 81 | lint/intermediates/ 82 | lint/generated/ 83 | lint/outputs/ 84 | lint/tmp/ 85 | # lint/reports/ 86 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | Android-Chat-App -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 20 | 22 | 23 | 24 | 26 | 27 | 28 |
29 | 30 | 31 | 32 | xmlns:android 33 | 34 | ^$ 35 | 36 | 37 | 38 |
39 |
40 | 41 | 42 | 43 | xmlns:.* 44 | 45 | ^$ 46 | 47 | 48 | BY_NAME 49 | 50 |
51 |
52 | 53 | 54 | 55 | .*:id 56 | 57 | http://schemas.android.com/apk/res/android 58 | 59 | 60 | 61 |
62 |
63 | 64 | 65 | 66 | .*:name 67 | 68 | http://schemas.android.com/apk/res/android 69 | 70 | 71 | 72 |
73 |
74 | 75 | 76 | 77 | name 78 | 79 | ^$ 80 | 81 | 82 | 83 |
84 |
85 | 86 | 87 | 88 | style 89 | 90 | ^$ 91 | 92 | 93 | 94 |
95 |
96 | 97 | 98 | 99 | .* 100 | 101 | ^$ 102 | 103 | 104 | BY_NAME 105 | 106 |
107 |
108 | 109 | 110 | 111 | .* 112 | 113 | http://schemas.android.com/apk/res/android 114 | 115 | 116 | ANDROID_ATTRIBUTE_ORDER 117 | 118 |
119 |
120 | 121 | 122 | 123 | .* 124 | 125 | .* 126 | 127 | 128 | BY_NAME 129 | 130 |
131 |
132 |
133 |
134 | 135 | 137 |
138 |
-------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/render.experimental.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Fredrik Bogg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chat App Android 2 | ![HeaderImage](github_images/header.png) 3 | 4 | ## Introduction 5 | This is a demo application built with the goal to create a fun and challenging application based on the MVVM architectural pattern. 6 | 7 | See below for more information. 8 | 9 | ## Technologies & Architecture 10 | 11 | #### Technologies 12 | Android, Kotlin 13 | 14 | #### Architecture 15 | Model-View-ViewModel (MVVM) 16 | 17 | #### Firebase 18 | * Authentication 19 | * Realtime Database 20 | * Storage 21 | 22 | #### Architecture Components 23 | [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel), [LiveData](https://developer.android.com/topic/libraries/architecture/livedata), [DataBinding](https://developer.android.com/topic/libraries/data-binding), 24 | [Navigation](https://developer.android.com/guide/navigation/) 25 | 26 | ## Features 27 | 28 | **Start:** Login/create account 29 | 30 | **Chats:** List of chats, online status, update on change 31 | 32 | **Notifications:** Accept/decline friend requests, notifications symbol 33 | 34 | **Users:** List of users 35 | 36 | **Settings:** Change image, change status, logout 37 | 38 | **Chat:** Send and show messages sorted by timestamp, online status, custom toolbar, update on change 39 | 40 | **Profile:** Add/remove friend, accept/decline friend request 41 | 42 | **General:** Auto login, bottom navigation, error messages with snackbar, progress bar 43 | 44 | ## Screenshots 45 | 46 | ### Start | Login | Create Account 47 | 48 |

49 | 50 | 51 | 52 |

53 | 54 | ### Chats | Notifications | Users 55 | 56 |

57 | 58 | 59 | 60 |

61 | 62 | ### Settings | Chat | Profile 63 | 64 |

65 | 66 | 67 | 68 |

69 | 70 | ### Firebase 71 |

72 | 73 |

74 | 75 | ## Setup 76 | #### Requirements 77 | * Basic knowledge about Android Studio 78 | * Basic knowledge about Firebase 79 | 80 | #### Firebase 81 | * Setup Authentication and use the Sign-in method 'Email/Password' 82 | * Setup Realtime Database 83 | * Setup Storage 84 | * Replace the file [google-services.json](app/google-services.json) 85 | * Note: Download the google-services.json file after the Firebase services are set up to automatically include the services in the json file. 86 | * Note: When updating the google-services.json file then make sure to invalidate the caches as well as doing a clean + rebuild. 87 | 88 | #### Project 89 | 1. Download and open the project in Android Studio 90 | 2. Connect your Android phone or use the emulator to start the application 91 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | apply plugin: 'com.google.gms.google-services' 5 | apply plugin: 'kotlin-kapt' 6 | 7 | android { 8 | compileSdkVersion 29 9 | buildToolsVersion "29.0.3" 10 | 11 | defaultConfig { 12 | applicationId "com.fredrikbogg.android_chat_app" 13 | minSdkVersion 26 14 | targetSdkVersion 29 15 | versionCode 1 16 | versionName "1.0" 17 | 18 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 19 | } 20 | 21 | buildTypes { 22 | release { 23 | minifyEnabled false 24 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 25 | } 26 | } 27 | 28 | buildFeatures { 29 | dataBinding = true 30 | viewBinding = true 31 | } 32 | 33 | compileOptions { 34 | sourceCompatibility JavaVersion.VERSION_1_8 35 | targetCompatibility JavaVersion.VERSION_1_8 36 | } 37 | kotlinOptions { 38 | jvmTarget = '1.8' 39 | } 40 | } 41 | 42 | dependencies { 43 | implementation fileTree(dir: "libs", include: ["*.jar"]) 44 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 45 | implementation 'androidx.core:core-ktx:1.3.1' 46 | implementation 'androidx.appcompat:appcompat:1.2.0' 47 | implementation 'com.google.android.material:material:1.2.0' 48 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 49 | implementation 'androidx.legacy:legacy-support-v4:1.0.0' 50 | 51 | //Navigation, lifecycle 52 | implementation 'androidx.navigation:navigation-fragment:2.3.0' 53 | implementation 'androidx.navigation:navigation-ui:2.3.0' 54 | implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' 55 | implementation 'androidx.navigation:navigation-fragment-ktx:2.3.0' 56 | implementation 'androidx.navigation:navigation-ui-ktx:2.3.0' 57 | implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0' 58 | 59 | // Firebase 60 | implementation 'com.google.firebase:firebase-database:19.3.1' 61 | implementation 'com.google.firebase:firebase-auth:19.3.2' 62 | implementation 'com.google.firebase:firebase-storage:19.1.1' 63 | 64 | // Picasso 65 | implementation 'com.squareup.picasso:picasso:2.71828' 66 | implementation 'jp.wasabeef:picasso-transformations:2.2.1' 67 | 68 | testImplementation 'junit:junit:4.13' 69 | androidTestImplementation 'androidx.test.ext:junit:1.1.1' 70 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' 71 | } 72 | -------------------------------------------------------------------------------- /app/google-services.json: -------------------------------------------------------------------------------- 1 | -EDIT THIS- -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/com/fredrikbogg/android_chat_app/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.fredrikbogg.android_chat_app", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 14 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgewe/Chat-App-Android/cff2f947a4496e46cbaa750b4e3fa768ba2308f0/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/App.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app 2 | 3 | import android.app.Application 4 | import com.fredrikbogg.android_chat_app.util.SharedPreferencesUtil 5 | 6 | 7 | class App : Application() { 8 | 9 | override fun onCreate() { 10 | super.onCreate() 11 | application = this 12 | } 13 | 14 | companion object { 15 | lateinit var application: Application 16 | private set 17 | 18 | var myUserID: String = "" 19 | get() { 20 | field = SharedPreferencesUtil.getUserID(application.applicationContext).orEmpty() 21 | return field 22 | } 23 | private set 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/data/Event.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app.data 2 | 3 | import androidx.lifecycle.Observer 4 | 5 | 6 | open class Event(private val content: T) { 7 | private var isHandled = false 8 | 9 | fun getContentIfNotHandled(): T? { 10 | return if (isHandled) { 11 | null 12 | } else { 13 | isHandled = true 14 | content 15 | } 16 | } 17 | } 18 | 19 | class EventObserver(private val onEventUnhandledContent: (T) -> Unit) : Observer> { 20 | override fun onChanged(event: Event?) { 21 | event?.getContentIfNotHandled()?.let { onEventUnhandledContent(it) } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/data/Result.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app.data 2 | 3 | 4 | sealed class Result { 5 | data class Success(val data: T? = null, val msg: String? = null) : Result() 6 | class Error(val msg: String? = null) : Result() 7 | object Loading : Result() 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/data/db/entity/Chat.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app.data.db.entity 2 | 3 | import com.google.firebase.database.PropertyName 4 | 5 | 6 | data class Chat( 7 | @get:PropertyName("lastMessage") @set:PropertyName("lastMessage") var lastMessage: Message = Message(), 8 | @get:PropertyName("info") @set:PropertyName("info") var info: ChatInfo = ChatInfo() 9 | ) 10 | 11 | data class ChatInfo( 12 | @get:PropertyName("id") @set:PropertyName("id") var id: String = "" 13 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/data/db/entity/Message.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app.data.db.entity 2 | 3 | import com.google.firebase.database.PropertyName 4 | import java.util.* 5 | 6 | 7 | data class Message( 8 | @get:PropertyName("senderID") @set:PropertyName("senderID") var senderID: String = "", 9 | @get:PropertyName("text") @set:PropertyName("text") var text: String = "", 10 | @get:PropertyName("epochTimeMs") @set:PropertyName("epochTimeMs") var epochTimeMs: Long = Date().time, 11 | @get:PropertyName("seen") @set:PropertyName("seen") var seen: Boolean = false 12 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/data/db/entity/User.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app.data.db.entity 2 | 3 | import com.google.firebase.database.PropertyName 4 | 5 | 6 | data class User( 7 | @get:PropertyName("info") @set:PropertyName("info") var info: UserInfo = UserInfo(), 8 | @get:PropertyName("friends") @set:PropertyName("friends") var friends: HashMap = HashMap(), 9 | @get:PropertyName("notifications") @set:PropertyName("notifications") var notifications: HashMap = HashMap(), 10 | @get:PropertyName("sentRequests") @set:PropertyName("sentRequests") var sentRequests: HashMap = HashMap() 11 | ) 12 | 13 | data class UserFriend( 14 | @get:PropertyName("userID") @set:PropertyName("userID") var userID: String = "" 15 | ) 16 | 17 | data class UserInfo( 18 | @get:PropertyName("id") @set:PropertyName("id") var id: String = "", 19 | @get:PropertyName("displayName") @set:PropertyName("displayName") var displayName: String = "", 20 | @get:PropertyName("status") @set:PropertyName("status") var status: String = "No status", 21 | @get:PropertyName("profileImageUrl") @set:PropertyName("profileImageUrl") var profileImageUrl: String = "", 22 | @get:PropertyName("online") @set:PropertyName("online") var online: Boolean = false 23 | ) 24 | 25 | data class UserNotification( 26 | @get:PropertyName("userID") @set:PropertyName("userID") var userID: String = "" 27 | ) 28 | 29 | data class UserRequest( 30 | @get:PropertyName("userID") @set:PropertyName("userID") var userID: String = "" 31 | ) 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/data/db/remote/FirebaseAuthSource.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app.data.db.remote 2 | 3 | import com.fredrikbogg.android_chat_app.data.model.CreateUser 4 | import com.fredrikbogg.android_chat_app.data.model.Login 5 | import com.fredrikbogg.android_chat_app.data.Result 6 | import com.google.android.gms.tasks.Task 7 | import com.google.firebase.auth.AuthResult 8 | import com.google.firebase.auth.FirebaseAuth 9 | import com.google.firebase.auth.FirebaseUser 10 | 11 | class FirebaseAuthStateObserver { 12 | 13 | private var authListener: FirebaseAuth.AuthStateListener? = null 14 | private var instance: FirebaseAuth? = null 15 | 16 | fun start(valueEventListener: FirebaseAuth.AuthStateListener, instance: FirebaseAuth) { 17 | this.authListener = valueEventListener 18 | this.instance = instance 19 | this.instance!!.addAuthStateListener(authListener!!) 20 | } 21 | 22 | fun clear() { 23 | authListener?.let { instance?.removeAuthStateListener(it) } 24 | } 25 | } 26 | 27 | class FirebaseAuthSource { 28 | 29 | companion object { 30 | val authInstance = FirebaseAuth.getInstance() 31 | } 32 | 33 | private fun attachAuthObserver(b: ((Result) -> Unit)): FirebaseAuth.AuthStateListener { 34 | return FirebaseAuth.AuthStateListener { 35 | if (it.currentUser == null) { 36 | b.invoke(Result.Error("No user")) 37 | } else { b.invoke(Result.Success(it.currentUser)) } 38 | } 39 | } 40 | 41 | fun loginWithEmailAndPassword(login: Login): Task { 42 | return authInstance.signInWithEmailAndPassword(login.email, login.password) 43 | } 44 | 45 | fun createUser(createUser: CreateUser): Task { 46 | return authInstance.createUserWithEmailAndPassword(createUser.email, createUser.password) 47 | } 48 | 49 | fun logout() { 50 | authInstance.signOut() 51 | } 52 | 53 | fun attachAuthStateObserver(firebaseAuthStateObserver: FirebaseAuthStateObserver, b: ((Result) -> Unit)) { 54 | val listener = attachAuthObserver(b) 55 | firebaseAuthStateObserver.start(listener, authInstance) 56 | } 57 | } 58 | 59 | -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/data/db/remote/FirebaseStorageSource.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app.data.db.remote 2 | 3 | import android.net.Uri 4 | import com.google.android.gms.tasks.Task 5 | import com.google.firebase.storage.FirebaseStorage 6 | 7 | // Task based 8 | class FirebaseStorageSource { 9 | private val storageInstance = FirebaseStorage.getInstance() 10 | 11 | fun uploadUserImage(userID: String, bArr: ByteArray): Task { 12 | val path = "user_photos/$userID/profile_image" 13 | val ref = storageInstance.reference.child(path) 14 | 15 | return ref.putBytes(bArr).continueWithTask { 16 | ref.downloadUrl 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/data/db/repository/AuthRepository.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app.data.db.repository 2 | 3 | import com.fredrikbogg.android_chat_app.data.model.CreateUser 4 | import com.fredrikbogg.android_chat_app.data.model.Login 5 | import com.fredrikbogg.android_chat_app.data.db.remote.FirebaseAuthSource 6 | import com.fredrikbogg.android_chat_app.data.db.remote.FirebaseAuthStateObserver 7 | import com.fredrikbogg.android_chat_app.data.Result 8 | import com.google.firebase.auth.FirebaseUser 9 | 10 | class AuthRepository{ 11 | private val firebaseAuthService = FirebaseAuthSource() 12 | 13 | fun observeAuthState(stateObserver: FirebaseAuthStateObserver, b: ((Result) -> Unit)){ 14 | firebaseAuthService.attachAuthStateObserver(stateObserver,b) 15 | } 16 | 17 | fun loginUser(login: Login, b: ((Result) -> Unit)) { 18 | b.invoke(Result.Loading) 19 | firebaseAuthService.loginWithEmailAndPassword(login).addOnSuccessListener { 20 | b.invoke(Result.Success(it.user)) 21 | }.addOnFailureListener { 22 | b.invoke(Result.Error(msg = it.message)) 23 | } 24 | } 25 | 26 | fun createUser(createUser: CreateUser, b: ((Result) -> Unit)) { 27 | b.invoke(Result.Loading) 28 | firebaseAuthService.createUser(createUser).addOnSuccessListener { 29 | b.invoke(Result.Success(it.user)) 30 | }.addOnFailureListener { 31 | b.invoke(Result.Error(msg = it.message)) 32 | } 33 | } 34 | 35 | fun logoutUser() { 36 | firebaseAuthService.logout() 37 | } 38 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/data/db/repository/DatabaseRepository.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app.data.db.repository 2 | 3 | import com.fredrikbogg.android_chat_app.data.db.entity.* 4 | import com.fredrikbogg.android_chat_app.data.db.remote.FirebaseDataSource 5 | import com.fredrikbogg.android_chat_app.data.db.remote.FirebaseReferenceChildObserver 6 | import com.fredrikbogg.android_chat_app.data.db.remote.FirebaseReferenceValueObserver 7 | import com.fredrikbogg.android_chat_app.data.Result 8 | import com.fredrikbogg.android_chat_app.util.wrapSnapshotToArrayList 9 | import com.fredrikbogg.android_chat_app.util.wrapSnapshotToClass 10 | 11 | 12 | class DatabaseRepository { 13 | private val firebaseDatabaseService = FirebaseDataSource() 14 | 15 | //region Update 16 | fun updateUserStatus(userID: String, status: String) { 17 | firebaseDatabaseService.updateUserStatus(userID, status) 18 | } 19 | 20 | fun updateNewMessage(messagesID: String, message: Message) { 21 | firebaseDatabaseService.pushNewMessage(messagesID, message) 22 | } 23 | 24 | fun updateNewUser(user: User) { 25 | firebaseDatabaseService.updateNewUser(user) 26 | } 27 | 28 | fun updateNewFriend(myUser: UserFriend, otherUser: UserFriend) { 29 | firebaseDatabaseService.updateNewFriend(myUser, otherUser) 30 | } 31 | 32 | fun updateNewSentRequest(userID: String, userRequest: UserRequest) { 33 | firebaseDatabaseService.updateNewSentRequest(userID, userRequest) 34 | } 35 | 36 | fun updateNewNotification(otherUserID: String, userNotification: UserNotification) { 37 | firebaseDatabaseService.updateNewNotification(otherUserID, userNotification) 38 | } 39 | 40 | fun updateChatLastMessage(chatID: String, message: Message) { 41 | firebaseDatabaseService.updateLastMessage(chatID, message) 42 | } 43 | 44 | fun updateNewChat(chat: Chat){ 45 | firebaseDatabaseService.updateNewChat(chat) 46 | } 47 | 48 | fun updateUserProfileImageUrl(userID: String, url: String){ 49 | firebaseDatabaseService.updateUserProfileImageUrl(userID, url) 50 | } 51 | 52 | //endregion 53 | 54 | //region Remove 55 | fun removeNotification(userID: String, notificationID: String) { 56 | firebaseDatabaseService.removeNotification(userID, notificationID) 57 | } 58 | 59 | fun removeFriend(userID: String, friendID: String) { 60 | firebaseDatabaseService.removeFriend(userID, friendID) 61 | } 62 | 63 | fun removeSentRequest(otherUserID: String, myUserID: String) { 64 | firebaseDatabaseService.removeSentRequest(otherUserID, myUserID) 65 | } 66 | 67 | fun removeChat(chatID: String) { 68 | firebaseDatabaseService.removeChat(chatID) 69 | } 70 | 71 | fun removeMessages(messagesID: String){ 72 | firebaseDatabaseService.removeMessages(messagesID) 73 | } 74 | 75 | //endregion 76 | 77 | //region Load Single 78 | 79 | fun loadUser(userID: String, b: ((Result) -> Unit)) { 80 | firebaseDatabaseService.loadUserTask(userID).addOnSuccessListener { 81 | b.invoke(Result.Success(wrapSnapshotToClass(User::class.java, it))) 82 | }.addOnFailureListener { b.invoke(Result.Error(it.message)) } 83 | } 84 | 85 | fun loadUserInfo(userID: String, b: ((Result) -> Unit)) { 86 | firebaseDatabaseService.loadUserInfoTask(userID).addOnSuccessListener { 87 | b.invoke(Result.Success(wrapSnapshotToClass(UserInfo::class.java, it))) 88 | }.addOnFailureListener { b.invoke(Result.Error(it.message)) } 89 | } 90 | 91 | fun loadChat(chatID: String, b: ((Result) -> Unit)) { 92 | firebaseDatabaseService.loadChatTask(chatID).addOnSuccessListener { 93 | b.invoke(Result.Success(wrapSnapshotToClass(Chat::class.java, it))) 94 | }.addOnFailureListener { b.invoke(Result.Error(it.message)) } 95 | } 96 | 97 | //endregion 98 | 99 | //region Load List 100 | 101 | fun loadUsers(b: ((Result>) -> Unit)) { 102 | b.invoke(Result.Loading) 103 | firebaseDatabaseService.loadUsersTask().addOnSuccessListener { 104 | val usersList = wrapSnapshotToArrayList(User::class.java, it) 105 | b.invoke(Result.Success(usersList)) 106 | }.addOnFailureListener { b.invoke(Result.Error(it.message)) } 107 | } 108 | 109 | fun loadFriends(userID: String, b: ((Result>) -> Unit)) { 110 | b.invoke(Result.Loading) 111 | firebaseDatabaseService.loadFriendsTask(userID).addOnSuccessListener { 112 | val friendsList = wrapSnapshotToArrayList(UserFriend::class.java, it) 113 | b.invoke(Result.Success(friendsList)) 114 | }.addOnFailureListener { b.invoke(Result.Error(it.message)) } 115 | } 116 | 117 | fun loadNotifications(userID: String, b: ((Result>) -> Unit)) { 118 | b.invoke(Result.Loading) 119 | firebaseDatabaseService.loadNotificationsTask(userID).addOnSuccessListener { 120 | val notificationsList = wrapSnapshotToArrayList(UserNotification::class.java, it) 121 | b.invoke(Result.Success(notificationsList)) 122 | }.addOnFailureListener { b.invoke(Result.Error(it.message)) } 123 | } 124 | 125 | //endregion 126 | 127 | //#region Load and Observe 128 | 129 | fun loadAndObserveUser(userID: String, observer: FirebaseReferenceValueObserver, b: ((Result) -> Unit)) { 130 | firebaseDatabaseService.attachUserObserver(User::class.java, userID, observer, b) 131 | } 132 | 133 | fun loadAndObserveUserInfo(userID: String, observer: FirebaseReferenceValueObserver, b: ((Result) -> Unit)) { 134 | firebaseDatabaseService.attachUserInfoObserver(UserInfo::class.java, userID, observer, b) 135 | } 136 | 137 | fun loadAndObserveUserNotifications(userID: String, observer: FirebaseReferenceValueObserver, b: ((Result>) -> Unit)){ 138 | firebaseDatabaseService.attachUserNotificationsObserver(UserNotification::class.java, userID, observer, b) 139 | } 140 | 141 | fun loadAndObserveMessagesAdded(messagesID: String, observer: FirebaseReferenceChildObserver, b: ((Result) -> Unit)) { 142 | firebaseDatabaseService.attachMessagesObserver(Message::class.java, messagesID, observer, b) 143 | } 144 | 145 | fun loadAndObserveChat(chatID: String, observer: FirebaseReferenceValueObserver, b: ((Result) -> Unit)) { 146 | firebaseDatabaseService.attachChatObserver(Chat::class.java, chatID, observer, b) 147 | } 148 | 149 | //endregion 150 | } 151 | 152 | -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/data/db/repository/StorageRepository.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app.data.db.repository 2 | 3 | import android.net.Uri 4 | import com.fredrikbogg.android_chat_app.data.db.remote.FirebaseStorageSource 5 | import com.fredrikbogg.android_chat_app.data.Result 6 | 7 | class StorageRepository { 8 | private val firebaseStorageService = FirebaseStorageSource() 9 | 10 | fun updateUserProfileImage(userID: String, byteArray: ByteArray, b: (Result) -> Unit) { 11 | b.invoke(Result.Loading) 12 | firebaseStorageService.uploadUserImage(userID, byteArray).addOnSuccessListener { 13 | b.invoke((Result.Success(it))) 14 | }.addOnFailureListener { 15 | b.invoke(Result.Error(it.message)) 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/data/model/ChatWithUserInfo.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app.data.model 2 | 3 | import com.fredrikbogg.android_chat_app.data.db.entity.Chat 4 | import com.fredrikbogg.android_chat_app.data.db.entity.UserInfo 5 | 6 | data class ChatWithUserInfo( 7 | var mChat: Chat, 8 | var mUserInfo: UserInfo 9 | ) 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/data/model/CreateUser.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app.data.model 2 | 3 | data class CreateUser( 4 | var displayName: String = "", 5 | var email: String = "", 6 | var password: String = "" 7 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/data/model/Login.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app.data.model 2 | 3 | data class Login( 4 | var email: String = "", 5 | var password: String = "" 6 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/ui/DefaultBindings.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app.ui 2 | 3 | import android.annotation.SuppressLint 4 | import android.widget.ImageView 5 | import android.widget.TextView 6 | import androidx.databinding.BindingAdapter 7 | import androidx.recyclerview.widget.RecyclerView 8 | import com.fredrikbogg.android_chat_app.R 9 | import com.squareup.picasso.Picasso 10 | import jp.wasabeef.picasso.transformations.BlurTransformation 11 | import java.text.SimpleDateFormat 12 | import java.util.* 13 | import java.util.concurrent.TimeUnit 14 | 15 | 16 | @BindingAdapter("bind_image_url_blur") 17 | fun bindBlurImageWithPicasso(imageView: ImageView, url: String?) { 18 | if (!url.isNullOrBlank()) { 19 | Picasso.get().load(url).error(R.drawable.ic_baseline_error_24) 20 | .transform(BlurTransformation(imageView.context, 15, 1)).into(imageView) 21 | } 22 | } 23 | 24 | @BindingAdapter("bind_image_url") 25 | fun bindImageWithPicasso(imageView: ImageView, url: String?) { 26 | when (url) { 27 | null -> Unit 28 | "" -> imageView.setBackgroundResource(R.drawable.ic_baseline_person_24) 29 | else -> Picasso.get().load(url).error(R.drawable.ic_baseline_error_24).into(imageView) 30 | } 31 | } 32 | 33 | @SuppressLint("SimpleDateFormat") 34 | @BindingAdapter("bind_epochTimeMsToDate_with_days_ago") 35 | fun TextView.bindEpochTimeMsToDateWithDaysAgo(epochTimeMs: Long) { 36 | val numOfDays = TimeUnit.MILLISECONDS.toDays(Date().time - epochTimeMs) 37 | 38 | this.text = when { 39 | numOfDays == 1.toLong() -> "Yesterday" 40 | numOfDays > 1.toLong() -> "$numOfDays days ago" 41 | else -> { 42 | val pat = 43 | SimpleDateFormat().toLocalizedPattern().replace("\\W?[YyMd]+\\W?".toRegex(), " ") 44 | val formatter = SimpleDateFormat(pat, Locale.getDefault()) 45 | formatter.format(Date(epochTimeMs)) 46 | } 47 | } 48 | } 49 | 50 | @SuppressLint("SimpleDateFormat") 51 | @BindingAdapter("bind_epochTimeMsToDate") 52 | fun TextView.bindEpochTimeMsToDate(epochTimeMs: Long) { 53 | if (epochTimeMs > 0) { 54 | val currentTimeMs = Date().time 55 | val numOfDays = TimeUnit.MILLISECONDS.toDays(currentTimeMs - epochTimeMs) 56 | 57 | val replacePattern = when { 58 | numOfDays >= 1.toLong() -> "Yy" 59 | else -> "YyMd" 60 | } 61 | val pat = SimpleDateFormat().toLocalizedPattern().replace("\\W?[$replacePattern]+\\W?".toRegex(), " ") 62 | val formatter = SimpleDateFormat(pat, Locale.getDefault()) 63 | this.text = formatter.format(Date(epochTimeMs)) 64 | } 65 | } 66 | 67 | @BindingAdapter("bind_disable_item_animator") 68 | fun bindDisableRecyclerViewItemAnimator(recyclerView: RecyclerView, disable: Boolean) { 69 | if (disable) { 70 | recyclerView.itemAnimator = null 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/ui/DefaultViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app.ui 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import com.fredrikbogg.android_chat_app.data.Event 7 | import com.fredrikbogg.android_chat_app.data.Result 8 | 9 | abstract class DefaultViewModel : ViewModel() { 10 | protected val mSnackBarText = MutableLiveData>() 11 | val snackBarText: LiveData> = mSnackBarText 12 | 13 | private val mDataLoading = MutableLiveData>() 14 | val dataLoading: LiveData> = mDataLoading 15 | 16 | protected fun onResult(mutableLiveData: MutableLiveData? = null, result: Result) { 17 | when (result) { 18 | is Result.Loading -> mDataLoading.value = Event(true) 19 | 20 | is Result.Error -> { 21 | mDataLoading.value = Event(false) 22 | result.msg?.let { mSnackBarText.value = Event(it) } 23 | } 24 | 25 | is Result.Success -> { 26 | mDataLoading.value = Event(false) 27 | result.data?.let { mutableLiveData?.value = it } 28 | result.msg?.let { mSnackBarText.value = Event(it) } 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/ui/chat/ChatFragment.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app.ui.chat 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.MenuItem 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import androidx.appcompat.app.AppCompatActivity 9 | import androidx.fragment.app.Fragment 10 | import androidx.fragment.app.viewModels 11 | import androidx.navigation.fragment.findNavController 12 | import androidx.recyclerview.widget.RecyclerView 13 | import com.fredrikbogg.android_chat_app.databinding.FragmentChatBinding 14 | import com.fredrikbogg.android_chat_app.databinding.ToolbarAddonChatBinding 15 | import kotlinx.android.synthetic.main.fragment_chat.* 16 | 17 | 18 | class ChatFragment : Fragment() { 19 | 20 | companion object { 21 | const val ARGS_KEY_USER_ID = "bundle_user_id" 22 | const val ARGS_KEY_OTHER_USER_ID = "bundle_other_user_id" 23 | const val ARGS_KEY_CHAT_ID = "bundle_other_chat_id" 24 | } 25 | 26 | private val viewModel: ChatViewModel by viewModels { 27 | ChatViewModelFactory( 28 | requireArguments().getString(ARGS_KEY_USER_ID)!!, 29 | requireArguments().getString(ARGS_KEY_OTHER_USER_ID)!!, 30 | requireArguments().getString(ARGS_KEY_CHAT_ID)!! 31 | ) 32 | } 33 | 34 | private lateinit var viewDataBinding: FragmentChatBinding 35 | private lateinit var listAdapter: MessagesListAdapter 36 | private lateinit var listAdapterObserver: RecyclerView.AdapterDataObserver 37 | private lateinit var toolbarAddonChatBinding: ToolbarAddonChatBinding 38 | 39 | override fun onDestroy() { 40 | super.onDestroy() 41 | removeCustomToolbar() 42 | } 43 | 44 | override fun onCreateView( 45 | inflater: LayoutInflater, container: ViewGroup?, 46 | savedInstanceState: Bundle? 47 | ): View? { 48 | viewDataBinding = 49 | FragmentChatBinding.inflate(inflater, container, false).apply { viewmodel = viewModel } 50 | viewDataBinding.lifecycleOwner = this.viewLifecycleOwner 51 | setHasOptionsMenu(true) 52 | 53 | toolbarAddonChatBinding = 54 | ToolbarAddonChatBinding.inflate(inflater, container, false) 55 | .apply { viewmodel = viewModel } 56 | toolbarAddonChatBinding.lifecycleOwner = this.viewLifecycleOwner 57 | 58 | return viewDataBinding.root 59 | } 60 | 61 | override fun onActivityCreated(savedInstanceState: Bundle?) { 62 | super.onActivityCreated(savedInstanceState) 63 | setupCustomToolbar() 64 | setupListAdapter() 65 | } 66 | 67 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 68 | when (item.itemId) { 69 | android.R.id.home -> { 70 | findNavController().popBackStack() 71 | return true 72 | } 73 | } 74 | return super.onOptionsItemSelected(item) 75 | } 76 | 77 | private fun removeCustomToolbar() { 78 | val supportActionBar = (activity as AppCompatActivity?)!!.supportActionBar 79 | supportActionBar!!.setDisplayShowCustomEnabled(false) 80 | supportActionBar.customView = null 81 | } 82 | 83 | private fun setupCustomToolbar() { 84 | val supportActionBar = (activity as AppCompatActivity?)!!.supportActionBar 85 | supportActionBar!!.setDisplayShowCustomEnabled(true) 86 | supportActionBar.customView = toolbarAddonChatBinding.root 87 | } 88 | 89 | private fun setupListAdapter() { 90 | val viewModel = viewDataBinding.viewmodel 91 | if (viewModel != null) { 92 | listAdapterObserver = (object : RecyclerView.AdapterDataObserver() { 93 | override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { 94 | messagesRecyclerView.scrollToPosition(positionStart) 95 | } 96 | }) 97 | listAdapter = 98 | MessagesListAdapter(viewModel, requireArguments().getString(ARGS_KEY_USER_ID)!!) 99 | listAdapter.registerAdapterDataObserver(listAdapterObserver) 100 | viewDataBinding.messagesRecyclerView.adapter = listAdapter 101 | } else { 102 | throw Exception("The viewmodel is not initialized") 103 | } 104 | } 105 | 106 | override fun onDestroyView() { 107 | super.onDestroyView() 108 | listAdapter.unregisterAdapterDataObserver(listAdapterObserver) 109 | } 110 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/ui/chat/ChatViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app.ui.chat 2 | 3 | import androidx.lifecycle.* 4 | import com.fredrikbogg.android_chat_app.data.db.entity.Chat 5 | import com.fredrikbogg.android_chat_app.data.db.entity.Message 6 | import com.fredrikbogg.android_chat_app.data.db.entity.UserInfo 7 | import com.fredrikbogg.android_chat_app.data.db.remote.FirebaseReferenceChildObserver 8 | import com.fredrikbogg.android_chat_app.data.db.remote.FirebaseReferenceValueObserver 9 | import com.fredrikbogg.android_chat_app.data.db.repository.DatabaseRepository 10 | import com.fredrikbogg.android_chat_app.ui.DefaultViewModel 11 | import com.fredrikbogg.android_chat_app.data.Result 12 | import com.fredrikbogg.android_chat_app.util.addNewItem 13 | 14 | class ChatViewModelFactory(private val myUserID: String, private val otherUserID: String, private val chatID: String) : 15 | ViewModelProvider.Factory { 16 | override fun create(modelClass: Class): T { 17 | return ChatViewModel(myUserID, otherUserID, chatID) as T 18 | } 19 | } 20 | 21 | class ChatViewModel(private val myUserID: String, private val otherUserID: String, private val chatID: String) : DefaultViewModel() { 22 | 23 | private val dbRepository: DatabaseRepository = DatabaseRepository() 24 | 25 | private val _otherUser: MutableLiveData = MutableLiveData() 26 | private val _addedMessage = MutableLiveData() 27 | 28 | private val fbRefMessagesChildObserver = FirebaseReferenceChildObserver() 29 | private val fbRefUserInfoObserver = FirebaseReferenceValueObserver() 30 | 31 | val messagesList = MediatorLiveData>() 32 | val newMessageText = MutableLiveData() 33 | val otherUser: LiveData = _otherUser 34 | 35 | init { 36 | setupChat() 37 | checkAndUpdateLastMessageSeen() 38 | } 39 | 40 | override fun onCleared() { 41 | super.onCleared() 42 | fbRefMessagesChildObserver.clear() 43 | fbRefUserInfoObserver.clear() 44 | } 45 | 46 | private fun checkAndUpdateLastMessageSeen() { 47 | dbRepository.loadChat(chatID) { result: Result -> 48 | if (result is Result.Success && result.data != null) { 49 | result.data.lastMessage.let { 50 | if (!it.seen && it.senderID != myUserID) { 51 | it.seen = true 52 | dbRepository.updateChatLastMessage(chatID, it) 53 | } 54 | } 55 | } 56 | } 57 | } 58 | 59 | private fun setupChat() { 60 | dbRepository.loadAndObserveUserInfo(otherUserID, fbRefUserInfoObserver) { result: Result -> 61 | onResult(_otherUser, result) 62 | if (result is Result.Success && !fbRefMessagesChildObserver.isObserving()) { 63 | loadAndObserveNewMessages() 64 | } 65 | } 66 | } 67 | 68 | private fun loadAndObserveNewMessages() { 69 | messagesList.addSource(_addedMessage) { messagesList.addNewItem(it) } 70 | 71 | dbRepository.loadAndObserveMessagesAdded( 72 | chatID, 73 | fbRefMessagesChildObserver 74 | ) { result: Result -> 75 | onResult(_addedMessage, result) 76 | } 77 | } 78 | 79 | fun sendMessagePressed() { 80 | if (!newMessageText.value.isNullOrBlank()) { 81 | val newMsg = Message(myUserID, newMessageText.value!!) 82 | dbRepository.updateNewMessage(chatID, newMsg) 83 | dbRepository.updateChatLastMessage(chatID, newMsg) 84 | newMessageText.value = null 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/ui/chat/MessagesBindings.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app.ui.chat 2 | 3 | import android.view.View 4 | import androidx.databinding.BindingAdapter 5 | import androidx.recyclerview.widget.RecyclerView 6 | import com.fredrikbogg.android_chat_app.data.db.entity.Message 7 | import kotlin.math.abs 8 | 9 | @BindingAdapter("bind_messages_list") 10 | fun bindMessagesList(listView: RecyclerView, items: List?) { 11 | items?.let { 12 | (listView.adapter as MessagesListAdapter).submitList(items) 13 | listView.scrollToPosition(items.size - 1) 14 | } 15 | } 16 | 17 | @BindingAdapter("bind_message", "bind_message_viewModel") 18 | fun View.bindShouldMessageShowTimeText(message: Message, viewModel: ChatViewModel) { 19 | val halfHourInMilli = 1800000 20 | val index = viewModel.messagesList.value!!.indexOf(message) 21 | 22 | if (index == 0) { 23 | this.visibility = View.VISIBLE 24 | } else { 25 | val messageBefore = viewModel.messagesList.value!![index - 1] 26 | 27 | if (abs(messageBefore.epochTimeMs - message.epochTimeMs) > halfHourInMilli) { 28 | this.visibility = View.VISIBLE 29 | } else { 30 | this.visibility = View.GONE 31 | } 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/ui/chat/MessagesListAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app.ui.chat 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.recyclerview.widget.DiffUtil 6 | import androidx.recyclerview.widget.ListAdapter 7 | import androidx.recyclerview.widget.RecyclerView 8 | import com.fredrikbogg.android_chat_app.data.db.entity.Message 9 | import com.fredrikbogg.android_chat_app.databinding.ListItemMessageReceivedBinding 10 | import com.fredrikbogg.android_chat_app.databinding.ListItemMessageSentBinding 11 | 12 | class MessagesListAdapter internal constructor(private val viewModel: ChatViewModel, private val userId: String) : ListAdapter(MessageDiffCallback()) { 13 | 14 | private val holderTypeMessageReceived = 1 15 | private val holderTypeMessageSent = 2 16 | 17 | class ReceivedViewHolder(private val binding: ListItemMessageReceivedBinding) : 18 | RecyclerView.ViewHolder(binding.root) { 19 | fun bind(viewModel: ChatViewModel, item: Message) { 20 | binding.viewmodel = viewModel 21 | binding.message = item 22 | binding.executePendingBindings() 23 | } 24 | } 25 | 26 | class SentViewHolder(private val binding: ListItemMessageSentBinding) : 27 | RecyclerView.ViewHolder(binding.root) { 28 | fun bind(viewModel: ChatViewModel, item: Message) { 29 | binding.viewmodel = viewModel 30 | binding.message = item 31 | binding.executePendingBindings() 32 | } 33 | } 34 | 35 | override fun getItemViewType(position: Int): Int { 36 | return if (getItem(position).senderID != userId) { 37 | holderTypeMessageReceived 38 | } else { 39 | holderTypeMessageSent 40 | } 41 | } 42 | 43 | override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { 44 | when (holder.itemViewType) { 45 | holderTypeMessageSent -> (holder as SentViewHolder).bind( 46 | viewModel, 47 | getItem(position) 48 | ) 49 | holderTypeMessageReceived -> (holder as ReceivedViewHolder).bind( 50 | viewModel, 51 | getItem(position) 52 | ) 53 | } 54 | } 55 | 56 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { 57 | val layoutInflater = LayoutInflater.from(parent.context) 58 | 59 | return when (viewType) { 60 | holderTypeMessageSent -> { 61 | val binding = ListItemMessageSentBinding.inflate(layoutInflater, parent, false) 62 | SentViewHolder(binding) 63 | } 64 | holderTypeMessageReceived -> { 65 | val binding = ListItemMessageReceivedBinding.inflate(layoutInflater, parent, false) 66 | ReceivedViewHolder(binding) 67 | } 68 | else -> { 69 | throw Exception("Error reading holder type") 70 | } 71 | } 72 | } 73 | } 74 | 75 | class MessageDiffCallback : DiffUtil.ItemCallback() { 76 | override fun areItemsTheSame(oldItem: Message, newItem: Message): Boolean { 77 | return oldItem == newItem 78 | } 79 | 80 | override fun areContentsTheSame(oldItem: Message, newItem: Message): Boolean { 81 | return oldItem.epochTimeMs == newItem.epochTimeMs 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/ui/chats/ChatsBindings.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package com.fredrikbogg.android_chat_app.ui.chats 4 | 5 | import android.view.View 6 | import android.widget.TextView 7 | import androidx.databinding.BindingAdapter 8 | import androidx.recyclerview.widget.RecyclerView 9 | import com.fredrikbogg.android_chat_app.R 10 | import com.fredrikbogg.android_chat_app.data.model.ChatWithUserInfo 11 | import com.fredrikbogg.android_chat_app.data.db.entity.Message 12 | 13 | @BindingAdapter("bind_chats_list") 14 | fun bindChatsList(listView: RecyclerView, items: List?) { 15 | items?.let { (listView.adapter as ChatsListAdapter).submitList(items) } 16 | } 17 | 18 | @BindingAdapter("bind_chat_message_text", "bind_chat_message_text_viewModel") 19 | fun TextView.bindMessageYouToText(message: Message, viewModel: ChatsViewModel) { 20 | this.text = if (message.senderID == viewModel.myUserID) { 21 | "You: " + message.text 22 | } else { 23 | message.text 24 | } 25 | } 26 | 27 | @BindingAdapter("bind_message_view", "bind_message_textView", "bind_message", "bind_myUserID") 28 | fun View.bindMessageSeen(view: View, textView: TextView, message: Message, myUserID: String) { 29 | if (message.senderID != myUserID && !message.seen) { 30 | view.visibility = View.VISIBLE 31 | textView.setTextAppearance(R.style.MessageNotSeen) 32 | // textView.alpha = 1f 33 | } else { 34 | view.visibility = View.INVISIBLE 35 | textView.setTextAppearance(R.style.MessageSeen) 36 | // textView.alpha = 1f 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/ui/chats/ChatsFragment.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app.ui.chats 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.core.os.bundleOf 8 | import androidx.fragment.app.Fragment 9 | import androidx.fragment.app.viewModels 10 | import androidx.navigation.fragment.findNavController 11 | import com.fredrikbogg.android_chat_app.App 12 | import com.fredrikbogg.android_chat_app.R 13 | import com.fredrikbogg.android_chat_app.data.model.ChatWithUserInfo 14 | import com.fredrikbogg.android_chat_app.databinding.FragmentChatsBinding 15 | import com.fredrikbogg.android_chat_app.data.EventObserver 16 | import com.fredrikbogg.android_chat_app.ui.chat.ChatFragment 17 | import com.fredrikbogg.android_chat_app.util.convertTwoUserIDs 18 | 19 | class ChatsFragment : Fragment() { 20 | 21 | private val viewModel: ChatsViewModel by viewModels { ChatsViewModelFactory(App.myUserID) } 22 | private lateinit var viewDataBinding: FragmentChatsBinding 23 | private lateinit var listAdapter: ChatsListAdapter 24 | 25 | override fun onCreateView( 26 | inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? 27 | ): View? { 28 | viewDataBinding = 29 | FragmentChatsBinding.inflate(inflater, container, false).apply { viewmodel = viewModel } 30 | viewDataBinding.lifecycleOwner = this.viewLifecycleOwner 31 | return viewDataBinding.root 32 | } 33 | 34 | override fun onActivityCreated(savedInstanceState: Bundle?) { 35 | super.onActivityCreated(savedInstanceState) 36 | setupListAdapter() 37 | setupObservers() 38 | } 39 | 40 | private fun setupListAdapter() { 41 | val viewModel = viewDataBinding.viewmodel 42 | if (viewModel != null) { 43 | listAdapter = ChatsListAdapter(viewModel) 44 | viewDataBinding.chatsRecyclerView.adapter = listAdapter 45 | } else { 46 | throw Exception("The viewmodel is not initialized") 47 | } 48 | } 49 | 50 | private fun setupObservers() { 51 | viewModel.selectedChat.observe(viewLifecycleOwner, 52 | EventObserver { navigateToChat(it) }) 53 | } 54 | 55 | private fun navigateToChat(chatWithUserInfo: ChatWithUserInfo) { 56 | val bundle = bundleOf( 57 | ChatFragment.ARGS_KEY_USER_ID to App.myUserID, 58 | ChatFragment.ARGS_KEY_OTHER_USER_ID to chatWithUserInfo.mUserInfo.id, 59 | ChatFragment.ARGS_KEY_CHAT_ID to convertTwoUserIDs(App.myUserID, chatWithUserInfo.mUserInfo.id) 60 | ) 61 | findNavController().navigate(R.id.action_navigation_chats_to_chatFragment, bundle) 62 | } 63 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/ui/chats/ChatsListAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app.ui.chats 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.recyclerview.widget.DiffUtil 6 | import androidx.recyclerview.widget.ListAdapter 7 | import androidx.recyclerview.widget.RecyclerView 8 | import com.fredrikbogg.android_chat_app.data.model.ChatWithUserInfo 9 | import com.fredrikbogg.android_chat_app.databinding.ListItemChatBinding 10 | 11 | class ChatsListAdapter internal constructor(private val viewModel: ChatsViewModel) : 12 | ListAdapter<(ChatWithUserInfo), ChatsListAdapter.ViewHolder>(ChatDiffCallback()) { 13 | 14 | class ViewHolder(private val binding: ListItemChatBinding) : 15 | RecyclerView.ViewHolder(binding.root) { 16 | fun bind(viewModel: ChatsViewModel, item: ChatWithUserInfo) { 17 | binding.viewmodel = viewModel 18 | binding.chatwithuserinfo = item 19 | binding.executePendingBindings() 20 | } 21 | } 22 | 23 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 24 | holder.bind(viewModel, getItem(position)) 25 | } 26 | 27 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 28 | val layoutInflater = LayoutInflater.from(parent.context) 29 | val binding = ListItemChatBinding.inflate(layoutInflater, parent, false) 30 | return ViewHolder(binding) 31 | } 32 | } 33 | 34 | class ChatDiffCallback : DiffUtil.ItemCallback() { 35 | override fun areItemsTheSame(oldItem: ChatWithUserInfo, itemWithUserInfo: ChatWithUserInfo): Boolean { 36 | return oldItem == itemWithUserInfo 37 | } 38 | 39 | override fun areContentsTheSame(oldItem: ChatWithUserInfo, itemWithUserInfo: ChatWithUserInfo): Boolean { 40 | return oldItem.mChat.info.id == itemWithUserInfo.mChat.info.id 41 | } 42 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/ui/chats/ChatsViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app.ui.chats 2 | 3 | import androidx.lifecycle.* 4 | import com.fredrikbogg.android_chat_app.data.Event 5 | import com.fredrikbogg.android_chat_app.data.Result 6 | import com.fredrikbogg.android_chat_app.data.db.entity.Chat 7 | import com.fredrikbogg.android_chat_app.data.model.ChatWithUserInfo 8 | import com.fredrikbogg.android_chat_app.data.db.entity.UserFriend 9 | import com.fredrikbogg.android_chat_app.data.db.entity.UserInfo 10 | import com.fredrikbogg.android_chat_app.data.db.remote.FirebaseReferenceValueObserver 11 | import com.fredrikbogg.android_chat_app.data.db.repository.DatabaseRepository 12 | import com.fredrikbogg.android_chat_app.ui.DefaultViewModel 13 | import com.fredrikbogg.android_chat_app.util.addNewItem 14 | import com.fredrikbogg.android_chat_app.util.convertTwoUserIDs 15 | import com.fredrikbogg.android_chat_app.util.updateItemAt 16 | 17 | 18 | class ChatsViewModelFactory(private val myUserID: String) : 19 | ViewModelProvider.Factory { 20 | override fun create(modelClass: Class): T { 21 | return ChatsViewModel(myUserID) as T 22 | } 23 | } 24 | 25 | class ChatsViewModel(val myUserID: String) : DefaultViewModel() { 26 | 27 | private val repository: DatabaseRepository = DatabaseRepository() 28 | private val firebaseReferenceObserverList = ArrayList() 29 | private val _updatedChatWithUserInfo = MutableLiveData() 30 | private val _selectedChat = MutableLiveData>() 31 | 32 | var selectedChat: LiveData> = _selectedChat 33 | val chatsList = MediatorLiveData>() 34 | 35 | init { 36 | chatsList.addSource(_updatedChatWithUserInfo) { newChat -> 37 | val chat = chatsList.value?.find { it.mChat.info.id == newChat.mChat.info.id } 38 | if (chat == null) { 39 | chatsList.addNewItem(newChat) 40 | } else { 41 | chatsList.updateItemAt(newChat, chatsList.value!!.indexOf(chat)) 42 | } 43 | } 44 | setupChats() 45 | } 46 | 47 | override fun onCleared() { 48 | super.onCleared() 49 | firebaseReferenceObserverList.forEach { it.clear() } 50 | } 51 | 52 | private fun setupChats() { 53 | loadFriends() 54 | } 55 | 56 | private fun loadFriends() { 57 | repository.loadFriends(myUserID) { result: Result> -> 58 | onResult(null, result) 59 | if (result is Result.Success) result.data?.forEach { loadUserInfo(it) } 60 | } 61 | } 62 | 63 | private fun loadUserInfo(userFriend: UserFriend) { 64 | repository.loadUserInfo(userFriend.userID) { result: Result -> 65 | onResult(null, result) 66 | if (result is Result.Success) result.data?.let { loadAndObserveChat(it) } 67 | } 68 | } 69 | 70 | private fun loadAndObserveChat(userInfo: UserInfo) { 71 | val observer = FirebaseReferenceValueObserver() 72 | firebaseReferenceObserverList.add(observer) 73 | repository.loadAndObserveChat(convertTwoUserIDs(myUserID, userInfo.id), observer) { result: Result -> 74 | if (result is Result.Success) { 75 | _updatedChatWithUserInfo.value = result.data?.let { ChatWithUserInfo(it, userInfo) } 76 | } else if (result is Result.Error) { 77 | chatsList.value?.let { 78 | val newList = mutableListOf().apply { addAll(it) } 79 | newList.removeIf { it2 -> result.msg.toString().contains(it2.mUserInfo.id) } 80 | chatsList.value = newList 81 | } 82 | } 83 | } 84 | } 85 | 86 | fun selectChatWithUserInfoPressed(chat: ChatWithUserInfo) { 87 | _selectedChat.value = Event(chat) 88 | } 89 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/ui/main/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app.ui.main 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import android.widget.ProgressBar 6 | import androidx.activity.viewModels 7 | import androidx.appcompat.app.AppCompatActivity 8 | import androidx.appcompat.widget.Toolbar 9 | import androidx.navigation.findNavController 10 | import androidx.navigation.ui.AppBarConfiguration 11 | import androidx.navigation.ui.setupActionBarWithNavController 12 | import androidx.navigation.ui.setupWithNavController 13 | import com.fredrikbogg.android_chat_app.R 14 | import com.fredrikbogg.android_chat_app.data.db.remote.FirebaseDataSource 15 | import com.fredrikbogg.android_chat_app.util.forceHideKeyboard 16 | import com.google.android.material.badge.BadgeDrawable 17 | import com.google.android.material.bottomnavigation.BottomNavigationView 18 | 19 | 20 | class MainActivity : AppCompatActivity() { 21 | 22 | private lateinit var navView: BottomNavigationView 23 | private lateinit var mainProgressBar: ProgressBar 24 | private lateinit var mainToolbar: Toolbar 25 | private lateinit var notificationsBadge: BadgeDrawable 26 | private val viewModel: MainViewModel by viewModels() 27 | 28 | override fun onCreate(savedInstanceState: Bundle?) { 29 | super.onCreate(savedInstanceState) 30 | setContentView(R.layout.activity_main) 31 | 32 | mainToolbar = findViewById(R.id.main_toolbar) 33 | navView = findViewById(R.id.nav_view) 34 | mainProgressBar = findViewById(R.id.main_progressBar) 35 | 36 | notificationsBadge = 37 | navView.getOrCreateBadge(R.id.navigation_notifications).apply { isVisible = false } 38 | 39 | setSupportActionBar(mainToolbar) 40 | 41 | val navController = findNavController(R.id.nav_host_fragment) 42 | navController.addOnDestinationChangedListener { _, destination, _ -> 43 | 44 | when (destination.id) { 45 | R.id.profileFragment -> navView.visibility = View.GONE 46 | R.id.chatFragment -> navView.visibility = View.GONE 47 | R.id.startFragment -> navView.visibility = View.GONE 48 | R.id.loginFragment -> navView.visibility = View.GONE 49 | R.id.createAccountFragment -> navView.visibility = View.GONE 50 | else -> navView.visibility = View.VISIBLE 51 | } 52 | showGlobalProgressBar(false) 53 | currentFocus?.rootView?.forceHideKeyboard() 54 | } 55 | 56 | val appBarConfiguration = AppBarConfiguration( 57 | setOf( 58 | R.id.navigation_chats, 59 | R.id.navigation_notifications, 60 | R.id.navigation_users, 61 | R.id.navigation_settings, 62 | R.id.startFragment 63 | ) 64 | ) 65 | 66 | setupActionBarWithNavController(navController, appBarConfiguration) 67 | navView.setupWithNavController(navController) 68 | } 69 | 70 | override fun onPause() { 71 | super.onPause() 72 | FirebaseDataSource.dbInstance.goOffline() 73 | } 74 | 75 | override fun onResume() { 76 | FirebaseDataSource.dbInstance.goOnline() 77 | setupViewModelObservers() 78 | super.onResume() 79 | } 80 | 81 | private fun setupViewModelObservers() { 82 | viewModel.userNotificationsList.observe(this, { 83 | if (it.size > 0) { 84 | notificationsBadge.number = it.size 85 | notificationsBadge.isVisible = true 86 | } else { 87 | notificationsBadge.isVisible = false 88 | } 89 | }) 90 | } 91 | 92 | fun showGlobalProgressBar(show: Boolean) { 93 | if (show) mainProgressBar.visibility = View.VISIBLE 94 | else mainProgressBar.visibility = View.GONE 95 | } 96 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/ui/main/MainViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app.ui.main 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import com.fredrikbogg.android_chat_app.App 7 | import com.fredrikbogg.android_chat_app.data.db.entity.UserNotification 8 | import com.fredrikbogg.android_chat_app.data.db.remote.FirebaseAuthStateObserver 9 | import com.fredrikbogg.android_chat_app.data.db.remote.FirebaseReferenceConnectedObserver 10 | import com.fredrikbogg.android_chat_app.data.db.remote.FirebaseReferenceValueObserver 11 | import com.fredrikbogg.android_chat_app.data.db.repository.AuthRepository 12 | import com.fredrikbogg.android_chat_app.data.db.repository.DatabaseRepository 13 | import com.fredrikbogg.android_chat_app.data.Result 14 | import com.google.firebase.auth.FirebaseUser 15 | 16 | 17 | class MainViewModel : ViewModel() { 18 | 19 | private val dbRepository = DatabaseRepository() 20 | private val authRepository = AuthRepository() 21 | 22 | private val _userNotificationsList = MutableLiveData>() 23 | 24 | private val fbRefNotificationsObserver = FirebaseReferenceValueObserver() 25 | private val fbAuthStateObserver = FirebaseAuthStateObserver() 26 | private val fbRefConnectedObserver = FirebaseReferenceConnectedObserver() 27 | private var userID = App.myUserID 28 | 29 | var userNotificationsList: LiveData> = _userNotificationsList 30 | 31 | init { 32 | setupAuthObserver() 33 | } 34 | 35 | override fun onCleared() { 36 | super.onCleared() 37 | fbRefNotificationsObserver.clear() 38 | fbRefConnectedObserver.clear() 39 | fbAuthStateObserver.clear() 40 | } 41 | 42 | private fun setupAuthObserver(){ 43 | authRepository.observeAuthState(fbAuthStateObserver) { result: Result -> 44 | if (result is Result.Success) { 45 | userID = result.data!!.uid 46 | startObservingNotifications() 47 | fbRefConnectedObserver.start(userID) 48 | } else { 49 | fbRefConnectedObserver.clear() 50 | stopObservingNotifications() 51 | } 52 | } 53 | } 54 | 55 | private fun startObservingNotifications() { 56 | dbRepository.loadAndObserveUserNotifications(userID, fbRefNotificationsObserver) { result: Result> -> 57 | if (result is Result.Success) { 58 | _userNotificationsList.value = result.data 59 | } 60 | } 61 | } 62 | 63 | private fun stopObservingNotifications() { 64 | fbRefNotificationsObserver.clear() 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/ui/notifications/NotificationsBindings.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app.ui.notifications 2 | 3 | import androidx.databinding.BindingAdapter 4 | import androidx.recyclerview.widget.RecyclerView 5 | import com.fredrikbogg.android_chat_app.data.db.entity.UserInfo 6 | 7 | @BindingAdapter("bind_notifications_list") 8 | fun bindNotificationsList(listView: RecyclerView, items: List?) { 9 | items?.let { (listView.adapter as NotificationsListAdapter).submitList(items) } 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/ui/notifications/NotificationsFragment.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app.ui.notifications 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.Fragment 8 | import androidx.fragment.app.viewModels 9 | import com.fredrikbogg.android_chat_app.App 10 | import com.fredrikbogg.android_chat_app.databinding.FragmentNotificationsBinding 11 | 12 | class NotificationsFragment : Fragment() { 13 | 14 | private val viewModel: NotificationsViewModel by viewModels { NotificationsViewModelFactory(App.myUserID) } 15 | private lateinit var viewDataBinding: FragmentNotificationsBinding 16 | private lateinit var listAdapter: NotificationsListAdapter 17 | 18 | override fun onCreateView( 19 | inflater: LayoutInflater, container: ViewGroup?, 20 | savedInstanceState: Bundle? 21 | ): View? { 22 | viewDataBinding = FragmentNotificationsBinding.inflate(inflater, container, false) 23 | .apply { viewmodel = viewModel } 24 | viewDataBinding.lifecycleOwner = this.viewLifecycleOwner 25 | return viewDataBinding.root 26 | } 27 | 28 | override fun onActivityCreated(savedInstanceState: Bundle?) { 29 | super.onActivityCreated(savedInstanceState) 30 | setupListAdapter() 31 | } 32 | 33 | private fun setupListAdapter() { 34 | val viewModel = viewDataBinding.viewmodel 35 | if (viewModel != null) { 36 | listAdapter = NotificationsListAdapter(viewModel) 37 | viewDataBinding.usersRecyclerView.adapter = listAdapter 38 | } else { 39 | throw Exception("The viewmodel is not initialized") 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/ui/notifications/NotificationsListAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app.ui.notifications 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.recyclerview.widget.DiffUtil 6 | import androidx.recyclerview.widget.ListAdapter 7 | import androidx.recyclerview.widget.RecyclerView 8 | import com.fredrikbogg.android_chat_app.data.db.entity.UserInfo 9 | import com.fredrikbogg.android_chat_app.databinding.ListItemNotificationBinding 10 | 11 | 12 | class NotificationsListAdapter internal constructor(private val viewModel: NotificationsViewModel) : 13 | ListAdapter(UserInfoDiffCallback()) { 14 | 15 | class ViewHolder(private val binding: ListItemNotificationBinding) : 16 | RecyclerView.ViewHolder(binding.root) { 17 | fun bind(viewModel: NotificationsViewModel, item: UserInfo) { 18 | binding.viewmodel = viewModel 19 | binding.userinfo = item 20 | binding.executePendingBindings() 21 | } 22 | } 23 | 24 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 25 | holder.bind(viewModel, getItem(position)) 26 | } 27 | 28 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 29 | val layoutInflater = LayoutInflater.from(parent.context) 30 | val binding = ListItemNotificationBinding.inflate(layoutInflater, parent, false) 31 | return ViewHolder(binding) 32 | } 33 | } 34 | 35 | class UserInfoDiffCallback : DiffUtil.ItemCallback() { 36 | override fun areItemsTheSame(oldItem: UserInfo, newItem: UserInfo): Boolean { 37 | return oldItem == newItem 38 | } 39 | 40 | override fun areContentsTheSame(oldItem: UserInfo, newItem: UserInfo): Boolean { 41 | return oldItem.id == newItem.id 42 | } 43 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/ui/notifications/NotificationsViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app.ui.notifications 2 | 3 | import androidx.lifecycle.MediatorLiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import androidx.lifecycle.ViewModelProvider 7 | import com.fredrikbogg.android_chat_app.data.db.entity.* 8 | import com.fredrikbogg.android_chat_app.data.db.repository.DatabaseRepository 9 | import com.fredrikbogg.android_chat_app.ui.DefaultViewModel 10 | import com.fredrikbogg.android_chat_app.data.Result 11 | import com.fredrikbogg.android_chat_app.util.addNewItem 12 | import com.fredrikbogg.android_chat_app.util.removeItem 13 | import com.fredrikbogg.android_chat_app.util.convertTwoUserIDs 14 | 15 | class NotificationsViewModelFactory(private val myUserID: String) : 16 | ViewModelProvider.Factory { 17 | override fun create(modelClass: Class): T { 18 | return NotificationsViewModel(myUserID) as T 19 | } 20 | } 21 | 22 | class NotificationsViewModel(private val myUserID: String) : DefaultViewModel() { 23 | 24 | private val dbRepository: DatabaseRepository = DatabaseRepository() 25 | private val updatedUserInfo = MutableLiveData() 26 | private val userNotificationsList = MutableLiveData>() 27 | 28 | val usersInfoList = MediatorLiveData>() 29 | 30 | init { 31 | usersInfoList.addSource(updatedUserInfo) { usersInfoList.addNewItem(it) } 32 | loadNotifications() 33 | } 34 | 35 | private fun loadNotifications() { 36 | dbRepository.loadNotifications(myUserID) { result: Result> -> 37 | onResult(userNotificationsList, result) 38 | if (result is Result.Success) result.data?.forEach { loadUserInfo(it) } 39 | } 40 | } 41 | 42 | private fun loadUserInfo(userNotification: UserNotification) { 43 | dbRepository.loadUserInfo(userNotification.userID) { result: Result -> 44 | onResult(updatedUserInfo, result) 45 | } 46 | } 47 | 48 | private fun updateNotification(otherUserInfo: UserInfo, removeOnly: Boolean) { 49 | val userNotification = userNotificationsList.value?.find { 50 | it.userID == otherUserInfo.id 51 | } 52 | 53 | if (userNotification != null) { 54 | if (!removeOnly) { 55 | dbRepository.updateNewFriend(UserFriend(myUserID), UserFriend(otherUserInfo.id)) 56 | val newChat = Chat().apply { 57 | info.id = convertTwoUserIDs(myUserID, otherUserInfo.id) 58 | lastMessage = Message(seen = true, text = "Say hello!") 59 | } 60 | dbRepository.updateNewChat(newChat) 61 | } 62 | dbRepository.removeNotification(myUserID, otherUserInfo.id) 63 | dbRepository.removeSentRequest(otherUserInfo.id, myUserID) 64 | 65 | usersInfoList.removeItem(otherUserInfo) 66 | userNotificationsList.removeItem(userNotification) 67 | } 68 | } 69 | 70 | fun acceptNotificationPressed(userInfo: UserInfo) { 71 | updateNotification(userInfo, false) 72 | } 73 | 74 | fun declineNotificationPressed(userInfo: UserInfo) { 75 | updateNotification(userInfo, true) 76 | } 77 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/ui/profile/ProfileFragment.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app.ui.profile 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.MenuItem 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import androidx.fragment.app.Fragment 9 | import androidx.fragment.app.viewModels 10 | import androidx.navigation.fragment.findNavController 11 | import com.fredrikbogg.android_chat_app.App 12 | import com.fredrikbogg.android_chat_app.databinding.FragmentProfileBinding 13 | import com.fredrikbogg.android_chat_app.data.EventObserver 14 | import com.fredrikbogg.android_chat_app.util.showSnackBar 15 | import com.fredrikbogg.android_chat_app.ui.main.MainActivity 16 | import com.fredrikbogg.android_chat_app.util.forceHideKeyboard 17 | 18 | 19 | class ProfileFragment : Fragment() { 20 | 21 | companion object { 22 | const val ARGS_KEY_USER_ID = "bundle_user_id" 23 | } 24 | 25 | private val viewModel: ProfileViewModel by viewModels { 26 | ProfileViewModelFactory(App.myUserID, requireArguments().getString(ARGS_KEY_USER_ID)!!) 27 | } 28 | 29 | private lateinit var viewDataBinding: FragmentProfileBinding 30 | 31 | override fun onCreateView( 32 | inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? 33 | ): View? { 34 | viewDataBinding = FragmentProfileBinding.inflate(inflater, container, false) 35 | .apply { viewmodel = viewModel } 36 | viewDataBinding.lifecycleOwner = this.viewLifecycleOwner 37 | setHasOptionsMenu(true) 38 | return viewDataBinding.root 39 | } 40 | 41 | override fun onActivityCreated(savedInstanceState: Bundle?) { 42 | super.onActivityCreated(savedInstanceState) 43 | setupObservers() 44 | } 45 | 46 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 47 | when (item.itemId) { 48 | android.R.id.home -> { 49 | findNavController().popBackStack() 50 | return true 51 | } 52 | } 53 | return super.onOptionsItemSelected(item) 54 | } 55 | 56 | private fun setupObservers() { 57 | viewModel.dataLoading.observe(viewLifecycleOwner, 58 | EventObserver { (activity as MainActivity).showGlobalProgressBar(it) }) 59 | 60 | viewModel.snackBarText.observe(viewLifecycleOwner, 61 | EventObserver { text -> 62 | view?.showSnackBar(text) 63 | view?.forceHideKeyboard() 64 | }) 65 | } 66 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/ui/profile/ProfileViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app.ui.profile 2 | 3 | import androidx.lifecycle.* 4 | import com.fredrikbogg.android_chat_app.data.db.entity.* 5 | import com.fredrikbogg.android_chat_app.data.db.remote.FirebaseReferenceValueObserver 6 | import com.fredrikbogg.android_chat_app.data.db.repository.DatabaseRepository 7 | import com.fredrikbogg.android_chat_app.ui.DefaultViewModel 8 | import com.fredrikbogg.android_chat_app.data.Result 9 | import com.fredrikbogg.android_chat_app.util.convertTwoUserIDs 10 | 11 | 12 | class ProfileViewModelFactory(private val myUserID: String, private val otherUserID: String) : 13 | ViewModelProvider.Factory { 14 | override fun create(modelClass: Class): T { 15 | return ProfileViewModel(myUserID, otherUserID) as T 16 | } 17 | } 18 | 19 | enum class LayoutState { 20 | IS_FRIEND, NOT_FRIEND, ACCEPT_DECLINE, REQUEST_SENT 21 | } 22 | 23 | class ProfileViewModel(private val myUserID: String, private val userID: String) : 24 | DefaultViewModel() { 25 | 26 | private val repository: DatabaseRepository = DatabaseRepository() 27 | private val firebaseReferenceObserver = FirebaseReferenceValueObserver() 28 | private val _myUser: MutableLiveData = MutableLiveData() 29 | private val _otherUser: MutableLiveData = MutableLiveData() 30 | 31 | val otherUser: LiveData = _otherUser 32 | val layoutState = MediatorLiveData() 33 | 34 | init { 35 | layoutState.addSource(_myUser) { updateLayoutState(it, _otherUser.value) } 36 | setupProfile() 37 | } 38 | 39 | override fun onCleared() { 40 | super.onCleared() 41 | firebaseReferenceObserver.clear() 42 | } 43 | 44 | private fun updateLayoutState(myUser: User?, otherUser: User?) { 45 | if (myUser != null && otherUser != null) { 46 | layoutState.value = when { 47 | myUser.friends[otherUser.info.id] != null -> LayoutState.IS_FRIEND 48 | myUser.notifications[otherUser.info.id] != null -> LayoutState.ACCEPT_DECLINE 49 | myUser.sentRequests[otherUser.info.id] != null -> LayoutState.REQUEST_SENT 50 | else -> LayoutState.NOT_FRIEND 51 | } 52 | } 53 | } 54 | 55 | private fun setupProfile() { 56 | repository.loadUser(userID) { result: Result -> 57 | onResult(_otherUser, result) 58 | if (result is Result.Success) { 59 | repository.loadAndObserveUser(myUserID, firebaseReferenceObserver) { result2: Result -> 60 | onResult(_myUser, result2) 61 | } 62 | } 63 | } 64 | } 65 | 66 | fun addFriendPressed() { 67 | repository.updateNewSentRequest(myUserID, UserRequest(_otherUser.value!!.info.id)) 68 | repository.updateNewNotification(_otherUser.value!!.info.id, UserNotification(myUserID)) 69 | } 70 | 71 | fun removeFriendPressed() { 72 | repository.removeFriend(myUserID, _otherUser.value!!.info.id) 73 | repository.removeChat(convertTwoUserIDs(myUserID, _otherUser.value!!.info.id)) 74 | repository.removeMessages(convertTwoUserIDs(myUserID, _otherUser.value!!.info.id)) 75 | } 76 | 77 | fun acceptFriendRequestPressed() { 78 | repository.updateNewFriend(UserFriend(myUserID), UserFriend(_otherUser.value!!.info.id)) 79 | 80 | val newChat = Chat().apply { 81 | info.id = convertTwoUserIDs(myUserID, _otherUser.value!!.info.id) 82 | lastMessage = Message(seen = true, text = "Say hello!") 83 | } 84 | 85 | repository.updateNewChat(newChat) 86 | repository.removeNotification(myUserID, _otherUser.value!!.info.id) 87 | repository.removeSentRequest(_otherUser.value!!.info.id, myUserID) 88 | } 89 | 90 | fun declineFriendRequestPressed() { 91 | repository.removeSentRequest(myUserID, _otherUser.value!!.info.id) 92 | repository.removeNotification(myUserID, _otherUser.value!!.info.id) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/ui/settings/SettingsFragment.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app.ui.settings 2 | 3 | import android.app.Activity.RESULT_OK 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.os.Bundle 7 | import android.view.LayoutInflater 8 | import android.view.MenuItem 9 | import android.view.View 10 | import android.view.ViewGroup 11 | import android.widget.EditText 12 | import androidx.appcompat.app.AlertDialog 13 | import androidx.fragment.app.Fragment 14 | import androidx.fragment.app.viewModels 15 | import androidx.navigation.fragment.findNavController 16 | import com.fredrikbogg.android_chat_app.App 17 | import com.fredrikbogg.android_chat_app.R 18 | import com.fredrikbogg.android_chat_app.databinding.FragmentSettingsBinding 19 | import com.fredrikbogg.android_chat_app.data.EventObserver 20 | import com.fredrikbogg.android_chat_app.util.SharedPreferencesUtil 21 | import com.fredrikbogg.android_chat_app.util.convertFileToByteArray 22 | 23 | 24 | class SettingsFragment : Fragment() { 25 | 26 | private val viewModel: SettingsViewModel by viewModels { SettingsViewModelFactory(App.myUserID) } 27 | 28 | private lateinit var viewDataBinding: FragmentSettingsBinding 29 | private val selectImageIntentRequestCode = 1 30 | 31 | override fun onCreateView( 32 | inflater: LayoutInflater, container: ViewGroup?, 33 | savedInstanceState: Bundle? 34 | ): View? { 35 | viewDataBinding = FragmentSettingsBinding.inflate(inflater, container, false) 36 | .apply { viewmodel = viewModel } 37 | viewDataBinding.lifecycleOwner = this.viewLifecycleOwner 38 | setHasOptionsMenu(true) 39 | 40 | return viewDataBinding.root 41 | } 42 | 43 | override fun onActivityCreated(savedInstanceState: Bundle?) { 44 | super.onActivityCreated(savedInstanceState) 45 | setupObservers() 46 | } 47 | 48 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 49 | when (item.itemId) { 50 | android.R.id.home -> { 51 | findNavController().popBackStack() 52 | return true 53 | } 54 | } 55 | return super.onOptionsItemSelected(item) 56 | } 57 | 58 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { 59 | super.onActivityResult(requestCode, resultCode, data) 60 | if (resultCode == RESULT_OK && requestCode == selectImageIntentRequestCode) { 61 | data?.data?.let { uri -> 62 | convertFileToByteArray(requireContext(), uri).let { 63 | viewModel.changeUserImage(it) 64 | } 65 | } 66 | } 67 | } 68 | 69 | private fun setupObservers() { 70 | viewModel.editStatusEvent.observe(viewLifecycleOwner, 71 | EventObserver { showEditStatusDialog() }) 72 | 73 | viewModel.editImageEvent.observe(viewLifecycleOwner, 74 | EventObserver { startSelectImageIntent() }) 75 | 76 | viewModel.logoutEvent.observe(viewLifecycleOwner, 77 | EventObserver { 78 | SharedPreferencesUtil.removeUserID(requireContext()) 79 | navigateToStart() 80 | }) 81 | } 82 | 83 | private fun showEditStatusDialog() { 84 | val input = EditText(requireActivity() as Context) 85 | AlertDialog.Builder(requireActivity()).apply { 86 | setTitle("Status:") 87 | setView(input) 88 | setPositiveButton("Ok") { _, _ -> 89 | val textInput = input.text.toString() 90 | if (!textInput.isBlank() && textInput.length <= 40) { 91 | viewModel.changeUserStatus(textInput) 92 | } 93 | } 94 | setNegativeButton("Cancel") { _, _ -> } 95 | show() 96 | } 97 | } 98 | 99 | private fun startSelectImageIntent() { 100 | val selectImageIntent = Intent(Intent.ACTION_GET_CONTENT) 101 | selectImageIntent.type = "image/*" 102 | startActivityForResult(selectImageIntent, selectImageIntentRequestCode) 103 | } 104 | 105 | private fun navigateToStart() { 106 | findNavController().navigate(R.id.action_navigation_settings_to_startFragment) 107 | } 108 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/ui/settings/SettingsViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app.ui.settings 2 | 3 | import android.net.Uri 4 | import androidx.lifecycle.LiveData 5 | import androidx.lifecycle.MutableLiveData 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.ViewModelProvider 8 | import com.fredrikbogg.android_chat_app.data.db.entity.UserInfo 9 | import com.fredrikbogg.android_chat_app.data.db.remote.FirebaseReferenceValueObserver 10 | import com.fredrikbogg.android_chat_app.data.db.repository.AuthRepository 11 | import com.fredrikbogg.android_chat_app.data.db.repository.DatabaseRepository 12 | import com.fredrikbogg.android_chat_app.data.db.repository.StorageRepository 13 | import com.fredrikbogg.android_chat_app.ui.DefaultViewModel 14 | import com.fredrikbogg.android_chat_app.data.Event 15 | import com.fredrikbogg.android_chat_app.data.Result 16 | 17 | class SettingsViewModelFactory(private val userID: String) : ViewModelProvider.Factory { 18 | override fun create(modelClass: Class): T { 19 | return SettingsViewModel(userID) as T 20 | } 21 | } 22 | 23 | class SettingsViewModel(private val userID: String) : DefaultViewModel() { 24 | 25 | private val dbRepository: DatabaseRepository = DatabaseRepository() 26 | private val storageRepository = StorageRepository() 27 | private val authRepository = AuthRepository() 28 | 29 | private val _userInfo: MutableLiveData = MutableLiveData() 30 | val userInfo: LiveData = _userInfo 31 | 32 | private val _editStatusEvent = MutableLiveData>() 33 | val editStatusEvent: LiveData> = _editStatusEvent 34 | 35 | private val _editImageEvent = MutableLiveData>() 36 | val editImageEvent: LiveData> = _editImageEvent 37 | 38 | private val _logoutEvent = MutableLiveData>() 39 | val logoutEvent: LiveData> = _logoutEvent 40 | 41 | private val firebaseReferenceObserver = FirebaseReferenceValueObserver() 42 | 43 | init { 44 | loadAndObserveUserInfo() 45 | } 46 | 47 | override fun onCleared() { 48 | super.onCleared() 49 | firebaseReferenceObserver.clear() 50 | } 51 | 52 | private fun loadAndObserveUserInfo() { 53 | dbRepository.loadAndObserveUserInfo(userID, firebaseReferenceObserver) 54 | { result: Result -> onResult(_userInfo, result) } 55 | } 56 | 57 | fun changeUserStatus(status: String) { 58 | dbRepository.updateUserStatus(userID, status) 59 | } 60 | 61 | fun changeUserImage(byteArray: ByteArray) { 62 | storageRepository.updateUserProfileImage(userID, byteArray) { result: Result -> 63 | onResult(null, result) 64 | if (result is Result.Success) { 65 | dbRepository.updateUserProfileImageUrl(userID, result.data.toString()) 66 | } 67 | } 68 | } 69 | 70 | fun changeUserImagePressed() { 71 | _editImageEvent.value = Event(Unit) 72 | } 73 | 74 | fun changeUserStatusPressed() { 75 | _editStatusEvent.value = Event(Unit) 76 | } 77 | 78 | fun logoutUserPressed() { 79 | authRepository.logoutUser() 80 | _logoutEvent.value = Event(Unit) 81 | } 82 | } 83 | 84 | -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/ui/start/StartFragment.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app.ui.start 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.Fragment 8 | import androidx.fragment.app.viewModels 9 | import androidx.navigation.fragment.findNavController 10 | import com.fredrikbogg.android_chat_app.R 11 | import com.fredrikbogg.android_chat_app.databinding.FragmentStartBinding 12 | import com.fredrikbogg.android_chat_app.data.EventObserver 13 | import com.fredrikbogg.android_chat_app.util.SharedPreferencesUtil 14 | 15 | class StartFragment : Fragment() { 16 | 17 | private val viewModel by viewModels() 18 | private lateinit var viewDataBinding: FragmentStartBinding 19 | 20 | override fun onCreateView( 21 | inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? 22 | ): View? { 23 | viewDataBinding = 24 | FragmentStartBinding.inflate(inflater, container, false).apply { viewmodel = viewModel } 25 | viewDataBinding.lifecycleOwner = this.viewLifecycleOwner 26 | setHasOptionsMenu(false) 27 | return viewDataBinding.root 28 | } 29 | 30 | override fun onActivityCreated(savedInstanceState: Bundle?) { 31 | super.onActivityCreated(savedInstanceState) 32 | setupObservers() 33 | 34 | if (userIsAlreadyLoggedIn()) { 35 | navigateDirectlyToChats() 36 | } 37 | } 38 | 39 | private fun userIsAlreadyLoggedIn(): Boolean { 40 | return SharedPreferencesUtil.getUserID(requireContext()) != null 41 | } 42 | 43 | private fun setupObservers() { 44 | viewModel.loginEvent.observe(viewLifecycleOwner, EventObserver { navigateToLogin() }) 45 | viewModel.createAccountEvent.observe( 46 | viewLifecycleOwner, EventObserver { navigateToCreateAccount() }) 47 | } 48 | 49 | private fun navigateDirectlyToChats() { 50 | findNavController().navigate(R.id.action_startFragment_to_navigation_chats) 51 | } 52 | 53 | private fun navigateToLogin() { 54 | findNavController().navigate(R.id.action_startFragment_to_loginFragment) 55 | } 56 | 57 | private fun navigateToCreateAccount() { 58 | findNavController().navigate(R.id.action_startFragment_to_createAccountFragment) 59 | } 60 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/ui/start/StartViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app.ui.start 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import com.fredrikbogg.android_chat_app.data.Event 7 | 8 | class StartViewModel : ViewModel() { 9 | 10 | private val _loginEvent = MutableLiveData>() 11 | private val _createAccountEvent = MutableLiveData>() 12 | 13 | val loginEvent: LiveData> = _loginEvent 14 | val createAccountEvent: LiveData> = _createAccountEvent 15 | 16 | fun goToLoginPressed() { 17 | _loginEvent.value = Event(Unit) 18 | } 19 | 20 | fun goToCreateAccountPressed() { 21 | _createAccountEvent.value = Event(Unit) 22 | } 23 | } 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/ui/start/createAccount/CreateAccountFragment.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app.ui.start.createAccount 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.MenuItem 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import androidx.fragment.app.Fragment 9 | import androidx.fragment.app.viewModels 10 | import androidx.navigation.fragment.findNavController 11 | import com.fredrikbogg.android_chat_app.data.EventObserver 12 | import com.fredrikbogg.android_chat_app.R 13 | import com.fredrikbogg.android_chat_app.databinding.FragmentCreateAccountBinding 14 | import com.fredrikbogg.android_chat_app.ui.main.MainActivity 15 | import com.fredrikbogg.android_chat_app.util.SharedPreferencesUtil 16 | import com.fredrikbogg.android_chat_app.util.forceHideKeyboard 17 | import com.fredrikbogg.android_chat_app.util.showSnackBar 18 | 19 | class CreateAccountFragment : Fragment() { 20 | 21 | private val viewModel by viewModels() 22 | private lateinit var viewDataBinding: FragmentCreateAccountBinding 23 | 24 | override fun onCreateView( 25 | inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? 26 | ): View? { 27 | viewDataBinding = FragmentCreateAccountBinding.inflate(inflater, container, false) 28 | .apply { viewmodel = viewModel } 29 | viewDataBinding.lifecycleOwner = this.viewLifecycleOwner 30 | setHasOptionsMenu(true) 31 | return viewDataBinding.root 32 | } 33 | 34 | override fun onActivityCreated(savedInstanceState: Bundle?) { 35 | super.onActivityCreated(savedInstanceState) 36 | setupObservers() 37 | } 38 | 39 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 40 | when (item.itemId) { 41 | android.R.id.home -> { 42 | findNavController().popBackStack() 43 | return true 44 | } 45 | } 46 | return super.onOptionsItemSelected(item) 47 | } 48 | 49 | private fun setupObservers() { 50 | viewModel.dataLoading.observe(viewLifecycleOwner, 51 | EventObserver { (activity as MainActivity).showGlobalProgressBar(it) }) 52 | 53 | viewModel.snackBarText.observe(viewLifecycleOwner, 54 | EventObserver { text -> 55 | view?.showSnackBar(text) 56 | view?.forceHideKeyboard() 57 | }) 58 | 59 | viewModel.isCreatedEvent.observe(viewLifecycleOwner, EventObserver { 60 | SharedPreferencesUtil.saveUserID(requireContext(), it.uid) 61 | navigateToChats() 62 | }) 63 | } 64 | 65 | private fun navigateToChats() { 66 | findNavController().navigate(R.id.action_createAccountFragment_to_navigation_chats) 67 | } 68 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/ui/start/createAccount/CreateAccountViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app.ui.start.createAccount 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import com.fredrikbogg.android_chat_app.data.Event 6 | import com.fredrikbogg.android_chat_app.data.Result 7 | import com.fredrikbogg.android_chat_app.data.db.entity.User 8 | import com.fredrikbogg.android_chat_app.data.db.repository.AuthRepository 9 | import com.fredrikbogg.android_chat_app.data.db.repository.DatabaseRepository 10 | import com.fredrikbogg.android_chat_app.data.model.CreateUser 11 | import com.fredrikbogg.android_chat_app.ui.DefaultViewModel 12 | import com.fredrikbogg.android_chat_app.util.isEmailValid 13 | import com.fredrikbogg.android_chat_app.util.isTextValid 14 | import com.google.firebase.auth.FirebaseUser 15 | 16 | class CreateAccountViewModel : DefaultViewModel() { 17 | 18 | private val dbRepository = DatabaseRepository() 19 | private val authRepository = AuthRepository() 20 | private val mIsCreatedEvent = MutableLiveData>() 21 | 22 | val isCreatedEvent: LiveData> = mIsCreatedEvent 23 | val displayNameText = MutableLiveData() // Two way 24 | val emailText = MutableLiveData() // Two way 25 | val passwordText = MutableLiveData() // Two way 26 | val isCreatingAccount = MutableLiveData() 27 | 28 | private fun createAccount() { 29 | isCreatingAccount.value = true 30 | val createUser = 31 | CreateUser(displayNameText.value!!, emailText.value!!, passwordText.value!!) 32 | 33 | authRepository.createUser(createUser) { result: Result -> 34 | onResult(null, result) 35 | if (result is Result.Success) { 36 | mIsCreatedEvent.value = Event(result.data!!) 37 | dbRepository.updateNewUser(User().apply { 38 | info.id = result.data.uid 39 | info.displayName = createUser.displayName 40 | }) 41 | } 42 | if (result is Result.Success || result is Result.Error) isCreatingAccount.value = false 43 | } 44 | } 45 | 46 | fun createAccountPressed() { 47 | if (!isTextValid(2, displayNameText.value)) { 48 | mSnackBarText.value = Event("Display name is too short") 49 | return 50 | } 51 | 52 | if (!isEmailValid(emailText.value.toString())) { 53 | mSnackBarText.value = Event("Invalid email format") 54 | return 55 | } 56 | if (!isTextValid(6, passwordText.value)) { 57 | mSnackBarText.value = Event("Password is too short") 58 | return 59 | } 60 | 61 | createAccount() 62 | } 63 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/ui/start/login/LoginFragment.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app.ui.start.login 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.MenuItem 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import androidx.fragment.app.Fragment 9 | import androidx.fragment.app.viewModels 10 | import androidx.navigation.fragment.findNavController 11 | import com.fredrikbogg.android_chat_app.R 12 | import com.fredrikbogg.android_chat_app.databinding.FragmentLoginBinding 13 | import com.fredrikbogg.android_chat_app.data.EventObserver 14 | import com.fredrikbogg.android_chat_app.util.showSnackBar 15 | import com.fredrikbogg.android_chat_app.ui.main.MainActivity 16 | import com.fredrikbogg.android_chat_app.util.SharedPreferencesUtil 17 | import com.fredrikbogg.android_chat_app.util.forceHideKeyboard 18 | 19 | class LoginFragment : Fragment() { 20 | 21 | private val viewModel by viewModels() 22 | private lateinit var viewDataBinding: FragmentLoginBinding 23 | 24 | override fun onCreateView( 25 | inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? 26 | ): View? { 27 | viewDataBinding = FragmentLoginBinding.inflate(inflater, container, false) 28 | .apply { viewmodel = viewModel } 29 | viewDataBinding.lifecycleOwner = this.viewLifecycleOwner 30 | setHasOptionsMenu(true) 31 | return viewDataBinding.root 32 | } 33 | 34 | override fun onActivityCreated(savedInstanceState: Bundle?) { 35 | super.onActivityCreated(savedInstanceState) 36 | setupObservers() 37 | } 38 | 39 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 40 | when (item.itemId) { 41 | android.R.id.home -> { 42 | findNavController().popBackStack() 43 | return true 44 | } 45 | } 46 | return super.onOptionsItemSelected(item) 47 | } 48 | 49 | private fun setupObservers() { 50 | viewModel.dataLoading.observe(viewLifecycleOwner, 51 | EventObserver { (activity as MainActivity).showGlobalProgressBar(it) }) 52 | 53 | viewModel.snackBarText.observe(viewLifecycleOwner, 54 | EventObserver { text -> 55 | view?.showSnackBar(text) 56 | view?.forceHideKeyboard() 57 | }) 58 | 59 | viewModel.isLoggedInEvent.observe(viewLifecycleOwner, EventObserver { 60 | SharedPreferencesUtil.saveUserID(requireContext(), it.uid) 61 | navigateToChats() 62 | }) 63 | } 64 | 65 | private fun navigateToChats() { 66 | findNavController().navigate(R.id.action_loginFragment_to_navigation_chats) 67 | } 68 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/ui/start/login/LoginViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app.ui.start.login 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import com.fredrikbogg.android_chat_app.data.model.Login 6 | import com.fredrikbogg.android_chat_app.data.db.repository.AuthRepository 7 | import com.fredrikbogg.android_chat_app.ui.DefaultViewModel 8 | import com.fredrikbogg.android_chat_app.data.Event 9 | import com.fredrikbogg.android_chat_app.data.Result 10 | import com.fredrikbogg.android_chat_app.util.isEmailValid 11 | import com.fredrikbogg.android_chat_app.util.isTextValid 12 | import com.google.firebase.auth.FirebaseUser 13 | 14 | class LoginViewModel : DefaultViewModel() { 15 | 16 | private val authRepository = AuthRepository() 17 | private val _isLoggedInEvent = MutableLiveData>() 18 | 19 | val isLoggedInEvent: LiveData> = _isLoggedInEvent 20 | val emailText = MutableLiveData() // Two way 21 | val passwordText = MutableLiveData() // Two way 22 | val isLoggingIn = MutableLiveData() // Two way 23 | 24 | private fun login() { 25 | isLoggingIn.value = true 26 | val login = Login(emailText.value!!, passwordText.value!!) 27 | 28 | authRepository.loginUser(login) { result: Result -> 29 | onResult(null, result) 30 | if (result is Result.Success) _isLoggedInEvent.value = Event(result.data!!) 31 | if (result is Result.Success || result is Result.Error) isLoggingIn.value = false 32 | } 33 | } 34 | 35 | fun loginPressed() { 36 | if (!isEmailValid(emailText.value.toString())) { 37 | mSnackBarText.value = Event("Invalid email format") 38 | return 39 | } 40 | if (!isTextValid(6, passwordText.value)) { 41 | mSnackBarText.value = Event("Password is too short") 42 | return 43 | } 44 | 45 | login() 46 | } 47 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/ui/users/UsersBindings.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app.ui.users 2 | 3 | import androidx.databinding.BindingAdapter 4 | import androidx.recyclerview.widget.RecyclerView 5 | import com.fredrikbogg.android_chat_app.data.db.entity.User 6 | 7 | @BindingAdapter("bind_users_list") 8 | fun bindUsersList(listView: RecyclerView, items: List?) { 9 | items?.let { (listView.adapter as UsersListAdapter).submitList(items) } 10 | } 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/ui/users/UsersFragment.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app.ui.users 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.core.os.bundleOf 8 | import androidx.fragment.app.Fragment 9 | import androidx.fragment.app.viewModels 10 | import androidx.navigation.fragment.findNavController 11 | import com.fredrikbogg.android_chat_app.App 12 | import com.fredrikbogg.android_chat_app.R 13 | import com.fredrikbogg.android_chat_app.databinding.FragmentUsersBinding 14 | import com.fredrikbogg.android_chat_app.data.EventObserver 15 | import com.fredrikbogg.android_chat_app.ui.profile.ProfileFragment 16 | 17 | 18 | class UsersFragment : Fragment() { 19 | 20 | private val viewModel: UsersViewModel by viewModels { UsersViewModelFactory(App.myUserID) } 21 | private lateinit var viewDataBinding: FragmentUsersBinding 22 | private lateinit var listAdapter: UsersListAdapter 23 | 24 | override fun onCreateView( 25 | inflater: LayoutInflater, container: ViewGroup?, 26 | savedInstanceState: Bundle? 27 | ): View? { 28 | viewDataBinding = 29 | FragmentUsersBinding.inflate(inflater, container, false).apply { viewmodel = viewModel } 30 | viewDataBinding.lifecycleOwner = this.viewLifecycleOwner 31 | return viewDataBinding.root 32 | } 33 | 34 | override fun onActivityCreated(savedInstanceState: Bundle?) { 35 | super.onActivityCreated(savedInstanceState) 36 | setupListAdapter() 37 | setupObservers() 38 | } 39 | 40 | private fun setupListAdapter() { 41 | val viewModel = viewDataBinding.viewmodel 42 | if (viewModel != null) { 43 | listAdapter = UsersListAdapter(viewModel) 44 | viewDataBinding.usersRecyclerView.adapter = listAdapter 45 | } else { 46 | throw Exception("The viewmodel is not initialized") 47 | } 48 | } 49 | 50 | private fun setupObservers() { 51 | viewModel.selectedUser.observe(viewLifecycleOwner, EventObserver { navigateToProfile(it.info.id) }) 52 | } 53 | 54 | private fun navigateToProfile(userID: String) { 55 | val bundle = bundleOf(ProfileFragment.ARGS_KEY_USER_ID to userID) 56 | findNavController().navigate(R.id.action_navigation_users_to_profileFragment, bundle) 57 | } 58 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/ui/users/UsersListAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app.ui.users 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.recyclerview.widget.DiffUtil 6 | import androidx.recyclerview.widget.ListAdapter 7 | import androidx.recyclerview.widget.RecyclerView 8 | import com.fredrikbogg.android_chat_app.data.db.entity.User 9 | import com.fredrikbogg.android_chat_app.databinding.ListItemUserBinding 10 | 11 | 12 | class UsersListAdapter internal constructor(private val viewModel: UsersViewModel) : 13 | ListAdapter(UserDiffCallback()) { 14 | 15 | class ViewHolder(private val binding: ListItemUserBinding) : 16 | RecyclerView.ViewHolder(binding.root) { 17 | fun bind(viewModel: UsersViewModel, item: User) { 18 | binding.viewmodel = viewModel 19 | binding.user = item 20 | binding.executePendingBindings() 21 | } 22 | } 23 | 24 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 25 | holder.bind(viewModel, getItem(position)) 26 | } 27 | 28 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 29 | val layoutInflater = LayoutInflater.from(parent.context) 30 | val binding = ListItemUserBinding.inflate(layoutInflater, parent, false) 31 | return ViewHolder(binding) 32 | } 33 | } 34 | 35 | class UserDiffCallback : DiffUtil.ItemCallback() { 36 | override fun areItemsTheSame(oldItem: User, newItem: User): Boolean { 37 | return oldItem == newItem 38 | } 39 | 40 | override fun areContentsTheSame(oldItem: User, newItem: User): Boolean { 41 | return oldItem.info.id == newItem.info.id 42 | } 43 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/ui/users/UsersViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app.ui.users 2 | 3 | import androidx.lifecycle.* 4 | import com.fredrikbogg.android_chat_app.data.db.entity.User 5 | import com.fredrikbogg.android_chat_app.data.db.repository.DatabaseRepository 6 | import com.fredrikbogg.android_chat_app.ui.DefaultViewModel 7 | import com.fredrikbogg.android_chat_app.data.Event 8 | import com.fredrikbogg.android_chat_app.data.Result 9 | 10 | 11 | class UsersViewModelFactory(private val myUserID: String) : 12 | ViewModelProvider.Factory { 13 | override fun create(modelClass: Class): T { 14 | return UsersViewModel(myUserID) as T 15 | } 16 | } 17 | 18 | class UsersViewModel(private val myUserID: String) : DefaultViewModel() { 19 | private val repository: DatabaseRepository = DatabaseRepository() 20 | 21 | private val _selectedUser = MutableLiveData>() 22 | var selectedUser: LiveData> = _selectedUser 23 | private val updatedUsersList = MutableLiveData>() 24 | val usersList = MediatorLiveData>() 25 | 26 | init { 27 | usersList.addSource(updatedUsersList) { mutableList -> 28 | usersList.value = updatedUsersList.value?.filter { it.info.id != myUserID } 29 | } 30 | loadUsers() 31 | } 32 | 33 | private fun loadUsers() { 34 | repository.loadUsers { result: Result> -> 35 | onResult(updatedUsersList, result) 36 | } 37 | } 38 | 39 | fun selectUser(user: User) { 40 | _selectedUser.value = Event(user) 41 | } 42 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/util/FileConverterUtil.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app.util 2 | 3 | import android.content.Context 4 | import android.graphics.Bitmap 5 | import android.graphics.BitmapFactory 6 | import android.net.Uri 7 | import java.io.ByteArrayOutputStream 8 | import java.io.InputStream 9 | 10 | fun convertFileToByteArray(context: Context, uri: Uri): ByteArray { 11 | val inputStream: InputStream? = context.contentResolver.openInputStream(uri) 12 | val bitmap = BitmapFactory.decodeStream(inputStream) 13 | val byteArrayOutputStream = ByteArrayOutputStream() 14 | bitmap.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayOutputStream) 15 | 16 | return byteArrayOutputStream.toByteArray() 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/util/FirebaseUtil.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app.util 2 | 3 | import com.google.firebase.database.DataSnapshot 4 | 5 | fun wrapSnapshotToClass(className: Class, snap: DataSnapshot): T? { 6 | return snap.getValue(className) 7 | } 8 | 9 | fun wrapSnapshotToArrayList(className: Class, snap: DataSnapshot): MutableList { 10 | val arrayList: MutableList = arrayListOf() 11 | for (child in snap.children) { 12 | child.getValue(className)?.let { arrayList.add(it) } 13 | } 14 | return arrayList 15 | } 16 | 17 | // Always returns the same combined id when comparing the two users id's 18 | fun convertTwoUserIDs(userID1: String, userID2: String): String { 19 | return if (userID1 < userID2) { 20 | userID2 + userID1 21 | } else { 22 | userID1 + userID2 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/util/LiveDataExt.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app.util 2 | 3 | import androidx.lifecycle.MutableLiveData 4 | 5 | fun MutableLiveData>.addNewItem(item: T) { 6 | val newList = mutableListOf() 7 | this.value?.let { newList.addAll(it) } 8 | newList.add(item) 9 | this.value = newList 10 | } 11 | 12 | fun MutableLiveData>.updateItemAt(item: T, index: Int) { 13 | val newList = mutableListOf() 14 | this.value?.let { newList.addAll(it) } 15 | newList[index] = item 16 | this.value = newList 17 | } 18 | 19 | fun MutableLiveData>.removeItem(item: T) { 20 | val newList = mutableListOf() 21 | this.value?.let { newList.addAll(it) } 22 | newList.remove(item) 23 | this.value = newList 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/util/SharedPreferencesUtil.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app.util 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | 6 | object SharedPreferencesUtil { 7 | private const val PACKAGE_NAME = "com.fredrikbogg.android_chat_app" 8 | private const val KEY_USER_ID = "user_info" 9 | 10 | private fun getPrefs(context: Context): SharedPreferences { 11 | return context.getSharedPreferences(PACKAGE_NAME, Context.MODE_PRIVATE) 12 | } 13 | 14 | fun getUserID(context: Context): String? { 15 | return getPrefs(context).getString(KEY_USER_ID, null) 16 | } 17 | 18 | fun saveUserID(context: Context, userID: String) { 19 | getPrefs(context).edit().putString(KEY_USER_ID, userID).apply() 20 | } 21 | 22 | fun removeUserID(context: Context) { 23 | getPrefs(context).edit().remove(KEY_USER_ID).apply() 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/util/TextUtil.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app.util 2 | 3 | fun isEmailValid(email: CharSequence): Boolean { 4 | return android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches() 5 | } 6 | 7 | fun isTextValid(minLength: Int, text: String?): Boolean { 8 | if (text.isNullOrBlank() || text.length < minLength) { 9 | return false 10 | } 11 | return true 12 | } 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/fredrikbogg/android_chat_app/util/ViewExt.kt: -------------------------------------------------------------------------------- 1 | package com.fredrikbogg.android_chat_app.util 2 | 3 | import android.content.Context 4 | import android.view.View 5 | import android.view.inputmethod.InputMethodManager 6 | import com.fredrikbogg.android_chat_app.R 7 | import com.google.android.material.snackbar.Snackbar 8 | 9 | fun View.forceHideKeyboard() { 10 | val inputManager: InputMethodManager = 11 | this.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager 12 | inputManager.hideSoftInputFromWindow(this.windowToken, InputMethodManager.HIDE_NOT_ALWAYS) 13 | } 14 | 15 | fun View.showSnackBar(text: String) { 16 | Snackbar.make(this.rootView.findViewById(R.id.container), text, Snackbar.LENGTH_SHORT).show() 17 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/chat_box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgewe/Chat-App-Android/cff2f947a4496e46cbaa750b4e3fa768ba2308f0/app/src/main/res/drawable-v24/chat_box.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/rounded_rectangle_primary.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/rounded_rectangle_secondary.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_chat_bubble_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_error_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_notifications_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_people_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_person_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_settings_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/round_circle_online_green.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/round_circle_primary.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/font/nunito.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/font/nunito_bold.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/font/nunito_extrabold.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/font/nunito_semibold.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 16 | 17 | 30 | 31 | 42 | 43 | 56 | 57 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_chat.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 19 | 20 | 34 | 35 | 43 | 44 | 54 | 55 | 69 | 70 |