├── .gitattributes
├── .gitignore
├── .idea
├── .name
├── codeStyles
│ ├── Project.xml
│ └── codeStyleConfig.xml
├── jarRepositories.xml
├── misc.xml
├── render.experimental.xml
└── runConfigurations.xml
├── LICENSE
├── README.md
├── app
├── .gitignore
├── build.gradle
├── google-services.json
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── fredrikbogg
│ │ └── android_chat_app
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── ic_launcher-playstore.png
│ ├── java
│ │ └── com
│ │ │ └── fredrikbogg
│ │ │ └── android_chat_app
│ │ │ ├── App.kt
│ │ │ ├── data
│ │ │ ├── Event.kt
│ │ │ ├── Result.kt
│ │ │ ├── db
│ │ │ │ ├── entity
│ │ │ │ │ ├── Chat.kt
│ │ │ │ │ ├── Message.kt
│ │ │ │ │ └── User.kt
│ │ │ │ ├── remote
│ │ │ │ │ ├── FirebaseAuthSource.kt
│ │ │ │ │ ├── FirebaseDatabaseSource.kt
│ │ │ │ │ └── FirebaseStorageSource.kt
│ │ │ │ └── repository
│ │ │ │ │ ├── AuthRepository.kt
│ │ │ │ │ ├── DatabaseRepository.kt
│ │ │ │ │ └── StorageRepository.kt
│ │ │ └── model
│ │ │ │ ├── ChatWithUserInfo.kt
│ │ │ │ ├── CreateUser.kt
│ │ │ │ └── Login.kt
│ │ │ ├── ui
│ │ │ ├── DefaultBindings.kt
│ │ │ ├── DefaultViewModel.kt
│ │ │ ├── chat
│ │ │ │ ├── ChatFragment.kt
│ │ │ │ ├── ChatViewModel.kt
│ │ │ │ ├── MessagesBindings.kt
│ │ │ │ └── MessagesListAdapter.kt
│ │ │ ├── chats
│ │ │ │ ├── ChatsBindings.kt
│ │ │ │ ├── ChatsFragment.kt
│ │ │ │ ├── ChatsListAdapter.kt
│ │ │ │ └── ChatsViewModel.kt
│ │ │ ├── main
│ │ │ │ ├── MainActivity.kt
│ │ │ │ └── MainViewModel.kt
│ │ │ ├── notifications
│ │ │ │ ├── NotificationsBindings.kt
│ │ │ │ ├── NotificationsFragment.kt
│ │ │ │ ├── NotificationsListAdapter.kt
│ │ │ │ └── NotificationsViewModel.kt
│ │ │ ├── profile
│ │ │ │ ├── ProfileFragment.kt
│ │ │ │ └── ProfileViewModel.kt
│ │ │ ├── settings
│ │ │ │ ├── SettingsFragment.kt
│ │ │ │ └── SettingsViewModel.kt
│ │ │ ├── start
│ │ │ │ ├── StartFragment.kt
│ │ │ │ ├── StartViewModel.kt
│ │ │ │ ├── createAccount
│ │ │ │ │ ├── CreateAccountFragment.kt
│ │ │ │ │ └── CreateAccountViewModel.kt
│ │ │ │ └── login
│ │ │ │ │ ├── LoginFragment.kt
│ │ │ │ │ └── LoginViewModel.kt
│ │ │ └── users
│ │ │ │ ├── UsersBindings.kt
│ │ │ │ ├── UsersFragment.kt
│ │ │ │ ├── UsersListAdapter.kt
│ │ │ │ └── UsersViewModel.kt
│ │ │ └── util
│ │ │ ├── FileConverterUtil.kt
│ │ │ ├── FirebaseUtil.kt
│ │ │ ├── LiveDataExt.kt
│ │ │ ├── SharedPreferencesUtil.kt
│ │ │ ├── TextUtil.kt
│ │ │ └── ViewExt.kt
│ └── res
│ │ ├── drawable-v24
│ │ ├── chat_box.png
│ │ ├── rounded_rectangle_primary.xml
│ │ └── rounded_rectangle_secondary.xml
│ │ ├── drawable
│ │ ├── ic_baseline_chat_bubble_24.xml
│ │ ├── ic_baseline_error_24.xml
│ │ ├── ic_baseline_notifications_24.xml
│ │ ├── ic_baseline_people_24.xml
│ │ ├── ic_baseline_person_24.xml
│ │ ├── ic_baseline_settings_24.xml
│ │ ├── round_circle_online_green.xml
│ │ └── round_circle_primary.xml
│ │ ├── font
│ │ ├── nunito.xml
│ │ ├── nunito_bold.xml
│ │ ├── nunito_extrabold.xml
│ │ └── nunito_semibold.xml
│ │ ├── layout
│ │ ├── activity_main.xml
│ │ ├── fragment_chat.xml
│ │ ├── fragment_chats.xml
│ │ ├── fragment_create_account.xml
│ │ ├── fragment_login.xml
│ │ ├── fragment_notifications.xml
│ │ ├── fragment_profile.xml
│ │ ├── fragment_settings.xml
│ │ ├── fragment_start.xml
│ │ ├── fragment_users.xml
│ │ ├── list_item_chat.xml
│ │ ├── list_item_message_received.xml
│ │ ├── list_item_message_sent.xml
│ │ ├── list_item_notification.xml
│ │ ├── list_item_user.xml
│ │ ├── toolbar_addon_chat.xml
│ │ └── toolbar_main.xml
│ │ ├── menu
│ │ └── bottom_nav_menu.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_foreground.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_foreground.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_foreground.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_foreground.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_foreground.png
│ │ └── ic_launcher_round.png
│ │ ├── navigation
│ │ └── mobile_navigation.xml
│ │ └── values
│ │ ├── colors.xml
│ │ ├── dimens.xml
│ │ ├── font_certs.xml
│ │ ├── ic_launcher_background.xml
│ │ ├── preloaded_fonts.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ └── test
│ └── java
│ └── com
│ └── fredrikbogg
│ └── android_chat_app
│ └── ExampleUnitTest.kt
├── build.gradle
├── github_images
├── chat.png
├── chats.png
├── create.png
├── db.png
├── header.png
├── login.png
├── notifications.png
├── profile.png
├── settings.png
├── start.png
└── users.png
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Built application files
2 | *.apk
3 | *.aar
4 | *.ap_
5 | *.aab
6 |
7 | # Files for the ART/Dalvik VM
8 | *.dex
9 |
10 | # Java class files
11 | *.class
12 |
13 | # Generated files
14 | bin/
15 | gen/
16 | out/
17 | # Uncomment the following line in case you need and you don't have the release build type files in your app
18 | # release/
19 |
20 | # Gradle files
21 | .gradle/
22 | build/
23 |
24 | # Local configuration file (sdk path, etc)
25 | local.properties
26 |
27 | # Proguard folder generated by Eclipse
28 | proguard/
29 |
30 | # Log Files
31 | *.log
32 |
33 | # Android Studio Navigation editor temp files
34 | .navigation/
35 |
36 | # Android Studio captures folder
37 | captures/
38 |
39 | # IntelliJ
40 | *.iml
41 | .idea/workspace.xml
42 | .idea/tasks.xml
43 | .idea/gradle.xml
44 | .idea/assetWizardSettings.xml
45 | .idea/dictionaries
46 | .idea/libraries
47 | # Android Studio 3 in .gitignore file.
48 | .idea/caches
49 | .idea/modules.xml
50 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you
51 | .idea/navEditor.xml
52 |
53 | # Keystore files
54 | # Uncomment the following lines if you do not want to check your keystore files in.
55 | #*.jks
56 | #*.keystore
57 |
58 | # External native build folder generated in Android Studio 2.2 and later
59 | .externalNativeBuild
60 | .cxx/
61 |
62 | # Google Services (e.g. APIs or Firebase)
63 | # google-services.json
64 |
65 | # Freeline
66 | freeline.py
67 | freeline/
68 | freeline_project_description.json
69 |
70 | # fastlane
71 | fastlane/report.xml
72 | fastlane/Preview.html
73 | fastlane/screenshots
74 | fastlane/test_output
75 | fastlane/readme.md
76 |
77 | # Version control
78 | vcs.xml
79 |
80 | # lint
81 | lint/intermediates/
82 | lint/generated/
83 | lint/outputs/
84 | lint/tmp/
85 | # lint/reports/
86 |
--------------------------------------------------------------------------------
/.idea/.name:
--------------------------------------------------------------------------------
1 | Android-Chat-App
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | xmlns:android
33 |
34 | ^$
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | xmlns:.*
44 |
45 | ^$
46 |
47 |
48 | BY_NAME
49 |
50 |
51 |
52 |
53 |
54 |
55 | .*:id
56 |
57 | http://schemas.android.com/apk/res/android
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | .*:name
67 |
68 | http://schemas.android.com/apk/res/android
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | name
78 |
79 | ^$
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 | style
89 |
90 | ^$
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 | .*
100 |
101 | ^$
102 |
103 |
104 | BY_NAME
105 |
106 |
107 |
108 |
109 |
110 |
111 | .*
112 |
113 | http://schemas.android.com/apk/res/android
114 |
115 |
116 | ANDROID_ATTRIBUTE_ORDER
117 |
118 |
119 |
120 |
121 |
122 |
123 | .*
124 |
125 | .*
126 |
127 |
128 | BY_NAME
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/jarRepositories.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/render.experimental.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Fredrik Bogg
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Chat App Android
2 | 
3 |
4 | ## Introduction
5 | This is a demo application built with the goal to create a fun and challenging application based on the MVVM architectural pattern.
6 |
7 | See below for more information.
8 |
9 | ## Technologies & Architecture
10 |
11 | #### Technologies
12 | Android, Kotlin
13 |
14 | #### Architecture
15 | Model-View-ViewModel (MVVM)
16 |
17 | #### Firebase
18 | * Authentication
19 | * Realtime Database
20 | * Storage
21 |
22 | #### Architecture Components
23 | [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel), [LiveData](https://developer.android.com/topic/libraries/architecture/livedata), [DataBinding](https://developer.android.com/topic/libraries/data-binding),
24 | [Navigation](https://developer.android.com/guide/navigation/)
25 |
26 | ## Features
27 |
28 | **Start:** Login/create account
29 |
30 | **Chats:** List of chats, online status, update on change
31 |
32 | **Notifications:** Accept/decline friend requests, notifications symbol
33 |
34 | **Users:** List of users
35 |
36 | **Settings:** Change image, change status, logout
37 |
38 | **Chat:** Send and show messages sorted by timestamp, online status, custom toolbar, update on change
39 |
40 | **Profile:** Add/remove friend, accept/decline friend request
41 |
42 | **General:** Auto login, bottom navigation, error messages with snackbar, progress bar
43 |
44 | ## Screenshots
45 |
46 | ### Start | Login | Create Account
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | ### Chats | Notifications | Users
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | ### Settings | Chat | Profile
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | ### Firebase
71 |
72 |
73 |
74 |
75 | ## Setup
76 | #### Requirements
77 | * Basic knowledge about Android Studio
78 | * Basic knowledge about Firebase
79 |
80 | #### Firebase
81 | * Setup Authentication and use the Sign-in method 'Email/Password'
82 | * Setup Realtime Database
83 | * Setup Storage
84 | * Replace the file [google-services.json](app/google-services.json)
85 | * Note: Download the google-services.json file after the Firebase services are set up to automatically include the services in the json file.
86 | * Note: When updating the google-services.json file then make sure to invalidate the caches as well as doing a clean + rebuild.
87 |
88 | #### Project
89 | 1. Download and open the project in Android Studio
90 | 2. Connect your Android phone or use the emulator to start the application
91 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-android-extensions'
4 | apply plugin: 'com.google.gms.google-services'
5 | apply plugin: 'kotlin-kapt'
6 |
7 | android {
8 | compileSdkVersion 29
9 | buildToolsVersion "29.0.3"
10 |
11 | defaultConfig {
12 | applicationId "com.fredrikbogg.android_chat_app"
13 | minSdkVersion 26
14 | targetSdkVersion 29
15 | versionCode 1
16 | versionName "1.0"
17 |
18 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
19 | }
20 |
21 | buildTypes {
22 | release {
23 | minifyEnabled false
24 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
25 | }
26 | }
27 |
28 | buildFeatures {
29 | dataBinding = true
30 | viewBinding = true
31 | }
32 |
33 | compileOptions {
34 | sourceCompatibility JavaVersion.VERSION_1_8
35 | targetCompatibility JavaVersion.VERSION_1_8
36 | }
37 | kotlinOptions {
38 | jvmTarget = '1.8'
39 | }
40 | }
41 |
42 | dependencies {
43 | implementation fileTree(dir: "libs", include: ["*.jar"])
44 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
45 | implementation 'androidx.core:core-ktx:1.3.1'
46 | implementation 'androidx.appcompat:appcompat:1.2.0'
47 | implementation 'com.google.android.material:material:1.2.0'
48 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
49 | implementation 'androidx.legacy:legacy-support-v4:1.0.0'
50 |
51 | //Navigation, lifecycle
52 | implementation 'androidx.navigation:navigation-fragment:2.3.0'
53 | implementation 'androidx.navigation:navigation-ui:2.3.0'
54 | implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
55 | implementation 'androidx.navigation:navigation-fragment-ktx:2.3.0'
56 | implementation 'androidx.navigation:navigation-ui-ktx:2.3.0'
57 | implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
58 |
59 | // Firebase
60 | implementation 'com.google.firebase:firebase-database:19.3.1'
61 | implementation 'com.google.firebase:firebase-auth:19.3.2'
62 | implementation 'com.google.firebase:firebase-storage:19.1.1'
63 |
64 | // Picasso
65 | implementation 'com.squareup.picasso:picasso:2.71828'
66 | implementation 'jp.wasabeef:picasso-transformations:2.2.1'
67 |
68 | testImplementation 'junit:junit:4.13'
69 | androidTestImplementation 'androidx.test.ext:junit:1.1.1'
70 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
71 | }
72 |
--------------------------------------------------------------------------------
/app/google-services.json:
--------------------------------------------------------------------------------
1 | -EDIT THIS-
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/fredrikbogg/android_chat_app/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.fredrikbogg.android_chat_app", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
14 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dgewe/Chat-App-Android/cff2f947a4496e46cbaa750b4e3fa768ba2308f0/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/App.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app
2 |
3 | import android.app.Application
4 | import com.fredrikbogg.android_chat_app.util.SharedPreferencesUtil
5 |
6 |
7 | class App : Application() {
8 |
9 | override fun onCreate() {
10 | super.onCreate()
11 | application = this
12 | }
13 |
14 | companion object {
15 | lateinit var application: Application
16 | private set
17 |
18 | var myUserID: String = ""
19 | get() {
20 | field = SharedPreferencesUtil.getUserID(application.applicationContext).orEmpty()
21 | return field
22 | }
23 | private set
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/data/Event.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app.data
2 |
3 | import androidx.lifecycle.Observer
4 |
5 |
6 | open class Event(private val content: T) {
7 | private var isHandled = false
8 |
9 | fun getContentIfNotHandled(): T? {
10 | return if (isHandled) {
11 | null
12 | } else {
13 | isHandled = true
14 | content
15 | }
16 | }
17 | }
18 |
19 | class EventObserver(private val onEventUnhandledContent: (T) -> Unit) : Observer> {
20 | override fun onChanged(event: Event?) {
21 | event?.getContentIfNotHandled()?.let { onEventUnhandledContent(it) }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/data/Result.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app.data
2 |
3 |
4 | sealed class Result {
5 | data class Success(val data: T? = null, val msg: String? = null) : Result()
6 | class Error(val msg: String? = null) : Result()
7 | object Loading : Result()
8 | }
9 |
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/data/db/entity/Chat.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app.data.db.entity
2 |
3 | import com.google.firebase.database.PropertyName
4 |
5 |
6 | data class Chat(
7 | @get:PropertyName("lastMessage") @set:PropertyName("lastMessage") var lastMessage: Message = Message(),
8 | @get:PropertyName("info") @set:PropertyName("info") var info: ChatInfo = ChatInfo()
9 | )
10 |
11 | data class ChatInfo(
12 | @get:PropertyName("id") @set:PropertyName("id") var id: String = ""
13 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/data/db/entity/Message.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app.data.db.entity
2 |
3 | import com.google.firebase.database.PropertyName
4 | import java.util.*
5 |
6 |
7 | data class Message(
8 | @get:PropertyName("senderID") @set:PropertyName("senderID") var senderID: String = "",
9 | @get:PropertyName("text") @set:PropertyName("text") var text: String = "",
10 | @get:PropertyName("epochTimeMs") @set:PropertyName("epochTimeMs") var epochTimeMs: Long = Date().time,
11 | @get:PropertyName("seen") @set:PropertyName("seen") var seen: Boolean = false
12 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/data/db/entity/User.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app.data.db.entity
2 |
3 | import com.google.firebase.database.PropertyName
4 |
5 |
6 | data class User(
7 | @get:PropertyName("info") @set:PropertyName("info") var info: UserInfo = UserInfo(),
8 | @get:PropertyName("friends") @set:PropertyName("friends") var friends: HashMap = HashMap(),
9 | @get:PropertyName("notifications") @set:PropertyName("notifications") var notifications: HashMap = HashMap(),
10 | @get:PropertyName("sentRequests") @set:PropertyName("sentRequests") var sentRequests: HashMap = HashMap()
11 | )
12 |
13 | data class UserFriend(
14 | @get:PropertyName("userID") @set:PropertyName("userID") var userID: String = ""
15 | )
16 |
17 | data class UserInfo(
18 | @get:PropertyName("id") @set:PropertyName("id") var id: String = "",
19 | @get:PropertyName("displayName") @set:PropertyName("displayName") var displayName: String = "",
20 | @get:PropertyName("status") @set:PropertyName("status") var status: String = "No status",
21 | @get:PropertyName("profileImageUrl") @set:PropertyName("profileImageUrl") var profileImageUrl: String = "",
22 | @get:PropertyName("online") @set:PropertyName("online") var online: Boolean = false
23 | )
24 |
25 | data class UserNotification(
26 | @get:PropertyName("userID") @set:PropertyName("userID") var userID: String = ""
27 | )
28 |
29 | data class UserRequest(
30 | @get:PropertyName("userID") @set:PropertyName("userID") var userID: String = ""
31 | )
32 |
33 |
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/data/db/remote/FirebaseAuthSource.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app.data.db.remote
2 |
3 | import com.fredrikbogg.android_chat_app.data.model.CreateUser
4 | import com.fredrikbogg.android_chat_app.data.model.Login
5 | import com.fredrikbogg.android_chat_app.data.Result
6 | import com.google.android.gms.tasks.Task
7 | import com.google.firebase.auth.AuthResult
8 | import com.google.firebase.auth.FirebaseAuth
9 | import com.google.firebase.auth.FirebaseUser
10 |
11 | class FirebaseAuthStateObserver {
12 |
13 | private var authListener: FirebaseAuth.AuthStateListener? = null
14 | private var instance: FirebaseAuth? = null
15 |
16 | fun start(valueEventListener: FirebaseAuth.AuthStateListener, instance: FirebaseAuth) {
17 | this.authListener = valueEventListener
18 | this.instance = instance
19 | this.instance!!.addAuthStateListener(authListener!!)
20 | }
21 |
22 | fun clear() {
23 | authListener?.let { instance?.removeAuthStateListener(it) }
24 | }
25 | }
26 |
27 | class FirebaseAuthSource {
28 |
29 | companion object {
30 | val authInstance = FirebaseAuth.getInstance()
31 | }
32 |
33 | private fun attachAuthObserver(b: ((Result) -> Unit)): FirebaseAuth.AuthStateListener {
34 | return FirebaseAuth.AuthStateListener {
35 | if (it.currentUser == null) {
36 | b.invoke(Result.Error("No user"))
37 | } else { b.invoke(Result.Success(it.currentUser)) }
38 | }
39 | }
40 |
41 | fun loginWithEmailAndPassword(login: Login): Task {
42 | return authInstance.signInWithEmailAndPassword(login.email, login.password)
43 | }
44 |
45 | fun createUser(createUser: CreateUser): Task {
46 | return authInstance.createUserWithEmailAndPassword(createUser.email, createUser.password)
47 | }
48 |
49 | fun logout() {
50 | authInstance.signOut()
51 | }
52 |
53 | fun attachAuthStateObserver(firebaseAuthStateObserver: FirebaseAuthStateObserver, b: ((Result) -> Unit)) {
54 | val listener = attachAuthObserver(b)
55 | firebaseAuthStateObserver.start(listener, authInstance)
56 | }
57 | }
58 |
59 |
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/data/db/remote/FirebaseStorageSource.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app.data.db.remote
2 |
3 | import android.net.Uri
4 | import com.google.android.gms.tasks.Task
5 | import com.google.firebase.storage.FirebaseStorage
6 |
7 | // Task based
8 | class FirebaseStorageSource {
9 | private val storageInstance = FirebaseStorage.getInstance()
10 |
11 | fun uploadUserImage(userID: String, bArr: ByteArray): Task {
12 | val path = "user_photos/$userID/profile_image"
13 | val ref = storageInstance.reference.child(path)
14 |
15 | return ref.putBytes(bArr).continueWithTask {
16 | ref.downloadUrl
17 | }
18 | }
19 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/data/db/repository/AuthRepository.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app.data.db.repository
2 |
3 | import com.fredrikbogg.android_chat_app.data.model.CreateUser
4 | import com.fredrikbogg.android_chat_app.data.model.Login
5 | import com.fredrikbogg.android_chat_app.data.db.remote.FirebaseAuthSource
6 | import com.fredrikbogg.android_chat_app.data.db.remote.FirebaseAuthStateObserver
7 | import com.fredrikbogg.android_chat_app.data.Result
8 | import com.google.firebase.auth.FirebaseUser
9 |
10 | class AuthRepository{
11 | private val firebaseAuthService = FirebaseAuthSource()
12 |
13 | fun observeAuthState(stateObserver: FirebaseAuthStateObserver, b: ((Result) -> Unit)){
14 | firebaseAuthService.attachAuthStateObserver(stateObserver,b)
15 | }
16 |
17 | fun loginUser(login: Login, b: ((Result) -> Unit)) {
18 | b.invoke(Result.Loading)
19 | firebaseAuthService.loginWithEmailAndPassword(login).addOnSuccessListener {
20 | b.invoke(Result.Success(it.user))
21 | }.addOnFailureListener {
22 | b.invoke(Result.Error(msg = it.message))
23 | }
24 | }
25 |
26 | fun createUser(createUser: CreateUser, b: ((Result) -> Unit)) {
27 | b.invoke(Result.Loading)
28 | firebaseAuthService.createUser(createUser).addOnSuccessListener {
29 | b.invoke(Result.Success(it.user))
30 | }.addOnFailureListener {
31 | b.invoke(Result.Error(msg = it.message))
32 | }
33 | }
34 |
35 | fun logoutUser() {
36 | firebaseAuthService.logout()
37 | }
38 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/data/db/repository/DatabaseRepository.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app.data.db.repository
2 |
3 | import com.fredrikbogg.android_chat_app.data.db.entity.*
4 | import com.fredrikbogg.android_chat_app.data.db.remote.FirebaseDataSource
5 | import com.fredrikbogg.android_chat_app.data.db.remote.FirebaseReferenceChildObserver
6 | import com.fredrikbogg.android_chat_app.data.db.remote.FirebaseReferenceValueObserver
7 | import com.fredrikbogg.android_chat_app.data.Result
8 | import com.fredrikbogg.android_chat_app.util.wrapSnapshotToArrayList
9 | import com.fredrikbogg.android_chat_app.util.wrapSnapshotToClass
10 |
11 |
12 | class DatabaseRepository {
13 | private val firebaseDatabaseService = FirebaseDataSource()
14 |
15 | //region Update
16 | fun updateUserStatus(userID: String, status: String) {
17 | firebaseDatabaseService.updateUserStatus(userID, status)
18 | }
19 |
20 | fun updateNewMessage(messagesID: String, message: Message) {
21 | firebaseDatabaseService.pushNewMessage(messagesID, message)
22 | }
23 |
24 | fun updateNewUser(user: User) {
25 | firebaseDatabaseService.updateNewUser(user)
26 | }
27 |
28 | fun updateNewFriend(myUser: UserFriend, otherUser: UserFriend) {
29 | firebaseDatabaseService.updateNewFriend(myUser, otherUser)
30 | }
31 |
32 | fun updateNewSentRequest(userID: String, userRequest: UserRequest) {
33 | firebaseDatabaseService.updateNewSentRequest(userID, userRequest)
34 | }
35 |
36 | fun updateNewNotification(otherUserID: String, userNotification: UserNotification) {
37 | firebaseDatabaseService.updateNewNotification(otherUserID, userNotification)
38 | }
39 |
40 | fun updateChatLastMessage(chatID: String, message: Message) {
41 | firebaseDatabaseService.updateLastMessage(chatID, message)
42 | }
43 |
44 | fun updateNewChat(chat: Chat){
45 | firebaseDatabaseService.updateNewChat(chat)
46 | }
47 |
48 | fun updateUserProfileImageUrl(userID: String, url: String){
49 | firebaseDatabaseService.updateUserProfileImageUrl(userID, url)
50 | }
51 |
52 | //endregion
53 |
54 | //region Remove
55 | fun removeNotification(userID: String, notificationID: String) {
56 | firebaseDatabaseService.removeNotification(userID, notificationID)
57 | }
58 |
59 | fun removeFriend(userID: String, friendID: String) {
60 | firebaseDatabaseService.removeFriend(userID, friendID)
61 | }
62 |
63 | fun removeSentRequest(otherUserID: String, myUserID: String) {
64 | firebaseDatabaseService.removeSentRequest(otherUserID, myUserID)
65 | }
66 |
67 | fun removeChat(chatID: String) {
68 | firebaseDatabaseService.removeChat(chatID)
69 | }
70 |
71 | fun removeMessages(messagesID: String){
72 | firebaseDatabaseService.removeMessages(messagesID)
73 | }
74 |
75 | //endregion
76 |
77 | //region Load Single
78 |
79 | fun loadUser(userID: String, b: ((Result) -> Unit)) {
80 | firebaseDatabaseService.loadUserTask(userID).addOnSuccessListener {
81 | b.invoke(Result.Success(wrapSnapshotToClass(User::class.java, it)))
82 | }.addOnFailureListener { b.invoke(Result.Error(it.message)) }
83 | }
84 |
85 | fun loadUserInfo(userID: String, b: ((Result) -> Unit)) {
86 | firebaseDatabaseService.loadUserInfoTask(userID).addOnSuccessListener {
87 | b.invoke(Result.Success(wrapSnapshotToClass(UserInfo::class.java, it)))
88 | }.addOnFailureListener { b.invoke(Result.Error(it.message)) }
89 | }
90 |
91 | fun loadChat(chatID: String, b: ((Result) -> Unit)) {
92 | firebaseDatabaseService.loadChatTask(chatID).addOnSuccessListener {
93 | b.invoke(Result.Success(wrapSnapshotToClass(Chat::class.java, it)))
94 | }.addOnFailureListener { b.invoke(Result.Error(it.message)) }
95 | }
96 |
97 | //endregion
98 |
99 | //region Load List
100 |
101 | fun loadUsers(b: ((Result>) -> Unit)) {
102 | b.invoke(Result.Loading)
103 | firebaseDatabaseService.loadUsersTask().addOnSuccessListener {
104 | val usersList = wrapSnapshotToArrayList(User::class.java, it)
105 | b.invoke(Result.Success(usersList))
106 | }.addOnFailureListener { b.invoke(Result.Error(it.message)) }
107 | }
108 |
109 | fun loadFriends(userID: String, b: ((Result>) -> Unit)) {
110 | b.invoke(Result.Loading)
111 | firebaseDatabaseService.loadFriendsTask(userID).addOnSuccessListener {
112 | val friendsList = wrapSnapshotToArrayList(UserFriend::class.java, it)
113 | b.invoke(Result.Success(friendsList))
114 | }.addOnFailureListener { b.invoke(Result.Error(it.message)) }
115 | }
116 |
117 | fun loadNotifications(userID: String, b: ((Result>) -> Unit)) {
118 | b.invoke(Result.Loading)
119 | firebaseDatabaseService.loadNotificationsTask(userID).addOnSuccessListener {
120 | val notificationsList = wrapSnapshotToArrayList(UserNotification::class.java, it)
121 | b.invoke(Result.Success(notificationsList))
122 | }.addOnFailureListener { b.invoke(Result.Error(it.message)) }
123 | }
124 |
125 | //endregion
126 |
127 | //#region Load and Observe
128 |
129 | fun loadAndObserveUser(userID: String, observer: FirebaseReferenceValueObserver, b: ((Result) -> Unit)) {
130 | firebaseDatabaseService.attachUserObserver(User::class.java, userID, observer, b)
131 | }
132 |
133 | fun loadAndObserveUserInfo(userID: String, observer: FirebaseReferenceValueObserver, b: ((Result) -> Unit)) {
134 | firebaseDatabaseService.attachUserInfoObserver(UserInfo::class.java, userID, observer, b)
135 | }
136 |
137 | fun loadAndObserveUserNotifications(userID: String, observer: FirebaseReferenceValueObserver, b: ((Result>) -> Unit)){
138 | firebaseDatabaseService.attachUserNotificationsObserver(UserNotification::class.java, userID, observer, b)
139 | }
140 |
141 | fun loadAndObserveMessagesAdded(messagesID: String, observer: FirebaseReferenceChildObserver, b: ((Result) -> Unit)) {
142 | firebaseDatabaseService.attachMessagesObserver(Message::class.java, messagesID, observer, b)
143 | }
144 |
145 | fun loadAndObserveChat(chatID: String, observer: FirebaseReferenceValueObserver, b: ((Result) -> Unit)) {
146 | firebaseDatabaseService.attachChatObserver(Chat::class.java, chatID, observer, b)
147 | }
148 |
149 | //endregion
150 | }
151 |
152 |
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/data/db/repository/StorageRepository.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app.data.db.repository
2 |
3 | import android.net.Uri
4 | import com.fredrikbogg.android_chat_app.data.db.remote.FirebaseStorageSource
5 | import com.fredrikbogg.android_chat_app.data.Result
6 |
7 | class StorageRepository {
8 | private val firebaseStorageService = FirebaseStorageSource()
9 |
10 | fun updateUserProfileImage(userID: String, byteArray: ByteArray, b: (Result) -> Unit) {
11 | b.invoke(Result.Loading)
12 | firebaseStorageService.uploadUserImage(userID, byteArray).addOnSuccessListener {
13 | b.invoke((Result.Success(it)))
14 | }.addOnFailureListener {
15 | b.invoke(Result.Error(it.message))
16 | }
17 | }
18 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/data/model/ChatWithUserInfo.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app.data.model
2 |
3 | import com.fredrikbogg.android_chat_app.data.db.entity.Chat
4 | import com.fredrikbogg.android_chat_app.data.db.entity.UserInfo
5 |
6 | data class ChatWithUserInfo(
7 | var mChat: Chat,
8 | var mUserInfo: UserInfo
9 | )
10 |
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/data/model/CreateUser.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app.data.model
2 |
3 | data class CreateUser(
4 | var displayName: String = "",
5 | var email: String = "",
6 | var password: String = ""
7 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/data/model/Login.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app.data.model
2 |
3 | data class Login(
4 | var email: String = "",
5 | var password: String = ""
6 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/ui/DefaultBindings.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app.ui
2 |
3 | import android.annotation.SuppressLint
4 | import android.widget.ImageView
5 | import android.widget.TextView
6 | import androidx.databinding.BindingAdapter
7 | import androidx.recyclerview.widget.RecyclerView
8 | import com.fredrikbogg.android_chat_app.R
9 | import com.squareup.picasso.Picasso
10 | import jp.wasabeef.picasso.transformations.BlurTransformation
11 | import java.text.SimpleDateFormat
12 | import java.util.*
13 | import java.util.concurrent.TimeUnit
14 |
15 |
16 | @BindingAdapter("bind_image_url_blur")
17 | fun bindBlurImageWithPicasso(imageView: ImageView, url: String?) {
18 | if (!url.isNullOrBlank()) {
19 | Picasso.get().load(url).error(R.drawable.ic_baseline_error_24)
20 | .transform(BlurTransformation(imageView.context, 15, 1)).into(imageView)
21 | }
22 | }
23 |
24 | @BindingAdapter("bind_image_url")
25 | fun bindImageWithPicasso(imageView: ImageView, url: String?) {
26 | when (url) {
27 | null -> Unit
28 | "" -> imageView.setBackgroundResource(R.drawable.ic_baseline_person_24)
29 | else -> Picasso.get().load(url).error(R.drawable.ic_baseline_error_24).into(imageView)
30 | }
31 | }
32 |
33 | @SuppressLint("SimpleDateFormat")
34 | @BindingAdapter("bind_epochTimeMsToDate_with_days_ago")
35 | fun TextView.bindEpochTimeMsToDateWithDaysAgo(epochTimeMs: Long) {
36 | val numOfDays = TimeUnit.MILLISECONDS.toDays(Date().time - epochTimeMs)
37 |
38 | this.text = when {
39 | numOfDays == 1.toLong() -> "Yesterday"
40 | numOfDays > 1.toLong() -> "$numOfDays days ago"
41 | else -> {
42 | val pat =
43 | SimpleDateFormat().toLocalizedPattern().replace("\\W?[YyMd]+\\W?".toRegex(), " ")
44 | val formatter = SimpleDateFormat(pat, Locale.getDefault())
45 | formatter.format(Date(epochTimeMs))
46 | }
47 | }
48 | }
49 |
50 | @SuppressLint("SimpleDateFormat")
51 | @BindingAdapter("bind_epochTimeMsToDate")
52 | fun TextView.bindEpochTimeMsToDate(epochTimeMs: Long) {
53 | if (epochTimeMs > 0) {
54 | val currentTimeMs = Date().time
55 | val numOfDays = TimeUnit.MILLISECONDS.toDays(currentTimeMs - epochTimeMs)
56 |
57 | val replacePattern = when {
58 | numOfDays >= 1.toLong() -> "Yy"
59 | else -> "YyMd"
60 | }
61 | val pat = SimpleDateFormat().toLocalizedPattern().replace("\\W?[$replacePattern]+\\W?".toRegex(), " ")
62 | val formatter = SimpleDateFormat(pat, Locale.getDefault())
63 | this.text = formatter.format(Date(epochTimeMs))
64 | }
65 | }
66 |
67 | @BindingAdapter("bind_disable_item_animator")
68 | fun bindDisableRecyclerViewItemAnimator(recyclerView: RecyclerView, disable: Boolean) {
69 | if (disable) {
70 | recyclerView.itemAnimator = null
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/ui/DefaultViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app.ui
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MutableLiveData
5 | import androidx.lifecycle.ViewModel
6 | import com.fredrikbogg.android_chat_app.data.Event
7 | import com.fredrikbogg.android_chat_app.data.Result
8 |
9 | abstract class DefaultViewModel : ViewModel() {
10 | protected val mSnackBarText = MutableLiveData>()
11 | val snackBarText: LiveData> = mSnackBarText
12 |
13 | private val mDataLoading = MutableLiveData>()
14 | val dataLoading: LiveData> = mDataLoading
15 |
16 | protected fun onResult(mutableLiveData: MutableLiveData? = null, result: Result) {
17 | when (result) {
18 | is Result.Loading -> mDataLoading.value = Event(true)
19 |
20 | is Result.Error -> {
21 | mDataLoading.value = Event(false)
22 | result.msg?.let { mSnackBarText.value = Event(it) }
23 | }
24 |
25 | is Result.Success -> {
26 | mDataLoading.value = Event(false)
27 | result.data?.let { mutableLiveData?.value = it }
28 | result.msg?.let { mSnackBarText.value = Event(it) }
29 | }
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/ui/chat/ChatFragment.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app.ui.chat
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.MenuItem
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import androidx.appcompat.app.AppCompatActivity
9 | import androidx.fragment.app.Fragment
10 | import androidx.fragment.app.viewModels
11 | import androidx.navigation.fragment.findNavController
12 | import androidx.recyclerview.widget.RecyclerView
13 | import com.fredrikbogg.android_chat_app.databinding.FragmentChatBinding
14 | import com.fredrikbogg.android_chat_app.databinding.ToolbarAddonChatBinding
15 | import kotlinx.android.synthetic.main.fragment_chat.*
16 |
17 |
18 | class ChatFragment : Fragment() {
19 |
20 | companion object {
21 | const val ARGS_KEY_USER_ID = "bundle_user_id"
22 | const val ARGS_KEY_OTHER_USER_ID = "bundle_other_user_id"
23 | const val ARGS_KEY_CHAT_ID = "bundle_other_chat_id"
24 | }
25 |
26 | private val viewModel: ChatViewModel by viewModels {
27 | ChatViewModelFactory(
28 | requireArguments().getString(ARGS_KEY_USER_ID)!!,
29 | requireArguments().getString(ARGS_KEY_OTHER_USER_ID)!!,
30 | requireArguments().getString(ARGS_KEY_CHAT_ID)!!
31 | )
32 | }
33 |
34 | private lateinit var viewDataBinding: FragmentChatBinding
35 | private lateinit var listAdapter: MessagesListAdapter
36 | private lateinit var listAdapterObserver: RecyclerView.AdapterDataObserver
37 | private lateinit var toolbarAddonChatBinding: ToolbarAddonChatBinding
38 |
39 | override fun onDestroy() {
40 | super.onDestroy()
41 | removeCustomToolbar()
42 | }
43 |
44 | override fun onCreateView(
45 | inflater: LayoutInflater, container: ViewGroup?,
46 | savedInstanceState: Bundle?
47 | ): View? {
48 | viewDataBinding =
49 | FragmentChatBinding.inflate(inflater, container, false).apply { viewmodel = viewModel }
50 | viewDataBinding.lifecycleOwner = this.viewLifecycleOwner
51 | setHasOptionsMenu(true)
52 |
53 | toolbarAddonChatBinding =
54 | ToolbarAddonChatBinding.inflate(inflater, container, false)
55 | .apply { viewmodel = viewModel }
56 | toolbarAddonChatBinding.lifecycleOwner = this.viewLifecycleOwner
57 |
58 | return viewDataBinding.root
59 | }
60 |
61 | override fun onActivityCreated(savedInstanceState: Bundle?) {
62 | super.onActivityCreated(savedInstanceState)
63 | setupCustomToolbar()
64 | setupListAdapter()
65 | }
66 |
67 | override fun onOptionsItemSelected(item: MenuItem): Boolean {
68 | when (item.itemId) {
69 | android.R.id.home -> {
70 | findNavController().popBackStack()
71 | return true
72 | }
73 | }
74 | return super.onOptionsItemSelected(item)
75 | }
76 |
77 | private fun removeCustomToolbar() {
78 | val supportActionBar = (activity as AppCompatActivity?)!!.supportActionBar
79 | supportActionBar!!.setDisplayShowCustomEnabled(false)
80 | supportActionBar.customView = null
81 | }
82 |
83 | private fun setupCustomToolbar() {
84 | val supportActionBar = (activity as AppCompatActivity?)!!.supportActionBar
85 | supportActionBar!!.setDisplayShowCustomEnabled(true)
86 | supportActionBar.customView = toolbarAddonChatBinding.root
87 | }
88 |
89 | private fun setupListAdapter() {
90 | val viewModel = viewDataBinding.viewmodel
91 | if (viewModel != null) {
92 | listAdapterObserver = (object : RecyclerView.AdapterDataObserver() {
93 | override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
94 | messagesRecyclerView.scrollToPosition(positionStart)
95 | }
96 | })
97 | listAdapter =
98 | MessagesListAdapter(viewModel, requireArguments().getString(ARGS_KEY_USER_ID)!!)
99 | listAdapter.registerAdapterDataObserver(listAdapterObserver)
100 | viewDataBinding.messagesRecyclerView.adapter = listAdapter
101 | } else {
102 | throw Exception("The viewmodel is not initialized")
103 | }
104 | }
105 |
106 | override fun onDestroyView() {
107 | super.onDestroyView()
108 | listAdapter.unregisterAdapterDataObserver(listAdapterObserver)
109 | }
110 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/ui/chat/ChatViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app.ui.chat
2 |
3 | import androidx.lifecycle.*
4 | import com.fredrikbogg.android_chat_app.data.db.entity.Chat
5 | import com.fredrikbogg.android_chat_app.data.db.entity.Message
6 | import com.fredrikbogg.android_chat_app.data.db.entity.UserInfo
7 | import com.fredrikbogg.android_chat_app.data.db.remote.FirebaseReferenceChildObserver
8 | import com.fredrikbogg.android_chat_app.data.db.remote.FirebaseReferenceValueObserver
9 | import com.fredrikbogg.android_chat_app.data.db.repository.DatabaseRepository
10 | import com.fredrikbogg.android_chat_app.ui.DefaultViewModel
11 | import com.fredrikbogg.android_chat_app.data.Result
12 | import com.fredrikbogg.android_chat_app.util.addNewItem
13 |
14 | class ChatViewModelFactory(private val myUserID: String, private val otherUserID: String, private val chatID: String) :
15 | ViewModelProvider.Factory {
16 | override fun create(modelClass: Class): T {
17 | return ChatViewModel(myUserID, otherUserID, chatID) as T
18 | }
19 | }
20 |
21 | class ChatViewModel(private val myUserID: String, private val otherUserID: String, private val chatID: String) : DefaultViewModel() {
22 |
23 | private val dbRepository: DatabaseRepository = DatabaseRepository()
24 |
25 | private val _otherUser: MutableLiveData = MutableLiveData()
26 | private val _addedMessage = MutableLiveData()
27 |
28 | private val fbRefMessagesChildObserver = FirebaseReferenceChildObserver()
29 | private val fbRefUserInfoObserver = FirebaseReferenceValueObserver()
30 |
31 | val messagesList = MediatorLiveData>()
32 | val newMessageText = MutableLiveData()
33 | val otherUser: LiveData = _otherUser
34 |
35 | init {
36 | setupChat()
37 | checkAndUpdateLastMessageSeen()
38 | }
39 |
40 | override fun onCleared() {
41 | super.onCleared()
42 | fbRefMessagesChildObserver.clear()
43 | fbRefUserInfoObserver.clear()
44 | }
45 |
46 | private fun checkAndUpdateLastMessageSeen() {
47 | dbRepository.loadChat(chatID) { result: Result ->
48 | if (result is Result.Success && result.data != null) {
49 | result.data.lastMessage.let {
50 | if (!it.seen && it.senderID != myUserID) {
51 | it.seen = true
52 | dbRepository.updateChatLastMessage(chatID, it)
53 | }
54 | }
55 | }
56 | }
57 | }
58 |
59 | private fun setupChat() {
60 | dbRepository.loadAndObserveUserInfo(otherUserID, fbRefUserInfoObserver) { result: Result ->
61 | onResult(_otherUser, result)
62 | if (result is Result.Success && !fbRefMessagesChildObserver.isObserving()) {
63 | loadAndObserveNewMessages()
64 | }
65 | }
66 | }
67 |
68 | private fun loadAndObserveNewMessages() {
69 | messagesList.addSource(_addedMessage) { messagesList.addNewItem(it) }
70 |
71 | dbRepository.loadAndObserveMessagesAdded(
72 | chatID,
73 | fbRefMessagesChildObserver
74 | ) { result: Result ->
75 | onResult(_addedMessage, result)
76 | }
77 | }
78 |
79 | fun sendMessagePressed() {
80 | if (!newMessageText.value.isNullOrBlank()) {
81 | val newMsg = Message(myUserID, newMessageText.value!!)
82 | dbRepository.updateNewMessage(chatID, newMsg)
83 | dbRepository.updateChatLastMessage(chatID, newMsg)
84 | newMessageText.value = null
85 | }
86 | }
87 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/ui/chat/MessagesBindings.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app.ui.chat
2 |
3 | import android.view.View
4 | import androidx.databinding.BindingAdapter
5 | import androidx.recyclerview.widget.RecyclerView
6 | import com.fredrikbogg.android_chat_app.data.db.entity.Message
7 | import kotlin.math.abs
8 |
9 | @BindingAdapter("bind_messages_list")
10 | fun bindMessagesList(listView: RecyclerView, items: List?) {
11 | items?.let {
12 | (listView.adapter as MessagesListAdapter).submitList(items)
13 | listView.scrollToPosition(items.size - 1)
14 | }
15 | }
16 |
17 | @BindingAdapter("bind_message", "bind_message_viewModel")
18 | fun View.bindShouldMessageShowTimeText(message: Message, viewModel: ChatViewModel) {
19 | val halfHourInMilli = 1800000
20 | val index = viewModel.messagesList.value!!.indexOf(message)
21 |
22 | if (index == 0) {
23 | this.visibility = View.VISIBLE
24 | } else {
25 | val messageBefore = viewModel.messagesList.value!![index - 1]
26 |
27 | if (abs(messageBefore.epochTimeMs - message.epochTimeMs) > halfHourInMilli) {
28 | this.visibility = View.VISIBLE
29 | } else {
30 | this.visibility = View.GONE
31 | }
32 | }
33 | }
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/ui/chat/MessagesListAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app.ui.chat
2 |
3 | import android.view.LayoutInflater
4 | import android.view.ViewGroup
5 | import androidx.recyclerview.widget.DiffUtil
6 | import androidx.recyclerview.widget.ListAdapter
7 | import androidx.recyclerview.widget.RecyclerView
8 | import com.fredrikbogg.android_chat_app.data.db.entity.Message
9 | import com.fredrikbogg.android_chat_app.databinding.ListItemMessageReceivedBinding
10 | import com.fredrikbogg.android_chat_app.databinding.ListItemMessageSentBinding
11 |
12 | class MessagesListAdapter internal constructor(private val viewModel: ChatViewModel, private val userId: String) : ListAdapter(MessageDiffCallback()) {
13 |
14 | private val holderTypeMessageReceived = 1
15 | private val holderTypeMessageSent = 2
16 |
17 | class ReceivedViewHolder(private val binding: ListItemMessageReceivedBinding) :
18 | RecyclerView.ViewHolder(binding.root) {
19 | fun bind(viewModel: ChatViewModel, item: Message) {
20 | binding.viewmodel = viewModel
21 | binding.message = item
22 | binding.executePendingBindings()
23 | }
24 | }
25 |
26 | class SentViewHolder(private val binding: ListItemMessageSentBinding) :
27 | RecyclerView.ViewHolder(binding.root) {
28 | fun bind(viewModel: ChatViewModel, item: Message) {
29 | binding.viewmodel = viewModel
30 | binding.message = item
31 | binding.executePendingBindings()
32 | }
33 | }
34 |
35 | override fun getItemViewType(position: Int): Int {
36 | return if (getItem(position).senderID != userId) {
37 | holderTypeMessageReceived
38 | } else {
39 | holderTypeMessageSent
40 | }
41 | }
42 |
43 | override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
44 | when (holder.itemViewType) {
45 | holderTypeMessageSent -> (holder as SentViewHolder).bind(
46 | viewModel,
47 | getItem(position)
48 | )
49 | holderTypeMessageReceived -> (holder as ReceivedViewHolder).bind(
50 | viewModel,
51 | getItem(position)
52 | )
53 | }
54 | }
55 |
56 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
57 | val layoutInflater = LayoutInflater.from(parent.context)
58 |
59 | return when (viewType) {
60 | holderTypeMessageSent -> {
61 | val binding = ListItemMessageSentBinding.inflate(layoutInflater, parent, false)
62 | SentViewHolder(binding)
63 | }
64 | holderTypeMessageReceived -> {
65 | val binding = ListItemMessageReceivedBinding.inflate(layoutInflater, parent, false)
66 | ReceivedViewHolder(binding)
67 | }
68 | else -> {
69 | throw Exception("Error reading holder type")
70 | }
71 | }
72 | }
73 | }
74 |
75 | class MessageDiffCallback : DiffUtil.ItemCallback() {
76 | override fun areItemsTheSame(oldItem: Message, newItem: Message): Boolean {
77 | return oldItem == newItem
78 | }
79 |
80 | override fun areContentsTheSame(oldItem: Message, newItem: Message): Boolean {
81 | return oldItem.epochTimeMs == newItem.epochTimeMs
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/ui/chats/ChatsBindings.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("unused")
2 |
3 | package com.fredrikbogg.android_chat_app.ui.chats
4 |
5 | import android.view.View
6 | import android.widget.TextView
7 | import androidx.databinding.BindingAdapter
8 | import androidx.recyclerview.widget.RecyclerView
9 | import com.fredrikbogg.android_chat_app.R
10 | import com.fredrikbogg.android_chat_app.data.model.ChatWithUserInfo
11 | import com.fredrikbogg.android_chat_app.data.db.entity.Message
12 |
13 | @BindingAdapter("bind_chats_list")
14 | fun bindChatsList(listView: RecyclerView, items: List?) {
15 | items?.let { (listView.adapter as ChatsListAdapter).submitList(items) }
16 | }
17 |
18 | @BindingAdapter("bind_chat_message_text", "bind_chat_message_text_viewModel")
19 | fun TextView.bindMessageYouToText(message: Message, viewModel: ChatsViewModel) {
20 | this.text = if (message.senderID == viewModel.myUserID) {
21 | "You: " + message.text
22 | } else {
23 | message.text
24 | }
25 | }
26 |
27 | @BindingAdapter("bind_message_view", "bind_message_textView", "bind_message", "bind_myUserID")
28 | fun View.bindMessageSeen(view: View, textView: TextView, message: Message, myUserID: String) {
29 | if (message.senderID != myUserID && !message.seen) {
30 | view.visibility = View.VISIBLE
31 | textView.setTextAppearance(R.style.MessageNotSeen)
32 | // textView.alpha = 1f
33 | } else {
34 | view.visibility = View.INVISIBLE
35 | textView.setTextAppearance(R.style.MessageSeen)
36 | // textView.alpha = 1f
37 | }
38 | }
39 |
40 |
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/ui/chats/ChatsFragment.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app.ui.chats
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import androidx.core.os.bundleOf
8 | import androidx.fragment.app.Fragment
9 | import androidx.fragment.app.viewModels
10 | import androidx.navigation.fragment.findNavController
11 | import com.fredrikbogg.android_chat_app.App
12 | import com.fredrikbogg.android_chat_app.R
13 | import com.fredrikbogg.android_chat_app.data.model.ChatWithUserInfo
14 | import com.fredrikbogg.android_chat_app.databinding.FragmentChatsBinding
15 | import com.fredrikbogg.android_chat_app.data.EventObserver
16 | import com.fredrikbogg.android_chat_app.ui.chat.ChatFragment
17 | import com.fredrikbogg.android_chat_app.util.convertTwoUserIDs
18 |
19 | class ChatsFragment : Fragment() {
20 |
21 | private val viewModel: ChatsViewModel by viewModels { ChatsViewModelFactory(App.myUserID) }
22 | private lateinit var viewDataBinding: FragmentChatsBinding
23 | private lateinit var listAdapter: ChatsListAdapter
24 |
25 | override fun onCreateView(
26 | inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
27 | ): View? {
28 | viewDataBinding =
29 | FragmentChatsBinding.inflate(inflater, container, false).apply { viewmodel = viewModel }
30 | viewDataBinding.lifecycleOwner = this.viewLifecycleOwner
31 | return viewDataBinding.root
32 | }
33 |
34 | override fun onActivityCreated(savedInstanceState: Bundle?) {
35 | super.onActivityCreated(savedInstanceState)
36 | setupListAdapter()
37 | setupObservers()
38 | }
39 |
40 | private fun setupListAdapter() {
41 | val viewModel = viewDataBinding.viewmodel
42 | if (viewModel != null) {
43 | listAdapter = ChatsListAdapter(viewModel)
44 | viewDataBinding.chatsRecyclerView.adapter = listAdapter
45 | } else {
46 | throw Exception("The viewmodel is not initialized")
47 | }
48 | }
49 |
50 | private fun setupObservers() {
51 | viewModel.selectedChat.observe(viewLifecycleOwner,
52 | EventObserver { navigateToChat(it) })
53 | }
54 |
55 | private fun navigateToChat(chatWithUserInfo: ChatWithUserInfo) {
56 | val bundle = bundleOf(
57 | ChatFragment.ARGS_KEY_USER_ID to App.myUserID,
58 | ChatFragment.ARGS_KEY_OTHER_USER_ID to chatWithUserInfo.mUserInfo.id,
59 | ChatFragment.ARGS_KEY_CHAT_ID to convertTwoUserIDs(App.myUserID, chatWithUserInfo.mUserInfo.id)
60 | )
61 | findNavController().navigate(R.id.action_navigation_chats_to_chatFragment, bundle)
62 | }
63 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/ui/chats/ChatsListAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app.ui.chats
2 |
3 | import android.view.LayoutInflater
4 | import android.view.ViewGroup
5 | import androidx.recyclerview.widget.DiffUtil
6 | import androidx.recyclerview.widget.ListAdapter
7 | import androidx.recyclerview.widget.RecyclerView
8 | import com.fredrikbogg.android_chat_app.data.model.ChatWithUserInfo
9 | import com.fredrikbogg.android_chat_app.databinding.ListItemChatBinding
10 |
11 | class ChatsListAdapter internal constructor(private val viewModel: ChatsViewModel) :
12 | ListAdapter<(ChatWithUserInfo), ChatsListAdapter.ViewHolder>(ChatDiffCallback()) {
13 |
14 | class ViewHolder(private val binding: ListItemChatBinding) :
15 | RecyclerView.ViewHolder(binding.root) {
16 | fun bind(viewModel: ChatsViewModel, item: ChatWithUserInfo) {
17 | binding.viewmodel = viewModel
18 | binding.chatwithuserinfo = item
19 | binding.executePendingBindings()
20 | }
21 | }
22 |
23 | override fun onBindViewHolder(holder: ViewHolder, position: Int) {
24 | holder.bind(viewModel, getItem(position))
25 | }
26 |
27 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
28 | val layoutInflater = LayoutInflater.from(parent.context)
29 | val binding = ListItemChatBinding.inflate(layoutInflater, parent, false)
30 | return ViewHolder(binding)
31 | }
32 | }
33 |
34 | class ChatDiffCallback : DiffUtil.ItemCallback() {
35 | override fun areItemsTheSame(oldItem: ChatWithUserInfo, itemWithUserInfo: ChatWithUserInfo): Boolean {
36 | return oldItem == itemWithUserInfo
37 | }
38 |
39 | override fun areContentsTheSame(oldItem: ChatWithUserInfo, itemWithUserInfo: ChatWithUserInfo): Boolean {
40 | return oldItem.mChat.info.id == itemWithUserInfo.mChat.info.id
41 | }
42 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/ui/chats/ChatsViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app.ui.chats
2 |
3 | import androidx.lifecycle.*
4 | import com.fredrikbogg.android_chat_app.data.Event
5 | import com.fredrikbogg.android_chat_app.data.Result
6 | import com.fredrikbogg.android_chat_app.data.db.entity.Chat
7 | import com.fredrikbogg.android_chat_app.data.model.ChatWithUserInfo
8 | import com.fredrikbogg.android_chat_app.data.db.entity.UserFriend
9 | import com.fredrikbogg.android_chat_app.data.db.entity.UserInfo
10 | import com.fredrikbogg.android_chat_app.data.db.remote.FirebaseReferenceValueObserver
11 | import com.fredrikbogg.android_chat_app.data.db.repository.DatabaseRepository
12 | import com.fredrikbogg.android_chat_app.ui.DefaultViewModel
13 | import com.fredrikbogg.android_chat_app.util.addNewItem
14 | import com.fredrikbogg.android_chat_app.util.convertTwoUserIDs
15 | import com.fredrikbogg.android_chat_app.util.updateItemAt
16 |
17 |
18 | class ChatsViewModelFactory(private val myUserID: String) :
19 | ViewModelProvider.Factory {
20 | override fun create(modelClass: Class): T {
21 | return ChatsViewModel(myUserID) as T
22 | }
23 | }
24 |
25 | class ChatsViewModel(val myUserID: String) : DefaultViewModel() {
26 |
27 | private val repository: DatabaseRepository = DatabaseRepository()
28 | private val firebaseReferenceObserverList = ArrayList()
29 | private val _updatedChatWithUserInfo = MutableLiveData()
30 | private val _selectedChat = MutableLiveData>()
31 |
32 | var selectedChat: LiveData> = _selectedChat
33 | val chatsList = MediatorLiveData>()
34 |
35 | init {
36 | chatsList.addSource(_updatedChatWithUserInfo) { newChat ->
37 | val chat = chatsList.value?.find { it.mChat.info.id == newChat.mChat.info.id }
38 | if (chat == null) {
39 | chatsList.addNewItem(newChat)
40 | } else {
41 | chatsList.updateItemAt(newChat, chatsList.value!!.indexOf(chat))
42 | }
43 | }
44 | setupChats()
45 | }
46 |
47 | override fun onCleared() {
48 | super.onCleared()
49 | firebaseReferenceObserverList.forEach { it.clear() }
50 | }
51 |
52 | private fun setupChats() {
53 | loadFriends()
54 | }
55 |
56 | private fun loadFriends() {
57 | repository.loadFriends(myUserID) { result: Result> ->
58 | onResult(null, result)
59 | if (result is Result.Success) result.data?.forEach { loadUserInfo(it) }
60 | }
61 | }
62 |
63 | private fun loadUserInfo(userFriend: UserFriend) {
64 | repository.loadUserInfo(userFriend.userID) { result: Result ->
65 | onResult(null, result)
66 | if (result is Result.Success) result.data?.let { loadAndObserveChat(it) }
67 | }
68 | }
69 |
70 | private fun loadAndObserveChat(userInfo: UserInfo) {
71 | val observer = FirebaseReferenceValueObserver()
72 | firebaseReferenceObserverList.add(observer)
73 | repository.loadAndObserveChat(convertTwoUserIDs(myUserID, userInfo.id), observer) { result: Result ->
74 | if (result is Result.Success) {
75 | _updatedChatWithUserInfo.value = result.data?.let { ChatWithUserInfo(it, userInfo) }
76 | } else if (result is Result.Error) {
77 | chatsList.value?.let {
78 | val newList = mutableListOf().apply { addAll(it) }
79 | newList.removeIf { it2 -> result.msg.toString().contains(it2.mUserInfo.id) }
80 | chatsList.value = newList
81 | }
82 | }
83 | }
84 | }
85 |
86 | fun selectChatWithUserInfoPressed(chat: ChatWithUserInfo) {
87 | _selectedChat.value = Event(chat)
88 | }
89 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/ui/main/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app.ui.main
2 |
3 | import android.os.Bundle
4 | import android.view.View
5 | import android.widget.ProgressBar
6 | import androidx.activity.viewModels
7 | import androidx.appcompat.app.AppCompatActivity
8 | import androidx.appcompat.widget.Toolbar
9 | import androidx.navigation.findNavController
10 | import androidx.navigation.ui.AppBarConfiguration
11 | import androidx.navigation.ui.setupActionBarWithNavController
12 | import androidx.navigation.ui.setupWithNavController
13 | import com.fredrikbogg.android_chat_app.R
14 | import com.fredrikbogg.android_chat_app.data.db.remote.FirebaseDataSource
15 | import com.fredrikbogg.android_chat_app.util.forceHideKeyboard
16 | import com.google.android.material.badge.BadgeDrawable
17 | import com.google.android.material.bottomnavigation.BottomNavigationView
18 |
19 |
20 | class MainActivity : AppCompatActivity() {
21 |
22 | private lateinit var navView: BottomNavigationView
23 | private lateinit var mainProgressBar: ProgressBar
24 | private lateinit var mainToolbar: Toolbar
25 | private lateinit var notificationsBadge: BadgeDrawable
26 | private val viewModel: MainViewModel by viewModels()
27 |
28 | override fun onCreate(savedInstanceState: Bundle?) {
29 | super.onCreate(savedInstanceState)
30 | setContentView(R.layout.activity_main)
31 |
32 | mainToolbar = findViewById(R.id.main_toolbar)
33 | navView = findViewById(R.id.nav_view)
34 | mainProgressBar = findViewById(R.id.main_progressBar)
35 |
36 | notificationsBadge =
37 | navView.getOrCreateBadge(R.id.navigation_notifications).apply { isVisible = false }
38 |
39 | setSupportActionBar(mainToolbar)
40 |
41 | val navController = findNavController(R.id.nav_host_fragment)
42 | navController.addOnDestinationChangedListener { _, destination, _ ->
43 |
44 | when (destination.id) {
45 | R.id.profileFragment -> navView.visibility = View.GONE
46 | R.id.chatFragment -> navView.visibility = View.GONE
47 | R.id.startFragment -> navView.visibility = View.GONE
48 | R.id.loginFragment -> navView.visibility = View.GONE
49 | R.id.createAccountFragment -> navView.visibility = View.GONE
50 | else -> navView.visibility = View.VISIBLE
51 | }
52 | showGlobalProgressBar(false)
53 | currentFocus?.rootView?.forceHideKeyboard()
54 | }
55 |
56 | val appBarConfiguration = AppBarConfiguration(
57 | setOf(
58 | R.id.navigation_chats,
59 | R.id.navigation_notifications,
60 | R.id.navigation_users,
61 | R.id.navigation_settings,
62 | R.id.startFragment
63 | )
64 | )
65 |
66 | setupActionBarWithNavController(navController, appBarConfiguration)
67 | navView.setupWithNavController(navController)
68 | }
69 |
70 | override fun onPause() {
71 | super.onPause()
72 | FirebaseDataSource.dbInstance.goOffline()
73 | }
74 |
75 | override fun onResume() {
76 | FirebaseDataSource.dbInstance.goOnline()
77 | setupViewModelObservers()
78 | super.onResume()
79 | }
80 |
81 | private fun setupViewModelObservers() {
82 | viewModel.userNotificationsList.observe(this, {
83 | if (it.size > 0) {
84 | notificationsBadge.number = it.size
85 | notificationsBadge.isVisible = true
86 | } else {
87 | notificationsBadge.isVisible = false
88 | }
89 | })
90 | }
91 |
92 | fun showGlobalProgressBar(show: Boolean) {
93 | if (show) mainProgressBar.visibility = View.VISIBLE
94 | else mainProgressBar.visibility = View.GONE
95 | }
96 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/ui/main/MainViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app.ui.main
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MutableLiveData
5 | import androidx.lifecycle.ViewModel
6 | import com.fredrikbogg.android_chat_app.App
7 | import com.fredrikbogg.android_chat_app.data.db.entity.UserNotification
8 | import com.fredrikbogg.android_chat_app.data.db.remote.FirebaseAuthStateObserver
9 | import com.fredrikbogg.android_chat_app.data.db.remote.FirebaseReferenceConnectedObserver
10 | import com.fredrikbogg.android_chat_app.data.db.remote.FirebaseReferenceValueObserver
11 | import com.fredrikbogg.android_chat_app.data.db.repository.AuthRepository
12 | import com.fredrikbogg.android_chat_app.data.db.repository.DatabaseRepository
13 | import com.fredrikbogg.android_chat_app.data.Result
14 | import com.google.firebase.auth.FirebaseUser
15 |
16 |
17 | class MainViewModel : ViewModel() {
18 |
19 | private val dbRepository = DatabaseRepository()
20 | private val authRepository = AuthRepository()
21 |
22 | private val _userNotificationsList = MutableLiveData>()
23 |
24 | private val fbRefNotificationsObserver = FirebaseReferenceValueObserver()
25 | private val fbAuthStateObserver = FirebaseAuthStateObserver()
26 | private val fbRefConnectedObserver = FirebaseReferenceConnectedObserver()
27 | private var userID = App.myUserID
28 |
29 | var userNotificationsList: LiveData> = _userNotificationsList
30 |
31 | init {
32 | setupAuthObserver()
33 | }
34 |
35 | override fun onCleared() {
36 | super.onCleared()
37 | fbRefNotificationsObserver.clear()
38 | fbRefConnectedObserver.clear()
39 | fbAuthStateObserver.clear()
40 | }
41 |
42 | private fun setupAuthObserver(){
43 | authRepository.observeAuthState(fbAuthStateObserver) { result: Result ->
44 | if (result is Result.Success) {
45 | userID = result.data!!.uid
46 | startObservingNotifications()
47 | fbRefConnectedObserver.start(userID)
48 | } else {
49 | fbRefConnectedObserver.clear()
50 | stopObservingNotifications()
51 | }
52 | }
53 | }
54 |
55 | private fun startObservingNotifications() {
56 | dbRepository.loadAndObserveUserNotifications(userID, fbRefNotificationsObserver) { result: Result> ->
57 | if (result is Result.Success) {
58 | _userNotificationsList.value = result.data
59 | }
60 | }
61 | }
62 |
63 | private fun stopObservingNotifications() {
64 | fbRefNotificationsObserver.clear()
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/ui/notifications/NotificationsBindings.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app.ui.notifications
2 |
3 | import androidx.databinding.BindingAdapter
4 | import androidx.recyclerview.widget.RecyclerView
5 | import com.fredrikbogg.android_chat_app.data.db.entity.UserInfo
6 |
7 | @BindingAdapter("bind_notifications_list")
8 | fun bindNotificationsList(listView: RecyclerView, items: List?) {
9 | items?.let { (listView.adapter as NotificationsListAdapter).submitList(items) }
10 | }
11 |
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/ui/notifications/NotificationsFragment.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app.ui.notifications
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import androidx.fragment.app.Fragment
8 | import androidx.fragment.app.viewModels
9 | import com.fredrikbogg.android_chat_app.App
10 | import com.fredrikbogg.android_chat_app.databinding.FragmentNotificationsBinding
11 |
12 | class NotificationsFragment : Fragment() {
13 |
14 | private val viewModel: NotificationsViewModel by viewModels { NotificationsViewModelFactory(App.myUserID) }
15 | private lateinit var viewDataBinding: FragmentNotificationsBinding
16 | private lateinit var listAdapter: NotificationsListAdapter
17 |
18 | override fun onCreateView(
19 | inflater: LayoutInflater, container: ViewGroup?,
20 | savedInstanceState: Bundle?
21 | ): View? {
22 | viewDataBinding = FragmentNotificationsBinding.inflate(inflater, container, false)
23 | .apply { viewmodel = viewModel }
24 | viewDataBinding.lifecycleOwner = this.viewLifecycleOwner
25 | return viewDataBinding.root
26 | }
27 |
28 | override fun onActivityCreated(savedInstanceState: Bundle?) {
29 | super.onActivityCreated(savedInstanceState)
30 | setupListAdapter()
31 | }
32 |
33 | private fun setupListAdapter() {
34 | val viewModel = viewDataBinding.viewmodel
35 | if (viewModel != null) {
36 | listAdapter = NotificationsListAdapter(viewModel)
37 | viewDataBinding.usersRecyclerView.adapter = listAdapter
38 | } else {
39 | throw Exception("The viewmodel is not initialized")
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/ui/notifications/NotificationsListAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app.ui.notifications
2 |
3 | import android.view.LayoutInflater
4 | import android.view.ViewGroup
5 | import androidx.recyclerview.widget.DiffUtil
6 | import androidx.recyclerview.widget.ListAdapter
7 | import androidx.recyclerview.widget.RecyclerView
8 | import com.fredrikbogg.android_chat_app.data.db.entity.UserInfo
9 | import com.fredrikbogg.android_chat_app.databinding.ListItemNotificationBinding
10 |
11 |
12 | class NotificationsListAdapter internal constructor(private val viewModel: NotificationsViewModel) :
13 | ListAdapter(UserInfoDiffCallback()) {
14 |
15 | class ViewHolder(private val binding: ListItemNotificationBinding) :
16 | RecyclerView.ViewHolder(binding.root) {
17 | fun bind(viewModel: NotificationsViewModel, item: UserInfo) {
18 | binding.viewmodel = viewModel
19 | binding.userinfo = item
20 | binding.executePendingBindings()
21 | }
22 | }
23 |
24 | override fun onBindViewHolder(holder: ViewHolder, position: Int) {
25 | holder.bind(viewModel, getItem(position))
26 | }
27 |
28 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
29 | val layoutInflater = LayoutInflater.from(parent.context)
30 | val binding = ListItemNotificationBinding.inflate(layoutInflater, parent, false)
31 | return ViewHolder(binding)
32 | }
33 | }
34 |
35 | class UserInfoDiffCallback : DiffUtil.ItemCallback() {
36 | override fun areItemsTheSame(oldItem: UserInfo, newItem: UserInfo): Boolean {
37 | return oldItem == newItem
38 | }
39 |
40 | override fun areContentsTheSame(oldItem: UserInfo, newItem: UserInfo): Boolean {
41 | return oldItem.id == newItem.id
42 | }
43 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/ui/notifications/NotificationsViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app.ui.notifications
2 |
3 | import androidx.lifecycle.MediatorLiveData
4 | import androidx.lifecycle.MutableLiveData
5 | import androidx.lifecycle.ViewModel
6 | import androidx.lifecycle.ViewModelProvider
7 | import com.fredrikbogg.android_chat_app.data.db.entity.*
8 | import com.fredrikbogg.android_chat_app.data.db.repository.DatabaseRepository
9 | import com.fredrikbogg.android_chat_app.ui.DefaultViewModel
10 | import com.fredrikbogg.android_chat_app.data.Result
11 | import com.fredrikbogg.android_chat_app.util.addNewItem
12 | import com.fredrikbogg.android_chat_app.util.removeItem
13 | import com.fredrikbogg.android_chat_app.util.convertTwoUserIDs
14 |
15 | class NotificationsViewModelFactory(private val myUserID: String) :
16 | ViewModelProvider.Factory {
17 | override fun create(modelClass: Class): T {
18 | return NotificationsViewModel(myUserID) as T
19 | }
20 | }
21 |
22 | class NotificationsViewModel(private val myUserID: String) : DefaultViewModel() {
23 |
24 | private val dbRepository: DatabaseRepository = DatabaseRepository()
25 | private val updatedUserInfo = MutableLiveData()
26 | private val userNotificationsList = MutableLiveData>()
27 |
28 | val usersInfoList = MediatorLiveData>()
29 |
30 | init {
31 | usersInfoList.addSource(updatedUserInfo) { usersInfoList.addNewItem(it) }
32 | loadNotifications()
33 | }
34 |
35 | private fun loadNotifications() {
36 | dbRepository.loadNotifications(myUserID) { result: Result> ->
37 | onResult(userNotificationsList, result)
38 | if (result is Result.Success) result.data?.forEach { loadUserInfo(it) }
39 | }
40 | }
41 |
42 | private fun loadUserInfo(userNotification: UserNotification) {
43 | dbRepository.loadUserInfo(userNotification.userID) { result: Result ->
44 | onResult(updatedUserInfo, result)
45 | }
46 | }
47 |
48 | private fun updateNotification(otherUserInfo: UserInfo, removeOnly: Boolean) {
49 | val userNotification = userNotificationsList.value?.find {
50 | it.userID == otherUserInfo.id
51 | }
52 |
53 | if (userNotification != null) {
54 | if (!removeOnly) {
55 | dbRepository.updateNewFriend(UserFriend(myUserID), UserFriend(otherUserInfo.id))
56 | val newChat = Chat().apply {
57 | info.id = convertTwoUserIDs(myUserID, otherUserInfo.id)
58 | lastMessage = Message(seen = true, text = "Say hello!")
59 | }
60 | dbRepository.updateNewChat(newChat)
61 | }
62 | dbRepository.removeNotification(myUserID, otherUserInfo.id)
63 | dbRepository.removeSentRequest(otherUserInfo.id, myUserID)
64 |
65 | usersInfoList.removeItem(otherUserInfo)
66 | userNotificationsList.removeItem(userNotification)
67 | }
68 | }
69 |
70 | fun acceptNotificationPressed(userInfo: UserInfo) {
71 | updateNotification(userInfo, false)
72 | }
73 |
74 | fun declineNotificationPressed(userInfo: UserInfo) {
75 | updateNotification(userInfo, true)
76 | }
77 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/ui/profile/ProfileFragment.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app.ui.profile
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.MenuItem
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import androidx.fragment.app.Fragment
9 | import androidx.fragment.app.viewModels
10 | import androidx.navigation.fragment.findNavController
11 | import com.fredrikbogg.android_chat_app.App
12 | import com.fredrikbogg.android_chat_app.databinding.FragmentProfileBinding
13 | import com.fredrikbogg.android_chat_app.data.EventObserver
14 | import com.fredrikbogg.android_chat_app.util.showSnackBar
15 | import com.fredrikbogg.android_chat_app.ui.main.MainActivity
16 | import com.fredrikbogg.android_chat_app.util.forceHideKeyboard
17 |
18 |
19 | class ProfileFragment : Fragment() {
20 |
21 | companion object {
22 | const val ARGS_KEY_USER_ID = "bundle_user_id"
23 | }
24 |
25 | private val viewModel: ProfileViewModel by viewModels {
26 | ProfileViewModelFactory(App.myUserID, requireArguments().getString(ARGS_KEY_USER_ID)!!)
27 | }
28 |
29 | private lateinit var viewDataBinding: FragmentProfileBinding
30 |
31 | override fun onCreateView(
32 | inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
33 | ): View? {
34 | viewDataBinding = FragmentProfileBinding.inflate(inflater, container, false)
35 | .apply { viewmodel = viewModel }
36 | viewDataBinding.lifecycleOwner = this.viewLifecycleOwner
37 | setHasOptionsMenu(true)
38 | return viewDataBinding.root
39 | }
40 |
41 | override fun onActivityCreated(savedInstanceState: Bundle?) {
42 | super.onActivityCreated(savedInstanceState)
43 | setupObservers()
44 | }
45 |
46 | override fun onOptionsItemSelected(item: MenuItem): Boolean {
47 | when (item.itemId) {
48 | android.R.id.home -> {
49 | findNavController().popBackStack()
50 | return true
51 | }
52 | }
53 | return super.onOptionsItemSelected(item)
54 | }
55 |
56 | private fun setupObservers() {
57 | viewModel.dataLoading.observe(viewLifecycleOwner,
58 | EventObserver { (activity as MainActivity).showGlobalProgressBar(it) })
59 |
60 | viewModel.snackBarText.observe(viewLifecycleOwner,
61 | EventObserver { text ->
62 | view?.showSnackBar(text)
63 | view?.forceHideKeyboard()
64 | })
65 | }
66 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/ui/profile/ProfileViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app.ui.profile
2 |
3 | import androidx.lifecycle.*
4 | import com.fredrikbogg.android_chat_app.data.db.entity.*
5 | import com.fredrikbogg.android_chat_app.data.db.remote.FirebaseReferenceValueObserver
6 | import com.fredrikbogg.android_chat_app.data.db.repository.DatabaseRepository
7 | import com.fredrikbogg.android_chat_app.ui.DefaultViewModel
8 | import com.fredrikbogg.android_chat_app.data.Result
9 | import com.fredrikbogg.android_chat_app.util.convertTwoUserIDs
10 |
11 |
12 | class ProfileViewModelFactory(private val myUserID: String, private val otherUserID: String) :
13 | ViewModelProvider.Factory {
14 | override fun create(modelClass: Class): T {
15 | return ProfileViewModel(myUserID, otherUserID) as T
16 | }
17 | }
18 |
19 | enum class LayoutState {
20 | IS_FRIEND, NOT_FRIEND, ACCEPT_DECLINE, REQUEST_SENT
21 | }
22 |
23 | class ProfileViewModel(private val myUserID: String, private val userID: String) :
24 | DefaultViewModel() {
25 |
26 | private val repository: DatabaseRepository = DatabaseRepository()
27 | private val firebaseReferenceObserver = FirebaseReferenceValueObserver()
28 | private val _myUser: MutableLiveData = MutableLiveData()
29 | private val _otherUser: MutableLiveData = MutableLiveData()
30 |
31 | val otherUser: LiveData = _otherUser
32 | val layoutState = MediatorLiveData()
33 |
34 | init {
35 | layoutState.addSource(_myUser) { updateLayoutState(it, _otherUser.value) }
36 | setupProfile()
37 | }
38 |
39 | override fun onCleared() {
40 | super.onCleared()
41 | firebaseReferenceObserver.clear()
42 | }
43 |
44 | private fun updateLayoutState(myUser: User?, otherUser: User?) {
45 | if (myUser != null && otherUser != null) {
46 | layoutState.value = when {
47 | myUser.friends[otherUser.info.id] != null -> LayoutState.IS_FRIEND
48 | myUser.notifications[otherUser.info.id] != null -> LayoutState.ACCEPT_DECLINE
49 | myUser.sentRequests[otherUser.info.id] != null -> LayoutState.REQUEST_SENT
50 | else -> LayoutState.NOT_FRIEND
51 | }
52 | }
53 | }
54 |
55 | private fun setupProfile() {
56 | repository.loadUser(userID) { result: Result ->
57 | onResult(_otherUser, result)
58 | if (result is Result.Success) {
59 | repository.loadAndObserveUser(myUserID, firebaseReferenceObserver) { result2: Result ->
60 | onResult(_myUser, result2)
61 | }
62 | }
63 | }
64 | }
65 |
66 | fun addFriendPressed() {
67 | repository.updateNewSentRequest(myUserID, UserRequest(_otherUser.value!!.info.id))
68 | repository.updateNewNotification(_otherUser.value!!.info.id, UserNotification(myUserID))
69 | }
70 |
71 | fun removeFriendPressed() {
72 | repository.removeFriend(myUserID, _otherUser.value!!.info.id)
73 | repository.removeChat(convertTwoUserIDs(myUserID, _otherUser.value!!.info.id))
74 | repository.removeMessages(convertTwoUserIDs(myUserID, _otherUser.value!!.info.id))
75 | }
76 |
77 | fun acceptFriendRequestPressed() {
78 | repository.updateNewFriend(UserFriend(myUserID), UserFriend(_otherUser.value!!.info.id))
79 |
80 | val newChat = Chat().apply {
81 | info.id = convertTwoUserIDs(myUserID, _otherUser.value!!.info.id)
82 | lastMessage = Message(seen = true, text = "Say hello!")
83 | }
84 |
85 | repository.updateNewChat(newChat)
86 | repository.removeNotification(myUserID, _otherUser.value!!.info.id)
87 | repository.removeSentRequest(_otherUser.value!!.info.id, myUserID)
88 | }
89 |
90 | fun declineFriendRequestPressed() {
91 | repository.removeSentRequest(myUserID, _otherUser.value!!.info.id)
92 | repository.removeNotification(myUserID, _otherUser.value!!.info.id)
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/ui/settings/SettingsFragment.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app.ui.settings
2 |
3 | import android.app.Activity.RESULT_OK
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.os.Bundle
7 | import android.view.LayoutInflater
8 | import android.view.MenuItem
9 | import android.view.View
10 | import android.view.ViewGroup
11 | import android.widget.EditText
12 | import androidx.appcompat.app.AlertDialog
13 | import androidx.fragment.app.Fragment
14 | import androidx.fragment.app.viewModels
15 | import androidx.navigation.fragment.findNavController
16 | import com.fredrikbogg.android_chat_app.App
17 | import com.fredrikbogg.android_chat_app.R
18 | import com.fredrikbogg.android_chat_app.databinding.FragmentSettingsBinding
19 | import com.fredrikbogg.android_chat_app.data.EventObserver
20 | import com.fredrikbogg.android_chat_app.util.SharedPreferencesUtil
21 | import com.fredrikbogg.android_chat_app.util.convertFileToByteArray
22 |
23 |
24 | class SettingsFragment : Fragment() {
25 |
26 | private val viewModel: SettingsViewModel by viewModels { SettingsViewModelFactory(App.myUserID) }
27 |
28 | private lateinit var viewDataBinding: FragmentSettingsBinding
29 | private val selectImageIntentRequestCode = 1
30 |
31 | override fun onCreateView(
32 | inflater: LayoutInflater, container: ViewGroup?,
33 | savedInstanceState: Bundle?
34 | ): View? {
35 | viewDataBinding = FragmentSettingsBinding.inflate(inflater, container, false)
36 | .apply { viewmodel = viewModel }
37 | viewDataBinding.lifecycleOwner = this.viewLifecycleOwner
38 | setHasOptionsMenu(true)
39 |
40 | return viewDataBinding.root
41 | }
42 |
43 | override fun onActivityCreated(savedInstanceState: Bundle?) {
44 | super.onActivityCreated(savedInstanceState)
45 | setupObservers()
46 | }
47 |
48 | override fun onOptionsItemSelected(item: MenuItem): Boolean {
49 | when (item.itemId) {
50 | android.R.id.home -> {
51 | findNavController().popBackStack()
52 | return true
53 | }
54 | }
55 | return super.onOptionsItemSelected(item)
56 | }
57 |
58 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
59 | super.onActivityResult(requestCode, resultCode, data)
60 | if (resultCode == RESULT_OK && requestCode == selectImageIntentRequestCode) {
61 | data?.data?.let { uri ->
62 | convertFileToByteArray(requireContext(), uri).let {
63 | viewModel.changeUserImage(it)
64 | }
65 | }
66 | }
67 | }
68 |
69 | private fun setupObservers() {
70 | viewModel.editStatusEvent.observe(viewLifecycleOwner,
71 | EventObserver { showEditStatusDialog() })
72 |
73 | viewModel.editImageEvent.observe(viewLifecycleOwner,
74 | EventObserver { startSelectImageIntent() })
75 |
76 | viewModel.logoutEvent.observe(viewLifecycleOwner,
77 | EventObserver {
78 | SharedPreferencesUtil.removeUserID(requireContext())
79 | navigateToStart()
80 | })
81 | }
82 |
83 | private fun showEditStatusDialog() {
84 | val input = EditText(requireActivity() as Context)
85 | AlertDialog.Builder(requireActivity()).apply {
86 | setTitle("Status:")
87 | setView(input)
88 | setPositiveButton("Ok") { _, _ ->
89 | val textInput = input.text.toString()
90 | if (!textInput.isBlank() && textInput.length <= 40) {
91 | viewModel.changeUserStatus(textInput)
92 | }
93 | }
94 | setNegativeButton("Cancel") { _, _ -> }
95 | show()
96 | }
97 | }
98 |
99 | private fun startSelectImageIntent() {
100 | val selectImageIntent = Intent(Intent.ACTION_GET_CONTENT)
101 | selectImageIntent.type = "image/*"
102 | startActivityForResult(selectImageIntent, selectImageIntentRequestCode)
103 | }
104 |
105 | private fun navigateToStart() {
106 | findNavController().navigate(R.id.action_navigation_settings_to_startFragment)
107 | }
108 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/ui/settings/SettingsViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app.ui.settings
2 |
3 | import android.net.Uri
4 | import androidx.lifecycle.LiveData
5 | import androidx.lifecycle.MutableLiveData
6 | import androidx.lifecycle.ViewModel
7 | import androidx.lifecycle.ViewModelProvider
8 | import com.fredrikbogg.android_chat_app.data.db.entity.UserInfo
9 | import com.fredrikbogg.android_chat_app.data.db.remote.FirebaseReferenceValueObserver
10 | import com.fredrikbogg.android_chat_app.data.db.repository.AuthRepository
11 | import com.fredrikbogg.android_chat_app.data.db.repository.DatabaseRepository
12 | import com.fredrikbogg.android_chat_app.data.db.repository.StorageRepository
13 | import com.fredrikbogg.android_chat_app.ui.DefaultViewModel
14 | import com.fredrikbogg.android_chat_app.data.Event
15 | import com.fredrikbogg.android_chat_app.data.Result
16 |
17 | class SettingsViewModelFactory(private val userID: String) : ViewModelProvider.Factory {
18 | override fun create(modelClass: Class): T {
19 | return SettingsViewModel(userID) as T
20 | }
21 | }
22 |
23 | class SettingsViewModel(private val userID: String) : DefaultViewModel() {
24 |
25 | private val dbRepository: DatabaseRepository = DatabaseRepository()
26 | private val storageRepository = StorageRepository()
27 | private val authRepository = AuthRepository()
28 |
29 | private val _userInfo: MutableLiveData = MutableLiveData()
30 | val userInfo: LiveData = _userInfo
31 |
32 | private val _editStatusEvent = MutableLiveData>()
33 | val editStatusEvent: LiveData> = _editStatusEvent
34 |
35 | private val _editImageEvent = MutableLiveData>()
36 | val editImageEvent: LiveData> = _editImageEvent
37 |
38 | private val _logoutEvent = MutableLiveData>()
39 | val logoutEvent: LiveData> = _logoutEvent
40 |
41 | private val firebaseReferenceObserver = FirebaseReferenceValueObserver()
42 |
43 | init {
44 | loadAndObserveUserInfo()
45 | }
46 |
47 | override fun onCleared() {
48 | super.onCleared()
49 | firebaseReferenceObserver.clear()
50 | }
51 |
52 | private fun loadAndObserveUserInfo() {
53 | dbRepository.loadAndObserveUserInfo(userID, firebaseReferenceObserver)
54 | { result: Result -> onResult(_userInfo, result) }
55 | }
56 |
57 | fun changeUserStatus(status: String) {
58 | dbRepository.updateUserStatus(userID, status)
59 | }
60 |
61 | fun changeUserImage(byteArray: ByteArray) {
62 | storageRepository.updateUserProfileImage(userID, byteArray) { result: Result ->
63 | onResult(null, result)
64 | if (result is Result.Success) {
65 | dbRepository.updateUserProfileImageUrl(userID, result.data.toString())
66 | }
67 | }
68 | }
69 |
70 | fun changeUserImagePressed() {
71 | _editImageEvent.value = Event(Unit)
72 | }
73 |
74 | fun changeUserStatusPressed() {
75 | _editStatusEvent.value = Event(Unit)
76 | }
77 |
78 | fun logoutUserPressed() {
79 | authRepository.logoutUser()
80 | _logoutEvent.value = Event(Unit)
81 | }
82 | }
83 |
84 |
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/ui/start/StartFragment.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app.ui.start
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import androidx.fragment.app.Fragment
8 | import androidx.fragment.app.viewModels
9 | import androidx.navigation.fragment.findNavController
10 | import com.fredrikbogg.android_chat_app.R
11 | import com.fredrikbogg.android_chat_app.databinding.FragmentStartBinding
12 | import com.fredrikbogg.android_chat_app.data.EventObserver
13 | import com.fredrikbogg.android_chat_app.util.SharedPreferencesUtil
14 |
15 | class StartFragment : Fragment() {
16 |
17 | private val viewModel by viewModels()
18 | private lateinit var viewDataBinding: FragmentStartBinding
19 |
20 | override fun onCreateView(
21 | inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
22 | ): View? {
23 | viewDataBinding =
24 | FragmentStartBinding.inflate(inflater, container, false).apply { viewmodel = viewModel }
25 | viewDataBinding.lifecycleOwner = this.viewLifecycleOwner
26 | setHasOptionsMenu(false)
27 | return viewDataBinding.root
28 | }
29 |
30 | override fun onActivityCreated(savedInstanceState: Bundle?) {
31 | super.onActivityCreated(savedInstanceState)
32 | setupObservers()
33 |
34 | if (userIsAlreadyLoggedIn()) {
35 | navigateDirectlyToChats()
36 | }
37 | }
38 |
39 | private fun userIsAlreadyLoggedIn(): Boolean {
40 | return SharedPreferencesUtil.getUserID(requireContext()) != null
41 | }
42 |
43 | private fun setupObservers() {
44 | viewModel.loginEvent.observe(viewLifecycleOwner, EventObserver { navigateToLogin() })
45 | viewModel.createAccountEvent.observe(
46 | viewLifecycleOwner, EventObserver { navigateToCreateAccount() })
47 | }
48 |
49 | private fun navigateDirectlyToChats() {
50 | findNavController().navigate(R.id.action_startFragment_to_navigation_chats)
51 | }
52 |
53 | private fun navigateToLogin() {
54 | findNavController().navigate(R.id.action_startFragment_to_loginFragment)
55 | }
56 |
57 | private fun navigateToCreateAccount() {
58 | findNavController().navigate(R.id.action_startFragment_to_createAccountFragment)
59 | }
60 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/ui/start/StartViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app.ui.start
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MutableLiveData
5 | import androidx.lifecycle.ViewModel
6 | import com.fredrikbogg.android_chat_app.data.Event
7 |
8 | class StartViewModel : ViewModel() {
9 |
10 | private val _loginEvent = MutableLiveData>()
11 | private val _createAccountEvent = MutableLiveData>()
12 |
13 | val loginEvent: LiveData> = _loginEvent
14 | val createAccountEvent: LiveData> = _createAccountEvent
15 |
16 | fun goToLoginPressed() {
17 | _loginEvent.value = Event(Unit)
18 | }
19 |
20 | fun goToCreateAccountPressed() {
21 | _createAccountEvent.value = Event(Unit)
22 | }
23 | }
24 |
25 |
26 |
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/ui/start/createAccount/CreateAccountFragment.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app.ui.start.createAccount
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.MenuItem
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import androidx.fragment.app.Fragment
9 | import androidx.fragment.app.viewModels
10 | import androidx.navigation.fragment.findNavController
11 | import com.fredrikbogg.android_chat_app.data.EventObserver
12 | import com.fredrikbogg.android_chat_app.R
13 | import com.fredrikbogg.android_chat_app.databinding.FragmentCreateAccountBinding
14 | import com.fredrikbogg.android_chat_app.ui.main.MainActivity
15 | import com.fredrikbogg.android_chat_app.util.SharedPreferencesUtil
16 | import com.fredrikbogg.android_chat_app.util.forceHideKeyboard
17 | import com.fredrikbogg.android_chat_app.util.showSnackBar
18 |
19 | class CreateAccountFragment : Fragment() {
20 |
21 | private val viewModel by viewModels()
22 | private lateinit var viewDataBinding: FragmentCreateAccountBinding
23 |
24 | override fun onCreateView(
25 | inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
26 | ): View? {
27 | viewDataBinding = FragmentCreateAccountBinding.inflate(inflater, container, false)
28 | .apply { viewmodel = viewModel }
29 | viewDataBinding.lifecycleOwner = this.viewLifecycleOwner
30 | setHasOptionsMenu(true)
31 | return viewDataBinding.root
32 | }
33 |
34 | override fun onActivityCreated(savedInstanceState: Bundle?) {
35 | super.onActivityCreated(savedInstanceState)
36 | setupObservers()
37 | }
38 |
39 | override fun onOptionsItemSelected(item: MenuItem): Boolean {
40 | when (item.itemId) {
41 | android.R.id.home -> {
42 | findNavController().popBackStack()
43 | return true
44 | }
45 | }
46 | return super.onOptionsItemSelected(item)
47 | }
48 |
49 | private fun setupObservers() {
50 | viewModel.dataLoading.observe(viewLifecycleOwner,
51 | EventObserver { (activity as MainActivity).showGlobalProgressBar(it) })
52 |
53 | viewModel.snackBarText.observe(viewLifecycleOwner,
54 | EventObserver { text ->
55 | view?.showSnackBar(text)
56 | view?.forceHideKeyboard()
57 | })
58 |
59 | viewModel.isCreatedEvent.observe(viewLifecycleOwner, EventObserver {
60 | SharedPreferencesUtil.saveUserID(requireContext(), it.uid)
61 | navigateToChats()
62 | })
63 | }
64 |
65 | private fun navigateToChats() {
66 | findNavController().navigate(R.id.action_createAccountFragment_to_navigation_chats)
67 | }
68 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/ui/start/createAccount/CreateAccountViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app.ui.start.createAccount
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MutableLiveData
5 | import com.fredrikbogg.android_chat_app.data.Event
6 | import com.fredrikbogg.android_chat_app.data.Result
7 | import com.fredrikbogg.android_chat_app.data.db.entity.User
8 | import com.fredrikbogg.android_chat_app.data.db.repository.AuthRepository
9 | import com.fredrikbogg.android_chat_app.data.db.repository.DatabaseRepository
10 | import com.fredrikbogg.android_chat_app.data.model.CreateUser
11 | import com.fredrikbogg.android_chat_app.ui.DefaultViewModel
12 | import com.fredrikbogg.android_chat_app.util.isEmailValid
13 | import com.fredrikbogg.android_chat_app.util.isTextValid
14 | import com.google.firebase.auth.FirebaseUser
15 |
16 | class CreateAccountViewModel : DefaultViewModel() {
17 |
18 | private val dbRepository = DatabaseRepository()
19 | private val authRepository = AuthRepository()
20 | private val mIsCreatedEvent = MutableLiveData>()
21 |
22 | val isCreatedEvent: LiveData> = mIsCreatedEvent
23 | val displayNameText = MutableLiveData() // Two way
24 | val emailText = MutableLiveData() // Two way
25 | val passwordText = MutableLiveData() // Two way
26 | val isCreatingAccount = MutableLiveData()
27 |
28 | private fun createAccount() {
29 | isCreatingAccount.value = true
30 | val createUser =
31 | CreateUser(displayNameText.value!!, emailText.value!!, passwordText.value!!)
32 |
33 | authRepository.createUser(createUser) { result: Result ->
34 | onResult(null, result)
35 | if (result is Result.Success) {
36 | mIsCreatedEvent.value = Event(result.data!!)
37 | dbRepository.updateNewUser(User().apply {
38 | info.id = result.data.uid
39 | info.displayName = createUser.displayName
40 | })
41 | }
42 | if (result is Result.Success || result is Result.Error) isCreatingAccount.value = false
43 | }
44 | }
45 |
46 | fun createAccountPressed() {
47 | if (!isTextValid(2, displayNameText.value)) {
48 | mSnackBarText.value = Event("Display name is too short")
49 | return
50 | }
51 |
52 | if (!isEmailValid(emailText.value.toString())) {
53 | mSnackBarText.value = Event("Invalid email format")
54 | return
55 | }
56 | if (!isTextValid(6, passwordText.value)) {
57 | mSnackBarText.value = Event("Password is too short")
58 | return
59 | }
60 |
61 | createAccount()
62 | }
63 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/ui/start/login/LoginFragment.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app.ui.start.login
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.MenuItem
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import androidx.fragment.app.Fragment
9 | import androidx.fragment.app.viewModels
10 | import androidx.navigation.fragment.findNavController
11 | import com.fredrikbogg.android_chat_app.R
12 | import com.fredrikbogg.android_chat_app.databinding.FragmentLoginBinding
13 | import com.fredrikbogg.android_chat_app.data.EventObserver
14 | import com.fredrikbogg.android_chat_app.util.showSnackBar
15 | import com.fredrikbogg.android_chat_app.ui.main.MainActivity
16 | import com.fredrikbogg.android_chat_app.util.SharedPreferencesUtil
17 | import com.fredrikbogg.android_chat_app.util.forceHideKeyboard
18 |
19 | class LoginFragment : Fragment() {
20 |
21 | private val viewModel by viewModels()
22 | private lateinit var viewDataBinding: FragmentLoginBinding
23 |
24 | override fun onCreateView(
25 | inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
26 | ): View? {
27 | viewDataBinding = FragmentLoginBinding.inflate(inflater, container, false)
28 | .apply { viewmodel = viewModel }
29 | viewDataBinding.lifecycleOwner = this.viewLifecycleOwner
30 | setHasOptionsMenu(true)
31 | return viewDataBinding.root
32 | }
33 |
34 | override fun onActivityCreated(savedInstanceState: Bundle?) {
35 | super.onActivityCreated(savedInstanceState)
36 | setupObservers()
37 | }
38 |
39 | override fun onOptionsItemSelected(item: MenuItem): Boolean {
40 | when (item.itemId) {
41 | android.R.id.home -> {
42 | findNavController().popBackStack()
43 | return true
44 | }
45 | }
46 | return super.onOptionsItemSelected(item)
47 | }
48 |
49 | private fun setupObservers() {
50 | viewModel.dataLoading.observe(viewLifecycleOwner,
51 | EventObserver { (activity as MainActivity).showGlobalProgressBar(it) })
52 |
53 | viewModel.snackBarText.observe(viewLifecycleOwner,
54 | EventObserver { text ->
55 | view?.showSnackBar(text)
56 | view?.forceHideKeyboard()
57 | })
58 |
59 | viewModel.isLoggedInEvent.observe(viewLifecycleOwner, EventObserver {
60 | SharedPreferencesUtil.saveUserID(requireContext(), it.uid)
61 | navigateToChats()
62 | })
63 | }
64 |
65 | private fun navigateToChats() {
66 | findNavController().navigate(R.id.action_loginFragment_to_navigation_chats)
67 | }
68 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/ui/start/login/LoginViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app.ui.start.login
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MutableLiveData
5 | import com.fredrikbogg.android_chat_app.data.model.Login
6 | import com.fredrikbogg.android_chat_app.data.db.repository.AuthRepository
7 | import com.fredrikbogg.android_chat_app.ui.DefaultViewModel
8 | import com.fredrikbogg.android_chat_app.data.Event
9 | import com.fredrikbogg.android_chat_app.data.Result
10 | import com.fredrikbogg.android_chat_app.util.isEmailValid
11 | import com.fredrikbogg.android_chat_app.util.isTextValid
12 | import com.google.firebase.auth.FirebaseUser
13 |
14 | class LoginViewModel : DefaultViewModel() {
15 |
16 | private val authRepository = AuthRepository()
17 | private val _isLoggedInEvent = MutableLiveData>()
18 |
19 | val isLoggedInEvent: LiveData> = _isLoggedInEvent
20 | val emailText = MutableLiveData() // Two way
21 | val passwordText = MutableLiveData() // Two way
22 | val isLoggingIn = MutableLiveData() // Two way
23 |
24 | private fun login() {
25 | isLoggingIn.value = true
26 | val login = Login(emailText.value!!, passwordText.value!!)
27 |
28 | authRepository.loginUser(login) { result: Result ->
29 | onResult(null, result)
30 | if (result is Result.Success) _isLoggedInEvent.value = Event(result.data!!)
31 | if (result is Result.Success || result is Result.Error) isLoggingIn.value = false
32 | }
33 | }
34 |
35 | fun loginPressed() {
36 | if (!isEmailValid(emailText.value.toString())) {
37 | mSnackBarText.value = Event("Invalid email format")
38 | return
39 | }
40 | if (!isTextValid(6, passwordText.value)) {
41 | mSnackBarText.value = Event("Password is too short")
42 | return
43 | }
44 |
45 | login()
46 | }
47 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/ui/users/UsersBindings.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app.ui.users
2 |
3 | import androidx.databinding.BindingAdapter
4 | import androidx.recyclerview.widget.RecyclerView
5 | import com.fredrikbogg.android_chat_app.data.db.entity.User
6 |
7 | @BindingAdapter("bind_users_list")
8 | fun bindUsersList(listView: RecyclerView, items: List?) {
9 | items?.let { (listView.adapter as UsersListAdapter).submitList(items) }
10 | }
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/ui/users/UsersFragment.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app.ui.users
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import androidx.core.os.bundleOf
8 | import androidx.fragment.app.Fragment
9 | import androidx.fragment.app.viewModels
10 | import androidx.navigation.fragment.findNavController
11 | import com.fredrikbogg.android_chat_app.App
12 | import com.fredrikbogg.android_chat_app.R
13 | import com.fredrikbogg.android_chat_app.databinding.FragmentUsersBinding
14 | import com.fredrikbogg.android_chat_app.data.EventObserver
15 | import com.fredrikbogg.android_chat_app.ui.profile.ProfileFragment
16 |
17 |
18 | class UsersFragment : Fragment() {
19 |
20 | private val viewModel: UsersViewModel by viewModels { UsersViewModelFactory(App.myUserID) }
21 | private lateinit var viewDataBinding: FragmentUsersBinding
22 | private lateinit var listAdapter: UsersListAdapter
23 |
24 | override fun onCreateView(
25 | inflater: LayoutInflater, container: ViewGroup?,
26 | savedInstanceState: Bundle?
27 | ): View? {
28 | viewDataBinding =
29 | FragmentUsersBinding.inflate(inflater, container, false).apply { viewmodel = viewModel }
30 | viewDataBinding.lifecycleOwner = this.viewLifecycleOwner
31 | return viewDataBinding.root
32 | }
33 |
34 | override fun onActivityCreated(savedInstanceState: Bundle?) {
35 | super.onActivityCreated(savedInstanceState)
36 | setupListAdapter()
37 | setupObservers()
38 | }
39 |
40 | private fun setupListAdapter() {
41 | val viewModel = viewDataBinding.viewmodel
42 | if (viewModel != null) {
43 | listAdapter = UsersListAdapter(viewModel)
44 | viewDataBinding.usersRecyclerView.adapter = listAdapter
45 | } else {
46 | throw Exception("The viewmodel is not initialized")
47 | }
48 | }
49 |
50 | private fun setupObservers() {
51 | viewModel.selectedUser.observe(viewLifecycleOwner, EventObserver { navigateToProfile(it.info.id) })
52 | }
53 |
54 | private fun navigateToProfile(userID: String) {
55 | val bundle = bundleOf(ProfileFragment.ARGS_KEY_USER_ID to userID)
56 | findNavController().navigate(R.id.action_navigation_users_to_profileFragment, bundle)
57 | }
58 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/ui/users/UsersListAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app.ui.users
2 |
3 | import android.view.LayoutInflater
4 | import android.view.ViewGroup
5 | import androidx.recyclerview.widget.DiffUtil
6 | import androidx.recyclerview.widget.ListAdapter
7 | import androidx.recyclerview.widget.RecyclerView
8 | import com.fredrikbogg.android_chat_app.data.db.entity.User
9 | import com.fredrikbogg.android_chat_app.databinding.ListItemUserBinding
10 |
11 |
12 | class UsersListAdapter internal constructor(private val viewModel: UsersViewModel) :
13 | ListAdapter(UserDiffCallback()) {
14 |
15 | class ViewHolder(private val binding: ListItemUserBinding) :
16 | RecyclerView.ViewHolder(binding.root) {
17 | fun bind(viewModel: UsersViewModel, item: User) {
18 | binding.viewmodel = viewModel
19 | binding.user = item
20 | binding.executePendingBindings()
21 | }
22 | }
23 |
24 | override fun onBindViewHolder(holder: ViewHolder, position: Int) {
25 | holder.bind(viewModel, getItem(position))
26 | }
27 |
28 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
29 | val layoutInflater = LayoutInflater.from(parent.context)
30 | val binding = ListItemUserBinding.inflate(layoutInflater, parent, false)
31 | return ViewHolder(binding)
32 | }
33 | }
34 |
35 | class UserDiffCallback : DiffUtil.ItemCallback() {
36 | override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
37 | return oldItem == newItem
38 | }
39 |
40 | override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
41 | return oldItem.info.id == newItem.info.id
42 | }
43 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/ui/users/UsersViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app.ui.users
2 |
3 | import androidx.lifecycle.*
4 | import com.fredrikbogg.android_chat_app.data.db.entity.User
5 | import com.fredrikbogg.android_chat_app.data.db.repository.DatabaseRepository
6 | import com.fredrikbogg.android_chat_app.ui.DefaultViewModel
7 | import com.fredrikbogg.android_chat_app.data.Event
8 | import com.fredrikbogg.android_chat_app.data.Result
9 |
10 |
11 | class UsersViewModelFactory(private val myUserID: String) :
12 | ViewModelProvider.Factory {
13 | override fun create(modelClass: Class): T {
14 | return UsersViewModel(myUserID) as T
15 | }
16 | }
17 |
18 | class UsersViewModel(private val myUserID: String) : DefaultViewModel() {
19 | private val repository: DatabaseRepository = DatabaseRepository()
20 |
21 | private val _selectedUser = MutableLiveData>()
22 | var selectedUser: LiveData> = _selectedUser
23 | private val updatedUsersList = MutableLiveData>()
24 | val usersList = MediatorLiveData>()
25 |
26 | init {
27 | usersList.addSource(updatedUsersList) { mutableList ->
28 | usersList.value = updatedUsersList.value?.filter { it.info.id != myUserID }
29 | }
30 | loadUsers()
31 | }
32 |
33 | private fun loadUsers() {
34 | repository.loadUsers { result: Result> ->
35 | onResult(updatedUsersList, result)
36 | }
37 | }
38 |
39 | fun selectUser(user: User) {
40 | _selectedUser.value = Event(user)
41 | }
42 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/util/FileConverterUtil.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app.util
2 |
3 | import android.content.Context
4 | import android.graphics.Bitmap
5 | import android.graphics.BitmapFactory
6 | import android.net.Uri
7 | import java.io.ByteArrayOutputStream
8 | import java.io.InputStream
9 |
10 | fun convertFileToByteArray(context: Context, uri: Uri): ByteArray {
11 | val inputStream: InputStream? = context.contentResolver.openInputStream(uri)
12 | val bitmap = BitmapFactory.decodeStream(inputStream)
13 | val byteArrayOutputStream = ByteArrayOutputStream()
14 | bitmap.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayOutputStream)
15 |
16 | return byteArrayOutputStream.toByteArray()
17 | }
18 |
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/util/FirebaseUtil.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app.util
2 |
3 | import com.google.firebase.database.DataSnapshot
4 |
5 | fun wrapSnapshotToClass(className: Class, snap: DataSnapshot): T? {
6 | return snap.getValue(className)
7 | }
8 |
9 | fun wrapSnapshotToArrayList(className: Class, snap: DataSnapshot): MutableList {
10 | val arrayList: MutableList = arrayListOf()
11 | for (child in snap.children) {
12 | child.getValue(className)?.let { arrayList.add(it) }
13 | }
14 | return arrayList
15 | }
16 |
17 | // Always returns the same combined id when comparing the two users id's
18 | fun convertTwoUserIDs(userID1: String, userID2: String): String {
19 | return if (userID1 < userID2) {
20 | userID2 + userID1
21 | } else {
22 | userID1 + userID2
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/util/LiveDataExt.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app.util
2 |
3 | import androidx.lifecycle.MutableLiveData
4 |
5 | fun MutableLiveData>.addNewItem(item: T) {
6 | val newList = mutableListOf()
7 | this.value?.let { newList.addAll(it) }
8 | newList.add(item)
9 | this.value = newList
10 | }
11 |
12 | fun MutableLiveData>.updateItemAt(item: T, index: Int) {
13 | val newList = mutableListOf()
14 | this.value?.let { newList.addAll(it) }
15 | newList[index] = item
16 | this.value = newList
17 | }
18 |
19 | fun MutableLiveData>.removeItem(item: T) {
20 | val newList = mutableListOf()
21 | this.value?.let { newList.addAll(it) }
22 | newList.remove(item)
23 | this.value = newList
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/util/SharedPreferencesUtil.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app.util
2 |
3 | import android.content.Context
4 | import android.content.SharedPreferences
5 |
6 | object SharedPreferencesUtil {
7 | private const val PACKAGE_NAME = "com.fredrikbogg.android_chat_app"
8 | private const val KEY_USER_ID = "user_info"
9 |
10 | private fun getPrefs(context: Context): SharedPreferences {
11 | return context.getSharedPreferences(PACKAGE_NAME, Context.MODE_PRIVATE)
12 | }
13 |
14 | fun getUserID(context: Context): String? {
15 | return getPrefs(context).getString(KEY_USER_ID, null)
16 | }
17 |
18 | fun saveUserID(context: Context, userID: String) {
19 | getPrefs(context).edit().putString(KEY_USER_ID, userID).apply()
20 | }
21 |
22 | fun removeUserID(context: Context) {
23 | getPrefs(context).edit().remove(KEY_USER_ID).apply()
24 | }
25 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/util/TextUtil.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app.util
2 |
3 | fun isEmailValid(email: CharSequence): Boolean {
4 | return android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches()
5 | }
6 |
7 | fun isTextValid(minLength: Int, text: String?): Boolean {
8 | if (text.isNullOrBlank() || text.length < minLength) {
9 | return false
10 | }
11 | return true
12 | }
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/java/com/fredrikbogg/android_chat_app/util/ViewExt.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app.util
2 |
3 | import android.content.Context
4 | import android.view.View
5 | import android.view.inputmethod.InputMethodManager
6 | import com.fredrikbogg.android_chat_app.R
7 | import com.google.android.material.snackbar.Snackbar
8 |
9 | fun View.forceHideKeyboard() {
10 | val inputManager: InputMethodManager =
11 | this.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
12 | inputManager.hideSoftInputFromWindow(this.windowToken, InputMethodManager.HIDE_NOT_ALWAYS)
13 | }
14 |
15 | fun View.showSnackBar(text: String) {
16 | Snackbar.make(this.rootView.findViewById(R.id.container), text, Snackbar.LENGTH_SHORT).show()
17 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/chat_box.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dgewe/Chat-App-Android/cff2f947a4496e46cbaa750b4e3fa768ba2308f0/app/src/main/res/drawable-v24/chat_box.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/rounded_rectangle_primary.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/rounded_rectangle_secondary.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_chat_bubble_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_error_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_notifications_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_people_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_person_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_settings_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/round_circle_online_green.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/round_circle_primary.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/font/nunito.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/font/nunito_bold.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/font/nunito_extrabold.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/font/nunito_semibold.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
16 |
17 |
30 |
31 |
42 |
43 |
56 |
57 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_chat.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
12 |
13 |
14 |
15 |
19 |
20 |
34 |
35 |
43 |
44 |
54 |
55 |
69 |
70 |
79 |
80 |
81 |
82 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_chats.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
12 |
13 |
14 |
15 |
19 |
20 |
23 |
24 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_create_account.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
12 |
13 |
14 |
15 |
18 |
19 |
31 |
32 |
45 |
46 |
51 |
52 |
65 |
66 |
67 |
73 |
74 |
87 |
88 |
89 |
96 |
97 |
110 |
111 |
112 |
113 |
114 |
129 |
130 |
136 |
137 |
138 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_login.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
12 |
13 |
14 |
15 |
18 |
19 |
31 |
32 |
45 |
46 |
51 |
52 |
65 |
66 |
67 |
74 |
75 |
89 |
90 |
91 |
92 |
93 |
107 |
108 |
114 |
115 |
116 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_notifications.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
12 |
13 |
14 |
15 |
19 |
20 |
23 |
24 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
12 |
13 |
14 |
15 |
19 |
20 |
35 |
36 |
47 |
48 |
61 |
62 |
70 |
71 |
72 |
83 |
84 |
93 |
94 |
104 |
105 |
115 |
116 |
126 |
127 |
128 |
129 |
130 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_start.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
12 |
13 |
14 |
15 |
18 |
19 |
32 |
33 |
47 |
48 |
63 |
64 |
70 |
71 |
83 |
84 |
98 |
99 |
100 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_users.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
12 |
13 |
14 |
15 |
19 |
20 |
23 |
24 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/list_item_chat.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
14 |
15 |
18 |
19 |
20 |
21 |
34 |
35 |
44 |
45 |
54 |
55 |
56 |
57 |
66 |
67 |
77 |
78 |
90 |
91 |
104 |
105 |
114 |
115 |
121 |
122 |
134 |
135 |
136 |
137 |
138 |
139 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/list_item_message_received.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
12 |
13 |
16 |
17 |
18 |
19 |
25 |
26 |
36 |
37 |
53 |
54 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/list_item_message_sent.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
10 |
13 |
14 |
17 |
18 |
19 |
25 |
26 |
36 |
37 |
53 |
54 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/list_item_notification.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
12 |
13 |
16 |
17 |
18 |
19 |
25 |
26 |
34 |
35 |
44 |
45 |
46 |
59 |
60 |
72 |
73 |
82 |
83 |
93 |
94 |
95 |
96 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/list_item_user.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
12 |
13 |
16 |
17 |
18 |
19 |
28 |
29 |
37 |
38 |
47 |
48 |
49 |
63 |
64 |
78 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/toolbar_addon_chat.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
13 |
14 |
15 |
21 |
22 |
23 |
29 |
30 |
42 |
43 |
52 |
53 |
54 |
64 |
65 |
66 |
67 |
76 |
77 |
87 |
88 |
99 |
100 |
101 |
102 |
103 |
104 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/toolbar_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/bottom_nav_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dgewe/Chat-App-Android/cff2f947a4496e46cbaa750b4e3fa768ba2308f0/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dgewe/Chat-App-Android/cff2f947a4496e46cbaa750b4e3fa768ba2308f0/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dgewe/Chat-App-Android/cff2f947a4496e46cbaa750b4e3fa768ba2308f0/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dgewe/Chat-App-Android/cff2f947a4496e46cbaa750b4e3fa768ba2308f0/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dgewe/Chat-App-Android/cff2f947a4496e46cbaa750b4e3fa768ba2308f0/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dgewe/Chat-App-Android/cff2f947a4496e46cbaa750b4e3fa768ba2308f0/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dgewe/Chat-App-Android/cff2f947a4496e46cbaa750b4e3fa768ba2308f0/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dgewe/Chat-App-Android/cff2f947a4496e46cbaa750b4e3fa768ba2308f0/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dgewe/Chat-App-Android/cff2f947a4496e46cbaa750b4e3fa768ba2308f0/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dgewe/Chat-App-Android/cff2f947a4496e46cbaa750b4e3fa768ba2308f0/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dgewe/Chat-App-Android/cff2f947a4496e46cbaa750b4e3fa768ba2308f0/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dgewe/Chat-App-Android/cff2f947a4496e46cbaa750b4e3fa768ba2308f0/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dgewe/Chat-App-Android/cff2f947a4496e46cbaa750b4e3fa768ba2308f0/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dgewe/Chat-App-Android/cff2f947a4496e46cbaa750b4e3fa768ba2308f0/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dgewe/Chat-App-Android/cff2f947a4496e46cbaa750b4e3fa768ba2308f0/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/navigation/mobile_navigation.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
12 |
15 |
16 |
21 |
26 |
29 |
30 |
35 |
40 |
41 |
46 |
50 |
54 |
57 |
60 |
65 |
66 |
71 |
76 |
77 |
82 |
87 |
88 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #2D9CDB
4 | @color/colorPrimary
5 | #F3D231
6 | #ff6262
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values/font_certs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | - @array/com_google_android_gms_fonts_certs_dev
5 | - @array/com_google_android_gms_fonts_certs_prod
6 |
7 |
8 | -
9 | MIIEqDCCA5CgAwIBAgIJANWFuGx90071MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAeFw0wODA0MTUyMzM2NTZaFw0zNTA5MDEyMzM2NTZaMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHThvbbR24kT9ixcOd9W+EaBPWW+wPPKQmsHxajtWjmQwWfna8mZuSeJS48LIgAZlKkpFeVyxW0qMBujb8X8ETrWy550NaFtI6t9+u7hZeTfHwqNvacKhp1RbE6dBRGWynwMVX8XW8N1+UjFaq6GCJukT4qmpN2afb8sCjUigq0GuMwYXrFVee74bQgLHWGJwPmvmLHC69EH6kWr22ijx4OKXlSIx2xT1AsSHee70w5iDBiK4aph27yH3TxkXy9V89TDdexAcKk/cVHYNnDBapcavl7y0RiQ4biu8ymM8Ga/nmzhRKya6G0cGw8CAQOjgfwwgfkwHQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMIHJBgNVHSMEgcEwgb6AFI0cxb6VTEM8YYY6FbBMvAPyT+CyoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJANWFuGx90071MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABnTDPEF+3iSP0wNfdIjIz1AlnrPzgAIHVvXxunW7SBrDhEglQZBbKJEk5kT0mtKoOD1JMrSu1xuTKEBahWRbqHsXclaXjoBADb0kkjVEJu/Lh5hgYZnOjvlba8Ld7HCKePCVePoTJBdI4fvugnL8TsgK05aIskyY0hKI9L8KfqfGTl1lzOv2KoWD0KWwtAWPoGChZxmQ+nBli+gwYMzM1vAkP+aayLe0a1EQimlOalO762r0GXO0ks+UeXde2Z4e+8S/pf7pITEI/tP+MxJTALw9QUWEv9lKTk+jkbqxbsh8nfBUapfKqYn0eidpwq2AzVp3juYl7//fKnaPhJD9gs=
10 |
11 |
12 |
13 | -
14 | MIIEQzCCAyugAwIBAgIJAMLgh0ZkSjCNMA0GCSqGSIb3DQEBBAUAMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDAeFw0wODA4MjEyMzEzMzRaFw0zNjAxMDcyMzEzMzRaMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBAKtWLgDYO6IIrgqWbxJOKdoR8qtW0I9Y4sypEwPpt1TTcvZApxsdyxMJZ2JORland2qSGT2y5b+3JKkedxiLDmpHpDsz2WCbdxgxRczfey5YZnTJ4VZbH0xqWVW/8lGmPav5xVwnIiJS6HXk+BVKZF+JcWjAsb/GEuq/eFdpuzSqeYTcfi6idkyugwfYwXFU1+5fZKUaRKYCwkkFQVfcAs1fXA5V+++FGfvjJ/CxURaSxaBvGdGDhfXE28LWuT9ozCl5xw4Yq5OGazvV24mZVSoOO0yZ31j7kYvtwYK6NeADwbSxDdJEqO4k//0zOHKrUiGYXtqw/A0LFFtqoZKFjnkCAQOjgdkwgdYwHQYDVR0OBBYEFMd9jMIhF1Ylmn/Tgt9r45jk14alMIGmBgNVHSMEgZ4wgZuAFMd9jMIhF1Ylmn/Tgt9r45jk14aloXikdjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEUMBIGA1UEChMLR29vZ2xlIEluYy4xEDAOBgNVBAsTB0FuZHJvaWQxEDAOBgNVBAMTB0FuZHJvaWSCCQDC4IdGZEowjTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBAUAA4IBAQBt0lLO74UwLDYKqs6Tm8/yzKkEu116FmH4rkaymUIE0P9KaMftGlMexFlaYjzmB2OxZyl6euNXEsQH8gjwyxCUKRJNexBiGcCEyj6z+a1fuHHvkiaai+KL8W1EyNmgjmyy8AW7P+LLlkR+ho5zEHatRbM/YAnqGcFh5iZBqpknHf1SKMXFh4dd239FJ1jWYfbMDMy3NS5CTMQ2XFI1MvcyUTdZPErjQfTbQe3aDQsQcafEQPD+nqActifKZ0Np0IS9L9kR/wbNvyz6ENwPiTrjV2KRkEjH78ZMcUQXg0L3BYHJ3lc69Vs5Ddf9uUGGMYldX3WfMBEmh/9iFBDAaTCK
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFFFFF
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/preloaded_fonts.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | - @font/nunito
5 | - @font/nunito_bold
6 | - @font/nunito_extrabold
7 | - @font/nunito_semibold
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Chat App
3 | Chats
4 | Notifications
5 | Users
6 | Settings
7 | Profile
8 | Chat
9 | User image
10 | Accept
11 | Decline
12 | Enter message
13 | SEND
14 | Create a new account
15 | Display name
16 | Email
17 | Password
18 | Create
19 | Login to your account
20 | Add Friend
21 | Remove Friend
22 | Accept Friend Request
23 | Request sent
24 | Decline Friend Request
25 | Change Image
26 | Logout
27 | Change Status
28 | Create account
29 | New friend request
30 | login
31 | Welcome to Quick Chat
32 | Login or create an account to get started
33 | Chat icon
34 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
16 |
17 |
21 |
22 |
25 |
26 |
29 |
--------------------------------------------------------------------------------
/app/src/test/java/com/fredrikbogg/android_chat_app/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.fredrikbogg.android_chat_app
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | buildscript {
3 | ext.kotlin_version = "1.4.0"
4 | repositories {
5 | google()
6 | jcenter()
7 | }
8 | dependencies {
9 | classpath "com.android.tools.build:gradle:4.0.1"
10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
11 | classpath 'com.google.gms:google-services:4.3.3'
12 |
13 | // NOTE: Do not place your application dependencies here; they belong
14 | // in the individual module build.gradle files
15 | }
16 | }
17 |
18 | allprojects {
19 | repositories {
20 | google()
21 | jcenter()
22 | }
23 | }
24 |
25 | task clean(type: Delete) {
26 | delete rootProject.buildDir
27 | }
--------------------------------------------------------------------------------
/github_images/chat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dgewe/Chat-App-Android/cff2f947a4496e46cbaa750b4e3fa768ba2308f0/github_images/chat.png
--------------------------------------------------------------------------------
/github_images/chats.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dgewe/Chat-App-Android/cff2f947a4496e46cbaa750b4e3fa768ba2308f0/github_images/chats.png
--------------------------------------------------------------------------------
/github_images/create.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dgewe/Chat-App-Android/cff2f947a4496e46cbaa750b4e3fa768ba2308f0/github_images/create.png
--------------------------------------------------------------------------------
/github_images/db.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dgewe/Chat-App-Android/cff2f947a4496e46cbaa750b4e3fa768ba2308f0/github_images/db.png
--------------------------------------------------------------------------------
/github_images/header.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dgewe/Chat-App-Android/cff2f947a4496e46cbaa750b4e3fa768ba2308f0/github_images/header.png
--------------------------------------------------------------------------------
/github_images/login.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dgewe/Chat-App-Android/cff2f947a4496e46cbaa750b4e3fa768ba2308f0/github_images/login.png
--------------------------------------------------------------------------------
/github_images/notifications.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dgewe/Chat-App-Android/cff2f947a4496e46cbaa750b4e3fa768ba2308f0/github_images/notifications.png
--------------------------------------------------------------------------------
/github_images/profile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dgewe/Chat-App-Android/cff2f947a4496e46cbaa750b4e3fa768ba2308f0/github_images/profile.png
--------------------------------------------------------------------------------
/github_images/settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dgewe/Chat-App-Android/cff2f947a4496e46cbaa750b4e3fa768ba2308f0/github_images/settings.png
--------------------------------------------------------------------------------
/github_images/start.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dgewe/Chat-App-Android/cff2f947a4496e46cbaa750b4e3fa768ba2308f0/github_images/start.png
--------------------------------------------------------------------------------
/github_images/users.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dgewe/Chat-App-Android/cff2f947a4496e46cbaa750b4e3fa768ba2308f0/github_images/users.png
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dgewe/Chat-App-Android/cff2f947a4496e46cbaa750b4e3fa768ba2308f0/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Aug 05 11:42:06 CEST 2020
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 | rootProject.name = "Android-Chat-App"
--------------------------------------------------------------------------------