refactor: 重构数据持久化方案
- 将 ObjectBox 替换为 Room 作为本地数据库 - 优化部分代码结构,提高可维护性
This commit is contained in:
parent
2e9e115c7e
commit
ff751834e4
6
.idea/AndroidProjectSystem.xml
Normal file
6
.idea/AndroidProjectSystem.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AndroidProjectSystem">
|
||||
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
|
||||
</component>
|
||||
</project>
|
@ -11,9 +11,10 @@
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
<option value="$PROJECT_DIR$/core_common" />
|
||||
<option value="$PROJECT_DIR$/core_network" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="resolveExternalAnnotations" value="false" />
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
|
@ -1,4 +1,3 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
||||
|
@ -4,7 +4,7 @@ plugins {
|
||||
id("com.google.devtools.ksp")
|
||||
// id("therouter")
|
||||
id("org.jetbrains.kotlin.plugin.serialization")
|
||||
id("kotlin-kapt")
|
||||
// id("kotlin-kapt")
|
||||
}
|
||||
|
||||
android {
|
||||
@ -17,7 +17,7 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.kaixed.kchat"
|
||||
minSdk = 28
|
||||
minSdk = 30
|
||||
targetSdk = 34
|
||||
versionCode = 1
|
||||
versionName = "0.0.1"
|
||||
@ -36,16 +36,17 @@ android {
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
jvmTarget = "11"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":core_common"))
|
||||
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.appcompat)
|
||||
@ -59,13 +60,15 @@ dependencies {
|
||||
implementation(libs.emoji2)
|
||||
implementation(libs.preference)
|
||||
|
||||
implementation(libs.androidx.startup.runtime)
|
||||
|
||||
implementation(libs.okhttp)
|
||||
implementation(libs.mmkv)
|
||||
implementation(libs.gson)
|
||||
|
||||
implementation(libs.objectbox.kotlin)
|
||||
debugImplementation(libs.objectbox.android.objectbrowser)
|
||||
releaseImplementation(libs.objectbox.android)
|
||||
// implementation(libs.objectbox.kotlin)
|
||||
// debugImplementation(libs.objectbox.android.objectbrowser)
|
||||
// releaseImplementation(libs.objectbox.android)
|
||||
|
||||
implementation(libs.glide)
|
||||
implementation(libs.lottie)
|
||||
@ -97,6 +100,15 @@ dependencies {
|
||||
implementation(libs.scanplus)
|
||||
// implementation(libs.therouter)
|
||||
// ksp(libs.therouter.ksp)
|
||||
|
||||
// Room
|
||||
implementation(libs.androidx.room.runtime)
|
||||
ksp(libs.androidx.room.compiler)
|
||||
implementation(libs.androidx.room.ktx)
|
||||
implementation(libs.androidx.room.paging)
|
||||
implementation(libs.androidx.paging.runtime.ktx)
|
||||
|
||||
debugImplementation(libs.leakcanary.android)
|
||||
}
|
||||
|
||||
apply(plugin = "io.objectbox")
|
||||
//apply(plugin = "io.objectbox")
|
||||
|
@ -154,7 +154,7 @@
|
||||
},
|
||||
{
|
||||
"id": "5:2885532406154205395",
|
||||
"lastPropertyId": "7:7996607163318458427",
|
||||
"lastPropertyId": "9:1671273767790122969",
|
||||
"name": "UserInfo",
|
||||
"properties": [
|
||||
{
|
||||
@ -194,6 +194,16 @@
|
||||
"id": "7:7996607163318458427",
|
||||
"name": "status",
|
||||
"type": 9
|
||||
},
|
||||
{
|
||||
"id": "8:6299664727899448104",
|
||||
"name": "accessToken",
|
||||
"type": 9
|
||||
},
|
||||
{
|
||||
"id": "9:1671273767790122969",
|
||||
"name": "refreshToken",
|
||||
"type": 9
|
||||
}
|
||||
],
|
||||
"relations": []
|
||||
|
@ -2,6 +2,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- 相机权限 -->
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.CAMERA" /> <!-- 文件读取权限,Android 12及更低版本申请 -->
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
@ -21,9 +22,6 @@
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.SEND_SMS" />
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32"
|
||||
@ -35,6 +33,7 @@
|
||||
android:appComponentFactory="androidx.core.app.CoreComponentFactory"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:hardwareAccelerated="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
@ -42,7 +41,6 @@
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.KChatAndroid"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:hardwareAccelerated="true"
|
||||
tools:targetApi="31">
|
||||
<activity
|
||||
android:name=".ui.activity.QrCodeActivity"
|
||||
@ -159,6 +157,19 @@
|
||||
android:exported="false" />
|
||||
|
||||
<service android:name=".service.WebSocketService" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
android:authorities="${applicationId}.androidx-startup"
|
||||
android:exported="false"
|
||||
tools:node="merge">
|
||||
<meta-data
|
||||
android:name="com.kaixed.kchat.startup.MMKVInitializer"
|
||||
android:value="androidx.startup" />
|
||||
<meta-data
|
||||
android:name="com.kaixed.kchat.startup.ScreenUtilsInitializer"
|
||||
android:value="androidx.startup" />
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
</manifest>
|
@ -1,11 +1,10 @@
|
||||
package com.kaixed.kchat
|
||||
|
||||
import android.app.Application
|
||||
import com.kaixed.kchat.data.local.box.ObjectBox
|
||||
import com.kaixed.kchat.data.local.box.ObjectBox.getBoxStore
|
||||
import android.content.Context
|
||||
import com.kaixed.kchat.manager.NetworkManager
|
||||
import com.kaixed.kchat.utils.ScreenUtils
|
||||
import com.tencent.mmkv.MMKV
|
||||
import io.objectbox.android.Admin
|
||||
|
||||
|
||||
/**
|
||||
@ -14,12 +13,16 @@ import io.objectbox.android.Admin
|
||||
*/
|
||||
class App : Application() {
|
||||
|
||||
companion object {
|
||||
private lateinit var appContext: Context
|
||||
|
||||
fun getAppContext(): Context {
|
||||
return appContext
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
MMKV.initialize(this)
|
||||
|
||||
ObjectBox.init(this)
|
||||
Admin(getBoxStore()).start(this)
|
||||
ScreenUtils.init(this)
|
||||
appContext = this
|
||||
}
|
||||
}
|
||||
|
@ -1,30 +1,47 @@
|
||||
package com.kaixed.kchat.data
|
||||
|
||||
import com.kaixed.kchat.data.local.box.ObjectBox.getBoxStore
|
||||
import com.kaixed.kchat.data.local.AppDatabase
|
||||
import com.kaixed.kchat.data.local.entity.Contact
|
||||
import com.kaixed.kchat.data.local.entity.Conversation
|
||||
import com.kaixed.kchat.data.local.entity.Messages
|
||||
import com.kaixed.kchat.data.local.entity.UserInfo
|
||||
import io.objectbox.Box
|
||||
import io.objectbox.kotlin.boxFor
|
||||
import com.kaixed.kchat.utils.ConstantsUtils.getUsername
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
/**
|
||||
* @Author: kaixed
|
||||
* @Date: 2024/12/14 23:06
|
||||
*/
|
||||
object DataBase {
|
||||
val contactBox: Box<Contact> by lazy { getBoxStore().boxFor() }
|
||||
|
||||
val messagesBox: Box<Messages> by lazy { getBoxStore().boxFor() }
|
||||
private val contactDao = AppDatabase.getDatabase().contactDao()
|
||||
private val userInfoDao = AppDatabase.getDatabase().userInfoDao()
|
||||
private val conversationDao = AppDatabase.getDatabase().conversationDao()
|
||||
private val messagesDao = AppDatabase.getDatabase().messagesDao()
|
||||
|
||||
val conversationBox: Box<Conversation> by lazy { getBoxStore().boxFor() }
|
||||
fun clearAllDatabase() {
|
||||
val db = AppDatabase.getDatabase() // 获取 Room 数据库实例
|
||||
db.clearAllTables() // 清空所有表的数据
|
||||
}
|
||||
|
||||
val userInfoBox: Box<UserInfo> by lazy { getBoxStore().boxFor() }
|
||||
fun getUserInfo(): UserInfo? {
|
||||
return runBlocking {
|
||||
userInfoDao.getUserInfoByUsername(getUsername())
|
||||
}
|
||||
}
|
||||
|
||||
fun cleanAllData() {
|
||||
contactBox.removeAll()
|
||||
messagesBox.removeAll()
|
||||
conversationBox.removeAll()
|
||||
userInfoBox.removeAll()
|
||||
fun deleteMsgByLocalId(localId: Long) {
|
||||
}
|
||||
|
||||
suspend fun getContactByUsername(contactId: String): Contact? {
|
||||
return contactDao.getContactByUsername(contactId)
|
||||
}
|
||||
|
||||
fun saveUserInfo(info: UserInfo) {
|
||||
|
||||
}
|
||||
|
||||
fun isMyFriend(string: String): Boolean {
|
||||
return runBlocking {
|
||||
contactDao.isMyFriend(string)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
package com.kaixed.kchat.data
|
||||
|
||||
import com.kaixed.kchat.data.local.entity.Contact
|
||||
import com.kaixed.kchat.data.local.entity.Contact_
|
||||
import com.kaixed.kchat.data.local.entity.Conversation_
|
||||
import com.kaixed.kchat.data.local.entity.Messages
|
||||
import com.kaixed.kchat.data.local.entity.Messages_
|
||||
import com.kaixed.kchat.data.local.entity.UserInfo
|
||||
import io.objectbox.query.QueryBuilder
|
||||
|
||||
/**
|
||||
* @Author: kaixed
|
||||
* @Date: 2024/11/24 13:34
|
||||
*/
|
||||
object LocalDatabase {
|
||||
|
||||
private val contactBox = DataBase.contactBox
|
||||
|
||||
private val messagesBox = DataBase.messagesBox
|
||||
|
||||
private val conversationBox = DataBase.conversationBox
|
||||
|
||||
private val userInfoBox = DataBase.userInfoBox
|
||||
|
||||
fun saveUserInfo(userInfo: UserInfo) {
|
||||
userInfoBox.put(userInfo)
|
||||
}
|
||||
|
||||
fun cleanChatHistory(contactId: String) {
|
||||
messagesBox.query(Messages_.talkerId.equal(contactId)).build().remove()
|
||||
conversationBox.query(Conversation_.talkerId.equal(contactId)).build().remove()
|
||||
}
|
||||
|
||||
fun isMyFriend(contactId: String): Boolean {
|
||||
return getContactByUsername(contactId) != null
|
||||
}
|
||||
|
||||
fun getContactByUsername(contactId: String): Contact? {
|
||||
return contactBox.query(Contact_.username.equal(contactId)).build().findFirst()
|
||||
}
|
||||
|
||||
fun getMessagesWithContact(contactId: String, offset: Long, limit: Long): List<Messages> {
|
||||
val query = messagesBox
|
||||
.query(Messages_.talkerId.equal(contactId))
|
||||
.order(Messages_.timestamp, QueryBuilder.DESCENDING)
|
||||
.build()
|
||||
return query.find(offset, limit)
|
||||
}
|
||||
|
||||
fun getMoreMessages(contactId: String, msgLocalId: Long, limit: Long): List<Messages> {
|
||||
val query = messagesBox
|
||||
.query(Messages_.talkerId.equal(contactId))
|
||||
.lessOrEqual(Messages_.msgLocalId, msgLocalId)
|
||||
.order(Messages_.timestamp, QueryBuilder.DESCENDING)
|
||||
.build()
|
||||
val offset = 0
|
||||
return query.find(offset.toLong(), limit)
|
||||
}
|
||||
|
||||
fun getAllHistoryMessages(contactId: String, msgLocalId: Long): List<Messages> {
|
||||
val query = messagesBox
|
||||
.query(Messages_.talkerId.equal(contactId))
|
||||
.greaterOrEqual(Messages_.msgLocalId, msgLocalId)
|
||||
.order(Messages_.timestamp, QueryBuilder.DESCENDING)
|
||||
.build()
|
||||
return query.find()
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
package com.kaixed.kchat.data.local
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import com.kaixed.kchat.App
|
||||
import com.kaixed.kchat.data.local.dao.ContactDao
|
||||
import com.kaixed.kchat.data.local.dao.ConversationDao
|
||||
import com.kaixed.kchat.data.local.dao.MessagesDao
|
||||
import com.kaixed.kchat.data.local.dao.UserInfoDao
|
||||
import com.kaixed.kchat.data.local.entity.Contact
|
||||
import com.kaixed.kchat.data.local.entity.Conversation
|
||||
import com.kaixed.kchat.data.local.entity.Messages
|
||||
import com.kaixed.kchat.data.local.entity.UserInfo
|
||||
|
||||
@Database(
|
||||
entities = [
|
||||
Contact::class,
|
||||
Conversation::class,
|
||||
Messages::class,
|
||||
UserInfo::class
|
||||
], version = 1, exportSchema = false
|
||||
)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun userInfoDao(): UserInfoDao
|
||||
abstract fun contactDao(): ContactDao
|
||||
abstract fun messagesDao(): MessagesDao
|
||||
abstract fun conversationDao(): ConversationDao
|
||||
|
||||
companion object {
|
||||
@Volatile
|
||||
private var INSTANCE: AppDatabase? = null
|
||||
|
||||
fun getDatabase(): AppDatabase {
|
||||
return INSTANCE ?: synchronized(this) {
|
||||
val instance = Room.databaseBuilder(
|
||||
App.getAppContext(),
|
||||
AppDatabase::class.java,
|
||||
"app_database"
|
||||
).build()
|
||||
INSTANCE = instance
|
||||
instance
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
package com.kaixed.kchat.data.local.box
|
||||
|
||||
import android.content.Context
|
||||
import com.kaixed.kchat.data.local.entity.MyObjectBox
|
||||
import io.objectbox.Box
|
||||
import io.objectbox.BoxStore
|
||||
import io.objectbox.kotlin.boxFor
|
||||
|
||||
/**
|
||||
* @Author: kaixed
|
||||
* @Date: 2024/10/24 16:57
|
||||
*/
|
||||
object ObjectBox {
|
||||
private lateinit var boxStore: BoxStore
|
||||
|
||||
fun init(context: Context) {
|
||||
boxStore = MyObjectBox.builder()
|
||||
.androidContext(context)
|
||||
.build()
|
||||
}
|
||||
|
||||
fun getBoxStore(): BoxStore = boxStore
|
||||
|
||||
fun <T> getBox(entityClass: Class<T>): Box<T> {
|
||||
return boxStore.boxFor(entityClass)
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package com.kaixed.kchat.data.local.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import androidx.room.Update
|
||||
import com.kaixed.kchat.data.local.entity.Contact
|
||||
|
||||
@Dao
|
||||
interface ContactDao {
|
||||
@Query("SELECT * FROM contact WHERE username = :contactId LIMIT 1")
|
||||
suspend fun getContactByUsername(contactId: String): Contact?
|
||||
|
||||
@Query("SELECT * FROM contact")
|
||||
suspend fun getAllContacts(): List<Contact>
|
||||
|
||||
@Query("SELECT EXISTS (SELECT 1 FROM contact WHERE username = :contactId)")
|
||||
suspend fun isMyFriend(contactId: String): Boolean
|
||||
|
||||
@Query("DELETE FROM contact WHERE username = :contactId")
|
||||
suspend fun deleteContactByUsername(contactId: String): Int
|
||||
|
||||
// 更新联系人
|
||||
@Update
|
||||
suspend fun updateContact(contact: Contact)
|
||||
|
||||
@Update
|
||||
suspend fun updateContacts(contacts: List<Contact>)
|
||||
|
||||
@Insert
|
||||
suspend fun insertContact(contact: Contact)
|
||||
|
||||
@Insert
|
||||
suspend fun insertContacts(contacts: List<Contact>)
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package com.kaixed.kchat.data.local.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import androidx.room.Update
|
||||
import com.kaixed.kchat.data.local.entity.Conversation
|
||||
|
||||
@Dao
|
||||
interface ConversationDao {
|
||||
@Query("DELETE FROM conversation WHERE talkerId = :contactId")
|
||||
suspend fun deleteConversationsByContactId(contactId: String)
|
||||
|
||||
@Query("SELECT * FROM conversation WHERE isShow = 1 ORDER BY timestamp DESC")
|
||||
suspend fun getConversations(): List<Conversation>
|
||||
|
||||
@Query("SELECT * FROM conversation WHERE talkerId = :contactId LIMIT 1")
|
||||
suspend fun getConversationByUsername(contactId: String): Conversation?
|
||||
|
||||
@Insert
|
||||
suspend fun insertConversation(conversation: Conversation)
|
||||
|
||||
@Delete
|
||||
suspend fun deleteConversation(conversation: Conversation)
|
||||
|
||||
@Update
|
||||
suspend fun updateConversation(conversation: Conversation)
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
package com.kaixed.kchat.data.local.dao
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Update
|
||||
import com.kaixed.kchat.data.local.entity.Messages
|
||||
|
||||
@Dao
|
||||
interface MessagesDao {
|
||||
|
||||
@Query("select * from messages where talkerId = :contactId and msgLocalId <= :msgLocalId order by timestamp desc limit :limit")
|
||||
suspend fun getMessageByContactId(
|
||||
contactId: String,
|
||||
msgLocalId: Long,
|
||||
limit: Long
|
||||
): List<Messages>
|
||||
|
||||
@Query("select * from messages where talkerId = :contactId order by timestamp desc limit :limit")
|
||||
suspend fun getMessageByContactId(contactId: String, limit: Long): List<Messages>
|
||||
|
||||
@Query("SELECT * FROM messages WHERE talkerId = :contactId ORDER BY timestamp DESC LIMIT :limit OFFSET :offset")
|
||||
suspend fun getMessagesWithContact(contactId: String, offset: Long, limit: Long): List<Messages>
|
||||
|
||||
@Query("SELECT * FROM messages WHERE talkerId = :contactId AND msgLocalId <= :msgLocalId ORDER BY timestamp DESC LIMIT :limit")
|
||||
suspend fun getMoreMessages(contactId: String, msgLocalId: Long, limit: Long): List<Messages>
|
||||
|
||||
@Query("SELECT * FROM messages WHERE talkerId = :contactId AND msgLocalId >= :msgLocalId ORDER BY timestamp DESC")
|
||||
suspend fun getAllHistoryMessages(contactId: String, msgLocalId: Long): List<Messages>
|
||||
|
||||
@Query("DELETE FROM messages WHERE talkerId = :contactId")
|
||||
suspend fun deleteMessagesByContactId(contactId: String)
|
||||
|
||||
@Query("SELECT * FROM messages WHERE talkerId = :contactId ORDER BY timestamp ASC")
|
||||
fun selectHistoryMessages(contactId: String): PagingSource<Int, Messages>
|
||||
|
||||
// ASC 更早的消息在最上面
|
||||
@Query("SELECT * FROM messages WHERE talkerId = :contactId ORDER BY timestamp ASC")
|
||||
fun getMessagesWithContact(contactId: String): PagingSource<Int, Messages>
|
||||
|
||||
@Query("SELECT * FROM messages WHERE talkerId = :contactId AND msgLocalId < :maxMsgId ORDER BY timestamp ASC LIMIT :limit")
|
||||
fun getMessagesBefore(contactId: String, maxMsgId: Long, limit: Int): List<Messages>
|
||||
|
||||
@Insert
|
||||
suspend fun insertAll(messages: List<Messages>)
|
||||
|
||||
@Insert
|
||||
suspend fun insert(messages: Messages): Long
|
||||
|
||||
@Query("SELECT * FROM messages ORDER BY timestamp DESC")
|
||||
fun getMessagesPaged(): PagingSource<Int, Messages>
|
||||
|
||||
@Query("SELECT * FROM messages ORDER BY timestamp DESC")
|
||||
fun getPagedMessages(): PagingSource<Int, Messages>
|
||||
|
||||
@Query("SELECT * FROM messages WHERE content LIKE :keyword")
|
||||
suspend fun getMessagesContainingKeyword(keyword: String): List<Messages>
|
||||
|
||||
@Query("SELECT * FROM messages WHERE talkerId = :talkerId AND timestamp <= :currentTimestamp ORDER BY timestamp DESC LIMIT 1")
|
||||
suspend fun getLatestMessage(talkerId: String, currentTimestamp: Long): Messages?
|
||||
|
||||
@Query("SELECT * FROM messages WHERE msgLocalId = :localId")
|
||||
suspend fun getMessageByLocalId(localId: Long): Messages
|
||||
|
||||
@Update
|
||||
suspend fun updateMessage(messages: Messages)
|
||||
|
||||
@Query("SELECT * FROM messages WHERE talkerId = :talkerId ORDER BY timestamp DESC")
|
||||
fun getPagedMessages(talkerId: String): PagingSource<Int, Messages>
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package com.kaixed.kchat.data.local.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.kaixed.kchat.data.local.entity.UserInfo
|
||||
|
||||
@Dao
|
||||
interface UserInfoDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun saveUserInfo(userInfo: UserInfo)
|
||||
|
||||
@Query("SELECT * FROM user_info WHERE username = :username LIMIT 1")
|
||||
suspend fun getUserInfoByUsername(username: String): UserInfo?
|
||||
}
|
@ -1,21 +1,19 @@
|
||||
package com.kaixed.kchat.data.local.entity
|
||||
|
||||
import io.objectbox.annotation.Entity
|
||||
import io.objectbox.annotation.Id
|
||||
import io.objectbox.annotation.Index
|
||||
import kotlinx.serialization.Serializable
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
/**
|
||||
* @Author: kaixed
|
||||
* @Date: 2024/11/4 13:39
|
||||
*/
|
||||
@Entity
|
||||
@Serializable
|
||||
@Entity(tableName = "contact")
|
||||
data class Contact(
|
||||
@Id var id: Long = 0L,
|
||||
@Index var username: String,
|
||||
@Index var nickname: String,
|
||||
@Index var remark: String? = null,
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
var id: Long = 0L,
|
||||
var username: String,
|
||||
var nickname: String,
|
||||
var remark: String? = null,
|
||||
var signature: String? = null,
|
||||
var avatarUrl: String? = null,
|
||||
var status: String? = null,
|
||||
|
@ -1,19 +1,17 @@
|
||||
package com.kaixed.kchat.data.local.entity
|
||||
|
||||
import io.objectbox.annotation.Entity
|
||||
import io.objectbox.annotation.Id
|
||||
import io.objectbox.annotation.Index
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
/**
|
||||
* @Author: kaixed
|
||||
* @Date: 2024/10/24 17:07
|
||||
*/
|
||||
|
||||
@Entity
|
||||
@Entity(tableName = "conversation")
|
||||
data class Conversation(
|
||||
@Id
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
var id: Long = 0L,
|
||||
@Index
|
||||
var talkerId: String,
|
||||
var nickname: String,
|
||||
var avatarUrl: String,
|
||||
|
@ -1,7 +1,7 @@
|
||||
package com.kaixed.kchat.data.local.entity
|
||||
|
||||
import io.objectbox.annotation.Entity
|
||||
import io.objectbox.annotation.Id
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
@ -9,10 +9,9 @@ import kotlinx.serialization.Serializable
|
||||
* @Date: 2024/10/24 17:08
|
||||
*/
|
||||
|
||||
@Entity
|
||||
@Serializable
|
||||
@Entity(tableName = "messages")
|
||||
data class Messages(
|
||||
@Id
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
var msgLocalId: Long = 0L,
|
||||
var msgSvrId: Long = 0L,
|
||||
var content: String,
|
||||
|
@ -1,8 +1,7 @@
|
||||
package com.kaixed.kchat.data.local.entity
|
||||
|
||||
import io.objectbox.annotation.Entity
|
||||
import io.objectbox.annotation.Id
|
||||
import io.objectbox.annotation.Index
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
@ -10,12 +9,10 @@ import kotlinx.serialization.Serializable
|
||||
* @Date: 2024/11/6 19:24
|
||||
*/
|
||||
|
||||
@Entity
|
||||
@Serializable
|
||||
@Entity(tableName = "user_info")
|
||||
data class UserInfo(
|
||||
@Id
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
var id: Long = 0,
|
||||
@Index
|
||||
var username: String,
|
||||
var nickname: String,
|
||||
var avatarUrl: String,
|
||||
|
@ -1,25 +1,19 @@
|
||||
package com.kaixed.kchat.data.repository
|
||||
|
||||
import com.kaixed.kchat.data.local.box.ObjectBox.getBoxStore
|
||||
import com.kaixed.core.common.utils.Pinyin4jUtil
|
||||
import com.kaixed.kchat.data.local.dao.ContactDao
|
||||
import com.kaixed.kchat.data.local.entity.Contact
|
||||
import com.kaixed.kchat.data.local.entity.Contact_
|
||||
import com.kaixed.kchat.data.model.friend.FriendRequestItem
|
||||
import com.kaixed.kchat.data.model.search.SearchUser
|
||||
import com.kaixed.kchat.network.ApiCall.apiCall
|
||||
import com.kaixed.kchat.network.RetrofitClient
|
||||
import com.kaixed.kchat.utils.ConstantsUtils.getUsername
|
||||
import com.kaixed.kchat.utils.Pinyin4jUtil
|
||||
import com.kaixed.kchat.utils.handle.ContactUtil
|
||||
import io.objectbox.Box
|
||||
|
||||
class ContactRepository {
|
||||
class ContactRepository(private val contactDao: ContactDao) {
|
||||
|
||||
private val friendApiService = RetrofitClient.friendApiService
|
||||
|
||||
private val contactBox: Box<Contact> by lazy {
|
||||
getBoxStore().boxFor(Contact::class.java)
|
||||
}
|
||||
|
||||
// 获取联系人请求列表
|
||||
suspend fun getContactRequestList(username: String): Result<List<FriendRequestItem>?> {
|
||||
return apiCall(
|
||||
@ -53,9 +47,7 @@ class ContactRepository {
|
||||
apiCall = { friendApiService.deleteContact(username, contactId) },
|
||||
errorMessage = "删除好友失败"
|
||||
).onSuccess {
|
||||
val con =
|
||||
contactBox.query(Contact_.username.equal(contactId)).build().findFirst()
|
||||
contactBox.remove(con!!)
|
||||
contactDao.deleteContactByUsername(contactId)
|
||||
}
|
||||
}
|
||||
|
||||
@ -106,16 +98,26 @@ class ContactRepository {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getContactsInDb(): List<Contact>? {
|
||||
val contacts = contactDao.getAllContacts()
|
||||
return if (contacts.isNotEmpty()) {
|
||||
contacts.sortedWith(::compareContacts)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setRemark(userId: String, contactId: String, remark: String): Result<String?> {
|
||||
return apiCall(
|
||||
apiCall = { friendApiService.setRemark(userId, contactId, remark) },
|
||||
errorMessage = "设置备注失败"
|
||||
).onSuccess {
|
||||
val con = contactBox.query(Contact_.username.equal(contactId)).build().findFirst()
|
||||
if (con != null) {
|
||||
con.remark = remark
|
||||
con.remarkquanpin = Pinyin4jUtil.toPinyin(remark)
|
||||
contactBox.put(con)
|
||||
val contact = contactDao.getContactByUsername(contactId)
|
||||
contact?.apply {
|
||||
this.remark = remark
|
||||
this.remarkquanpin = Pinyin4jUtil.toPinyin(remark)
|
||||
}?.let { contact ->
|
||||
contactDao.updateContact(contact)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,28 @@
|
||||
package com.kaixed.kchat.data.repository
|
||||
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingData
|
||||
import com.kaixed.kchat.data.local.dao.MessagesDao
|
||||
import com.kaixed.kchat.data.local.entity.Messages
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* @Author: kaixed
|
||||
* @Date: 2024/12/11 9:43
|
||||
*/
|
||||
class MessagesRepository {
|
||||
class MessagesRepository(private val messagesDao: MessagesDao) {
|
||||
|
||||
fun getMessageFlow(): Flow<PagingData<Messages>> {
|
||||
return Pager(
|
||||
config = PagingConfig(
|
||||
pageSize = 20, // 每页加载的数量
|
||||
enablePlaceholders = false,
|
||||
prefetchDistance = 5, // 当距离边缘还有5个项目时开始加载下一页
|
||||
initialLoadSize = 40 // 首次加载的数量
|
||||
),
|
||||
pagingSourceFactory = { messagesDao.getMessagesPaged() }
|
||||
).flow
|
||||
}
|
||||
|
||||
}
|
@ -1,26 +1,22 @@
|
||||
package com.kaixed.kchat.data.repository
|
||||
|
||||
import com.kaixed.kchat.data.local.box.ObjectBox.getBoxStore
|
||||
import com.kaixed.kchat.data.local.dao.UserInfoDao
|
||||
import com.kaixed.kchat.data.local.entity.UserInfo
|
||||
import com.kaixed.kchat.data.local.entity.UserInfo_
|
||||
import com.kaixed.kchat.data.model.request.RegisterRequest
|
||||
import com.kaixed.kchat.data.model.response.register.Register
|
||||
import com.kaixed.kchat.network.ApiCall.apiCall
|
||||
import com.kaixed.kchat.network.RetrofitClient
|
||||
import com.kaixed.kchat.utils.Constants.MMKV_USER_SESSION
|
||||
import com.tencent.mmkv.MMKV
|
||||
import io.objectbox.Box
|
||||
|
||||
/**
|
||||
* @Author: kaixed
|
||||
* @Date: 2024/11/23 14:15
|
||||
*/
|
||||
class UserAuthRepository {
|
||||
class UserAuthRepository(private val userInfoDao: UserInfoDao) {
|
||||
|
||||
private val authApiService = RetrofitClient.authApiService
|
||||
|
||||
private val userInfoBox: Box<UserInfo> by lazy { getBoxStore().boxFor(UserInfo::class.java) }
|
||||
|
||||
private val mmkv by lazy { MMKV.mmkvWithID(MMKV_USER_SESSION) }
|
||||
|
||||
/**
|
||||
@ -72,7 +68,7 @@ class UserAuthRepository {
|
||||
}
|
||||
}
|
||||
|
||||
private fun insertUserInfo(
|
||||
private suspend fun insertUserInfo(
|
||||
register: Register,
|
||||
telephone: String
|
||||
) {
|
||||
@ -83,22 +79,21 @@ class UserAuthRepository {
|
||||
signature = "",
|
||||
telephone = telephone
|
||||
)
|
||||
userInfoBox.put(userInfo)
|
||||
userInfoDao.saveUserInfo(userInfo)
|
||||
}
|
||||
|
||||
private fun updateDb(userInfo: UserInfo) {
|
||||
private suspend fun updateDb(userInfo: UserInfo) {
|
||||
mmkv.apply {
|
||||
encode("username", userInfo.username)
|
||||
encode("nickname", userInfo.nickname)
|
||||
encode("avatarUrl", userInfo.avatarUrl)
|
||||
}
|
||||
MMKV.defaultMMKV().encode("userLoginStatus", true)
|
||||
val user = userInfoBox
|
||||
.query(UserInfo_.username.equal(userInfo.username))
|
||||
.build()
|
||||
.findFirst()
|
||||
if (user == null) {
|
||||
userInfoBox.put(userInfo)
|
||||
val existingUser = userInfoDao.getUserInfoByUsername(userInfo.username)
|
||||
|
||||
if (existingUser == null) {
|
||||
// 如果用户不存在,插入新的用户信息
|
||||
userInfoDao.saveUserInfo(userInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,7 @@
|
||||
package com.kaixed.kchat.data.repository
|
||||
|
||||
import com.kaixed.kchat.data.local.box.ObjectBox.getBoxStore
|
||||
import com.kaixed.kchat.data.local.dao.UserInfoDao
|
||||
import com.kaixed.kchat.data.local.entity.UserInfo
|
||||
import com.kaixed.kchat.data.local.entity.UserInfo_
|
||||
import com.kaixed.kchat.data.model.request.UpdatePasswordRequest
|
||||
import com.kaixed.kchat.data.model.request.UserRequest
|
||||
import com.kaixed.kchat.data.model.search.SearchUser
|
||||
@ -18,7 +17,7 @@ import okhttp3.MultipartBody
|
||||
* @Author: kaixed
|
||||
* @Date: 2024/11/23 14:15
|
||||
*/
|
||||
class UserProfileRepository {
|
||||
class UserProfileRepository(private val userInfoDao: UserInfoDao) {
|
||||
|
||||
private val userApiService = RetrofitClient.userApiService
|
||||
|
||||
@ -67,29 +66,22 @@ class UserProfileRepository {
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateNickname(newNickname: String) {
|
||||
val userInfoBox = getBoxStore().boxFor(UserInfo::class.java)
|
||||
val user = userInfoBox
|
||||
.query(UserInfo_.username.equal(getUsername()))
|
||||
.build()
|
||||
.findFirst()
|
||||
|
||||
private suspend fun updateNickname(newNickname: String) {
|
||||
val userInfo = userInfoDao.getUserInfoByUsername(getUsername())
|
||||
MMKV.mmkvWithID(MMKV_USER_SESSION).putString(NICKNAME_KEY, newNickname)
|
||||
|
||||
user?.nickname = newNickname
|
||||
|
||||
if (user != null) {
|
||||
userInfoBox.put(user)
|
||||
userInfo?.nickname = newNickname
|
||||
userInfo?.apply {
|
||||
nickname = newNickname
|
||||
userInfoDao.saveUserInfo(this)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateDb(url: String) {
|
||||
val userInfoBox = getBoxStore().boxFor(UserInfo::class.java)
|
||||
val userInfo =
|
||||
userInfoBox.query(UserInfo_.username.equal(getUsername())).build().findFirst()
|
||||
if (userInfo != null) {
|
||||
userInfo.avatarUrl = url
|
||||
userInfoBox.put(userInfo)
|
||||
private suspend fun updateDb(url: String) {
|
||||
val userInfo = userInfoDao.getUserInfoByUsername(getUsername())
|
||||
userInfo?.apply {
|
||||
avatarUrl = url
|
||||
userInfoDao.saveUserInfo(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package com.kaixed.kchat.data.repository
|
||||
|
||||
import com.kaixed.kchat.data.model.response.search.User
|
||||
import com.kaixed.kchat.network.ApiCall.apiCall
|
||||
import com.kaixed.kchat.network.RetrofitClient
|
||||
|
||||
/**
|
||||
@ -12,20 +13,11 @@ class UserSearchRepository {
|
||||
private val userApiService = RetrofitClient.userApiService
|
||||
|
||||
// 获取用户列表
|
||||
suspend fun getUserList(username: String): Result<List<User>> {
|
||||
suspend fun getUserList(username: String): Result<List<User>?> {
|
||||
val requestParams = mapOf("username" to username)
|
||||
return try {
|
||||
val response = userApiService.fetchUserList(requestParams)
|
||||
if (response.isSuccess()) {
|
||||
val userList = response.getResponseData()
|
||||
userList?.let {
|
||||
Result.success(userList)
|
||||
} ?: Result.failure(Exception("用户列表为空"))
|
||||
} else {
|
||||
Result.failure(Exception(response.getResponseMsg()))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
return apiCall(
|
||||
apiCall = { userApiService.fetchUserList(requestParams) },
|
||||
errorMessage = "用户列表为空"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -2,16 +2,16 @@ package com.kaixed.kchat.manager
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.kaixed.kchat.data.DataBase
|
||||
import com.kaixed.kchat.data.LocalDatabase.getContactByUsername
|
||||
import com.kaixed.kchat.data.DataBase.getContactByUsername
|
||||
import com.kaixed.kchat.data.event.UnreadEvent
|
||||
import com.kaixed.kchat.data.local.entity.Contact_
|
||||
import com.kaixed.kchat.data.local.AppDatabase
|
||||
import com.kaixed.kchat.data.local.entity.Conversation
|
||||
import com.kaixed.kchat.data.local.entity.Conversation_
|
||||
import com.kaixed.kchat.data.local.entity.Messages
|
||||
import com.kaixed.kchat.data.local.entity.Messages_
|
||||
import com.kaixed.kchat.utils.ConstantsUtils.getCurrentContactId
|
||||
import com.kaixed.kchat.utils.ConstantsUtils.getUsername
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
|
||||
/**
|
||||
@ -23,12 +23,10 @@ object ConversationManager {
|
||||
private val _conversationLiveData = MutableLiveData<List<Conversation>?>()
|
||||
val conversations: LiveData<List<Conversation>?> get() = _conversationLiveData
|
||||
|
||||
private val conversationBox by lazy { DataBase.conversationBox }
|
||||
private val contactBox by lazy { DataBase.contactBox }
|
||||
private val messagesBox by lazy { DataBase.messagesBox }
|
||||
|
||||
private val conversationMap: MutableMap<String, Conversation> = mutableMapOf()
|
||||
|
||||
private val conversationDao = AppDatabase.getDatabase().conversationDao()
|
||||
|
||||
private var unreadCount = 0
|
||||
val unReadMsgCount get() = unreadCount
|
||||
|
||||
@ -37,25 +35,27 @@ object ConversationManager {
|
||||
}
|
||||
|
||||
private fun loadConversations() {
|
||||
val conversations = conversationBox.query(Conversation_.isShow.equal(true))
|
||||
.orderDesc(Conversation_.timestamp).build().find()
|
||||
val scope = CoroutineScope(Dispatchers.IO)
|
||||
scope.launch {
|
||||
val conversations = conversationDao.getConversations()
|
||||
|
||||
conversations.forEach { conversation ->
|
||||
if (conversation.unreadCount > 0) {
|
||||
unreadCount += conversation.unreadCount
|
||||
conversations.forEach { conversation ->
|
||||
if (conversation.unreadCount > 0) {
|
||||
unreadCount += conversation.unreadCount
|
||||
}
|
||||
conversationMap[conversation.talkerId] = conversation
|
||||
}
|
||||
conversationMap[conversation.talkerId] = conversation
|
||||
updateLiveData()
|
||||
}
|
||||
updateLiveData()
|
||||
}
|
||||
|
||||
fun handleMessages(messages: Messages) {
|
||||
suspend fun handleMessages(messages: Messages) {
|
||||
val talkerId = getCurrentContactId()
|
||||
if (messages.senderId != getUsername() && messages.talkerId != talkerId) {
|
||||
unreadCount++
|
||||
}
|
||||
val updatedConversation = updateConversation(messages)
|
||||
conversationBox.put(updatedConversation)
|
||||
conversationDao.updateConversation(updatedConversation)
|
||||
updateLiveData()
|
||||
}
|
||||
|
||||
@ -78,7 +78,7 @@ object ConversationManager {
|
||||
updateLiveData()
|
||||
}
|
||||
|
||||
fun deleteConversation(conversation: Conversation) {
|
||||
suspend fun deleteConversation(conversation: Conversation) {
|
||||
val count = conversationMap[conversation.talkerId]?.unreadCount
|
||||
count?.let {
|
||||
if (it > 0) {
|
||||
@ -86,48 +86,50 @@ object ConversationManager {
|
||||
}
|
||||
}
|
||||
conversationMap.remove(conversation.talkerId)
|
||||
conversationBox.query(Conversation_.talkerId.equal(conversation.talkerId)).build().remove()
|
||||
messagesBox.query(Messages_.talkerId.equal(conversation.talkerId)).build().remove()
|
||||
conversationDao.deleteConversationsByContactId(conversation.talkerId)
|
||||
val messagesDao = AppDatabase.getDatabase().messagesDao()
|
||||
messagesDao.deleteMessagesByContactId(conversation.talkerId)
|
||||
updateLiveData()
|
||||
}
|
||||
|
||||
fun cleanUnreadCount(talkerId: String) {
|
||||
suspend fun cleanUnreadCount(talkerId: String) {
|
||||
val con = conversationMap[talkerId]
|
||||
unreadCount -= con?.unreadCount ?: 0
|
||||
conversationMap[talkerId].apply {
|
||||
this?.unreadCount = 0
|
||||
}
|
||||
val conversation =
|
||||
conversationBox.query(Conversation_.talkerId.equal(talkerId)).build().findFirst()
|
||||
val conversation = conversationDao.getConversationByUsername(talkerId)
|
||||
|
||||
conversation?.let {
|
||||
it.unreadCount = 0
|
||||
conversationBox.put(it)
|
||||
conversation?.apply {
|
||||
unreadCount = 0
|
||||
conversationDao.updateConversation(this)
|
||||
}
|
||||
updateLiveData()
|
||||
}
|
||||
|
||||
fun updateDisturbStatus(contactId: String, isDisturb: Boolean) {
|
||||
private val contactDao = AppDatabase.getDatabase().contactDao()
|
||||
|
||||
suspend fun updateDisturbStatus(contactId: String, isDisturb: Boolean) {
|
||||
conversationMap[contactId]?.apply {
|
||||
this.doNotDisturb = isDisturb
|
||||
doNotDisturb = isDisturb
|
||||
conversationDao.updateConversation(this)
|
||||
}
|
||||
conversationBox.put(conversationMap[contactId]!!)
|
||||
|
||||
val dbContact =
|
||||
contactBox.query(Contact_.username.equal(contactId)).build().findFirst().apply {
|
||||
contactDao.getContactByUsername(contactId).apply {
|
||||
this?.doNotDisturb = isDisturb
|
||||
}
|
||||
contactBox.put(dbContact!!)
|
||||
contactDao.updateContact(dbContact!!)
|
||||
updateLiveData()
|
||||
}
|
||||
|
||||
fun updateUnreadCount(talkerId: String, isRead: Boolean) {
|
||||
suspend fun updateUnreadCount(talkerId: String, isRead: Boolean) {
|
||||
unreadCount = if (isRead) unreadCount - 1 else unreadCount + 1
|
||||
val conversation = conversationMap[talkerId]?.apply {
|
||||
this.unreadCount = if (isRead) 1 else 0
|
||||
}
|
||||
if (conversation != null) {
|
||||
conversationBox.put(conversation)
|
||||
conversationDao.insertConversation(conversation)
|
||||
}
|
||||
updateLiveData()
|
||||
}
|
||||
@ -138,7 +140,7 @@ object ConversationManager {
|
||||
_conversationLiveData.postValue(conversationMap.values.sortedByDescending { it.timestamp })
|
||||
}
|
||||
|
||||
private fun updateConversation(messages: Messages): Conversation {
|
||||
private suspend fun updateConversation(messages: Messages): Conversation {
|
||||
val existingConversation = conversationMap[messages.talkerId]
|
||||
return existingConversation?.apply {
|
||||
lastContent = if (messages.type == "4") "[图片]" else messages.content
|
||||
@ -148,7 +150,7 @@ object ConversationManager {
|
||||
} ?: createConversation(messages)
|
||||
}
|
||||
|
||||
private fun createConversation(messages: Messages): Conversation {
|
||||
private suspend fun createConversation(messages: Messages): Conversation {
|
||||
val contact = getContactByUsername(messages.talkerId)
|
||||
return Conversation(
|
||||
talkerId = messages.talkerId,
|
||||
|
@ -1,13 +1,13 @@
|
||||
package com.kaixed.kchat.manager
|
||||
|
||||
import com.kaixed.kchat.data.DataBase
|
||||
import com.kaixed.kchat.data.LocalDatabase
|
||||
import com.kaixed.kchat.data.LocalDatabase.getAllHistoryMessages
|
||||
import com.kaixed.kchat.data.LocalDatabase.getMessagesWithContact
|
||||
import com.kaixed.kchat.data.LocalDatabase.getMoreMessages
|
||||
import com.kaixed.kchat.data.local.AppDatabase
|
||||
import com.kaixed.kchat.data.local.entity.Messages
|
||||
import com.kaixed.kchat.data.repository.MessagesRepository
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlin.collections.isNotEmpty
|
||||
|
||||
/**
|
||||
* @Author: kaixed
|
||||
@ -22,41 +22,57 @@ object MessagesManager {
|
||||
val messages: StateFlow<List<Messages>> get() = _messages
|
||||
|
||||
private var contactId: String = ""
|
||||
|
||||
private var isHasHistory = false
|
||||
private var loading = false
|
||||
|
||||
private val messagesDao = AppDatabase.getDatabase().messagesDao()
|
||||
|
||||
|
||||
private var tempIndex: Long = 0
|
||||
|
||||
fun setContactId(contactId: String) {
|
||||
this.contactId = contactId
|
||||
}
|
||||
|
||||
suspend fun firstLoadMessages() {
|
||||
val messages = messagesDao.getMessageByContactId(contactId, LIMIT + 1)
|
||||
handleMessages(messages)
|
||||
}
|
||||
|
||||
suspend fun loadMoreMessages() {
|
||||
if (!isHasHistory) {
|
||||
return
|
||||
}
|
||||
val messages = messagesDao.getMessageByContactId(contactId, tempIndex, LIMIT + 1)
|
||||
handleMessages(messages)
|
||||
}
|
||||
|
||||
private fun handleMessages(messages: List<Messages>) {
|
||||
if (messages.isNotEmpty()) {
|
||||
val size = messages.size
|
||||
isHasHistory = size > LIMIT
|
||||
|
||||
_messages.value += if (isHasHistory) {
|
||||
tempIndex = messages[size - 1].msgLocalId
|
||||
messages.subList(0, LIMIT.toInt())
|
||||
} else {
|
||||
messages
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteMessage(msgLocalId: Long) {
|
||||
DataBase.messagesBox.remove(msgLocalId)
|
||||
DataBase.deleteMsgByLocalId(msgLocalId)
|
||||
val msg = _messages.value.toMutableList().apply {
|
||||
removeIf { it.msgLocalId == msgLocalId }
|
||||
}
|
||||
_messages.value = msg
|
||||
}
|
||||
|
||||
fun queryHistory(msgLocalId: Long): Int {
|
||||
_messages.value = emptyList()
|
||||
val msg = getAllHistoryMessages(contactId, msgLocalId)
|
||||
_messages.value = msg
|
||||
return msg.size
|
||||
}
|
||||
|
||||
fun setContactId(contactId: String) {
|
||||
this.contactId = contactId
|
||||
}
|
||||
|
||||
fun resetMessages() {
|
||||
_messages.value = emptyList()
|
||||
}
|
||||
|
||||
fun cleanMessages(timestamp: Long) {
|
||||
val messages = _messages.value.toMutableList()
|
||||
_messages.value = messages.apply {
|
||||
removeIf { it.timestamp < timestamp }
|
||||
}
|
||||
LocalDatabase.cleanChatHistory(contactId)
|
||||
}
|
||||
|
||||
fun receiveMessage(messages: Messages) {
|
||||
if (messages.talkerId != contactId) return
|
||||
if (_messages.value.first() == messages) return
|
||||
@ -66,49 +82,9 @@ object MessagesManager {
|
||||
}
|
||||
|
||||
fun sendMessages(messages: Messages) {
|
||||
_messages.value = _messages.value.toMutableList().apply {
|
||||
add(0, messages)
|
||||
}
|
||||
}
|
||||
|
||||
fun firstLoadMessages() {
|
||||
val messages = getMessagesWithContact(contactId, 0, LIMIT + 1)
|
||||
if (messages.isNotEmpty()) {
|
||||
val size = messages.size
|
||||
isHasHistory = size > LIMIT
|
||||
if (isHasHistory) {
|
||||
val messages1 = messages.subList(0, LIMIT.toInt())
|
||||
_messages.value = messages1
|
||||
tempIndex = messages[size - 1].msgLocalId
|
||||
} else {
|
||||
_messages.value = messages
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadMoreMessages() {
|
||||
if (loading) return
|
||||
|
||||
if (!isHasHistory) return
|
||||
|
||||
loading = true
|
||||
|
||||
val newMessages: List<Messages> =
|
||||
getMoreMessages(contactId, tempIndex, LIMIT + 1)
|
||||
|
||||
val size = newMessages.size
|
||||
tempIndex = newMessages[size - 1].msgLocalId
|
||||
isHasHistory = size > LIMIT
|
||||
|
||||
if (newMessages.isNotEmpty()) {
|
||||
val messages1 = if (isHasHistory) {
|
||||
newMessages.subList(0, LIMIT.toInt()).toMutableList()
|
||||
} else {
|
||||
newMessages.subList(0, newMessages.size).toMutableList()
|
||||
}
|
||||
_messages.value += messages1
|
||||
}
|
||||
|
||||
loading = false
|
||||
_messages.value = mutableListOf(messages) + _messages.value
|
||||
// _messages.value = _messages.value.toMutableList().apply {
|
||||
// add(0, messages)
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,19 @@
|
||||
package com.kaixed.kchat.manager
|
||||
|
||||
import android.content.Context
|
||||
|
||||
object NetworkManager {
|
||||
private lateinit var networkStateManager: NetworkStateManager
|
||||
|
||||
fun init(context: Context) {
|
||||
networkStateManager = NetworkStateManager(context.applicationContext)
|
||||
}
|
||||
|
||||
fun start() {
|
||||
networkStateManager.startListening()
|
||||
}
|
||||
|
||||
fun release() {
|
||||
networkStateManager.stopListening()
|
||||
}
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
package com.kaixed.kchat.manager
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkRequest
|
||||
import com.kaixed.kchat.App
|
||||
|
||||
class NetworkStateManager(context: Context) {
|
||||
private val connectivityManager =
|
||||
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
|
||||
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
currentNetworkState = NetworkState.CONNECTED
|
||||
notifyListeners()
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
currentNetworkState = NetworkState.DISCONNECTED
|
||||
notifyListeners()
|
||||
}
|
||||
|
||||
override fun onCapabilitiesChanged(network: Network, capabilities: NetworkCapabilities) {
|
||||
// 判断网络类型和质量
|
||||
currentNetworkState = when {
|
||||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> NetworkState.WIFI
|
||||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> NetworkState.CELLULAR
|
||||
else -> NetworkState.CONNECTED
|
||||
}
|
||||
|
||||
// 判断信号强度
|
||||
signalStrength = when {
|
||||
capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED)
|
||||
&& capabilities.linkDownstreamBandwidthKbps > 2000 -> SignalStrength.STRONG
|
||||
|
||||
capabilities.linkDownstreamBandwidthKbps < 500 -> SignalStrength.WEAK
|
||||
else -> SignalStrength.NORMAL
|
||||
}
|
||||
|
||||
notifyListeners()
|
||||
}
|
||||
}
|
||||
|
||||
private var currentNetworkState: NetworkState = NetworkState.UNKNOWN
|
||||
private var signalStrength: SignalStrength = SignalStrength.UNKNOWN
|
||||
private val listeners = mutableListOf<NetworkStateListener>()
|
||||
|
||||
fun startListening() {
|
||||
val request = NetworkRequest.Builder().build()
|
||||
connectivityManager.registerNetworkCallback(request, networkCallback)
|
||||
}
|
||||
|
||||
fun stopListening() {
|
||||
connectivityManager.unregisterNetworkCallback(networkCallback)
|
||||
}
|
||||
|
||||
fun getCurrentNetworkInfo(): Pair<NetworkState, SignalStrength> {
|
||||
return Pair(currentNetworkState, signalStrength)
|
||||
}
|
||||
|
||||
fun addListener(listener: NetworkStateListener) {
|
||||
listeners.add(listener)
|
||||
}
|
||||
|
||||
fun removeListener(listener: NetworkStateListener) {
|
||||
listeners.remove(listener)
|
||||
}
|
||||
|
||||
private fun notifyListeners() {
|
||||
listeners.forEach { it.onNetworkStateChanged(currentNetworkState, signalStrength) }
|
||||
}
|
||||
|
||||
enum class NetworkState {
|
||||
UNKNOWN, CONNECTED, DISCONNECTED, WIFI, CELLULAR
|
||||
}
|
||||
|
||||
enum class SignalStrength {
|
||||
UNKNOWN, WEAK, NORMAL, STRONG
|
||||
}
|
||||
|
||||
interface NetworkStateListener {
|
||||
fun onNetworkStateChanged(state: NetworkState, strength: SignalStrength)
|
||||
}
|
||||
}
|
@ -7,7 +7,7 @@ package com.kaixed.kchat.network
|
||||
object NetworkInterface {
|
||||
|
||||
// private const val URL = "app.kaixed.com/kchat"
|
||||
private const val URL = "192.168.31.18:6196"
|
||||
private const val URL = "192.168.225.209:6196"
|
||||
// private const val URL = "49.233.105.103:6000"
|
||||
// const val SERVER_URL = "https://$URL"
|
||||
const val SERVER_URL = "http://$URL"
|
||||
@ -44,7 +44,7 @@ object NetworkInterface {
|
||||
// 获取好友列表
|
||||
const val GET_FRIENDS = "friends/list"
|
||||
// 获取好友请求列表
|
||||
const val GET_FRIEND_LIST = "friends/requests/fetch"
|
||||
const val GET_FRIEND_REQUEST_LIST = "friends/requests/fetch"
|
||||
// 删除好友
|
||||
const val DELETE_FRIEND = "friends/delete-friend"
|
||||
// 设置好友备注
|
||||
|
@ -1,7 +1,5 @@
|
||||
package com.kaixed.kchat.network
|
||||
|
||||
import com.kaixed.kchat.network.interceptor.SignInterceptor
|
||||
import com.kaixed.kchat.network.interceptor.TokenRefreshInterceptor
|
||||
import com.kaixed.kchat.network.service.AuthApiService
|
||||
import com.kaixed.kchat.network.service.FileApiService
|
||||
import com.kaixed.kchat.network.service.FriendApiService
|
||||
@ -49,8 +47,8 @@ object RetrofitClient {
|
||||
private val client = OkHttpClient.Builder()
|
||||
.addInterceptor(loggingInterceptor)
|
||||
.pingInterval(15, TimeUnit.SECONDS)
|
||||
.addInterceptor(SignInterceptor()) // 签名拦截器
|
||||
.addInterceptor(TokenRefreshInterceptor()) // Token 拦截器
|
||||
// .addInterceptor(SignInterceptor()) // 签名拦截器
|
||||
// .addInterceptor(TokenRefreshInterceptor()) // Token 拦截器
|
||||
.build()
|
||||
|
||||
// 创建 Retrofit 实例(用于普通 API 调用)
|
||||
|
@ -1,5 +1,6 @@
|
||||
package com.kaixed.kchat.network.interceptor
|
||||
|
||||
import com.kaixed.kchat.utils.Constants
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Request
|
||||
@ -10,8 +11,6 @@ import java.util.*
|
||||
|
||||
class SignInterceptor : Interceptor {
|
||||
|
||||
private val secretKey = "YourSecretKey" // 后端秘钥
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
@ -26,7 +25,7 @@ class SignInterceptor : Interceptor {
|
||||
val params = getRequestParams(request)
|
||||
|
||||
// 生成签名
|
||||
val sign = generateServerSign(params, timestamp, nonce, secretKey)
|
||||
val sign = generateServerSign(params, timestamp, nonce)
|
||||
|
||||
// 构造新的请求,添加请求头
|
||||
val newRequest = request.newBuilder()
|
||||
@ -41,43 +40,46 @@ class SignInterceptor : Interceptor {
|
||||
|
||||
// 获取请求的参数,处理 POST 请求的 Map 格式数据,并确保按字典顺序排序
|
||||
private fun getRequestParams(request: Request): Map<String, String> {
|
||||
val params = mutableMapOf<String, String>()
|
||||
val params = TreeMap<String, String>()
|
||||
|
||||
// 处理 GET 请求
|
||||
if (request.method == "GET") {
|
||||
val url = request.url
|
||||
for (i in 0 until url.querySize) {
|
||||
params[url.queryParameterName(i)] = url.queryParameterValue(i) ?: ""
|
||||
}
|
||||
}
|
||||
|
||||
// 处理 POST 请求(Map 格式)
|
||||
else if (request.method == "POST") {
|
||||
val body = request.body
|
||||
if (body is FormBody) {
|
||||
// 获取所有参数,并确保按字典顺序排序
|
||||
for (i in 0 until body.size) {
|
||||
params[body.name(i)] = body.value(i)
|
||||
when (request.method) {
|
||||
"GET" -> {
|
||||
request.url.queryParameterNames.forEach { name ->
|
||||
params[name] = request.url.queryParameter(name) ?: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 按字典顺序排序参数
|
||||
return params.toSortedMap()
|
||||
"POST" -> {
|
||||
val body = request.body
|
||||
if (body is FormBody) {
|
||||
// 获取所有参数,并确保按字典顺序排序
|
||||
for (i in 0 until body.size) {
|
||||
params[body.name(i)] = body.value(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
// 其他请求方法不处理
|
||||
}
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
// 生成服务端签名
|
||||
private fun generateServerSign(params: Map<String, String>, timestamp: String, nonce: String, secretKey: String): String {
|
||||
val sortedParams = params.toSortedMap() // 确保是按字典顺序排序
|
||||
|
||||
private fun generateServerSign(
|
||||
sortedParams: Map<String, String>,
|
||||
timestamp: String,
|
||||
nonce: String,
|
||||
): String {
|
||||
// 拼接参数
|
||||
val sb = StringBuilder()
|
||||
for ((key, value) in sortedParams) {
|
||||
sortedParams.forEach { (key, value) ->
|
||||
sb.append("$key=$value&")
|
||||
}
|
||||
sb.append("timestamp=$timestamp&")
|
||||
sb.append("nonce=$nonce&")
|
||||
sb.append("secretKey=$secretKey")
|
||||
sb.append("secretKey=${Constants.Sign.SECRET_KEY}")
|
||||
|
||||
// 使用 SHA-256 进行加密
|
||||
return hashWithSHA256(sb.toString())
|
||||
|
@ -28,7 +28,6 @@ class TokenRefreshInterceptor() : Interceptor {
|
||||
val expiration = parseTokenManually(accessToken)
|
||||
val refreshTokenExpiration = parseTokenManually(refreshToken)
|
||||
val now = System.currentTimeMillis() / 1000
|
||||
Log.d("haha", now.toString())
|
||||
|
||||
// 如果 Token 距离过期时间小于 5 分钟,刷新 Token
|
||||
if (expiration - now < 300) {
|
||||
|
@ -6,7 +6,7 @@ import com.kaixed.kchat.data.model.search.SearchUser
|
||||
import com.kaixed.kchat.network.ApiResponse
|
||||
import com.kaixed.kchat.network.NetworkInterface.ACCEPT_FRIEND_REQUEST
|
||||
import com.kaixed.kchat.network.NetworkInterface.DELETE_FRIEND
|
||||
import com.kaixed.kchat.network.NetworkInterface.GET_FRIEND_LIST
|
||||
import com.kaixed.kchat.network.NetworkInterface.GET_FRIEND_REQUEST_LIST
|
||||
import com.kaixed.kchat.network.NetworkInterface.SEND_FRIEND_REQUEST
|
||||
import com.kaixed.kchat.network.NetworkInterface.SET_FRIEND_REMARK
|
||||
import retrofit2.http.Body
|
||||
@ -23,9 +23,9 @@ import retrofit2.http.Path
|
||||
interface FriendApiService {
|
||||
// 获取好友请求列表
|
||||
@FormUrlEncoded
|
||||
@POST(GET_FRIEND_LIST)
|
||||
@POST(GET_FRIEND_REQUEST_LIST)
|
||||
suspend fun getContactRequestList(
|
||||
@Field("userId") username: String
|
||||
@Field("userAccount") username: String
|
||||
): ApiResponse<List<FriendRequestItem>?>
|
||||
|
||||
// 接受好友请求
|
||||
|
@ -1,9 +1,11 @@
|
||||
package com.kaixed.kchat.processor
|
||||
|
||||
import com.kaixed.kchat.data.local.box.ObjectBox.getBox
|
||||
import com.kaixed.kchat.data.local.AppDatabase
|
||||
import com.kaixed.kchat.data.local.entity.Messages
|
||||
import com.kaixed.kchat.data.local.entity.Messages_
|
||||
import io.objectbox.Box
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* @Author: kaixed
|
||||
@ -13,20 +15,21 @@ object MessageProcessor {
|
||||
|
||||
private const val INTERVAL = 5
|
||||
|
||||
private val messagesBox: Box<Messages> by lazy { getBox(Messages::class.java) }
|
||||
private val messagesDao = AppDatabase.getDatabase().messagesDao()
|
||||
|
||||
fun processorMsg(messages: Messages): Messages {
|
||||
suspend fun processorMsg(messages: Messages): Messages {
|
||||
val curTimestamp = System.currentTimeMillis()
|
||||
val localMsg = messagesBox.query(Messages_.talkerId.equal(messages.talkerId))
|
||||
.lessOrEqual(Messages_.timestamp, curTimestamp)
|
||||
.orderDesc(Messages_.timestamp).build().findFirst()
|
||||
|
||||
var showTimer = true
|
||||
localMsg?.let {
|
||||
showTimer = curTimestamp - it.timestamp >= INTERVAL * 1L * 60 * 1000
|
||||
return withContext(Dispatchers.IO) {
|
||||
val localMsg = messagesDao.getLatestMessage(messages.talkerId, curTimestamp)
|
||||
|
||||
var showTimer = true
|
||||
localMsg?.let {
|
||||
showTimer = curTimestamp - it.timestamp >= INTERVAL * 1L * 60 * 1000
|
||||
}
|
||||
|
||||
messages.isShowTimer = showTimer
|
||||
messages
|
||||
}
|
||||
|
||||
messages.isShowTimer = showTimer
|
||||
return messages
|
||||
}
|
||||
}
|
22
app/src/main/kotlin/com/kaixed/kchat/send/MessageRepo.kt
Normal file
22
app/src/main/kotlin/com/kaixed/kchat/send/MessageRepo.kt
Normal file
@ -0,0 +1,22 @@
|
||||
package com.kaixed.kchat.send
|
||||
|
||||
// 模拟持久化存储,可考虑使用Room数据库代替。
|
||||
object MessageRepo {
|
||||
private val taskStore = mutableMapOf<Long, MessageTask>()
|
||||
|
||||
fun save(task: MessageTask) {
|
||||
taskStore[task.id] = task
|
||||
}
|
||||
|
||||
fun update(task: MessageTask) {
|
||||
taskStore[task.id] = task
|
||||
}
|
||||
|
||||
fun remove(taskId: Long) {
|
||||
taskStore.remove(taskId)
|
||||
}
|
||||
|
||||
fun loadAll(): List<MessageTask> {
|
||||
return taskStore.values.toList()
|
||||
}
|
||||
}
|
14
app/src/main/kotlin/com/kaixed/kchat/send/MessageTask.kt
Normal file
14
app/src/main/kotlin/com/kaixed/kchat/send/MessageTask.kt
Normal file
@ -0,0 +1,14 @@
|
||||
package com.kaixed.kchat.send
|
||||
|
||||
data class MessageTask(
|
||||
val id: Long,
|
||||
val payload: String,
|
||||
val maxRetry: Int = 5,
|
||||
var retryCount: Int = 0,
|
||||
var nextRetryTime: Long = System.currentTimeMillis(),
|
||||
var state: MessageState = MessageState.PENDING
|
||||
)
|
||||
|
||||
enum class MessageState {
|
||||
PENDING, SENDING, SUCCESS, FAILED
|
||||
}
|
16
app/src/main/kotlin/com/kaixed/kchat/send/NetworkMonitor.kt
Normal file
16
app/src/main/kotlin/com/kaixed/kchat/send/NetworkMonitor.kt
Normal file
@ -0,0 +1,16 @@
|
||||
package com.kaixed.kchat.send
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
// NetworkMonitor 用于监听网络状态变化,并通知 MessageUploadManager
|
||||
object NetworkMonitor {
|
||||
// 这里可以用 LiveData 或 Flow 来实时通知变化
|
||||
private val _networkAvailable = MutableStateFlow(true)
|
||||
val networkAvailable: StateFlow<Boolean> get() = _networkAvailable
|
||||
|
||||
// 模拟切换,实际需集成 ConnectivityManager 回调
|
||||
fun setNetworkAvailable(available: Boolean) {
|
||||
_networkAvailable.value = available
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
package com.kaixed.kchat.send
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
class RetryQueueManager {
|
||||
// 使用 MutableStateFlow 保存当前任务列表
|
||||
private val _taskFlow = MutableStateFlow<List<MessageTask>>(emptyList())
|
||||
val taskFlow: StateFlow<List<MessageTask>> get() = _taskFlow
|
||||
|
||||
// 添加任务到队列,并持久化
|
||||
fun addTask(task: MessageTask) {
|
||||
MessageRepo.save(task)
|
||||
_taskFlow.value = _taskFlow.value + task
|
||||
}
|
||||
|
||||
// 更新任务状态
|
||||
fun updateTask(task: MessageTask) {
|
||||
MessageRepo.update(task)
|
||||
_taskFlow.value = _taskFlow.value.map { if (it.id == task.id) task else it }
|
||||
}
|
||||
|
||||
// 从队列中移除任务
|
||||
fun removeTask(task: MessageTask) {
|
||||
MessageRepo.remove(task.id)
|
||||
_taskFlow.value = _taskFlow.value.filter { it.id != task.id }
|
||||
}
|
||||
|
||||
// 获取当前已到达可上传时间的任务(按 nextRetryTime 排序)
|
||||
fun getReadyTask(): MessageTask? {
|
||||
val now = System.currentTimeMillis()
|
||||
return _taskFlow.value
|
||||
.filter { it.state == MessageState.PENDING && it.nextRetryTime <= now }
|
||||
.sortedWith(compareBy<MessageTask> { it.nextRetryTime })
|
||||
.firstOrNull()
|
||||
}
|
||||
}
|
125
app/src/main/kotlin/com/kaixed/kchat/send/UploadWorker.kt
Normal file
125
app/src/main/kotlin/com/kaixed/kchat/send/UploadWorker.kt
Normal file
@ -0,0 +1,125 @@
|
||||
package com.kaixed.kchat.send
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import kotlin.math.pow
|
||||
import kotlin.random.Random
|
||||
|
||||
class UploadWorker(private val semaphore: Semaphore) {
|
||||
|
||||
// 模拟消息上传接口
|
||||
suspend fun upload(task: MessageTask): Boolean {
|
||||
// 模拟上传耗时 & 网络请求,可用 Retrofit 或 OkHttp 实现
|
||||
delay(500L) // 模拟网络延迟
|
||||
// 模拟成功率 80%
|
||||
return Random.nextInt(100) < 80
|
||||
}
|
||||
}
|
||||
|
||||
class MessageUploadManager(
|
||||
private val retryQueue: RetryQueueManager,
|
||||
private val worker: UploadWorker,
|
||||
private val ackTimeout: Long = 5000L // ACK 超时设为 5 秒
|
||||
) {
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
// 限制并发上传数量,例如最多 3 个同时上传
|
||||
private val semaphore = Semaphore(3)
|
||||
|
||||
// 启动调度流程,监视重传队列
|
||||
fun start() {
|
||||
// 持续监听任务队列
|
||||
scope.launch {
|
||||
while (isActive) {
|
||||
// 检查网络状态(这里应嵌入 NetworkMonitor 判断,可扩展为挂起函数)
|
||||
if (!isNetworkAvailable()) {
|
||||
delay(2000L)
|
||||
continue
|
||||
}
|
||||
|
||||
// 从队列中选择可上传任务
|
||||
val task = retryQueue.getReadyTask()
|
||||
if (task != null) {
|
||||
// 进入发送状态
|
||||
task.state = MessageState.SENDING
|
||||
retryQueue.updateTask(task)
|
||||
|
||||
// 限流并发执行上传任务
|
||||
semaphore.acquire()
|
||||
launch {
|
||||
val success = worker.upload(task)
|
||||
// 模拟等待 ACK 的逻辑
|
||||
// 如果上传成功,则等待 ackTimeout 时间后检查是否收到 ACK
|
||||
if (success) {
|
||||
// 调用 ackWaitTimer 监听 ACK
|
||||
withTimeoutOrNull(ackTimeout) {
|
||||
waitForAck(task.id.toString())
|
||||
}?.let {
|
||||
// 成功收到 ACK
|
||||
task.state = MessageState.SUCCESS
|
||||
retryQueue.updateTask(task)
|
||||
retryQueue.removeTask(task)
|
||||
} ?: run {
|
||||
// 超时未收到 ACK,则作为失败处理
|
||||
retryTask(task)
|
||||
}
|
||||
} else {
|
||||
// 上传失败处理
|
||||
retryTask(task)
|
||||
}
|
||||
semaphore.release()
|
||||
}
|
||||
} else {
|
||||
// 无任务时稍作延迟
|
||||
delay(1000L)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟网络状态检测函数,可结合 ConnectivityManager 使用
|
||||
private fun isNetworkAvailable(): Boolean {
|
||||
// 实际应检测网络连接
|
||||
return true
|
||||
}
|
||||
|
||||
// 模拟等待 ACK,实际可通过 Channel 或 WebSocket 收到 ack 后返回
|
||||
private suspend fun waitForAck(taskId: String) {
|
||||
// 这里模拟 ack 延迟,实际情况中应由 AckHandler 回调触发
|
||||
delay(300L) // 模拟短延时后收到 ACK
|
||||
}
|
||||
|
||||
// 重试逻辑:更新重试次数、计算下次重试时间,然后回到队列
|
||||
private fun retryTask(task: MessageTask) {
|
||||
task.retryCount++
|
||||
if (task.retryCount > task.maxRetry) {
|
||||
task.state = MessageState.FAILED
|
||||
retryQueue.updateTask(task)
|
||||
// 可在此处上报失败或通知用户
|
||||
} else {
|
||||
// 计算指数退避时间(如基于 2000ms 基数,再加上少量随机抖动)
|
||||
val newDelay =
|
||||
(2000L * 2.0.pow(task.retryCount.toDouble())).toLong() + Random.nextLong(500)
|
||||
task.nextRetryTime = System.currentTimeMillis() + newDelay
|
||||
task.state = MessageState.PENDING
|
||||
retryQueue.updateTask(task)
|
||||
}
|
||||
}
|
||||
|
||||
// 当收到 ACK 时调用,由 AckHandler 或网络消息回调触发
|
||||
fun onAckReceived(taskId: Long) {
|
||||
// 查找任务并更新状态
|
||||
val task = retryQueue.taskFlow.value.find { it.id == taskId }
|
||||
if (task != null) {
|
||||
task.state = MessageState.SUCCESS
|
||||
retryQueue.updateTask(task)
|
||||
retryQueue.removeTask(task)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package com.kaixed.kchat.service
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ChatViewModel : ViewModel() {
|
||||
fun sendChatMessage(content: String, msgLocalId: Long) {
|
||||
viewModelScope.launch {
|
||||
MessageRepository.sendMessage(content, msgLocalId).fold(
|
||||
onSuccess = { /* 消息发送成功 */ },
|
||||
onFailure = { /* 消息发送失败 */ }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun observeMessages() {
|
||||
viewModelScope.launch {
|
||||
MessageRepository.observeMessages().collect { message ->
|
||||
println("Received message: $message")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
16
app/src/main/kotlin/com/kaixed/kchat/service/MessageModel.kt
Normal file
16
app/src/main/kotlin/com/kaixed/kchat/service/MessageModel.kt
Normal file
@ -0,0 +1,16 @@
|
||||
package com.kaixed.kchat.service
|
||||
|
||||
data class MessageModel(
|
||||
var msgLocalId: Long = 0L,
|
||||
var msgSvrId: Long = 0L,
|
||||
var content: String,
|
||||
var timestamp: Long,
|
||||
var status: String = "normal",
|
||||
var senderId: String,
|
||||
var avatarUrl: String = "",
|
||||
var talkerId: String,
|
||||
var type: String,
|
||||
var show: Boolean = true,
|
||||
var isShowTimer: Boolean = false,
|
||||
var isSender: Boolean = false
|
||||
)
|
@ -0,0 +1,7 @@
|
||||
package com.kaixed.kchat.service
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface MessageReceiver {
|
||||
fun observeMessages(): Flow<String>
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
package com.kaixed.kchat.service
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
|
||||
object MessageRepository : MessageSender, MessageReceiver {
|
||||
|
||||
private val webSocketClient: WebSocketClient = OkHttpWebSocketClient
|
||||
|
||||
// 创建一个 Flow 来发送 WebSocket 的消息
|
||||
private val _outgoingMessages = MutableSharedFlow<String>(
|
||||
replay = 0,
|
||||
extraBufferCapacity = 10
|
||||
)
|
||||
|
||||
// 创建一个 Flow 来接收 WebSocket 的消息
|
||||
private val _incomingMessages = MutableSharedFlow<String>(
|
||||
replay = 0,
|
||||
extraBufferCapacity = 10
|
||||
)
|
||||
|
||||
init {
|
||||
// 设置 WebSocket 消息监听器
|
||||
webSocketClient.setMessageListener { text ->
|
||||
_incomingMessages.tryEmit(text)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun sendMessage(message: String, msgLocalId: Long): Result<Unit> {
|
||||
return try {
|
||||
if (webSocketClient.send(message, msgLocalId)) {
|
||||
_outgoingMessages.emit(message)
|
||||
Result.success(Unit)
|
||||
} else {
|
||||
Result.failure(Exception("WebSocket not connected"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun observeMessages(): Flow<String> = _incomingMessages.asSharedFlow()
|
||||
|
||||
fun observeOutgoingMessages(): Flow<String> = _outgoingMessages.asSharedFlow()
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package com.kaixed.kchat.service
|
||||
|
||||
interface MessageSender {
|
||||
suspend fun sendMessage(message: String, msgLocalId: Long): Result<Unit>
|
||||
}
|
@ -0,0 +1,124 @@
|
||||
package com.kaixed.kchat.service
|
||||
|
||||
import android.util.Log
|
||||
import com.google.gson.Gson
|
||||
import com.kaixed.kchat.data.local.entity.Messages
|
||||
import com.kaixed.kchat.network.NetworkInterface.WEBSOCKET
|
||||
import com.kaixed.kchat.network.NetworkInterface.WEBSOCKET_SERVER_URL
|
||||
import com.kaixed.kchat.network.OkhttpHelper
|
||||
import com.kaixed.kchat.utils.Constants.MMKV_COMMON_DATA
|
||||
import com.kaixed.kchat.utils.ConstantsUtils
|
||||
import com.kaixed.kchat.utils.ConstantsUtils.getUsername
|
||||
import com.tencent.mmkv.MMKV
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.WebSocket
|
||||
import okhttp3.WebSocketListener
|
||||
|
||||
object OkHttpWebSocketClient : WebSocketClient {
|
||||
|
||||
private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
private val url: String = "$WEBSOCKET_SERVER_URL$WEBSOCKET${getUsername()}"
|
||||
private lateinit var webSocket: WebSocket
|
||||
private var messageListener: ((String) -> Unit)? = null
|
||||
private var isConnected = false
|
||||
|
||||
private const val TAG = "WebSocketService"
|
||||
private const val WEBSOCKET_CLOSE_CODE = 1000
|
||||
|
||||
override fun connect() {
|
||||
establishConnection()
|
||||
}
|
||||
|
||||
override fun send(message: String, msgLocalId: Long): Boolean {
|
||||
return if (isConnected) {
|
||||
val kv = MMKV.mmkvWithID(MMKV_COMMON_DATA)
|
||||
kv.putLong("msgLocalId", msgLocalId)
|
||||
webSocket.send(message)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun close(code: Int, reason: String) {
|
||||
webSocket.close(code, reason)
|
||||
isConnected = false
|
||||
}
|
||||
|
||||
override fun setMessageListener(listener: (String) -> Unit) {
|
||||
this.messageListener = listener
|
||||
}
|
||||
|
||||
override fun isConnected(): Boolean = isConnected
|
||||
|
||||
private fun reconnect(attempt: Int = 1) {
|
||||
scope.launch {
|
||||
val delayMs = minOf(attempt * 1000L, 30000L) // 指数退避,最大 30 秒
|
||||
delay(delayMs)
|
||||
establishConnection()
|
||||
if (!isConnected()) {
|
||||
reconnect(attempt + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun establishConnection() {
|
||||
val request = Request.Builder()
|
||||
.url(url).build()
|
||||
val listener = EchoWebSocketListener()
|
||||
val client = OkhttpHelper.getInstance()
|
||||
webSocket = client.newWebSocket(request, listener)
|
||||
}
|
||||
|
||||
private class EchoWebSocketListener : WebSocketListener() {
|
||||
override fun onOpen(webSocket: WebSocket, response: Response) {
|
||||
Log.d(TAG, "WebSocket Opened")
|
||||
isConnected = true
|
||||
}
|
||||
|
||||
override fun onMessage(webSocket: WebSocket, text: String) {
|
||||
messageListener?.invoke(text)
|
||||
val messages = Gson().fromJson(text, Messages::class.java)
|
||||
|
||||
// serviceScope.launch {
|
||||
// if ("ack" == messages.type) {
|
||||
// val existedMessage =
|
||||
// messagesDao.getMessageByLocalId(messages.msgLocalId)
|
||||
//
|
||||
// existedMessage.apply {
|
||||
// timestamp = messages.timestamp
|
||||
// if (messages.msgSvrId == 0L) {
|
||||
// messages.msgSvrId = messages.msgLocalId
|
||||
// }
|
||||
// messagesDao.updateMessage(this)
|
||||
// }
|
||||
// } else {
|
||||
// messages.talkerId = messages.senderId
|
||||
// _messagesMutableLiveData.postValue(messages)
|
||||
// MessageProcessor.processorMsg(messages)
|
||||
// messagesDao.insert(messages)
|
||||
// ConversationManager.handleMessages(messages)
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
|
||||
isConnected = false
|
||||
webSocket.close(WEBSOCKET_CLOSE_CODE, null)
|
||||
Log.d(TAG, "WebSocket closing: $reason")
|
||||
establishConnection()
|
||||
}
|
||||
|
||||
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
|
||||
isConnected = false
|
||||
Log.d(TAG, "WebSocket closing: ${t.cause}")
|
||||
Log.d(TAG, "WebSocket closing: $t")
|
||||
establishConnection()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package com.kaixed.kchat.service
|
||||
|
||||
interface WebSocketClient {
|
||||
fun send(message: String, msgLocalId: Long): Boolean
|
||||
fun close(code: Int, reason: String)
|
||||
fun setMessageListener(listener: (String) -> Unit)
|
||||
fun connect()
|
||||
fun isConnected(): Boolean
|
||||
}
|
@ -6,9 +6,9 @@ import android.os.Binder
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import com.google.gson.Gson
|
||||
import com.kaixed.kchat.data.local.box.ObjectBox.getBoxStore
|
||||
import com.kaixed.kchat.data.local.entity.Contact
|
||||
import com.kaixed.kchat.data.local.entity.Contact_
|
||||
import com.kaixed.core.common.utils.SingleLiveEvent
|
||||
import com.kaixed.kchat.data.DataBase
|
||||
import com.kaixed.kchat.data.local.AppDatabase
|
||||
import com.kaixed.kchat.data.local.entity.Messages
|
||||
import com.kaixed.kchat.manager.ConversationManager
|
||||
import com.kaixed.kchat.manager.MessagesManager
|
||||
@ -16,16 +16,17 @@ import com.kaixed.kchat.network.NetworkInterface.WEBSOCKET
|
||||
import com.kaixed.kchat.network.NetworkInterface.WEBSOCKET_SERVER_URL
|
||||
import com.kaixed.kchat.network.OkhttpHelper
|
||||
import com.kaixed.kchat.processor.MessageProcessor
|
||||
import com.kaixed.kchat.service.OkHttpWebSocketClient
|
||||
import com.kaixed.kchat.utils.Constants.MMKV_COMMON_DATA
|
||||
import com.kaixed.kchat.utils.ConstantsUtils.getUsername
|
||||
import com.kaixed.kchat.utils.SingleLiveEvent
|
||||
import com.tencent.mmkv.MMKV
|
||||
import io.objectbox.Box
|
||||
import io.objectbox.kotlin.boxFor
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.WebSocket
|
||||
@ -42,26 +43,16 @@ class WebSocketService : Service() {
|
||||
private const val WEBSOCKET_CLOSE_CODE = 1000
|
||||
}
|
||||
|
||||
private val messagesBox: Box<Messages> by lazy { getBoxStore().boxFor() }
|
||||
|
||||
private val contactBox: Box<Contact> by lazy { getBoxStore().boxFor() }
|
||||
|
||||
private lateinit var username: String
|
||||
private val webSocketClient: WebSocketClient = OkHttpWebSocketClient
|
||||
|
||||
private val binder = LocalBinder()
|
||||
|
||||
private var webSocket: WebSocket? = null
|
||||
|
||||
private var heartbeatJob: Job? = null
|
||||
|
||||
private val serviceJob = Job()
|
||||
|
||||
private val serviceScope = CoroutineScope(IO + serviceJob)
|
||||
|
||||
private val _messagesMutableLiveData = SingleLiveEvent<Messages?>()
|
||||
|
||||
val messageLivedata: SingleLiveEvent<Messages?> get() = _messagesMutableLiveData
|
||||
|
||||
private val scope = CoroutineScope(IO + SupervisorJob())
|
||||
|
||||
inner class LocalBinder : Binder() {
|
||||
fun getService(): WebSocketService {
|
||||
return this@WebSocketService
|
||||
@ -74,99 +65,38 @@ class WebSocketService : Service() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
username = getUsername()
|
||||
if (!EventBus.getDefault().isRegistered(this)) {
|
||||
EventBus.getDefault().register(this)
|
||||
webSocketClient.connect()
|
||||
|
||||
collectOutgoingMessages()
|
||||
}
|
||||
|
||||
private fun collectOutgoingMessages() {
|
||||
scope.launch {
|
||||
MessageRepository.observeOutgoingMessages().collect { message ->
|
||||
// val json = Json.encodeToString(MessageModel.serializer(), message)
|
||||
val id = 0L
|
||||
webSocketClient.send(message, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
establishConnection()
|
||||
webSocketClient.connect()
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
fun onMessageEvent(contactId: String) {
|
||||
val timestamp = System.currentTimeMillis()
|
||||
MessagesManager.cleanMessages(timestamp)
|
||||
ConversationManager.hideConversation(contactId)
|
||||
}
|
||||
|
||||
fun sendMessage(jsonObject: String, msgLocalId: Long) {
|
||||
webSocket?.let {
|
||||
it.send(jsonObject)
|
||||
val kv = MMKV.mmkvWithID(MMKV_COMMON_DATA)
|
||||
kv.putLong("msgLocalId", msgLocalId)
|
||||
}
|
||||
}
|
||||
|
||||
fun storeOwnerMsg(messages: Messages) {
|
||||
if (messages.avatarUrl == "") {
|
||||
val contact =
|
||||
contactBox.query(Contact_.username.equal(messages.talkerId)).build().findFirst()
|
||||
messages.avatarUrl = contact?.avatarUrl ?: ""
|
||||
}
|
||||
ConversationManager.handleMessages(messages)
|
||||
}
|
||||
|
||||
private fun establishConnection() {
|
||||
if (webSocket == null) {
|
||||
val request = Request.Builder()
|
||||
.url("$WEBSOCKET_SERVER_URL$WEBSOCKET$username").build()
|
||||
val listener = EchoWebSocketListener()
|
||||
val client = OkhttpHelper.getInstance()
|
||||
webSocket = client.newWebSocket(request, listener)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class EchoWebSocketListener : WebSocketListener() {
|
||||
override fun onOpen(webSocket: WebSocket, response: Response) {
|
||||
Log.d(TAG, "WebSocket Opened")
|
||||
}
|
||||
|
||||
override fun onMessage(webSocket: WebSocket, text: String) {
|
||||
val messages = Gson().fromJson(text, Messages::class.java)
|
||||
|
||||
serviceScope.launch {
|
||||
if ("ack" == messages.type) {
|
||||
val existingMessage = messagesBox.get(messages.msgLocalId)
|
||||
existingMessage?.let {
|
||||
it.timestamp = messages.timestamp
|
||||
it.msgSvrId = messages.msgSvrId
|
||||
messagesBox.put(it)
|
||||
} ?: run {
|
||||
messagesBox.put(messages)
|
||||
}
|
||||
} else {
|
||||
messages.talkerId = messages.senderId
|
||||
_messagesMutableLiveData.postValue(messages)
|
||||
MessageProcessor.processorMsg(messages)
|
||||
messagesBox.put(messages)
|
||||
ConversationManager.handleMessages(messages)
|
||||
}
|
||||
scope.launch {
|
||||
if (messages.avatarUrl == "") {
|
||||
val contact = DataBase.getContactByUsername(messages.talkerId)
|
||||
messages.avatarUrl = contact?.avatarUrl ?: ""
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
|
||||
webSocket.close(WEBSOCKET_CLOSE_CODE, null)
|
||||
Log.d(TAG, "WebSocket closing: $reason")
|
||||
establishConnection()
|
||||
}
|
||||
|
||||
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
|
||||
Log.d(TAG, "WebSocket closing: ${t.cause}")
|
||||
Log.d(TAG, "WebSocket closing: $t")
|
||||
establishConnection()
|
||||
ConversationManager.handleMessages(messages)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
if (EventBus.getDefault().isRegistered(this)) {
|
||||
EventBus.getDefault().unregister(this)
|
||||
}
|
||||
serviceJob.cancel()
|
||||
heartbeatJob?.cancel()
|
||||
webSocket?.close(WEBSOCKET_CLOSE_CODE, "App exited")
|
||||
webSocketClient.close(WEBSOCKET_CLOSE_CODE, "App exited")
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,18 @@
|
||||
package com.kaixed.kchat.startup
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.startup.Initializer
|
||||
import com.tencent.mmkv.MMKV
|
||||
|
||||
class MMKVInitializer : Initializer<String> {
|
||||
override fun create(context: Context): String {
|
||||
Log.d("StartupTest", "MMKV Initialized")
|
||||
val rootDir = MMKV.initialize(context)
|
||||
return rootDir
|
||||
}
|
||||
|
||||
override fun dependencies(): List<Class<out Initializer<*>>> {
|
||||
return emptyList()
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package com.kaixed.kchat.startup
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.startup.Initializer
|
||||
import com.kaixed.kchat.utils.ScreenUtils
|
||||
|
||||
class ScreenUtilsInitializer : Initializer<ScreenUtils> {
|
||||
override fun create(context: Context): ScreenUtils {
|
||||
Log.d("StartupTest", "ScreenUtils Initialized")
|
||||
ScreenUtils.init(context.applicationContext)
|
||||
return ScreenUtils
|
||||
}
|
||||
|
||||
|
||||
override fun dependencies(): List<Class<out Initializer<*>>> =
|
||||
listOf(MMKVInitializer::class.java)
|
||||
}
|
@ -7,13 +7,6 @@ import androidx.activity.enableEdgeToEdge
|
||||
import com.kaixed.kchat.databinding.ActivityAddFriendsBinding
|
||||
import com.kaixed.kchat.ui.base.BaseActivity
|
||||
|
||||
/**
|
||||
* 该文件包含用于兼容不同 Android 版本的 Intent 获取 Parcelable 数据的扩展函数。
|
||||
* 通过扩展函数 getParcelableExtraCompat 解决了低版本和高版本 Android 系统间的差异。
|
||||
*
|
||||
* @author kaixed
|
||||
* @since 2025年1月26日
|
||||
*/
|
||||
class AddFriendsActivity : BaseActivity<ActivityAddFriendsBinding>() {
|
||||
|
||||
private val context: Context by lazy { this }
|
||||
|
@ -2,21 +2,23 @@ package com.kaixed.kchat.ui.activity
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.viewModels
|
||||
import com.kaixed.core.common.base.BaseActivity
|
||||
import com.kaixed.kchat.databinding.ActivityApplyAddFriendBinding
|
||||
import com.kaixed.kchat.ui.base.BaseActivity
|
||||
import com.kaixed.kchat.viewmodel.ContactViewModel
|
||||
|
||||
class ApplyAddFriendActivity : BaseActivity<ActivityApplyAddFriendBinding>() {
|
||||
|
||||
private val contactViewModel: ContactViewModel by viewModels()
|
||||
class ApplyAddFriendActivity : BaseActivity<ActivityApplyAddFriendBinding, ContactViewModel>() {
|
||||
|
||||
private lateinit var contactId: String
|
||||
|
||||
private lateinit var contactNickname: String
|
||||
|
||||
override fun inflateBinding(): ActivityApplyAddFriendBinding =
|
||||
ActivityApplyAddFriendBinding.inflate(layoutInflater)
|
||||
|
||||
override fun getViewModelClass(): Class<ContactViewModel> {
|
||||
return ContactViewModel::class.java
|
||||
}
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@ -28,7 +30,7 @@ class ApplyAddFriendActivity : BaseActivity<ActivityApplyAddFriendBinding>() {
|
||||
}
|
||||
|
||||
override fun observeData() {
|
||||
contactViewModel.addContactResult
|
||||
viewModel.addContactResult
|
||||
.observe(this) { result ->
|
||||
result.onSuccess {
|
||||
toast(result.getOrNull().toString())
|
||||
@ -52,7 +54,7 @@ class ApplyAddFriendActivity : BaseActivity<ActivityApplyAddFriendBinding>() {
|
||||
|
||||
private fun sendContactRequest(contactId: String?) {
|
||||
contactId?.let {
|
||||
contactViewModel.addContact(contactId, binding.etMessage.text.toString())
|
||||
viewModel.addContact(contactId, binding.etMessage.text.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,9 +4,9 @@ import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import com.bumptech.glide.Glide
|
||||
import com.kaixed.core.common.extension.getParcelableExtraCompat
|
||||
import com.kaixed.kchat.data.model.friend.FriendRequestItem
|
||||
import com.kaixed.kchat.databinding.ActivityApproveDetailBinding
|
||||
import com.kaixed.kchat.extensions.getParcelableExtraCompat
|
||||
import com.kaixed.kchat.ui.base.BaseActivity
|
||||
|
||||
class ApproveDetailActivity : BaseActivity<ActivityApproveDetailBinding>() {
|
||||
|
@ -6,7 +6,6 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
@ -21,6 +20,7 @@ import android.widget.LinearLayout
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
@ -28,22 +28,21 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.drake.softinput.getSoftInputHeight
|
||||
import com.drake.softinput.hasSoftInput
|
||||
import com.drake.softinput.setWindowSoftInput
|
||||
import com.kaixed.core.common.base.BaseActivity
|
||||
import com.kaixed.kchat.R
|
||||
import com.kaixed.kchat.data.event.UnreadEvent
|
||||
import com.kaixed.kchat.data.local.box.ObjectBox.getBoxStore
|
||||
import com.kaixed.kchat.data.local.AppDatabase
|
||||
import com.kaixed.kchat.data.local.entity.Messages
|
||||
import com.kaixed.kchat.data.model.FunctionItem
|
||||
import com.kaixed.kchat.databinding.ActivityChatBinding
|
||||
import com.kaixed.kchat.manager.ConversationManager
|
||||
import com.kaixed.kchat.manager.MessagesManager
|
||||
import com.kaixed.kchat.processor.MessageProcessor
|
||||
import com.kaixed.kchat.service.ChatViewModel
|
||||
import com.kaixed.kchat.service.WebSocketService
|
||||
import com.kaixed.kchat.service.WebSocketService.LocalBinder
|
||||
import com.kaixed.kchat.ui.adapter.ChatAdapter
|
||||
import com.kaixed.kchat.ui.adapter.EmojiAdapter
|
||||
import com.kaixed.kchat.ui.adapter.FunctionPanelAdapter
|
||||
import com.kaixed.kchat.ui.base.BaseActivity
|
||||
import com.kaixed.kchat.ui.i.IOnItemClickListener
|
||||
import com.kaixed.kchat.ui.i.OnItemClickListener
|
||||
import com.kaixed.kchat.ui.widget.LoadingDialogFragment
|
||||
@ -54,31 +53,27 @@ import com.kaixed.kchat.utils.ConstantsUtils.getKeyboardHeight
|
||||
import com.kaixed.kchat.utils.ConstantsUtils.getUsername
|
||||
import com.kaixed.kchat.utils.ImageEngines
|
||||
import com.kaixed.kchat.utils.ImageSpanUtil.insertEmoji
|
||||
import com.kaixed.kchat.utils.ScreenUtils
|
||||
import com.kaixed.kchat.viewmodel.FileViewModel
|
||||
import com.luck.picture.lib.basic.PictureSelector
|
||||
import com.luck.picture.lib.config.SelectMimeType
|
||||
import com.luck.picture.lib.entity.LocalMedia
|
||||
import com.luck.picture.lib.interfaces.OnResultCallbackListener
|
||||
import com.tencent.mmkv.MMKV
|
||||
import io.objectbox.Box
|
||||
import io.objectbox.kotlin.boxFor
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
import kotlin.math.max
|
||||
|
||||
class ChatActivity : BaseActivity<ActivityChatBinding>(), OnItemClickListener,
|
||||
class ChatActivity : BaseActivity<ActivityChatBinding, ChatViewModel>(), OnItemClickListener,
|
||||
IOnItemClickListener {
|
||||
|
||||
private var chatAdapter: ChatAdapter? = null
|
||||
|
||||
private var webSocketService: WebSocketService? = null
|
||||
|
||||
private val messagesBox: Box<Messages> by lazy { getBoxStore().boxFor() }
|
||||
private val messagesDao = AppDatabase.getDatabase().messagesDao()
|
||||
|
||||
private var contactId: String = ""
|
||||
|
||||
@ -112,24 +107,36 @@ class ChatActivity : BaseActivity<ActivityChatBinding>(), OnItemClickListener,
|
||||
return ActivityChatBinding.inflate(layoutInflater)
|
||||
}
|
||||
|
||||
override fun getViewModelClass(): Class<ChatViewModel> {
|
||||
return ChatViewModel::class.java
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
EventBus.getDefault().register(this)
|
||||
firstLoadData()
|
||||
MessagesManager.setContactId(contactId)
|
||||
firstLoadMessage()
|
||||
bindWebSocketService()
|
||||
setPanelChange()
|
||||
if (isSearchHistory) {
|
||||
val size = MessagesManager.queryHistory(msgLocalId)
|
||||
binding.recycleChatList.smoothScrollToPosition(size - 1)
|
||||
// if (isSearchHistory) {
|
||||
// val size = MessagesManager.queryHistory(msgLocalId)
|
||||
// binding.rvChatList.smoothScrollToPosition(size - 1)
|
||||
// }
|
||||
}
|
||||
|
||||
private fun firstLoadMessage() {
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
MessagesManager.firstLoadMessages()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (!isSearchHistory) {
|
||||
MessagesManager.firstLoadMessages()
|
||||
}
|
||||
// if (!isSearchHistory) {
|
||||
// firstLoadMessage()
|
||||
// }
|
||||
}
|
||||
|
||||
private val connection: ServiceConnection = object : ServiceConnection {
|
||||
@ -210,17 +217,21 @@ class ChatActivity : BaseActivity<ActivityChatBinding>(), OnItemClickListener,
|
||||
}
|
||||
|
||||
private fun lockContentViewHeight() {
|
||||
val layoutParams = binding.recycleChatList.layoutParams as LinearLayout.LayoutParams
|
||||
layoutParams.height = binding.recycleChatList.height
|
||||
layoutParams.weight = 0f
|
||||
binding.recycleChatList.layoutParams = layoutParams
|
||||
val layoutParams = binding.rvChatList.layoutParams as LinearLayout.LayoutParams
|
||||
layoutParams.apply {
|
||||
height = binding.rvChatList.height
|
||||
weight = 0f
|
||||
binding.rvChatList.layoutParams = this
|
||||
}
|
||||
}
|
||||
|
||||
private fun unlockContentViewHeight() {
|
||||
binding.recycleChatList.postDelayed({
|
||||
val layoutParams = binding.recycleChatList.layoutParams as LinearLayout.LayoutParams
|
||||
layoutParams.weight = 1f
|
||||
binding.recycleChatList.layoutParams = layoutParams
|
||||
binding.rvChatList.postDelayed({
|
||||
val layoutParams = binding.rvChatList.layoutParams as LinearLayout.LayoutParams
|
||||
layoutParams.apply {
|
||||
weight = 1F
|
||||
binding.rvChatList.layoutParams = this
|
||||
}
|
||||
}, UNBLOCK_DELAY_TIME)
|
||||
}
|
||||
|
||||
@ -328,31 +339,31 @@ class ChatActivity : BaseActivity<ActivityChatBinding>(), OnItemClickListener,
|
||||
private fun setRecycleView() {
|
||||
val layoutManager = LinearLayoutManager(this)
|
||||
layoutManager.reverseLayout = true
|
||||
binding.recycleChatList.layoutManager = layoutManager
|
||||
binding.rvChatList.layoutManager = layoutManager
|
||||
chatAdapter = ChatAdapter(this)
|
||||
binding.recycleChatList.adapter = chatAdapter
|
||||
}
|
||||
|
||||
private fun firstLoadData() {
|
||||
MessagesManager.setContactId(contactId)
|
||||
MessagesManager.firstLoadMessages()
|
||||
binding.rvChatList.adapter = chatAdapter
|
||||
binding.rvChatList.itemAnimator = null
|
||||
}
|
||||
|
||||
private fun loadMoreMessages() {
|
||||
MessagesManager.loadMoreMessages()
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
MessagesManager.loadMoreMessages()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun observeData() {
|
||||
webSocketService!!.messageLivedata.observe(this) {
|
||||
it?.let {
|
||||
handleMsg(it)
|
||||
}
|
||||
}
|
||||
// webSocketService!!.messageLivedata.observe(this) {
|
||||
// it?.let {
|
||||
// handleMsg(it)
|
||||
// }
|
||||
// }
|
||||
lifecycleScope.launch {
|
||||
MessagesManager.messages.collect {
|
||||
chatAdapter?.submitList(it)
|
||||
binding.recycleChatList.post {
|
||||
binding.recycleChatList.smoothScrollToPosition(0)
|
||||
binding.rvChatList.post {
|
||||
binding.rvChatList.smoothScrollToPosition(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -360,31 +371,26 @@ class ChatActivity : BaseActivity<ActivityChatBinding>(), OnItemClickListener,
|
||||
|
||||
override fun setupListeners() {
|
||||
binding.etInput.setOnClickListener {
|
||||
if (hasSoftInput()){
|
||||
if (hasSoftInput()) {
|
||||
MMKV.defaultMMKV().encode(KEYBOARD_HEIGHT, max(getSoftInputHeight(), 300))
|
||||
}
|
||||
}
|
||||
|
||||
setBackPressListener()
|
||||
|
||||
binding.gvFunctionPanel.selector = ColorDrawable(Color.TRANSPARENT)
|
||||
binding.gvFunctionPanel.selector = Color.TRANSPARENT.toDrawable()
|
||||
|
||||
binding.tvSend.setOnClickListener {
|
||||
val message = binding.etInput.text.toString().trim()
|
||||
sendMessage(contactId, message, "0")
|
||||
}
|
||||
|
||||
binding.recycleChatList.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
binding.rvChatList.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
if (recyclerView.canScrollVertically(-1)) {
|
||||
if (!recyclerView.canScrollVertically(-1)) {
|
||||
loadMoreMessages()
|
||||
}
|
||||
// val layoutManager = checkNotNull(recyclerView.layoutManager as LinearLayoutManager?)
|
||||
// val firstVisiblePosition = layoutManager.findLastVisibleItemPosition()
|
||||
// if (tempIndex + 1 == messagesList[firstVisiblePosition].msgLocalId && hasHistory && !loading) {
|
||||
// loadMoreMessages()
|
||||
// }
|
||||
}
|
||||
})
|
||||
|
||||
@ -416,11 +422,6 @@ class ChatActivity : BaseActivity<ActivityChatBinding>(), OnItemClickListener,
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleMsg(messages: Messages) {
|
||||
MessagesManager.receiveMessage(messages)
|
||||
binding.recycleChatList.smoothScrollToPosition(0)
|
||||
}
|
||||
|
||||
private fun sendMessage(talkerId: String, message: String, type: String) {
|
||||
val messages = Messages(
|
||||
content = message,
|
||||
@ -429,23 +430,33 @@ class ChatActivity : BaseActivity<ActivityChatBinding>(), OnItemClickListener,
|
||||
talkerId = talkerId,
|
||||
type = type,
|
||||
)
|
||||
val msg = MessageProcessor.processorMsg(messages)
|
||||
val id: Long = messagesBox.put(msg)
|
||||
lifecycleScope.launch {
|
||||
|
||||
val jsonObject = JSONObject()
|
||||
val jsonObject2 = JSONObject()
|
||||
jsonObject2.put("receiverId", contactId)
|
||||
jsonObject2.put("content", message)
|
||||
jsonObject2.put("msgLocalId", id)
|
||||
jsonObject.put("type", if (type == "4") "image" else "text")
|
||||
jsonObject.put("body", jsonObject2)
|
||||
withContext(Dispatchers.IO) {
|
||||
val msg = MessageProcessor.processorMsg(messages)
|
||||
val id: Long = messagesDao.insert(msg)
|
||||
|
||||
webSocketService!!.sendMessage(jsonObject.toString(), id)
|
||||
webSocketService!!.storeOwnerMsg(messages)
|
||||
val jsonObject = JSONObject()
|
||||
val jsonObject2 = JSONObject()
|
||||
jsonObject2.put("receiverId", contactId)
|
||||
jsonObject2.put("content", message)
|
||||
jsonObject2.put("msgLocalId", id)
|
||||
jsonObject.put("type", if (type == "4") "image" else "text")
|
||||
jsonObject.put("body", jsonObject2)
|
||||
|
||||
MessagesManager.sendMessages(messages)
|
||||
viewModel.sendChatMessage(jsonObject.toString(), id)
|
||||
// webSocketService!!.storeOwnerMsg(messages)
|
||||
ConversationManager.handleMessages(messages)
|
||||
|
||||
MessagesManager.sendMessages(messages)
|
||||
}
|
||||
|
||||
binding.etInput.setText("")
|
||||
binding.rvChatList.post {
|
||||
binding.rvChatList.smoothScrollToPosition(0)
|
||||
}
|
||||
}
|
||||
|
||||
binding.etInput.setText("")
|
||||
}
|
||||
|
||||
private fun bindWebSocketService() {
|
||||
@ -456,15 +467,8 @@ class ChatActivity : BaseActivity<ActivityChatBinding>(), OnItemClickListener,
|
||||
)
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
fun onMessageEvent(unreadEvent: UnreadEvent) {
|
||||
val unreadCount = unreadEvent.unreadCount
|
||||
binding.ctb.setUnReadCount(unreadCount)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
EventBus.getDefault().unregister(this)
|
||||
MessagesManager.resetMessages()
|
||||
mmkv.putString(CURRENT_CONTACT_ID, "")
|
||||
if (bound) {
|
||||
@ -497,7 +501,6 @@ class ChatActivity : BaseActivity<ActivityChatBinding>(), OnItemClickListener,
|
||||
fileViewModel.uploadFile(file)
|
||||
}
|
||||
|
||||
|
||||
//TODO: 待优化成微信选择上传模式,当前上传模式耗费用户等待时间
|
||||
private fun selectPicture() {
|
||||
PictureSelector.create(this).openGallery(SelectMimeType.ofImage())
|
||||
|
@ -3,20 +3,24 @@ package com.kaixed.kchat.ui.activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.bumptech.glide.Glide
|
||||
import com.kaixed.kchat.data.LocalDatabase.getContactByUsername
|
||||
import com.kaixed.kchat.data.DataBase
|
||||
import com.kaixed.kchat.data.local.entity.Contact
|
||||
import com.kaixed.kchat.databinding.ActivityChatDetailBinding
|
||||
import com.kaixed.kchat.manager.ConversationManager
|
||||
import com.kaixed.kchat.manager.MessagesManager
|
||||
import com.kaixed.kchat.ui.base.BaseActivity
|
||||
import com.kaixed.kchat.ui.widget.SwitchButton
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
|
||||
class ChatDetailActivity : BaseActivity<ActivityChatDetailBinding>() {
|
||||
|
||||
private lateinit var contactId: String
|
||||
|
||||
private val contact by lazy { getContactByUsername(contactId) }
|
||||
private lateinit var contact: Contact
|
||||
|
||||
override fun inflateBinding(): ActivityChatDetailBinding {
|
||||
return ActivityChatDetailBinding.inflate(layoutInflater)
|
||||
@ -24,7 +28,7 @@ class ChatDetailActivity : BaseActivity<ActivityChatDetailBinding>() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
|
||||
}
|
||||
|
||||
override fun initView() {
|
||||
@ -34,7 +38,10 @@ class ChatDetailActivity : BaseActivity<ActivityChatDetailBinding>() {
|
||||
|
||||
override fun initData() {
|
||||
contactId = intent.getStringExtra("contactId") ?: ""
|
||||
binding.ciSetDisturb.setSwitchChecked(contact?.doNotDisturb ?: false)
|
||||
lifecycleScope.launch {
|
||||
contact = DataBase.getContactByUsername(contactId)!!
|
||||
binding.ciSetDisturb.setSwitchChecked(contact?.doNotDisturb ?: false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun observeData() {
|
||||
@ -45,7 +52,9 @@ class ChatDetailActivity : BaseActivity<ActivityChatDetailBinding>() {
|
||||
binding.ciSetDisturb.sbSwitch.setOnCheckedChangeListener(object :
|
||||
SwitchButton.OnCheckedChangeListener {
|
||||
override fun onCheckedChanged(button: SwitchButton, isChecked: Boolean) {
|
||||
ConversationManager.updateDisturbStatus(contactId, isChecked)
|
||||
lifecycleScope.launch {
|
||||
ConversationManager.updateDisturbStatus(contactId, isChecked)
|
||||
}
|
||||
}
|
||||
})
|
||||
binding.ciCleanChatHistory.setOnClickListener {
|
||||
|
@ -5,15 +5,18 @@ import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.bumptech.glide.Glide
|
||||
import com.kaixed.kchat.data.DataBase
|
||||
import com.kaixed.kchat.data.local.entity.Contact
|
||||
import com.kaixed.kchat.data.local.entity.Contact_
|
||||
import com.kaixed.kchat.databinding.ActivityContactsDetailBinding
|
||||
import com.kaixed.kchat.ui.base.BaseActivity
|
||||
import com.kaixed.kchat.utils.ConstantsUtils.getAvatarUrl
|
||||
import com.kaixed.kchat.utils.ConstantsUtils.getNickName
|
||||
import com.kaixed.kchat.utils.ConstantsUtils.getUsername
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class ContactsDetailActivity : BaseActivity<ActivityContactsDetailBinding>() {
|
||||
|
||||
@ -95,19 +98,21 @@ class ContactsDetailActivity : BaseActivity<ActivityContactsDetailBinding>() {
|
||||
binding.ciSetRemarkAndLabel.visibility = View.GONE
|
||||
return
|
||||
}
|
||||
val contact: Contact by lazy {
|
||||
getContactInfo(contactId!!)
|
||||
}
|
||||
binding.tvContactId.text = "星联号: $contactId"
|
||||
binding.tvContactSignature.text = contact.signature
|
||||
if (contact.remark?.isNotEmpty() == true) {
|
||||
binding.tvRemark.text = contact.remark
|
||||
contactNickname = contact.remark
|
||||
binding.tvContactName.text = "昵称:${contact.nickname}"
|
||||
} else {
|
||||
contactNickname = contact.nickname
|
||||
binding.tvRemark.text = contact.nickname
|
||||
binding.tvContactName.visibility = View.GONE
|
||||
lifecycleScope.launch {
|
||||
val contact = getContactInfo(contactId!!)
|
||||
|
||||
// 使用获取到的联系人信息更新UI(这部分已在主线程执行)
|
||||
binding.tvContactId.text = "星联号: $contactId"
|
||||
binding.tvContactSignature.text = contact?.signature
|
||||
if (contact?.remark?.isNotEmpty() == true) {
|
||||
binding.tvRemark.text = contact.remark
|
||||
contactNickname = contact.remark
|
||||
binding.tvContactName.text = "昵称:${contact.nickname}"
|
||||
} else {
|
||||
contactNickname = contact?.nickname
|
||||
binding.tvRemark.text = contact?.nickname
|
||||
binding.tvContactName.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -116,10 +121,9 @@ class ContactsDetailActivity : BaseActivity<ActivityContactsDetailBinding>() {
|
||||
updateContent()
|
||||
}
|
||||
|
||||
private fun getContactInfo(contactId: String): Contact {
|
||||
return DataBase.contactBox
|
||||
.query(Contact_.username.equal(contactId))
|
||||
.build()
|
||||
.findFirst()!!
|
||||
private suspend fun getContactInfo(contactId: String): Contact? {
|
||||
return withContext(Dispatchers.IO) {
|
||||
DataBase.getContactByUsername(contactId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import androidx.core.view.updatePadding
|
||||
import androidx.core.widget.NestedScrollView
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.bumptech.glide.Glide
|
||||
import com.kaixed.core.common.utils.TextUtil
|
||||
import com.kaixed.kchat.R
|
||||
import com.kaixed.kchat.data.model.dynamic.Comment
|
||||
import com.kaixed.kchat.data.model.dynamic.FriendCircleItem
|
||||
@ -22,7 +23,6 @@ import com.kaixed.kchat.utils.ConstantsUtils
|
||||
import com.kaixed.kchat.utils.ConstantsUtils.getAvatarUrl
|
||||
import com.kaixed.kchat.utils.ConstantsUtils.getNickName
|
||||
import com.kaixed.kchat.utils.ScreenUtils.dp2px
|
||||
import com.kaixed.kchat.utils.TextUtil
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.random.Random
|
||||
|
@ -12,7 +12,7 @@ import androidx.core.content.ContextCompat
|
||||
import com.drake.softinput.getSoftInputHeight
|
||||
import com.drake.softinput.hasSoftInput
|
||||
import com.kaixed.kchat.R
|
||||
import com.kaixed.kchat.data.LocalDatabase
|
||||
import com.kaixed.kchat.data.DataBase
|
||||
import com.kaixed.kchat.data.local.entity.UserInfo
|
||||
import com.kaixed.kchat.databinding.ActivityLoginBinding
|
||||
import com.kaixed.kchat.ui.base.BaseActivity
|
||||
@ -153,8 +153,9 @@ class LoginActivity : BaseActivity<ActivityLoginBinding>() {
|
||||
return true
|
||||
}
|
||||
|
||||
//TODO 存储用户信息
|
||||
private fun saveUserInfo(user: UserInfo) {
|
||||
LocalDatabase.saveUserInfo(user)
|
||||
DataBase.saveUserInfo(user)
|
||||
MMKV.defaultMMKV().putString(ACCESS_TOKEN, user.accessToken)
|
||||
MMKV.defaultMMKV().putString(REFRESH_TOKEN, user.refreshToken)
|
||||
}
|
||||
|
@ -15,14 +15,13 @@ import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.bumptech.glide.Glide
|
||||
import com.huawei.hms.hmsscankit.ScanUtil
|
||||
import com.huawei.hms.ml.scan.HmsScan
|
||||
import com.kaixed.kchat.R
|
||||
import com.kaixed.kchat.data.LocalDatabase
|
||||
import com.kaixed.kchat.data.DataBase
|
||||
import com.kaixed.kchat.data.event.UnreadEvent
|
||||
import com.kaixed.kchat.data.local.box.ObjectBox.getBox
|
||||
import com.kaixed.kchat.data.local.entity.Contact
|
||||
import com.kaixed.kchat.data.local.AppDatabase
|
||||
import com.kaixed.kchat.data.local.dao.MessagesDao
|
||||
import com.kaixed.kchat.databinding.ActivityMainBinding
|
||||
import com.kaixed.kchat.ui.base.BaseActivity
|
||||
import com.kaixed.kchat.ui.fragment.ContactFragment
|
||||
@ -30,14 +29,13 @@ import com.kaixed.kchat.ui.fragment.DiscoveryFragment
|
||||
import com.kaixed.kchat.ui.fragment.HomeFragment
|
||||
import com.kaixed.kchat.ui.fragment.MineFragment
|
||||
import com.kaixed.kchat.ui.widget.LoadingDialogFragment
|
||||
import com.kaixed.kchat.utils.ConstantsUtils.getAvatarUrl
|
||||
import com.kaixed.kchat.utils.ConstantsUtils.getUsername
|
||||
import com.kaixed.kchat.utils.ConstantsUtils.isFirstLaunchApp
|
||||
import com.kaixed.kchat.viewmodel.ContactViewModel
|
||||
import io.objectbox.Box
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
@ -49,14 +47,14 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
||||
|
||||
private val colorBlack: Int by lazy { ContextCompat.getColor(this, R.color.black) }
|
||||
|
||||
private val contactBox: Box<Contact> by lazy { getBox(Contact::class.java) }
|
||||
|
||||
private val contactViewModel: ContactViewModel by viewModels()
|
||||
|
||||
private val navBinding by lazy { binding.bottomNavContainer }
|
||||
|
||||
private val loadingDialogFragment by lazy { LoadingDialogFragment.newInstance("同步数据中") }
|
||||
|
||||
private val messagesDao: MessagesDao = AppDatabase.getDatabase().messagesDao()
|
||||
|
||||
private val navItems by lazy {
|
||||
listOf(
|
||||
NavItem(navBinding.ivHome, navBinding.tvHome),
|
||||
@ -106,7 +104,9 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
||||
ScanUtil.SUCCESS -> {
|
||||
val hmsScan: HmsScan? = data.getParcelableExtra(ScanUtil.RESULT)
|
||||
hmsScan?.let {
|
||||
handleResult(it.showResult)
|
||||
lifecycleScope.launch {
|
||||
handleResult(it.showResult)
|
||||
}
|
||||
}
|
||||
}
|
||||
ScanUtil.ERROR_NO_READ_PERMISSION -> {
|
||||
@ -122,7 +122,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
||||
}
|
||||
|
||||
// 处理扫描结果
|
||||
private fun handleResult(result: String) {
|
||||
private suspend fun handleResult(result: String) {
|
||||
if (result.startsWith("kchat@")) {
|
||||
val contactId = result.substringAfter("kchat@")
|
||||
if (contactId == getUsername()) {
|
||||
@ -131,7 +131,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
||||
putExtra("isMine", true)
|
||||
})
|
||||
}
|
||||
if (LocalDatabase.isMyFriend(contactId)) {
|
||||
if (DataBase.isMyFriend(contactId)) {
|
||||
startActivity(Intent(this, ContactsDetailActivity::class.java).apply {
|
||||
putExtra("contactId", contactId)
|
||||
})
|
||||
@ -152,8 +152,14 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
||||
private fun setListener() {
|
||||
contactViewModel.contactListResult.observe(this@MainActivity) { result ->
|
||||
result.onSuccess {
|
||||
contactBox.put(it ?: emptyList())
|
||||
loadingDialogFragment.dismissLoading()
|
||||
//TODO 优化,使用协程
|
||||
val contactDao = AppDatabase.getDatabase().contactDao()
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
contactDao.insertContacts(it ?: emptyList())
|
||||
loadingDialogFragment.dismissLoading()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
contactViewModel.searchContactResult.observe(this@MainActivity) { result ->
|
||||
|
@ -14,19 +14,16 @@ import androidx.activity.viewModels
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.bumptech.glide.Glide
|
||||
import com.kaixed.kchat.data.local.box.ObjectBox.getBoxStore
|
||||
import com.kaixed.core.common.utils.TextUtil.extractDimensionsAndPrefix
|
||||
import com.kaixed.kchat.data.local.entity.UserInfo
|
||||
import com.kaixed.kchat.data.local.entity.UserInfo_
|
||||
import com.kaixed.kchat.databinding.ActivityProfileDetailBinding
|
||||
import com.kaixed.kchat.ui.base.BaseActivity
|
||||
import com.kaixed.kchat.ui.widget.LoadingDialogFragment
|
||||
import com.kaixed.kchat.utils.Constants.AVATAR_URL
|
||||
import com.kaixed.kchat.utils.Constants.MMKV_USER_SESSION
|
||||
import com.kaixed.kchat.utils.ConstantsUtils.getUsername
|
||||
import com.kaixed.kchat.utils.TextUtil.extractDimensionsAndPrefix
|
||||
import com.kaixed.kchat.viewmodel.UserViewModel
|
||||
import com.tencent.mmkv.MMKV
|
||||
import io.objectbox.Box
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.RequestBody.Companion.asRequestBody
|
||||
@ -35,8 +32,6 @@ import java.io.File
|
||||
|
||||
class ProfileDetailActivity : BaseActivity<ActivityProfileDetailBinding>() {
|
||||
|
||||
private val userInfoBox: Box<UserInfo> by lazy { getBoxStore().boxFor(UserInfo::class.java) }
|
||||
|
||||
private val userSessionMMKV by lazy { MMKV.mmkvWithID(MMKV_USER_SESSION) }
|
||||
|
||||
private val userViewModel: UserViewModel by viewModels()
|
||||
@ -184,14 +179,15 @@ class ProfileDetailActivity : BaseActivity<ActivityProfileDetailBinding>() {
|
||||
updateContent(username)
|
||||
}
|
||||
|
||||
//TODO 更新用户信息
|
||||
private fun updateContent(username: String) {
|
||||
val userInfo = userInfoBox.query(UserInfo_.username.equal(username)).build().findFirst()
|
||||
if (userInfo != null) {
|
||||
binding.ciKid.setItemDesc(userInfo.username)
|
||||
binding.ciNickname.setItemDesc(userInfo.nickname)
|
||||
val avatarUrl =
|
||||
extractDimensionsAndPrefix(userInfo.avatarUrl)?.first ?: userInfo.avatarUrl
|
||||
binding.ciAvatar.setItemIcon(avatarUrl)
|
||||
}
|
||||
// val userInfo = userInfoBox.query(UserInfo_.username.equal(username)).build().findFirst()
|
||||
// if (userInfo != null) {
|
||||
// binding.ciKid.setItemDesc(userInfo.username)
|
||||
// binding.ciNickname.setItemDesc(userInfo.nickname)
|
||||
// val avatarUrl =
|
||||
// extractDimensionsAndPrefix(userInfo.avatarUrl)?.first ?: userInfo.avatarUrl
|
||||
// binding.ciAvatar.setItemIcon(avatarUrl)
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
@ -6,8 +6,6 @@ import android.os.Looper
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import com.kaixed.kchat.data.local.box.ObjectBox.getBoxStore
|
||||
import com.kaixed.kchat.data.local.entity.UserInfo
|
||||
import com.kaixed.kchat.data.model.request.UserRequest
|
||||
import com.kaixed.kchat.databinding.ActivityRenameBinding
|
||||
import com.kaixed.kchat.ui.base.BaseActivity
|
||||
@ -15,7 +13,6 @@ import com.kaixed.kchat.ui.widget.LoadingDialogFragment
|
||||
import com.kaixed.kchat.utils.ConstantsUtils.getNickName
|
||||
import com.kaixed.kchat.utils.ConstantsUtils.getUsername
|
||||
import com.kaixed.kchat.viewmodel.UserViewModel
|
||||
import io.objectbox.Box
|
||||
|
||||
class RenameActivity : BaseActivity<ActivityRenameBinding>() {
|
||||
|
||||
|
@ -9,16 +9,16 @@ import androidx.activity.enableEdgeToEdge
|
||||
import androidx.core.text.buildSpannedString
|
||||
import androidx.core.text.inSpans
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.kaixed.kchat.data.local.box.ObjectBox.getBox
|
||||
import com.kaixed.kchat.data.local.AppDatabase
|
||||
import com.kaixed.kchat.data.local.entity.Messages
|
||||
import com.kaixed.kchat.data.local.entity.Messages_
|
||||
import com.kaixed.kchat.data.model.search.ChatHistoryItem
|
||||
import com.kaixed.kchat.databinding.ActivitySearchChatHistoryBinding
|
||||
import com.kaixed.kchat.ui.adapter.ChatHistoryAdapter
|
||||
import com.kaixed.kchat.ui.base.BaseActivity
|
||||
import com.kaixed.kchat.utils.ConstantsUtils.getUsername
|
||||
import io.objectbox.Box
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class SearchChatHistory : BaseActivity<ActivitySearchChatHistoryBinding>() {
|
||||
|
||||
@ -28,8 +28,6 @@ class SearchChatHistory : BaseActivity<ActivitySearchChatHistoryBinding>() {
|
||||
private const val VIEW_SELECT = 2
|
||||
}
|
||||
|
||||
private val messageBox: Box<Messages> by lazy { getBox(Messages::class.java) }
|
||||
|
||||
private val chatHistoryAdapter by lazy {
|
||||
ChatHistoryAdapter(
|
||||
contactAvatarUrl,
|
||||
@ -62,10 +60,14 @@ class SearchChatHistory : BaseActivity<ActivitySearchChatHistoryBinding>() {
|
||||
chatHistoryAdapter.setContactId(contactId)
|
||||
|
||||
chatHistoryAdapter.let { adapter ->
|
||||
val viewHolder = adapter.createViewHolder(binding.rvChatHistory, adapter.getItemViewType(0))
|
||||
val viewHolder =
|
||||
adapter.createViewHolder(binding.rvChatHistory, adapter.getItemViewType(0))
|
||||
val itemView = viewHolder.itemView
|
||||
itemView.measure(
|
||||
View.MeasureSpec.makeMeasureSpec(binding.rvChatHistory.width, View.MeasureSpec.EXACTLY),
|
||||
View.MeasureSpec.makeMeasureSpec(
|
||||
binding.rvChatHistory.width,
|
||||
View.MeasureSpec.EXACTLY
|
||||
),
|
||||
View.MeasureSpec.UNSPECIFIED
|
||||
)
|
||||
val itemHeight = itemView.measuredHeight
|
||||
@ -76,15 +78,18 @@ class SearchChatHistory : BaseActivity<ActivitySearchChatHistoryBinding>() {
|
||||
|
||||
private fun setListener() {
|
||||
binding.tvCancel.setOnClickListener { finish() }
|
||||
binding.etSearch.addTextChangedListener(afterTextChanged =
|
||||
{
|
||||
if (it?.length == 0) {
|
||||
chatHistoryAdapter.submitList(null)
|
||||
showView(VIEW_SELECT)
|
||||
} else {
|
||||
getData(it.toString())
|
||||
}
|
||||
})
|
||||
binding.etSearch.addTextChangedListener(
|
||||
afterTextChanged =
|
||||
{
|
||||
if (it?.length == 0) {
|
||||
chatHistoryAdapter.submitList(null)
|
||||
showView(VIEW_SELECT)
|
||||
} else {
|
||||
lifecycleScope.launch {
|
||||
getData(it.toString())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun showView(viewToShowIndex: Int) {
|
||||
@ -94,7 +99,7 @@ class SearchChatHistory : BaseActivity<ActivitySearchChatHistoryBinding>() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun getData(matchedField: String) {
|
||||
private suspend fun getData(matchedField: String) {
|
||||
val data = loadDataFromDb(matchedField)
|
||||
|
||||
val items = data.map {
|
||||
@ -130,9 +135,10 @@ class SearchChatHistory : BaseActivity<ActivitySearchChatHistoryBinding>() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadDataFromDb(content: String): List<Messages> {
|
||||
return messageBox.query(Messages_.content.contains(content)).orderDesc(Messages_.timestamp)
|
||||
.build().find()
|
||||
private suspend fun loadDataFromDb(content: String): List<Messages> {
|
||||
val messagesDao = AppDatabase.getDatabase().messagesDao()
|
||||
|
||||
return messagesDao.getMessagesContainingKeyword(content)
|
||||
}
|
||||
|
||||
override fun initData() {
|
||||
|
@ -12,7 +12,7 @@ import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import com.bumptech.glide.Glide
|
||||
import com.kaixed.kchat.data.LocalDatabase
|
||||
import com.kaixed.kchat.data.DataBase
|
||||
import com.kaixed.kchat.data.model.search.SearchUser
|
||||
import com.kaixed.kchat.databinding.ActivitySearchFriendsBinding
|
||||
import com.kaixed.kchat.ui.base.BaseActivity
|
||||
@ -106,7 +106,7 @@ class SearchFriendsActivity : BaseActivity<ActivitySearchFriendsBinding>() {
|
||||
result.onSuccess {
|
||||
it?.let {
|
||||
isSearchedMine = it.username == getUsername()
|
||||
if (LocalDatabase.isMyFriend(it.username)) {
|
||||
if (DataBase.isMyFriend(it.username)) {
|
||||
val username = it.username
|
||||
val intent =
|
||||
Intent(this, ContactsDetailActivity::class.java).apply {
|
||||
|
@ -12,8 +12,7 @@ import androidx.core.widget.addTextChangedListener
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import com.kaixed.kchat.R
|
||||
import com.kaixed.kchat.data.local.box.ObjectBox.getBoxStore
|
||||
import com.kaixed.kchat.data.LocalDatabase
|
||||
import com.kaixed.kchat.data.DataBase
|
||||
import com.kaixed.kchat.data.local.entity.Contact
|
||||
import com.kaixed.kchat.databinding.ActivitySetRemarkAndLabelBinding
|
||||
import com.kaixed.kchat.ui.base.BaseActivity
|
||||
@ -22,6 +21,7 @@ import com.kaixed.kchat.utils.ConstantsUtils.getUsername
|
||||
import com.kaixed.kchat.viewmodel.ContactViewModel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class SetRemarkAndLabelActivity : BaseActivity<ActivitySetRemarkAndLabelBinding>() {
|
||||
|
||||
@ -32,7 +32,9 @@ class SetRemarkAndLabelActivity : BaseActivity<ActivitySetRemarkAndLabelBinding>
|
||||
private var remark = ""
|
||||
|
||||
private val contact: Contact? by lazy {
|
||||
LocalDatabase.getContactByUsername(contactId!!)
|
||||
runBlocking {
|
||||
DataBase.getContactByUsername(contactId!!)
|
||||
}
|
||||
}
|
||||
|
||||
private val contactViewModel by lazy { ContactViewModel() }
|
||||
|
@ -19,7 +19,7 @@ class AccountSecurityActivity : BaseActivity<ActivityAccountSecurityBinding>() {
|
||||
}
|
||||
|
||||
override fun initData() {
|
||||
val userInfo = DataBase.userInfoBox.query().build().findFirst()
|
||||
val userInfo = DataBase.getUserInfo()
|
||||
userInfo?.let {
|
||||
binding.ciAccount.setItemDesc(userInfo.username)
|
||||
val telephone = userInfo.telephone.take(3) + "******" + userInfo.telephone.takeLast(2)
|
||||
|
@ -24,14 +24,11 @@ class UpdateTelephoneActivity : BaseActivity<ActivityUpdateTelephoneBinding>() {
|
||||
}
|
||||
|
||||
override fun initView() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun setupListeners() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun observeData() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
@ -1,19 +1,21 @@
|
||||
package com.kaixed.kchat.ui.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.drake.spannable.replaceSpan
|
||||
import com.drake.spannable.span.CenterImageSpan
|
||||
import com.kaixed.core.common.utils.TextUtil.extractDimensionsAndPrefix
|
||||
import com.kaixed.kchat.R
|
||||
import com.kaixed.kchat.data.local.box.ObjectBox.getBox
|
||||
import com.kaixed.kchat.data.local.entity.Messages
|
||||
import com.kaixed.kchat.databinding.ChatRecycleItemCustomNormalBinding
|
||||
import com.kaixed.kchat.databinding.ChatRecycleItemImageNormalBinding
|
||||
@ -22,24 +24,13 @@ import com.kaixed.kchat.manager.MessagesManager
|
||||
import com.kaixed.kchat.utils.ConstantsUtils
|
||||
import com.kaixed.kchat.utils.ConstantsUtils.getUsername
|
||||
import com.kaixed.kchat.utils.PopWindowUtil.showPopupWindow
|
||||
import com.kaixed.kchat.utils.TextUtil.extractDimensionsAndPrefix
|
||||
import com.kaixed.kchat.utils.ViewUtil.changeTimerVisibility
|
||||
import com.kaixed.kchat.utils.ViewUtil.setViewVisibility
|
||||
import io.objectbox.Box
|
||||
|
||||
|
||||
class ChatAdapter(
|
||||
private val context: Context
|
||||
) : ListAdapter<Messages, RecyclerView.ViewHolder>(object : DiffUtil.ItemCallback<Messages>() {
|
||||
override fun areItemsTheSame(oldItem: Messages, newItem: Messages): Boolean {
|
||||
return oldItem.msgLocalId == newItem.msgLocalId
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: Messages, newItem: Messages): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
|
||||
}) {
|
||||
) : ListAdapter<Messages, RecyclerView.ViewHolder>(DIFF_CALLBACK) {
|
||||
|
||||
companion object {
|
||||
const val CUSTOM = 0
|
||||
@ -50,6 +41,25 @@ class ChatAdapter(
|
||||
const val EMOJI = 10
|
||||
const val RED_PACKET = 12
|
||||
private const val TAG = "ChatAdapter"
|
||||
|
||||
// 移动到外部作为静态对象
|
||||
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<Messages>() {
|
||||
override fun areItemsTheSame(oldItem: Messages, newItem: Messages): Boolean {
|
||||
val isSame = oldItem.msgLocalId == newItem.msgLocalId
|
||||
if (!isSame) {
|
||||
Log.d(TAG, "Item changed: old=${oldItem.msgLocalId}, new=${newItem.msgLocalId}")
|
||||
}
|
||||
return isSame
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: Messages, newItem: Messages): Boolean {
|
||||
val changed = oldItem != newItem
|
||||
if (changed) {
|
||||
Log.d(TAG, "Item content changed: id=${oldItem.msgLocalId}")
|
||||
}
|
||||
return !changed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
@ -81,7 +91,7 @@ class ChatAdapter(
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
val singleMessage = getItem(position)
|
||||
val singleMessage = getItem(position) ?: return
|
||||
when (holder) {
|
||||
is CustomViewHolder -> {
|
||||
holder.bindData(singleMessage)
|
||||
@ -209,11 +219,6 @@ class ChatAdapter(
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return getItem(position).type.toInt()
|
||||
}
|
||||
|
||||
private fun updateDb(message: Messages) {
|
||||
val messagesBox: Box<Messages> = getBox(Messages::class.java)
|
||||
messagesBox.put(message)
|
||||
return getItem(position)?.type?.toInt() ?: 0
|
||||
}
|
||||
}
|
||||
|
@ -10,11 +10,11 @@ import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import com.bumptech.glide.Glide
|
||||
import com.drake.spannable.replaceSpanFirst
|
||||
import com.drake.spannable.span.HighlightSpan
|
||||
import com.kaixed.core.common.utils.TextUtil
|
||||
import com.kaixed.kchat.data.model.search.ChatHistoryItem
|
||||
import com.kaixed.kchat.databinding.ChatHistoryRecycleItemBinding
|
||||
import com.kaixed.kchat.ui.activity.ChatActivity
|
||||
import com.kaixed.kchat.utils.ConstantsUtils
|
||||
import com.kaixed.kchat.utils.TextUtil
|
||||
|
||||
/**
|
||||
* @Author: kaixed
|
||||
|
@ -14,12 +14,12 @@ import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.drake.spannable.replaceSpan
|
||||
import com.drake.spannable.span.CenterImageSpan
|
||||
import com.kaixed.core.common.utils.TextUtil
|
||||
import com.kaixed.kchat.R
|
||||
import com.kaixed.kchat.data.local.entity.Conversation
|
||||
import com.kaixed.kchat.databinding.ChatMainItemBinding
|
||||
import com.kaixed.kchat.ui.activity.ChatActivity
|
||||
import com.kaixed.kchat.ui.i.OnItemListener
|
||||
import com.kaixed.kchat.utils.TextUtil
|
||||
|
||||
/**
|
||||
* @Author: kaixed
|
||||
|
@ -5,11 +5,11 @@ import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.kaixed.core.common.utils.TextUtil
|
||||
import com.kaixed.kchat.data.model.dynamic.FriendCircleItem
|
||||
import com.kaixed.kchat.databinding.ItemFriendCircleBinding
|
||||
import com.kaixed.kchat.utils.ScreenUtils.dp2px
|
||||
import com.kaixed.kchat.utils.ScreenUtils.getScreenWidth
|
||||
import com.kaixed.kchat.utils.TextUtil
|
||||
|
||||
/**
|
||||
* @Author: kaixed
|
||||
|
@ -2,6 +2,7 @@ package com.kaixed.kchat.ui.base
|
||||
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.kaixed.kchat.manager.AppManager
|
||||
@ -16,6 +17,7 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
binding = inflateBinding()
|
||||
setContentView(binding.root)
|
||||
initData()
|
||||
|
@ -11,17 +11,15 @@ import android.view.ViewGroup
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.kaixed.core.common.base.BaseFragment
|
||||
import com.kaixed.kchat.data.local.entity.Contact
|
||||
import com.kaixed.kchat.databinding.FragmentContactBinding
|
||||
import com.kaixed.kchat.ui.adapter.FriendListAdapter
|
||||
import com.kaixed.kchat.ui.base.BaseFragment
|
||||
import com.kaixed.kchat.utils.ConstantsUtils.getUsername
|
||||
import com.kaixed.kchat.viewmodel.ContactViewModel
|
||||
|
||||
|
||||
class ContactFragment : BaseFragment<FragmentContactBinding>() {
|
||||
|
||||
private val contactViewModel: ContactViewModel by viewModels()
|
||||
class ContactFragment : BaseFragment<FragmentContactBinding, ContactViewModel>() {
|
||||
|
||||
private var loading = false
|
||||
|
||||
@ -64,8 +62,26 @@ class ContactFragment : BaseFragment<FragmentContactBinding>() {
|
||||
LocalBroadcastManager.getInstance(requireActivity()).registerReceiver(mReceiver, filter)
|
||||
}
|
||||
|
||||
override fun getViewModelClass(): Class<ContactViewModel> {
|
||||
return ContactViewModel::class.java
|
||||
}
|
||||
|
||||
private fun setObservation() {
|
||||
contactViewModel.contactRequestListResult.observe(viewLifecycleOwner) { result ->
|
||||
// 监听本地联系人列表加载结果
|
||||
viewModel.contactListInDbResult.observe(viewLifecycleOwner) { contacts ->
|
||||
val allItems = mutableListOf<Contact>().apply {
|
||||
addAll(getDefaultItems())
|
||||
addAll(contacts)
|
||||
}
|
||||
friendAdapter.updateData(allItems)
|
||||
binding.includeLoading.main.visibility = View.GONE
|
||||
binding.recycleFriendList.visibility = View.VISIBLE
|
||||
loading = false
|
||||
setupRecyclerView()
|
||||
}
|
||||
|
||||
// 监听请求列表
|
||||
viewModel.contactRequestListResult.observe(viewLifecycleOwner) { result ->
|
||||
result.onSuccess {
|
||||
val items = result.getOrNull()?.toMutableList() ?: emptyList()
|
||||
if (items.isNotEmpty()) {
|
||||
@ -92,7 +108,7 @@ class ContactFragment : BaseFragment<FragmentContactBinding>() {
|
||||
}
|
||||
|
||||
private fun loadFriendRequest() {
|
||||
contactViewModel.getContactRequestList(getUsername())
|
||||
viewModel.getContactRequestList(getUsername())
|
||||
}
|
||||
|
||||
private fun loadData() {
|
||||
@ -100,16 +116,7 @@ class ContactFragment : BaseFragment<FragmentContactBinding>() {
|
||||
binding.includeLoading.main.visibility = View.VISIBLE
|
||||
binding.recycleFriendList.visibility = View.INVISIBLE
|
||||
|
||||
val contacts = contactViewModel.loadFriendListInDb()
|
||||
val allItems = mutableListOf<Contact>().apply {
|
||||
addAll(getDefaultItems())
|
||||
addAll(contacts)
|
||||
}
|
||||
friendAdapter.updateData(allItems)
|
||||
binding.includeLoading.main.visibility = View.GONE
|
||||
binding.recycleFriendList.visibility = View.VISIBLE
|
||||
loading = false
|
||||
setupRecyclerView()
|
||||
viewModel.loadFriendListInDb() // 启动加载
|
||||
}
|
||||
|
||||
private fun setupRecyclerView() {
|
||||
|
@ -26,6 +26,7 @@ import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AppCompatActivity.BIND_AUTO_CREATE
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.huawei.hms.hmsscankit.ScanUtil
|
||||
import com.huawei.hms.ml.scan.HmsScan
|
||||
@ -47,6 +48,7 @@ import com.kaixed.kchat.ui.base.BaseFragment
|
||||
import com.kaixed.kchat.ui.i.OnDialogFragmentClickListener
|
||||
import com.kaixed.kchat.ui.i.OnItemListener
|
||||
import com.kaixed.kchat.ui.widget.HomeDialogFragment
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class HomeFragment : BaseFragment<FragmentHomeBinding>(), OnItemListener,
|
||||
OnDialogFragmentClickListener {
|
||||
@ -322,7 +324,9 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(), OnItemListener,
|
||||
|
||||
override fun onItemClick(talkerId: String) {
|
||||
this.talkerId = talkerId
|
||||
ConversationManager.cleanUnreadCount(talkerId)
|
||||
lifecycleScope.launch {
|
||||
ConversationManager.cleanUnreadCount(talkerId)
|
||||
}
|
||||
}
|
||||
|
||||
private var longClickTalkerId = ""
|
||||
@ -350,7 +354,9 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(), OnItemListener,
|
||||
|
||||
override fun onClickSetUnread() {
|
||||
if (con != null) {
|
||||
ConversationManager.updateUnreadCount(con!!.talkerId, con!!.unreadCount == 0)
|
||||
lifecycleScope.launch {
|
||||
ConversationManager.updateUnreadCount(con!!.talkerId, con!!.unreadCount == 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -364,6 +370,8 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(), OnItemListener,
|
||||
|
||||
override fun onClickDeleteConversation() {
|
||||
val con = getConversation()
|
||||
ConversationManager.deleteConversation(con)
|
||||
lifecycleScope.launch {
|
||||
ConversationManager.deleteConversation(con)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -95,7 +95,7 @@ class MyBottomSheetFragment : BottomSheetDialogFragment() {
|
||||
val mmkv = MMKV.mmkvWithID(MMKV_USER_SESSION)
|
||||
mmkv.clearAll()
|
||||
MMKV.defaultMMKV().putBoolean(USER_LOGIN_STATUS, false)
|
||||
DataBase.cleanAllData()
|
||||
DataBase.clearAllDatabase()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
|
@ -1,9 +0,0 @@
|
||||
package com.kaixed.kchat.utils
|
||||
|
||||
/**
|
||||
* @Author: kaixed
|
||||
* @Date: 2025/1/24 20:41
|
||||
*/
|
||||
class AccountUtils {
|
||||
|
||||
}
|
@ -7,6 +7,7 @@ import com.tencent.mmkv.MMKV
|
||||
* @Date: 2025/1/22 18:06
|
||||
*/
|
||||
object CacheUtils {
|
||||
|
||||
fun getAccessToken(): String {
|
||||
val accessToken = MMKV.defaultMMKV().getString(Constants.ACCESS_TOKEN, "")
|
||||
return accessToken ?: ""
|
||||
|
@ -6,6 +6,10 @@ package com.kaixed.kchat.utils
|
||||
*/
|
||||
object Constants {
|
||||
|
||||
object Sign {
|
||||
const val SECRET_KEY = "123"
|
||||
}
|
||||
|
||||
// mmkv
|
||||
const val MMKV_USER_SESSION: String = "userSession"
|
||||
const val MMKV_COMMON_DATA: String = "commonData"
|
||||
|
@ -1,5 +1,6 @@
|
||||
package com.kaixed.kchat.utils
|
||||
|
||||
import com.kaixed.core.common.utils.TextUtil.extractUrl
|
||||
import com.kaixed.kchat.utils.Constants.AVATAR_URL
|
||||
import com.kaixed.kchat.utils.Constants.CURRENT_CONTACT_ID
|
||||
import com.kaixed.kchat.utils.Constants.FIRST_LAUNCH_APP
|
||||
@ -31,7 +32,7 @@ object ConstantsUtils {
|
||||
|
||||
fun getAvatarUrl(): String {
|
||||
val avatarUrl = userSessionMMKV.getString(AVATAR_URL, "") ?: ""
|
||||
return TextUtil.extractUrl(avatarUrl)
|
||||
return extractUrl(avatarUrl)
|
||||
}
|
||||
|
||||
fun getStatusBarHeight(): Int =
|
||||
|
@ -0,0 +1,99 @@
|
||||
package com.kaixed.kchat.utils
|
||||
|
||||
import okhttp3.*
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.RandomAccessFile
|
||||
|
||||
class DownloadManager(
|
||||
private val url: String,
|
||||
private val destinationFile: File,
|
||||
private val callback: DownloadCallback
|
||||
) {
|
||||
|
||||
private val client = OkHttpClient()
|
||||
private var downloadedBytes: Long = 0L
|
||||
private var totalBytes: Long = 0L
|
||||
|
||||
init {
|
||||
// 如果目标文件已存在,则获取已下载的字节数
|
||||
if (destinationFile.exists()) {
|
||||
downloadedBytes = destinationFile.length()
|
||||
}
|
||||
}
|
||||
|
||||
// 开始下载文件
|
||||
fun startDownload() {
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.header("Range", "bytes=$downloadedBytes-") // 请求从已下载的字节数继续下载
|
||||
.build()
|
||||
|
||||
client.newCall(request).enqueue(object : Callback {
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
if (response.isSuccessful) {
|
||||
// 获取文件总大小
|
||||
val contentRange = response.header("Content-Range")
|
||||
if (contentRange != null) {
|
||||
totalBytes = contentRange.split("/")[1].toLong()
|
||||
}
|
||||
|
||||
// 获取响应体的输入流
|
||||
val body = response.body ?: return
|
||||
|
||||
// 使用 RandomAccessFile 写入文件
|
||||
val raf = RandomAccessFile(destinationFile, "rw")
|
||||
raf.seek(downloadedBytes)
|
||||
|
||||
val inputStream = body.byteStream()
|
||||
val buffer = ByteArray(8192)
|
||||
var bytesRead: Int
|
||||
|
||||
try {
|
||||
// 开始下载并写入文件
|
||||
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
|
||||
raf.write(buffer, 0, bytesRead)
|
||||
downloadedBytes += bytesRead
|
||||
|
||||
// 计算下载进度
|
||||
val progress = (downloadedBytes * 100) / totalBytes
|
||||
|
||||
// 回调进度更新
|
||||
callback.onProgress(progress)
|
||||
}
|
||||
|
||||
// 下载完成
|
||||
callback.onDownloadComplete()
|
||||
|
||||
} catch (e: IOException) {
|
||||
// 发生错误时回调
|
||||
callback.onError(e.message ?: "Unknown error")
|
||||
} finally {
|
||||
raf.close()
|
||||
inputStream.close()
|
||||
body.close()
|
||||
}
|
||||
} else {
|
||||
callback.onError("Failed to download: ${response.code}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
// 网络请求失败时回调
|
||||
callback.onError("Download failed: ${e.message}")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 取消下载
|
||||
fun cancelDownload() {
|
||||
// 可以扩展实现下载取消的功能,关闭请求等
|
||||
}
|
||||
|
||||
// 下载进度回调接口
|
||||
interface DownloadCallback {
|
||||
fun onProgress(progress: Long) // 下载进度(百分比)
|
||||
fun onDownloadComplete() // 下载完成
|
||||
fun onError(errorMessage: String) // 错误信息
|
||||
}
|
||||
}
|
@ -20,9 +20,12 @@ object ScreenUtils {
|
||||
private val defaultMMKV by lazy { MMKV.defaultMMKV() }
|
||||
|
||||
fun init(context: Context) {
|
||||
appContext = context
|
||||
if (!::appContext.isInitialized) {
|
||||
appContext = context.applicationContext
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun getScreenHeight(): Int {
|
||||
val screenHeight = defaultMMKV.getInt(SCREEN_HEIGHT, 0)
|
||||
if (screenHeight != 0) {
|
||||
|
@ -1,10 +1,10 @@
|
||||
package com.kaixed.kchat.utils
|
||||
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.constraintlayout.widget.ConstraintSet
|
||||
import com.kaixed.core.common.utils.TextUtil.getTimestampString
|
||||
import com.kaixed.kchat.data.local.entity.Messages
|
||||
|
||||
/**
|
||||
@ -48,11 +48,11 @@ object ViewUtil {
|
||||
|
||||
if (showTimer) {
|
||||
tvTimer.visibility = View.VISIBLE
|
||||
tvTimer.text = TextUtil.getTimestampString(singleMessage.timestamp)
|
||||
tvTimer.text = getTimestampString(singleMessage.timestamp)
|
||||
} else {
|
||||
tvTimer.visibility = View.GONE
|
||||
}
|
||||
tvTimer.visibility = if (showTimer) View.VISIBLE else View.GONE
|
||||
tvTimer.text = TextUtil.getTimestampString(singleMessage.timestamp)
|
||||
tvTimer.text = getTimestampString(singleMessage.timestamp)
|
||||
}
|
||||
}
|
@ -1,9 +1,8 @@
|
||||
package com.kaixed.kchat.utils.handle
|
||||
|
||||
import com.kaixed.kchat.data.local.box.ObjectBox.getBoxStore
|
||||
import com.kaixed.core.common.utils.Pinyin4jUtil
|
||||
import com.kaixed.kchat.data.local.AppDatabase
|
||||
import com.kaixed.kchat.data.local.entity.Contact
|
||||
import com.kaixed.kchat.utils.Pinyin4jUtil
|
||||
import io.objectbox.Box
|
||||
|
||||
/**
|
||||
* @Author: kaixed
|
||||
@ -11,9 +10,9 @@ import io.objectbox.Box
|
||||
*/
|
||||
object ContactUtil {
|
||||
|
||||
private val contactBox: Box<Contact> by lazy { getBoxStore().boxFor(Contact::class.java) }
|
||||
private val contactDao = AppDatabase.getDatabase().contactDao()
|
||||
|
||||
fun handleContact(contact: Contact) {
|
||||
suspend fun handleContact(contact: Contact) {
|
||||
val contactLists = getDbContactLists()
|
||||
contactLists.add(contact)
|
||||
contact.apply {
|
||||
@ -28,10 +27,10 @@ object ContactUtil {
|
||||
index - 1
|
||||
))
|
||||
}
|
||||
contactBox.put(contactLists)
|
||||
contactDao.updateContacts(contactLists)
|
||||
}
|
||||
|
||||
private fun getDbContactLists(): MutableList<Contact> {
|
||||
return contactBox.all.toMutableList()
|
||||
private suspend fun getDbContactLists(): MutableList<Contact> {
|
||||
return contactDao.getAllContacts() as MutableList<Contact>
|
||||
}
|
||||
}
|
||||
|
@ -4,18 +4,16 @@ import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.kaixed.kchat.data.local.box.ObjectBox.getBox
|
||||
import com.kaixed.kchat.data.local.AppDatabase
|
||||
import com.kaixed.kchat.data.local.entity.Contact
|
||||
import com.kaixed.kchat.data.model.friend.FriendRequestItem
|
||||
import com.kaixed.kchat.data.model.search.SearchUser
|
||||
import com.kaixed.kchat.data.repository.ContactRepository
|
||||
import com.kaixed.kchat.utils.SingleLiveEvent
|
||||
import io.objectbox.Box
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ContactViewModel : ViewModel() {
|
||||
|
||||
private val contactRepository = ContactRepository()
|
||||
private val contactRepository = ContactRepository(AppDatabase.getDatabase().contactDao())
|
||||
|
||||
// 获取联系人请求列表
|
||||
private val _contactRequestListResult =
|
||||
@ -96,38 +94,14 @@ class ContactViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun loadFriendListInDb(): List<Contact> {
|
||||
val contactBox: Box<Contact> = getBox(Contact::class.java)
|
||||
private val _contactListInDbResult = MutableLiveData<List<Contact>>()
|
||||
val contactListInDbResult: LiveData<List<Contact>> = _contactListInDbResult
|
||||
|
||||
val sortedContacts = contactBox.query()
|
||||
.sort { contact1, contact2 ->
|
||||
val str1 = contact1.remarkquanpin ?: contact1.quanpin ?: contact1.nickname
|
||||
val str2 = contact2.remarkquanpin ?: contact2.quanpin ?: contact2.nickname
|
||||
|
||||
val type1 = when {
|
||||
str1.matches(Regex("[a-zA-Z]+")) -> 1 // 字母
|
||||
str1.matches(Regex("[0-9]+")) -> 2 // 数字
|
||||
else -> 3 // 特殊字符
|
||||
}
|
||||
|
||||
val type2 = when {
|
||||
str2.matches(Regex("[a-zA-Z]+")) -> 1 // 字母
|
||||
str2.matches(Regex("[0-9]+")) -> 2 // 数字
|
||||
else -> 3 // 特殊字符
|
||||
}
|
||||
|
||||
// 比较类型,如果相同再按字母升序排序
|
||||
if (type1 != type2) {
|
||||
type1 - type2 // 按类型排序
|
||||
} else {
|
||||
str1.compareTo(str2) // 如果类型相同,按字母顺序排序
|
||||
}
|
||||
|
||||
}
|
||||
.build()
|
||||
.find()
|
||||
|
||||
return sortedContacts.toMutableList()
|
||||
fun loadFriendListInDb() {
|
||||
viewModelScope.launch {
|
||||
val result = contactRepository.getContactsInDb()
|
||||
_contactListInDbResult.postValue(result ?: emptyList())
|
||||
}
|
||||
}
|
||||
|
||||
fun loadFriendList(username: String) {
|
||||
|
@ -0,0 +1,28 @@
|
||||
package com.kaixed.kchat.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.cachedIn
|
||||
import com.kaixed.kchat.data.local.dao.MessagesDao
|
||||
import com.kaixed.kchat.data.local.entity.Messages
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class MessagesViewModel(
|
||||
private val messagesDao: MessagesDao
|
||||
) : ViewModel() {
|
||||
private val pagingConfig = PagingConfig(
|
||||
pageSize = 20, // 每页加载的数量
|
||||
enablePlaceholders = false,
|
||||
prefetchDistance = 5, // 当距离边缘还有5个项目时开始加载下一页
|
||||
initialLoadSize = 40 // 首次加载的数量
|
||||
)
|
||||
|
||||
// 接收当前联系人ID的消息流
|
||||
fun getMessageFlow(contactId: String): Flow<PagingData<Messages>> = Pager(
|
||||
config = pagingConfig,
|
||||
pagingSourceFactory = { messagesDao.getPagedMessages(contactId) }
|
||||
).flow.cachedIn(viewModelScope)
|
||||
}
|
@ -4,6 +4,7 @@ import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.kaixed.kchat.data.local.AppDatabase
|
||||
import com.kaixed.kchat.data.local.entity.UserInfo
|
||||
import com.kaixed.kchat.data.model.request.RegisterRequest
|
||||
import com.kaixed.kchat.data.model.request.UpdatePasswordRequest
|
||||
@ -19,9 +20,9 @@ import okhttp3.MultipartBody
|
||||
|
||||
class UserViewModel : ViewModel() {
|
||||
|
||||
private val userAuthRepo = UserAuthRepository()
|
||||
private val userAuthRepo = UserAuthRepository(AppDatabase.getDatabase().userInfoDao())
|
||||
|
||||
private val userProfileRepo = UserProfileRepository()
|
||||
private val userProfileRepo = UserProfileRepository(AppDatabase.getDatabase().userInfoDao())
|
||||
|
||||
private val userSearchRepo = UserSearchRepository()
|
||||
|
||||
@ -65,8 +66,8 @@ class UserViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
private val _userListResult = MutableLiveData<Result<List<User>>>()
|
||||
val userListResult: LiveData<Result<List<User>>> = _userListResult
|
||||
private val _userListResult = MutableLiveData<Result<List<User>?>>()
|
||||
val userListResult: LiveData<Result<List<User>?>> = _userListResult
|
||||
|
||||
// 获取用户列表
|
||||
fun getUserList(username: String) {
|
||||
|
@ -0,0 +1,16 @@
|
||||
package com.kaixed.kchat.viewmodel.factory
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.kaixed.kchat.data.local.dao.MessagesDao
|
||||
import com.kaixed.kchat.viewmodel.MessagesViewModel
|
||||
|
||||
class MessagesViewModelFactory(private val messagesDao: MessagesDao) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
if (modelClass.isAssignableFrom(MessagesViewModel::class.java)) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return MessagesViewModel(messagesDao) as T
|
||||
}
|
||||
throw IllegalArgumentException("Unknown ViewModel class")
|
||||
}
|
||||
}
|
@ -17,7 +17,7 @@
|
||||
app:titleName="contact" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycle_chat_list"
|
||||
android:id="@+id/rv_chat_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
|
@ -2,6 +2,6 @@
|
||||
<network-security-config>
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="true">49.233.105.103</domain>
|
||||
<domain includeSubdomains="true">192.168.31.18</domain>
|
||||
<domain includeSubdomains="true">192.168.225.209</domain>
|
||||
</domain-config>
|
||||
</network-security-config>
|
||||
|
@ -5,6 +5,7 @@ plugins {
|
||||
id("com.google.devtools.ksp") version "1.9.23-1.0.19" apply false
|
||||
// id ("cn.therouter.agp8") version "1.2.1" apply false
|
||||
id ("org.jetbrains.kotlin.plugin.serialization") version "1.9.23"
|
||||
alias(libs.plugins.androidLibrary) apply false
|
||||
}
|
||||
|
||||
buildscript {
|
||||
@ -14,7 +15,7 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath(libs.objectbox.gradle.plugin)
|
||||
// classpath(libs.objectbox.gradle.plugin)
|
||||
// classpath("com.android.tools.build:gradle:8.6.0")
|
||||
classpath(libs.agcp)
|
||||
}
|
||||
|
1
core_common/.gitignore
vendored
Normal file
1
core_common/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/build
|
51
core_common/build.gradle.kts
Normal file
51
core_common/build.gradle.kts
Normal file
@ -0,0 +1,51 @@
|
||||
plugins {
|
||||
alias(libs.plugins.androidLibrary)
|
||||
alias(libs.plugins.jetbrainsKotlinAndroid)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.kaixed.core.common"
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 30
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
|
||||
viewBinding {
|
||||
enable = true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.appcompat)
|
||||
implementation(libs.material)
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.ext.junit)
|
||||
androidTestImplementation(libs.espresso.core)
|
||||
|
||||
implementation(libs.pinyin4j)
|
||||
|
||||
implementation(libs.mmkv)
|
||||
}
|
0
core_common/consumer-rules.pro
Normal file
0
core_common/consumer-rules.pro
Normal file
21
core_common/proguard-rules.pro
vendored
Normal file
21
core_common/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
@ -0,0 +1,24 @@
|
||||
package com.kaixed.core.common
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("com.kaixed.core.common.test", appContext.packageName)
|
||||
}
|
||||
}
|
4
core_common/src/main/AndroidManifest.xml
Normal file
4
core_common/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
@ -0,0 +1,57 @@
|
||||
package com.kaixed.core.common.base
|
||||
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.viewbinding.ViewBinding
|
||||
|
||||
/**
|
||||
* @Author: kaixed
|
||||
* @Date: 2024/11/4 10:43
|
||||
*/
|
||||
abstract class BaseActivity<VB : ViewBinding, VM : ViewModel> : AppCompatActivity() {
|
||||
|
||||
protected lateinit var binding: VB
|
||||
|
||||
protected val viewModel: VM by lazy { ViewModelProvider(this)[getViewModelClass()] }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
binding = inflateBinding()
|
||||
setContentView(binding.root)
|
||||
initData()
|
||||
initView()
|
||||
setupListeners()
|
||||
observeData()
|
||||
// AppManager.instance.addActivity(this)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
// AppManager.instance.finishActivity(this)
|
||||
}
|
||||
|
||||
abstract fun initData()
|
||||
|
||||
abstract fun initView()
|
||||
|
||||
abstract fun setupListeners()
|
||||
|
||||
abstract fun observeData()
|
||||
|
||||
fun toast(msg: String) {
|
||||
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
fun toast() {
|
||||
Toast.makeText(this, "暂未开发", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
abstract fun inflateBinding(): VB
|
||||
|
||||
abstract fun getViewModelClass(): Class<VM>
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
package com.kaixed.core.common.base
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.kaixed.core.common.utils.ConstantsUtils.getStatusBarHeight
|
||||
|
||||
/**
|
||||
* @Author: kaixed
|
||||
* @Date: 2024/10/31 11:04
|
||||
*/
|
||||
abstract class BaseFragment<VB : ViewBinding, VM : ViewModel> : Fragment() {
|
||||
|
||||
private var _binding: VB? = null
|
||||
protected val binding get() = _binding!!
|
||||
protected val viewModel: VM by lazy { ViewModelProvider(this)[getViewModelClass()] }
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
_binding = inflateBinding(inflater, container)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
abstract fun inflateBinding(inflater: LayoutInflater, container: ViewGroup?): VB
|
||||
|
||||
// abstract fun isSetStatusBarPadding(): Boolean
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
setupStatusBarPadding(binding.root)
|
||||
}
|
||||
|
||||
private fun setupStatusBarPadding(rootView: View) {
|
||||
val statusBarHeight = getStatusBarHeight()
|
||||
rootView.updatePadding(top = statusBarHeight)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
abstract fun getViewModelClass(): Class<VM>
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package com.kaixed.kchat.extensions
|
||||
package com.kaixed.core.common.extension
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
@ -16,4 +16,8 @@ inline fun <reified T : Parcelable> Intent.getParcelableExtraCompat(key: String)
|
||||
@Suppress("DEPRECATION")
|
||||
getParcelableExtra(key)
|
||||
}
|
||||
}
|
||||
|
||||
fun Int.dp2px(density: Float): Int {
|
||||
return (this * density + 0.5f).toInt()
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package com.kaixed.core.common.utils
|
||||
|
||||
/**
|
||||
* @Author: kaixed
|
||||
* @Date: 2024/10/24 22:03
|
||||
*/
|
||||
object Constants {
|
||||
|
||||
object Sign {
|
||||
const val SECRET_KEY = "123"
|
||||
}
|
||||
|
||||
// mmkv
|
||||
const val MMKV_USER_SESSION: String = "userSession"
|
||||
const val MMKV_COMMON_DATA: String = "commonData"
|
||||
|
||||
const val USERNAME_KEY = "username"
|
||||
const val NICKNAME_KEY = "nickname"
|
||||
const val AVATAR_URL = "avatarUrl"
|
||||
const val FIRST_LAUNCH_APP = "firstLaunchApp"
|
||||
const val USER_LOGIN_STATUS = "userLoginStatus"
|
||||
const val STATUS_BAR_HEIGHT = "status_bar_height"
|
||||
const val KEYBOARD_HEIGHT = "keyboardHeight"
|
||||
const val CURRENT_CONTACT_ID = "currentContactId"
|
||||
|
||||
const val SCREEN_HEIGHT = "screenHeight"
|
||||
const val SCREEN_WIDTH = "screenWidth"
|
||||
|
||||
const val ACCESS_TOKEN = "accessToken"
|
||||
const val REFRESH_TOKEN = "refreshToken"
|
||||
|
||||
const val KEYBOARD_HEIGHT_RATIO = 0.15F
|
||||
const val KEYBOARD_DEFAULT_HEIGHT = 300
|
||||
const val STATUS_BAR_DEFAULT_HEIGHT: Int = 50
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
package com.kaixed.core.common.utils
|
||||
|
||||
import com.kaixed.core.common.utils.Constants.AVATAR_URL
|
||||
import com.kaixed.core.common.utils.Constants.CURRENT_CONTACT_ID
|
||||
import com.kaixed.core.common.utils.Constants.FIRST_LAUNCH_APP
|
||||
import com.kaixed.core.common.utils.Constants.KEYBOARD_DEFAULT_HEIGHT
|
||||
import com.kaixed.core.common.utils.Constants.KEYBOARD_HEIGHT
|
||||
import com.kaixed.core.common.utils.Constants.MMKV_COMMON_DATA
|
||||
import com.kaixed.core.common.utils.Constants.MMKV_USER_SESSION
|
||||
import com.kaixed.core.common.utils.Constants.NICKNAME_KEY
|
||||
import com.kaixed.core.common.utils.Constants.STATUS_BAR_DEFAULT_HEIGHT
|
||||
import com.kaixed.core.common.utils.Constants.STATUS_BAR_HEIGHT
|
||||
import com.kaixed.core.common.utils.Constants.USERNAME_KEY
|
||||
import com.kaixed.core.common.utils.TextUtil.extractUrl
|
||||
import com.tencent.mmkv.MMKV
|
||||
import kotlin.getValue
|
||||
|
||||
/**
|
||||
* @Author: kaixed
|
||||
* @Date: 2024/10/24 22:04
|
||||
*/
|
||||
object ConstantsUtils {
|
||||
|
||||
private val commonDataMMKV by lazy { MMKV.mmkvWithID(MMKV_COMMON_DATA) }
|
||||
private val userSessionMMKV by lazy { MMKV.mmkvWithID(MMKV_USER_SESSION) }
|
||||
private val defaultMMKV by lazy { MMKV.defaultMMKV() }
|
||||
|
||||
fun getKeyboardHeight(): Int =
|
||||
defaultMMKV.getInt(KEYBOARD_HEIGHT, KEYBOARD_DEFAULT_HEIGHT)
|
||||
|
||||
fun getUsername(): String =
|
||||
userSessionMMKV.getString(USERNAME_KEY, "") ?: ""
|
||||
|
||||
fun getAvatarUrl(): String {
|
||||
val avatarUrl = userSessionMMKV.getString(AVATAR_URL, "") ?: ""
|
||||
return extractUrl(avatarUrl)
|
||||
}
|
||||
|
||||
fun getStatusBarHeight(): Int =
|
||||
commonDataMMKV.getInt(STATUS_BAR_HEIGHT, STATUS_BAR_DEFAULT_HEIGHT)
|
||||
|
||||
fun getNickName(): String =
|
||||
userSessionMMKV.getString(NICKNAME_KEY, "") ?: ""
|
||||
|
||||
fun getCurrentContactId(): String =
|
||||
userSessionMMKV.getString(CURRENT_CONTACT_ID, "") ?: ""
|
||||
|
||||
fun isFirstLaunchApp(): Boolean {
|
||||
val isFirstLaunch = defaultMMKV.getBoolean(FIRST_LAUNCH_APP, true)
|
||||
if (isFirstLaunch) {
|
||||
defaultMMKV.putBoolean(FIRST_LAUNCH_APP, false)
|
||||
}
|
||||
return isFirstLaunch
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
package com.kaixed.core.common.utils
|
||||
|
||||
/**
|
||||
* @Author: kaixed
|
||||
* @Date: 2024/10/14 13:46
|
||||
*/
|
||||
object NetworkInterface {
|
||||
|
||||
// private const val URL = "app.kaixed.com/kchat"
|
||||
private const val URL = "192.168.225.209:6196"
|
||||
// private const val URL = "49.233.105.103:6000"
|
||||
// const val SERVER_URL = "https://$URL"
|
||||
const val SERVER_URL = "http://$URL"
|
||||
const val WEBSOCKET_SERVER_URL = "ws://$URL"
|
||||
const val WEBSOCKET = "/websocket/single/"
|
||||
|
||||
// 获取access-token
|
||||
const val ACCESS_TOKEN = "auth/access-token"
|
||||
// 获取refresh-token
|
||||
const val REFRESH_TOKEN = "auth/refresh-token"
|
||||
// 登录接口(根据用户名登录)
|
||||
const val LOGIN_BY_USERNAME = "auth/login/username"
|
||||
// 登录接口(根据电话登录)
|
||||
const val LOGIN_BY_TELEPHONE = "auth/login/telephone"
|
||||
// 注册接口
|
||||
const val REGISTER = "auth/register"
|
||||
|
||||
// 上传文件
|
||||
const val UPLOAD_FILE = "file/upload"
|
||||
|
||||
// 更新用户密码
|
||||
const val UPDATE_PASSWORD = "user/password/update"
|
||||
// 获取用户信息
|
||||
const val GET_USER_INFO = "user/info/fetch"
|
||||
// 更新用户信息
|
||||
const val UPDATE_USER_INFO = "user/info/update"
|
||||
// 上传用户头像
|
||||
const val UPLOAD_AVATAR = "user/upload-avatar"
|
||||
|
||||
// 发送好友请求
|
||||
const val SEND_FRIEND_REQUEST = "friends/requests/send"
|
||||
// 接受好友请求
|
||||
const val ACCEPT_FRIEND_REQUEST = "friends/requests/accept"
|
||||
// 获取好友列表
|
||||
const val GET_FRIENDS = "friends/list"
|
||||
// 获取好友请求列表
|
||||
const val GET_FRIEND_REQUEST_LIST = "friends/requests/fetch"
|
||||
// 删除好友
|
||||
const val DELETE_FRIEND = "friends/delete-friend"
|
||||
// 设置好友备注
|
||||
const val SET_FRIEND_REMARK = "friends/set-remark"
|
||||
|
||||
//撤回消息
|
||||
const val RECALL_MESSAGE = "message/withdraw"
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user