Repository: dgewe/Chat-App-Android Branch: master Commit: cff2f947a449 Files: 118 Total size: 198.1 KB Directory structure: gitextract_1onv52u2/ ├── .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 │ │ ├── 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/ │ │ │ ├── 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 │ │ ├── drawable-v24/ │ │ │ ├── rounded_rectangle_primary.xml │ │ │ └── rounded_rectangle_secondary.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 │ │ ├── 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 ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat └── settings.gradle ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ # Auto detect text files and perform LF normalization * text=auto ================================================ FILE: .gitignore ================================================ # Built application files *.apk *.aar *.ap_ *.aab # Files for the ART/Dalvik VM *.dex # Java class files *.class # Generated files bin/ gen/ out/ # Uncomment the following line in case you need and you don't have the release build type files in your app # release/ # Gradle files .gradle/ build/ # Local configuration file (sdk path, etc) local.properties # Proguard folder generated by Eclipse proguard/ # Log Files *.log # Android Studio Navigation editor temp files .navigation/ # Android Studio captures folder captures/ # IntelliJ *.iml .idea/workspace.xml .idea/tasks.xml .idea/gradle.xml .idea/assetWizardSettings.xml .idea/dictionaries .idea/libraries # Android Studio 3 in .gitignore file. .idea/caches .idea/modules.xml # Comment next line if keeping position of elements in Navigation Editor is relevant for you .idea/navEditor.xml # Keystore files # Uncomment the following lines if you do not want to check your keystore files in. #*.jks #*.keystore # External native build folder generated in Android Studio 2.2 and later .externalNativeBuild .cxx/ # Google Services (e.g. APIs or Firebase) # google-services.json # Freeline freeline.py freeline/ freeline_project_description.json # fastlane fastlane/report.xml fastlane/Preview.html fastlane/screenshots fastlane/test_output fastlane/readme.md # Version control vcs.xml # lint lint/intermediates/ lint/generated/ lint/outputs/ lint/tmp/ # lint/reports/ ================================================ FILE: .idea/.name ================================================ Android-Chat-App ================================================ FILE: .idea/codeStyles/Project.xml ================================================
xmlns:android ^$
xmlns:.* ^$ BY_NAME
.*:id http://schemas.android.com/apk/res/android
.*:name http://schemas.android.com/apk/res/android
name ^$
style ^$
.* ^$ BY_NAME
.* http://schemas.android.com/apk/res/android ANDROID_ATTRIBUTE_ORDER
.* .* BY_NAME
================================================ FILE: .idea/codeStyles/codeStyleConfig.xml ================================================ ================================================ FILE: .idea/jarRepositories.xml ================================================ ================================================ FILE: .idea/misc.xml ================================================ ================================================ FILE: .idea/render.experimental.xml ================================================ ================================================ FILE: .idea/runConfigurations.xml ================================================ ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 Fredrik Bogg Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Chat App Android ![HeaderImage](github_images/header.png) ## Introduction This is a demo application built with the goal to create a fun and challenging application based on the MVVM architectural pattern. See below for more information. ## Technologies & Architecture #### Technologies Android, Kotlin #### Architecture Model-View-ViewModel (MVVM) #### Firebase * Authentication * Realtime Database * Storage #### Architecture Components [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), [Navigation](https://developer.android.com/guide/navigation/) ## Features **Start:** Login/create account **Chats:** List of chats, online status, update on change **Notifications:** Accept/decline friend requests, notifications symbol **Users:** List of users **Settings:** Change image, change status, logout **Chat:** Send and show messages sorted by timestamp, online status, custom toolbar, update on change **Profile:** Add/remove friend, accept/decline friend request **General:** Auto login, bottom navigation, error messages with snackbar, progress bar ## Screenshots ### Start | Login | Create Account

### Chats | Notifications | Users

### Settings | Chat | Profile

### Firebase

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