├── client ├── .gitignore ├── app │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── distep │ │ │ └── chatclient │ │ │ └── ExampleInstrumentedTest.kt │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── distep │ │ │ │ └── chatclient │ │ │ │ ├── Application.kt │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── MainViewModel.kt │ │ │ │ ├── RecordAdapter.kt │ │ │ │ └── data │ │ │ │ ├── GsonLocalDateTimeAdapter.kt │ │ │ │ ├── db │ │ │ │ ├── AppDb.kt │ │ │ │ ├── Converters.kt │ │ │ │ ├── DBModule.kt │ │ │ │ └── dao │ │ │ │ │ ├── AbstractDao.kt │ │ │ │ │ └── MessageDao.kt │ │ │ │ ├── dto │ │ │ │ ├── ChatSocketMessage.kt │ │ │ │ └── ConvertUtils.kt │ │ │ │ └── entity │ │ │ │ ├── BaseEntity.kt │ │ │ │ └── Message.kt │ │ └── res │ │ │ ├── drawable-v24 │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── drawable │ │ │ ├── background_my_message.xml │ │ │ ├── background_other_message.xml │ │ │ └── ic_launcher_background.xml │ │ │ ├── layout │ │ │ ├── activity_main.xml │ │ │ └── message_item_layout.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ │ ├── values-night │ │ │ └── themes.xml │ │ │ ├── values │ │ │ ├── colors.xml │ │ │ ├── strings.xml │ │ │ └── themes.xml │ │ │ └── xml │ │ │ └── network_security_config.xml │ │ └── test │ │ └── java │ │ └── com │ │ └── distep │ │ └── chatclient │ │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle └── server ├── .gitignore ├── build.gradle.kts ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── src ├── main ├── kotlin │ └── org │ │ └── distep │ │ └── chat │ │ ├── ChatOnWebsocketApplication.kt │ │ ├── Links.kt │ │ ├── config │ │ └── WebSocketConfig.kt │ │ ├── controllers │ │ └── ChatController.kt │ │ └── dto │ │ └── ChatSocketMessage.kt └── resources │ └── application.properties └── test └── kotlin └── org └── distep └── chat └── WebsockettutorialApplicationTests.kt /client/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/* 5 | /.idea/caches 6 | /.idea/libraries 7 | /.idea/modules.xml 8 | /.idea/workspace.xml 9 | /.idea/navEditor.xml 10 | /.idea/assetWizardSettings.xml 11 | .DS_Store 12 | /build 13 | /captures 14 | .externalNativeBuild 15 | .cxx 16 | local.properties 17 | -------------------------------------------------------------------------------- /client/app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /client/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | id "org.jetbrains.kotlin.kapt" 5 | 6 | id 'kotlin-android-extensions' 7 | id 'dagger.hilt.android.plugin' 8 | } 9 | 10 | android { 11 | compileSdk 31 12 | 13 | defaultConfig { 14 | applicationId "com.distep.chatclient" 15 | minSdk 31 16 | targetSdk 31 17 | versionCode 1 18 | versionName "1.0" 19 | 20 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 21 | } 22 | 23 | buildTypes { 24 | release { 25 | minifyEnabled false 26 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 27 | } 28 | } 29 | compileOptions { 30 | sourceCompatibility JavaVersion.VERSION_1_8 31 | targetCompatibility JavaVersion.VERSION_1_8 32 | } 33 | kotlinOptions { 34 | jvmTarget = '1.8' 35 | } 36 | buildFeatures { 37 | viewBinding true 38 | } 39 | } 40 | 41 | def room_version = "2.4.1" 42 | 43 | dependencies { 44 | 45 | implementation 'androidx.core:core-ktx:1.7.0' 46 | implementation 'androidx.appcompat:appcompat:1.4.1' 47 | implementation 'com.google.android.material:material:1.5.0' 48 | implementation 'androidx.constraintlayout:constraintlayout:2.1.3' 49 | 50 | implementation "androidx.fragment:fragment-ktx:1.4.0" 51 | implementation 'androidx.legacy:legacy-support-v4:1.0.0' 52 | implementation 'androidx.recyclerview:recyclerview:1.2.1' 53 | implementation 'androidx.annotation:annotation:1.3.0' 54 | implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' 55 | implementation 'androidx.viewpager2:viewpager2:1.0.0' 56 | implementation 'androidx.work:work-runtime-ktx:2.8.0-alpha01' 57 | implementation "android.arch.paging:runtime:1.0.1" 58 | 59 | implementation 'androidx.hilt:hilt-work:1.0.0' 60 | kapt 'androidx.hilt:hilt-compiler:1.0.0' 61 | 62 | implementation "com.google.dagger:hilt-android:2.37" 63 | kapt "com.google.dagger:hilt-android-compiler:2.37" 64 | 65 | implementation "androidx.room:room-runtime:$room_version" 66 | kapt "androidx.room:room-compiler:$room_version" 67 | implementation "androidx.room:room-ktx:$room_version" 68 | implementation "androidx.room:room-rxjava2:$room_version" 69 | 70 | implementation "io.reactivex.rxjava2:rxandroid:2.1.1" 71 | implementation 'com.squareup.retrofit2:converter-gson:2.9.0' 72 | implementation 'com.squareup.okhttp3:okhttp:4.8.1' 73 | implementation 'com.github.NaikSoftware:StompProtocolAndroid:1.6.4' 74 | 75 | 76 | testImplementation 'junit:junit:4.13.2' 77 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 78 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 79 | } -------------------------------------------------------------------------------- /client/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 -------------------------------------------------------------------------------- /client/app/src/androidTest/java/com/distep/chatclient/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.distep.chatclient 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.distep.chatclient", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /client/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /client/app/src/main/java/com/distep/chatclient/Application.kt: -------------------------------------------------------------------------------- 1 | package com.distep.chatclient 2 | 3 | import android.app.Application 4 | import dagger.hilt.android.HiltAndroidApp 5 | 6 | 7 | @HiltAndroidApp 8 | class Application: Application() { 9 | } -------------------------------------------------------------------------------- /client/app/src/main/java/com/distep/chatclient/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.distep.chatclient 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import androidx.lifecycle.LiveData 6 | import androidx.paging.DataSource 7 | import androidx.paging.LivePagedListBuilder 8 | import androidx.paging.PagedList 9 | import androidx.recyclerview.widget.RecyclerView 10 | import androidx.viewpager2.widget.ViewPager2 11 | import com.distep.chatclient.data.db.AppDb 12 | import com.distep.chatclient.data.entity.Message 13 | import dagger.hilt.android.AndroidEntryPoint 14 | import kotlinx.android.synthetic.main.activity_main.* 15 | import javax.inject.Inject 16 | 17 | @AndroidEntryPoint 18 | class MainActivity : AppCompatActivity() { 19 | @Inject 20 | lateinit var db: AppDb 21 | 22 | @Inject 23 | lateinit var mainViewModel: MainViewModel 24 | 25 | private var viewPager2: ViewPager2? = null 26 | private var recyclerView: RecyclerView? = null 27 | private var recordAdapter: RecordAdapter? = null 28 | 29 | private var pagedListLiveData: LiveData>? = null 30 | 31 | private val config = PagedList.Config.Builder() 32 | .setEnablePlaceholders(false) 33 | .setPageSize(10) 34 | .build() 35 | 36 | override fun onCreate(savedInstanceState: Bundle?) { 37 | super.onCreate(savedInstanceState) 38 | setContentView(R.layout.activity_main) 39 | val textField = text_field 40 | send_message_btn.setOnClickListener { 41 | mainViewModel.sendMessage(textField.text.toString()) 42 | } 43 | 44 | initRecyclerView() 45 | } 46 | 47 | override fun onStart() { 48 | super.onStart() 49 | 50 | mainViewModel.liveChatState.observe(this){ 51 | if(it != null) { 52 | recordAdapter?.notifyDataSetChanged() 53 | } 54 | } 55 | } 56 | 57 | private fun initRecyclerView() { 58 | viewPager2 = chat_content 59 | viewPager2!!.orientation = ViewPager2.ORIENTATION_VERTICAL 60 | 61 | val field = ViewPager2::class.java.getDeclaredField("mRecyclerView") 62 | field.isAccessible = true 63 | recyclerView = field.get(viewPager2) as RecyclerView 64 | recyclerView!!.clearOnChildAttachStateChangeListeners() 65 | recyclerView!!.layoutManager 66 | 67 | 68 | setAdapter(true, config) 69 | } 70 | 71 | private fun setAdapter(init: Boolean, config: PagedList.Config) { 72 | val factory: DataSource.Factory = db.messageDao().getItems() 73 | 74 | pagedListLiveData?.removeObservers(this) 75 | pagedListLiveData = LivePagedListBuilder(factory, config) 76 | .build() 77 | 78 | pagedListLiveData?.observe(this) { results -> recordAdapter?.submitList(results) } 79 | 80 | recordAdapter = RecordAdapter() 81 | viewPager2!!.adapter = recordAdapter!! 82 | if (!init) { 83 | recyclerView!!.swapAdapter(recordAdapter!!, true) 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /client/app/src/main/java/com/distep/chatclient/MainViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.distep.chatclient 2 | 3 | import android.util.Log 4 | import androidx.lifecycle.LiveData 5 | import androidx.lifecycle.MutableLiveData 6 | import androidx.lifecycle.ViewModel 7 | import com.distep.chatclient.data.GsonLocalDateTimeAdapter 8 | import com.distep.chatclient.data.db.AppDb 9 | import com.distep.chatclient.data.dto.ChatSocketMessage 10 | import com.distep.chatclient.data.dto.dtoToEntity 11 | import com.distep.chatclient.data.dto.entityToDto 12 | import com.distep.chatclient.data.entity.Message 13 | import com.google.gson.Gson 14 | import com.google.gson.GsonBuilder 15 | import io.reactivex.Completable 16 | import io.reactivex.android.schedulers.AndroidSchedulers 17 | import io.reactivex.disposables.CompositeDisposable 18 | import io.reactivex.schedulers.Schedulers 19 | import kotlinx.coroutines.Dispatchers 20 | import kotlinx.coroutines.GlobalScope 21 | import kotlinx.coroutines.launch 22 | import ua.naiksoftware.stomp.Stomp 23 | import ua.naiksoftware.stomp.StompClient 24 | import ua.naiksoftware.stomp.dto.LifecycleEvent 25 | import ua.naiksoftware.stomp.dto.StompMessage 26 | import ua.naiksoftware.stomp.provider.OkHttpConnectionProvider.TAG 27 | import java.time.LocalDateTime 28 | import javax.inject.Inject 29 | 30 | class MainViewModel @Inject constructor( 31 | var db: AppDb 32 | ) : ViewModel() { 33 | companion object{ 34 | const val SOCKET_URL = "ws://10.0.2.2:8080/api/v1/chat/websocket" 35 | const val CHAT_TOPIC = "/topic/chat" 36 | const val CHAT_LINK_SOCKET = "/api/v1/chat/sock" 37 | } 38 | 39 | private val gson: Gson = GsonBuilder().registerTypeAdapter(LocalDateTime::class.java, 40 | GsonLocalDateTimeAdapter() 41 | ).create() 42 | private var mStompClient: StompClient? = null 43 | private var compositeDisposable: CompositeDisposable? = null 44 | 45 | private val _chatState = MutableLiveData() 46 | val liveChatState: LiveData = _chatState 47 | 48 | init { 49 | // val headerMap: Map = 50 | // Collections.singletonMap("Authorization", "Token") 51 | mStompClient = Stomp.over(Stomp.ConnectionProvider.OKHTTP, SOCKET_URL/*, headerMap*/) 52 | .withServerHeartbeat(30000) 53 | resetSubscriptions() 54 | initChat() 55 | } 56 | 57 | private fun initChat() { 58 | resetSubscriptions() 59 | 60 | if (mStompClient != null) { 61 | val topicSubscribe = mStompClient!!.topic(CHAT_TOPIC) 62 | .subscribeOn(Schedulers.io(), false) 63 | .observeOn(AndroidSchedulers.mainThread()) 64 | .subscribe({ topicMessage: StompMessage -> 65 | Log.d(TAG, topicMessage.payload) 66 | val message: ChatSocketMessage = 67 | gson.fromJson(topicMessage.payload, ChatSocketMessage::class.java) 68 | val newMessage = dtoToEntity(message) 69 | addMessage(newMessage) 70 | }, 71 | { 72 | Log.e(TAG, "Error!", it) 73 | } 74 | ) 75 | 76 | val lifecycleSubscribe = mStompClient!!.lifecycle() 77 | .subscribeOn(Schedulers.io(), false) 78 | .observeOn(AndroidSchedulers.mainThread()) 79 | .subscribe { lifecycleEvent: LifecycleEvent -> 80 | when (lifecycleEvent.type!!) { 81 | LifecycleEvent.Type.OPENED -> Log.d(TAG, "Stomp connection opened") 82 | LifecycleEvent.Type.ERROR -> Log.e(TAG, "Error", lifecycleEvent.exception) 83 | LifecycleEvent.Type.FAILED_SERVER_HEARTBEAT, 84 | LifecycleEvent.Type.CLOSED -> { 85 | Log.d(TAG, "Stomp connection closed") 86 | } 87 | } 88 | } 89 | 90 | compositeDisposable!!.add(lifecycleSubscribe) 91 | compositeDisposable!!.add(topicSubscribe) 92 | 93 | if (!mStompClient!!.isConnected) { 94 | mStompClient!!.connect() 95 | } 96 | 97 | 98 | } else { 99 | Log.e(TAG, "mStompClient is null!") 100 | } 101 | } 102 | 103 | fun sendMessage(text: String) { 104 | val message = Message(text = text, author = "Me") 105 | val chatSocketMessage = entityToDto(message) 106 | sendCompletable(mStompClient!!.send(CHAT_LINK_SOCKET, gson.toJson(chatSocketMessage))) 107 | addMessage(message) 108 | } 109 | 110 | private fun addMessage(message: Message) { 111 | GlobalScope.launch(Dispatchers.IO) { 112 | val id: Long = db.messageDao().insert(message) 113 | message.id = id 114 | 115 | } 116 | 117 | _chatState.value = message 118 | } 119 | 120 | private fun sendCompletable(request: Completable) { 121 | compositeDisposable?.add( 122 | request.subscribeOn(Schedulers.io()) 123 | .observeOn(AndroidSchedulers.mainThread()) 124 | .subscribe( 125 | { 126 | Log.d(TAG, "Stomp sended") 127 | }, 128 | { 129 | Log.e(TAG, "Stomp error", it) 130 | } 131 | ) 132 | ) 133 | } 134 | 135 | private fun resetSubscriptions() { 136 | if (compositeDisposable != null) { 137 | compositeDisposable!!.dispose() 138 | } 139 | 140 | compositeDisposable = CompositeDisposable() 141 | } 142 | 143 | override fun onCleared() { 144 | super.onCleared() 145 | 146 | mStompClient?.disconnect() 147 | compositeDisposable?.dispose() 148 | } 149 | } -------------------------------------------------------------------------------- /client/app/src/main/java/com/distep/chatclient/RecordAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.distep.chatclient 2 | 3 | import android.view.ContextThemeWrapper 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.TextView 8 | import androidx.appcompat.content.res.AppCompatResources.getDrawable 9 | import androidx.constraintlayout.widget.ConstraintLayout 10 | import androidx.core.view.updateLayoutParams 11 | import androidx.paging.PagedListAdapter 12 | import androidx.recyclerview.widget.DiffUtil 13 | import androidx.recyclerview.widget.RecyclerView 14 | import com.distep.chatclient.data.entity.Message 15 | import java.time.format.DateTimeFormatter 16 | 17 | 18 | class RecordAdapter() : 19 | PagedListAdapter( 20 | COUPON_COMPARATOR 21 | ) { 22 | 23 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CouponViewHolder { 24 | val context = ContextThemeWrapper( 25 | parent.context, 26 | viewType 27 | ) 28 | 29 | val view: View = LayoutInflater.from(context) 30 | .inflate(R.layout.message_item_layout, parent, false) 31 | 32 | return CouponViewHolder(view, context) 33 | } 34 | 35 | override fun onBindViewHolder(holder: CouponViewHolder, position: Int) { 36 | holder.bind(getItem(position)) 37 | } 38 | 39 | public override fun getItem(position: Int): Message? { 40 | return super.getItem(position) 41 | } 42 | 43 | companion object { 44 | val COUPON_COMPARATOR = object : DiffUtil.ItemCallback() { 45 | override fun areContentsTheSame(oldItem: Message, newItem: Message): Boolean = 46 | oldItem == newItem 47 | 48 | override fun areItemsTheSame(oldItem: Message, newItem: Message): Boolean = 49 | oldItem.id == newItem.id 50 | } 51 | } 52 | 53 | inner class CouponViewHolder(itemView: View, val context: ContextThemeWrapper) : 54 | RecyclerView.ViewHolder( 55 | itemView 56 | ) { 57 | 58 | private val dateView: TextView = itemView.findViewById(R.id.datetime) 59 | private val textView: TextView = itemView.findViewById(R.id.message_text) 60 | private val authorView: TextView = itemView.findViewById(R.id.author) 61 | private val formLayout: ConstraintLayout = itemView.findViewById(R.id.form_layout) 62 | 63 | fun bind(record: Message?) { 64 | if (record != null) { 65 | textView.text = record.text 66 | authorView.text = record.author 67 | dateView.text = record.datetime.format(DateTimeFormatter.ofPattern("HH:mm")) 68 | if(record.author == "Me") { 69 | formLayout.background = getDrawable(context, R.drawable.background_my_message) 70 | 71 | formLayout.updateLayoutParams { 72 | startToStart = ConstraintLayout.LayoutParams.UNSET 73 | endToEnd = ConstraintLayout.LayoutParams.PARENT_ID 74 | } 75 | } 76 | } 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /client/app/src/main/java/com/distep/chatclient/data/GsonLocalDateTimeAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.distep.chatclient.data 2 | 3 | import com.google.gson.JsonSerializer 4 | import com.google.gson.JsonDeserializer 5 | import kotlin.jvm.Synchronized 6 | import com.google.gson.JsonSerializationContext 7 | import com.google.gson.JsonElement 8 | import com.google.gson.JsonPrimitive 9 | import com.google.gson.JsonDeserializationContext 10 | import java.lang.reflect.Type 11 | import java.time.LocalDateTime 12 | import java.time.format.DateTimeFormatter 13 | 14 | // this class can't be static 15 | class GsonLocalDateTimeAdapter : JsonSerializer, JsonDeserializer { 16 | @Synchronized 17 | override fun serialize( 18 | date: LocalDateTime, 19 | type: Type, 20 | jsonSerializationContext: JsonSerializationContext 21 | ): JsonElement { 22 | return JsonPrimitive(date.format(DateTimeFormatter.ISO_DATE_TIME)) 23 | } 24 | 25 | @Synchronized 26 | override fun deserialize( 27 | jsonElement: JsonElement, 28 | type: Type, 29 | jsonDeserializationContext: JsonDeserializationContext 30 | ): LocalDateTime { 31 | return LocalDateTime.parse(jsonElement.asString) 32 | } 33 | } -------------------------------------------------------------------------------- /client/app/src/main/java/com/distep/chatclient/data/db/AppDb.kt: -------------------------------------------------------------------------------- 1 | package com.distep.chatclient.data.db 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | import androidx.room.TypeConverters 6 | import com.distep.chatclient.data.db.dao.MessageDao 7 | import com.distep.chatclient.data.entity.Message 8 | 9 | @Database( 10 | entities = [Message::class], 11 | version = 1 12 | ) 13 | @TypeConverters(Converters::class) 14 | abstract class AppDb : RoomDatabase() { 15 | abstract fun messageDao(): MessageDao 16 | } -------------------------------------------------------------------------------- /client/app/src/main/java/com/distep/chatclient/data/db/Converters.kt: -------------------------------------------------------------------------------- 1 | package com.distep.chatclient.data.db 2 | 3 | import androidx.room.TypeConverter 4 | import java.time.LocalDateTime 5 | import java.time.ZoneOffset 6 | import java.util.* 7 | 8 | class Converters { 9 | @TypeConverter 10 | fun fromTimestamp(value: Long?): LocalDateTime? { 11 | return value?.let { LocalDateTime.ofInstant(Date(value).toInstant(), ZoneOffset.UTC) } 12 | } 13 | 14 | @TypeConverter 15 | fun dateToTimestamp(date: LocalDateTime?): Long? { 16 | return date?.toInstant(ZoneOffset.UTC)?.toEpochMilli() 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /client/app/src/main/java/com/distep/chatclient/data/db/DBModule.kt: -------------------------------------------------------------------------------- 1 | package com.distep.chatclient.data.db 2 | 3 | import android.content.Context 4 | import androidx.room.Room 5 | import androidx.room.RoomDatabase 6 | import androidx.room.RoomDatabase.Builder 7 | import androidx.sqlite.db.SupportSQLiteDatabase 8 | import dagger.Module 9 | import dagger.Provides 10 | import dagger.hilt.InstallIn 11 | import dagger.hilt.android.qualifiers.ApplicationContext 12 | import dagger.hilt.components.SingletonComponent 13 | import javax.inject.Singleton 14 | 15 | 16 | @Module 17 | @InstallIn(SingletonComponent::class) 18 | object DBModule { 19 | @Provides 20 | @Singleton 21 | fun provideDatabase( 22 | @ApplicationContext appContext: Context 23 | ): AppDb { 24 | 25 | val rdc: RoomDatabase.Callback = object : RoomDatabase.Callback() { 26 | override fun onCreate(db: SupportSQLiteDatabase) { 27 | } 28 | 29 | override fun onOpen(db: SupportSQLiteDatabase) { 30 | } 31 | } 32 | 33 | val builder: Builder = 34 | Room.inMemoryDatabaseBuilder( 35 | appContext, 36 | AppDb::class.java 37 | ) 38 | 39 | return builder 40 | .allowMainThreadQueries() 41 | .fallbackToDestructiveMigration() 42 | .addCallback(rdc) 43 | .build() 44 | } 45 | } -------------------------------------------------------------------------------- /client/app/src/main/java/com/distep/chatclient/data/db/dao/AbstractDao.kt: -------------------------------------------------------------------------------- 1 | package com.distep.chatclient.data.db.dao 2 | 3 | import androidx.room.* 4 | import com.distep.chatclient.data.entity.BaseEntity 5 | 6 | @Dao 7 | interface AbstractDao { 8 | 9 | @Insert 10 | suspend fun insertAll(entities: List): LongArray 11 | 12 | @Insert 13 | suspend fun insert(entity: T): Long 14 | 15 | @Delete 16 | suspend fun delete(entity: T) 17 | 18 | @Update 19 | suspend fun update(entity: T) 20 | 21 | @Update 22 | suspend fun updateMany(entity: List) 23 | 24 | } 25 | -------------------------------------------------------------------------------- /client/app/src/main/java/com/distep/chatclient/data/db/dao/MessageDao.kt: -------------------------------------------------------------------------------- 1 | package com.distep.chatclient.data.db.dao 2 | 3 | import androidx.paging.DataSource 4 | import androidx.room.Dao 5 | import androidx.room.Query 6 | import com.distep.chatclient.data.entity.Message 7 | 8 | @Dao 9 | interface MessageDao: AbstractDao { 10 | @Query("SELECT * FROM message WHERE id=:id") 11 | suspend fun getOneById(id: Long): Message? 12 | 13 | @Query("SELECT * FROM message") 14 | suspend fun getAll(): List 15 | 16 | @Query("SELECT * FROM message ORDER BY date_time DESC") 17 | fun getItems(): DataSource.Factory 18 | } -------------------------------------------------------------------------------- /client/app/src/main/java/com/distep/chatclient/data/dto/ChatSocketMessage.kt: -------------------------------------------------------------------------------- 1 | package com.distep.chatclient.data.dto 2 | 3 | import java.time.LocalDateTime 4 | 5 | data class ChatSocketMessage( 6 | val text: String, 7 | val author: String, 8 | val datetime: LocalDateTime, 9 | var receiver: String? = null 10 | ) -------------------------------------------------------------------------------- /client/app/src/main/java/com/distep/chatclient/data/dto/ConvertUtils.kt: -------------------------------------------------------------------------------- 1 | package com.distep.chatclient.data.dto 2 | 3 | import com.distep.chatclient.data.entity.Message 4 | 5 | fun dtoToEntity(dto: ChatSocketMessage) : Message { 6 | return Message( 7 | dto.datetime, 8 | dto.text, 9 | dto.author, 10 | dto.receiver 11 | ) 12 | } 13 | 14 | fun entityToDto(entity: Message) : ChatSocketMessage { 15 | return ChatSocketMessage( 16 | entity.text, 17 | entity.author, 18 | entity.datetime, 19 | entity.receiver 20 | ) 21 | } -------------------------------------------------------------------------------- /client/app/src/main/java/com/distep/chatclient/data/entity/BaseEntity.kt: -------------------------------------------------------------------------------- 1 | package com.distep.chatclient.data.entity 2 | 3 | import androidx.room.PrimaryKey 4 | 5 | abstract class BaseEntity { 6 | @PrimaryKey(autoGenerate = true) 7 | var id: Long = 0 8 | } -------------------------------------------------------------------------------- /client/app/src/main/java/com/distep/chatclient/data/entity/Message.kt: -------------------------------------------------------------------------------- 1 | package com.distep.chatclient.data.entity 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import java.time.LocalDateTime 6 | import java.time.ZoneOffset 7 | 8 | @Entity( 9 | tableName = "message" 10 | ) 11 | data class Message( 12 | @ColumnInfo(name = "date_time") 13 | var datetime: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC), 14 | val text: String, 15 | val author: String, 16 | var receiver: String? = null 17 | ) : BaseEntity() -------------------------------------------------------------------------------- /client/app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /client/app/src/main/res/drawable/background_my_message.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | 9 | -------------------------------------------------------------------------------- /client/app/src/main/res/drawable/background_other_message.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | 9 | -------------------------------------------------------------------------------- /client/app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /client/app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 19 | 20 | 21 | 22 | 32 | 33 | 47 | 48 |