feat(sample): 添加示例应用的核心功能模块

- 集成依赖管理配置文件 libs.versions.toml,统一管理项目依赖版本
- 实现演示 API 接口定义,包含登录、注册、用户管理等 RESTful 端点
- 创建认证仓库 AuthRepository,处理用户会话管理和加密存储
- 开发登录和注册界面,实现用户身份验证流程
- 构建聊天界面 ChatScreen,支持消息收发和历史记录显示
- 实现联系人管理功能,包含好友搜索和添加删除操作
- 添加会话列表界面,展示最近聊天记录和未读消息提示
这个提交包含在:
XuqmGroup 2026-04-27 19:00:54 +08:00
父节点 6dd0fa8f49
当前提交 00f2ad04b7
共有 36 个文件被更改,包括 412 次插入12413 次删除

12145
.gitignore vendored

文件差异内容过多而无法显示 加载差异

查看文件

@ -6,7 +6,7 @@
```
XuqmGroup-AndroidSDK/
├── sdk-core/ # 核心初始化、HTTP、Token 存储
├── sdk-core/ # 核心初始化、HTTP、Token 存储、通用工具/组件
├── sdk-im/ # IMWebSocket 实时通信
├── sdk-push/ # 推送:设备 Token 注册
├── sdk-update/ # 版本管理检查更新、下载安装、RN 热更新
@ -53,21 +53,21 @@ dependencies {
### 1. 初始化Application.onCreate
```kotlin
XuqmSDK.init(
XuqmSDK.initialize(
context = this,
appKey = "ak_your_app_key",
appSecret = "as_your_app_secret",
apiBaseUrl = "https://api.xuqm.com",
imBaseUrl = "wss://im.xuqm.com",
appId = "ak_your_app_id",
debug = BuildConfig.DEBUG
)
```
### 2. 用户登录后设置 Token
### 2. 用户登录后初始化 IM
```kotlin
// 调用你的业务登录接口获得 token 后
XuqmSDK.tokenStore.save(token)
// 调用业务登录接口,拿到 userSig 后再登录 IM
val userSig = api.getUserSig(userId)
ImSDK.login(userId = userId, userSig = userSig)
// Push 设备注册在 PushSDK.initialize() 内部自动完成
```
---
@ -78,10 +78,7 @@ XuqmSDK.tokenStore.save(token)
| 参数 | 类型 | 说明 |
|------|------|------|
| `appKey` | String | 应用 Key租户平台获取 |
| `appSecret` | String | 应用 Secret |
| `apiBaseUrl` | String | 后端 API 地址 |
| `imBaseUrl` | String | IM WebSocket 地址wss:// |
| `appId` | String | 应用标识(租户平台获取) |
| `debug` | Boolean | 开启日志 |
### TokenStore
@ -172,18 +169,9 @@ data class ImMessage(
## sdk-push
### 注册推送 Token
### 推送接入
在获取到厂商推送 token 后调用:
```kotlin
PushSDK.registerToken(
appId = "ak_xxx",
userId = "user_001",
vendor = Vendor.HUAWEI, // HUAWEI / XIAOMI / OPPO / VIVO / HONOR / APNS
token = "device_push_token"
)
```
`PushSDK.initialize()` 后由 SDK 自动完成厂商初始化、设备注册与 token 上传,不再要求业务侧单独调用注册接口。
### 与 IM 联动

查看文件

@ -3,8 +3,6 @@ plugins {
alias(libs.plugins.android.library) apply false
alias(libs.plugins.kotlin.compose) apply false
alias(libs.plugins.kotlin.serialization) apply false
alias(libs.plugins.kotlin.kapt) apply false
alias(libs.plugins.hilt.android) apply false
}
group = "com.xuqm"

查看文件

@ -21,10 +21,12 @@ junit4 = "4.13.2"
androidxJunit = "1.3.0"
espresso = "3.7.0"
hilt = "2.56.2"
ksp = "2.3.7"
navigationCompose = "2.9.0"
securityCrypto = "1.0.0"
hiltNavigationCompose = "1.2.0"
viewmodelCompose = "2.10.0"
material = "1.13.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@ -41,6 +43,7 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
google-material = { group = "com.google.android.material", name = "material", version.ref = "material" }
androidx-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" }
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
@ -87,5 +90,5 @@ android-application = { id = "com.android.application", version.ref = "agp" }
android-library = { id = "com.android.library", version.ref = "agp" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }

查看文件

@ -1,8 +1,6 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.kotlin.kapt)
alias(libs.plugins.hilt.android)
}
android {
@ -25,12 +23,10 @@ android {
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions { jvmTarget = "21" }
buildFeatures {
compose = true
buildConfig = true
@ -48,13 +44,10 @@ dependencies {
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.google.material)
implementation(libs.androidx.navigation.compose)
implementation(libs.hilt.android)
kapt(libs.hilt.compiler)
implementation(libs.androidx.hilt.navigation.compose)
implementation(libs.coil.compose)
implementation(libs.coil.network.okhttp)

查看文件

@ -4,24 +4,18 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import com.xuqm.sdk.sample.data.repo.AuthRepository
import com.xuqm.sdk.sample.di.AppDependencies
import com.xuqm.sdk.sample.navigation.AppNavGraph
import com.xuqm.sdk.sample.ui.theme.XuqmTheme
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@Inject
lateinit var authRepository: AuthRepository
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
XuqmTheme {
AppNavGraph(authRepository = authRepository)
AppNavGraph(authRepository = AppDependencies.authRepository)
}
}
}

查看文件

@ -3,16 +3,16 @@ package com.xuqm.sdk.sample
import android.app.Application
import com.xuqm.sdk.XuqmSDK
import com.xuqm.sdk.core.LogLevel
import dagger.hilt.android.HiltAndroidApp
import com.xuqm.sdk.sample.di.AppDependencies
@HiltAndroidApp
class XuqmSampleApp : Application() {
override fun onCreate() {
super.onCreate()
AppDependencies.init(this)
XuqmSDK.initialize(
context = this,
appId = "your_app_id",
appId = "ak_demo_chat",
logLevel = if (BuildConfig.DEBUG) LogLevel.DEBUG else LogLevel.WARN,
)
}

查看文件

@ -1,46 +1,105 @@
package com.xuqm.sdk.sample.data.api
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Query
const val DEMO_BASE_URL = "https://dev.xuqinmin.com/"
const val DEMO_APP_ID = "ak_demo_chat"
data class DemoResponse<T>(
val code: Int = 0,
val message: String? = null,
val status: String? = null,
val data: T? = null,
val message: String? = null,
)
data class LoginRequest(val userId: String, val password: String)
data class LoginRequest(val appId: String, val userId: String, val password: String)
data class RegisterRequest(val userId: String, val password: String, val nickname: String)
data class RegisterRequest(val appId: String, val userId: String, val password: String, val nickname: String)
data class LoginData(val token: String, val userId: String, val nickname: String, val avatar: String?)
data class ResetPasswordRequest(val appId: String, val userId: String, val newPassword: String)
data class UserData(val userId: String, val nickname: String, val avatar: String?)
data class AuthProfile(
val appId: String,
val userId: String,
val nickname: String,
val avatar: String? = null,
val gender: String? = null,
)
data class UpdateProfileRequest(val nickname: String, val avatar: String?)
data class AuthResult(
val demoToken: String,
val imToken: String? = null,
val profile: AuthProfile,
)
data class ResetPasswordRequest(val oldPassword: String, val newPassword: String)
data class UserData(
val userId: String,
val nickname: String,
val avatar: String? = null,
val gender: String? = null,
)
data class UpdateProfileRequest(
val nickname: String,
val avatar: String? = null,
val gender: String? = null,
)
data class ChangePasswordRequest(val oldPassword: String, val newPassword: String)
interface DemoApi {
@POST("api/user/login")
suspend fun login(@Body request: LoginRequest): DemoResponse<LoginData>
@POST("api/demo/auth/login")
suspend fun login(@Body request: LoginRequest): DemoResponse<AuthResult>
@POST("api/user/register")
suspend fun register(@Body request: RegisterRequest): DemoResponse<LoginData>
@POST("api/demo/auth/register")
suspend fun register(@Body request: RegisterRequest): DemoResponse<AuthResult>
@GET("api/user/profile")
suspend fun getProfile(): DemoResponse<UserData>
@PUT("api/user/profile")
suspend fun updateProfile(@Body request: UpdateProfileRequest): DemoResponse<UserData>
@POST("api/user/reset-password")
@POST("api/demo/auth/reset-password")
suspend fun resetPassword(@Body request: ResetPasswordRequest): DemoResponse<Unit>
@GET("api/user/users")
suspend fun searchUsers(@Query("keyword") keyword: String): DemoResponse<List<UserData>>
@GET("api/demo/user/profile")
suspend fun getProfile(@Query("appId") appId: String = DEMO_APP_ID): DemoResponse<UserData>
@PUT("api/demo/user/profile")
suspend fun updateProfile(@Body request: UpdateProfileRequest): DemoResponse<UserData>
@POST("api/demo/user/change-password")
suspend fun changePassword(@Body request: ChangePasswordRequest): DemoResponse<Unit>
@GET("api/demo/users/search")
suspend fun searchUsers(
@Query("appId") appId: String = DEMO_APP_ID,
@Query("keyword") keyword: String,
): DemoResponse<List<UserData>>
}
object DemoApiFactory {
fun create(tokenProvider: () -> String?): DemoApi {
val authInterceptor = Interceptor { chain ->
val request = chain.request().newBuilder().apply {
tokenProvider()?.takeIf { it.isNotBlank() }?.let { header("Authorization", "Bearer $it") }
}.build()
chain.proceed(request)
}
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(authInterceptor)
.build()
return Retrofit.Builder()
.baseUrl(DEMO_BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(DemoApi::class.java)
}
}

查看文件

@ -2,75 +2,76 @@ package com.xuqm.sdk.sample.data.repo
import android.content.Context
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import androidx.security.crypto.MasterKeys
import com.xuqm.sdk.im.ImSDK
import com.xuqm.sdk.sample.data.api.DemoApi
import com.xuqm.sdk.sample.data.api.DEMO_APP_ID
import com.xuqm.sdk.sample.data.api.DemoApiFactory
import com.xuqm.sdk.sample.data.api.AuthResult
import com.xuqm.sdk.sample.data.api.ChangePasswordRequest
import com.xuqm.sdk.sample.data.api.LoginRequest
import com.xuqm.sdk.sample.data.api.RegisterRequest
import com.xuqm.sdk.sample.data.api.ResetPasswordRequest
import com.xuqm.sdk.sample.data.api.UpdateProfileRequest
import com.xuqm.sdk.sample.data.api.UserData
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class AuthRepository @Inject constructor(
@ApplicationContext private val context: Context,
private val api: DemoApi,
) {
class AuthRepository(context: Context) {
private val prefs = EncryptedSharedPreferences.create(
context,
"xuqm_demo_auth",
MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build(),
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
context,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
)
private val api: DemoApi = DemoApiFactory.create(::getDemoToken)
fun getDemoToken(): String? = prefs.getString("demo_token", null)
fun getCurrentUserId(): String? = prefs.getString("user_id", null)
fun getCurrentNickname(): String? = prefs.getString("nickname", null)
fun getCurrentAvatar(): String? = prefs.getString("avatar", null)
fun isLoggedIn(): Boolean = getDemoToken() != null
private fun saveSession(token: String, userId: String, nickname: String, avatar: String?) {
private fun saveSession(result: AuthResult) {
val profile = result.profile
prefs.edit()
.putString("demo_token", token)
.putString("user_id", userId)
.putString("nickname", nickname)
.putString("avatar", avatar)
.putString("demo_token", result.demoToken)
.putString("user_id", profile.userId)
.putString("nickname", profile.nickname)
.putString("avatar", profile.avatar)
.apply()
}
suspend fun login(userId: String, password: String): Result<UserData> =
withContext(Dispatchers.IO) {
runCatching {
val res = api.login(LoginRequest(userId, password))
val res = api.login(LoginRequest(DEMO_APP_ID, userId, password))
val data = requireNotNull(res.data) { res.message ?: "Login failed" }
saveSession(data.token, data.userId, data.nickname, data.avatar)
ImSDK.login(data.userId, data.nickname, data.avatar)
UserData(data.userId, data.nickname, data.avatar)
saveSession(data)
data.imToken?.takeIf { it.isNotBlank() }?.let { ImSDK.loginWithToken(data.profile.userId, it) }
?: ImSDK.login(data.profile.userId, data.profile.nickname, data.profile.avatar)
UserData(data.profile.userId, data.profile.nickname, data.profile.avatar, data.profile.gender)
}
}
suspend fun register(userId: String, password: String, nickname: String): Result<UserData> =
withContext(Dispatchers.IO) {
runCatching {
val res = api.register(RegisterRequest(userId, password, nickname))
val res = api.register(RegisterRequest(DEMO_APP_ID, userId, password, nickname))
val data = requireNotNull(res.data) { res.message ?: "Register failed" }
saveSession(data.token, data.userId, data.nickname, data.avatar)
ImSDK.login(data.userId, data.nickname, data.avatar)
UserData(data.userId, data.nickname, data.avatar)
saveSession(data)
data.imToken?.takeIf { it.isNotBlank() }?.let { ImSDK.loginWithToken(data.profile.userId, it) }
?: ImSDK.login(data.profile.userId, data.profile.nickname, data.profile.avatar)
UserData(data.profile.userId, data.profile.nickname, data.profile.avatar, data.profile.gender)
}
}
suspend fun getProfile(): Result<UserData> =
withContext(Dispatchers.IO) {
runCatching {
requireNotNull(api.getProfile().data) { "Failed to get profile" }
}
runCatching { requireNotNull(api.getProfile().data) { "Failed to get profile" } }
}
suspend fun updateProfile(nickname: String, avatar: String?): Result<UserData> =
@ -84,12 +85,12 @@ class AuthRepository @Inject constructor(
suspend fun resetPassword(oldPassword: String, newPassword: String): Result<Unit> =
withContext(Dispatchers.IO) {
runCatching { api.resetPassword(ResetPasswordRequest(oldPassword, newPassword)) }
runCatching { api.changePassword(ChangePasswordRequest(oldPassword, newPassword)) }.map { }
}
suspend fun searchUsers(keyword: String): Result<List<UserData>> =
withContext(Dispatchers.IO) {
runCatching { api.searchUsers(keyword).data ?: emptyList() }
runCatching { api.searchUsers(keyword = keyword).data ?: emptyList() }
}
fun logout() {

查看文件

@ -0,0 +1,15 @@
package com.xuqm.sdk.sample.di
import android.content.Context
import com.xuqm.sdk.sample.data.api.DemoApi
import com.xuqm.sdk.sample.data.repo.AuthRepository
object AppDependencies {
lateinit var authRepository: AuthRepository
private set
fun init(context: Context) {
authRepository = AuthRepository(context)
}
}

查看文件

@ -1,18 +0,0 @@
package com.xuqm.sdk.sample.di
import com.xuqm.sdk.network.ApiClient
import com.xuqm.sdk.sample.data.api.DemoApi
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideDemoApi(): DemoApi = ApiClient.create()
}

查看文件

@ -26,14 +26,21 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.xuqm.sdk.sample.di.AppDependencies
@Composable
fun LoginScreen(
onLoginSuccess: () -> Unit,
onNavigateToRegister: () -> Unit,
viewModel: LoginViewModel = hiltViewModel(),
viewModel: LoginViewModel = viewModel(
factory = viewModelFactory {
initializer { LoginViewModel(AppDependencies.authRepository) }
}
),
) {
val state by viewModel.state.collectAsStateWithLifecycle()

查看文件

@ -3,11 +3,9 @@ package com.xuqm.sdk.sample.ui.auth
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.xuqm.sdk.sample.data.repo.AuthRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
sealed interface LoginState {
data object Idle : LoginState
@ -16,10 +14,7 @@ sealed interface LoginState {
data class Error(val message: String) : LoginState
}
@HiltViewModel
class LoginViewModel @Inject constructor(
private val authRepository: AuthRepository,
) : ViewModel() {
class LoginViewModel(private val authRepository: AuthRepository) : ViewModel() {
private val _state = MutableStateFlow<LoginState>(LoginState.Idle)
val state: StateFlow<LoginState> = _state

查看文件

@ -29,21 +29,19 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.xuqm.sdk.sample.data.repo.AuthRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import com.xuqm.sdk.sample.di.AppDependencies
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class RegisterViewModel @Inject constructor(
private val authRepository: AuthRepository,
) : ViewModel() {
class RegisterViewModel(private val authRepository: AuthRepository) : ViewModel() {
private val _state = MutableStateFlow<LoginState>(LoginState.Idle)
val state: StateFlow<LoginState> = _state
@ -63,7 +61,11 @@ class RegisterViewModel @Inject constructor(
fun RegisterScreen(
onRegisterSuccess: () -> Unit,
onNavigateBack: () -> Unit,
viewModel: RegisterViewModel = hiltViewModel(),
viewModel: RegisterViewModel = viewModel(
factory = viewModelFactory {
initializer { RegisterViewModel(AppDependencies.authRepository) }
}
),
) {
val state by viewModel.state.collectAsStateWithLifecycle()

查看文件

@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
@ -39,9 +38,10 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.xuqm.sdk.im.model.ImMessage
import com.xuqm.sdk.ui.InitialAvatar
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -51,7 +51,7 @@ fun ChatScreen(
targetName: String,
onNavigateBack: () -> Unit,
onGroupSettings: (() -> Unit)? = null,
viewModel: ChatViewModel = hiltViewModel(),
viewModel: ChatViewModel = viewModel(),
) {
val messages by viewModel.messages.collectAsStateWithLifecycle()
val listState = rememberLazyListState()
@ -166,14 +166,7 @@ private fun MessageBubble(message: ImMessage, isOwn: Boolean) {
@Composable
private fun AvatarPlaceholder(userId: String) {
Surface(
modifier = Modifier.size(32.dp).clip(CircleShape),
color = MaterialTheme.colorScheme.secondaryContainer,
) {
Box(contentAlignment = Alignment.Center) {
Text(userId.take(1).uppercase(), style = MaterialTheme.typography.labelSmall)
}
}
InitialAvatar(text = userId, modifier = Modifier.size(32.dp))
}
private fun parseContent(message: ImMessage): String {

查看文件

@ -5,14 +5,11 @@ import androidx.lifecycle.viewModelScope
import com.xuqm.sdk.im.ImSDK
import com.xuqm.sdk.im.listener.ImEventListener
import com.xuqm.sdk.im.model.ImMessage
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class ChatViewModel @Inject constructor() : ViewModel() {
class ChatViewModel : ViewModel() {
private val _messages = MutableStateFlow<List<ImMessage>>(emptyList())
val messages: StateFlow<List<ImMessage>> = _messages

查看文件

@ -8,45 +8,39 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewModelScope
import com.xuqm.sdk.im.ImSDK
import com.xuqm.sdk.im.model.UserProfile
import com.xuqm.sdk.sample.data.api.UserData
import com.xuqm.sdk.sample.data.repo.AuthRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import com.xuqm.sdk.sample.di.AppDependencies
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.xuqm.sdk.ui.SearchBarField
@HiltViewModel
class ContactViewModel @Inject constructor(
class ContactViewModel(
private val authRepository: AuthRepository,
) : ViewModel() {
private val _friends = MutableStateFlow<List<UserProfile>>(emptyList())
val friends: StateFlow<List<UserProfile>> = _friends
private val _friends = MutableStateFlow<List<UserData>>(emptyList())
val friends: StateFlow<List<UserData>> = _friends
private val _searchResults = MutableStateFlow<List<UserData>>(emptyList())
val searchResults: StateFlow<List<UserData>> = _searchResults
@ -55,8 +49,12 @@ class ContactViewModel @Inject constructor(
fun loadFriends() {
viewModelScope.launch {
runCatching { ImSDK.listFriends() }
.onSuccess { _friends.value = it }
runCatching {
ImSDK.listFriends().mapNotNull { friendId ->
authRepository.searchUsers(friendId).getOrDefault(emptyList())
.firstOrNull { it.userId == friendId }
}
}.onSuccess { _friends.value = it }
}
}
@ -86,20 +84,24 @@ class ContactViewModel @Inject constructor(
@Composable
fun ContactScreen(
onOpenChat: (userId: String) -> Unit,
viewModel: ContactViewModel = hiltViewModel(),
viewModel: ContactViewModel = viewModel(
factory = viewModelFactory {
initializer { ContactViewModel(AppDependencies.authRepository) }
}
),
) {
val friends by viewModel.friends.collectAsStateWithLifecycle()
val searchResults by viewModel.searchResults.collectAsStateWithLifecycle()
var keyword by remember { mutableStateOf("") }
Column(modifier = Modifier.fillMaxSize()) {
OutlinedTextField(
SearchBarField(
value = keyword,
onValueChange = { keyword = it; viewModel.search(it) },
modifier = Modifier.fillMaxWidth().padding(12.dp),
placeholder = { Text("搜索用户 ID 或昵称") },
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
singleLine = true,
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
placeholder = "搜索用户 ID 或昵称",
)
if (keyword.isBlank()) {

查看文件

@ -13,13 +13,11 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Badge
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -32,19 +30,18 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import coil3.compose.AsyncImage
import com.xuqm.sdk.im.model.ConversationData
import com.xuqm.sdk.ui.InitialAvatar
import com.xuqm.sdk.utils.TimeFormatters
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@Composable
fun ConversationScreen(
onOpenChat: (targetId: String, chatType: String, targetName: String) -> Unit,
viewModel: ConversationViewModel = hiltViewModel(),
viewModel: ConversationViewModel = viewModel(),
) {
val conversations by viewModel.conversations.collectAsStateWithLifecycle()
val scope = rememberCoroutineScope()
@ -84,17 +81,7 @@ private fun ConversationItem(
.padding(horizontal = 16.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Surface(
modifier = Modifier.size(48.dp).clip(CircleShape),
color = MaterialTheme.colorScheme.secondaryContainer,
) {
Box(contentAlignment = Alignment.Center) {
Text(
conversation.targetId.take(1).uppercase(),
style = MaterialTheme.typography.titleMedium,
)
}
}
InitialAvatar(text = conversation.targetId, modifier = Modifier.size(48.dp))
Spacer(Modifier.width(12.dp))
@ -110,7 +97,7 @@ private fun ConversationItem(
color = MaterialTheme.colorScheme.primary)
}
Text(
formatTime(conversation.lastMsgTime),
TimeFormatters.formatConversationTime(conversation.lastMsgTime),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.outline,
)
@ -143,8 +130,3 @@ private fun ConversationItem(
}
}
}
private fun formatTime(timestamp: Long): String {
if (timestamp == 0L) return ""
return SimpleDateFormat("HH:mm", Locale.getDefault()).format(Date(timestamp))
}

查看文件

@ -3,17 +3,14 @@ package com.xuqm.sdk.sample.ui.conversation
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.xuqm.sdk.im.ImSDK
import com.xuqm.sdk.im.listener.ImEventListener
import com.xuqm.sdk.im.model.ConversationData
import com.xuqm.sdk.im.model.ImMessage
import com.xuqm.sdk.im.listener.ImEventListener
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class ConversationViewModel @Inject constructor() : ViewModel() {
class ConversationViewModel : ViewModel() {
private val _conversations = MutableStateFlow<List<ConversationData>>(emptyList())
val conversations: StateFlow<List<ConversationData>> = _conversations

查看文件

@ -37,7 +37,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.xuqm.sdk.im.ImSDK
import com.xuqm.sdk.im.model.ImGroup
@ -47,7 +47,7 @@ import com.xuqm.sdk.im.model.ImGroup
fun GroupListScreen(
onOpenGroupChat: (groupId: String, groupName: String) -> Unit,
onGroupSettings: (groupId: String) -> Unit,
viewModel: GroupViewModel = hiltViewModel(),
viewModel: GroupViewModel = viewModel(),
) {
val groups by viewModel.groups.collectAsStateWithLifecycle()
var showCreateDialog by remember { mutableStateOf(false) }
@ -150,7 +150,7 @@ private fun CreateGroupDialog(onDismiss: () -> Unit, onCreate: (String, List<Str
fun GroupSettingsScreen(
groupId: String,
onNavigateBack: () -> Unit,
viewModel: GroupViewModel = hiltViewModel(),
viewModel: GroupViewModel = viewModel(),
) {
val group by viewModel.currentGroup.collectAsStateWithLifecycle()

查看文件

@ -4,14 +4,11 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.xuqm.sdk.im.ImSDK
import com.xuqm.sdk.im.model.ImGroup
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class GroupViewModel @Inject constructor() : ViewModel() {
class GroupViewModel : ViewModel() {
private val _groups = MutableStateFlow<List<ImGroup>>(emptyList())
val groups: StateFlow<List<ImGroup>> = _groups

查看文件

@ -27,16 +27,17 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewModelScope
import com.xuqm.sdk.sample.data.repo.AuthRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import com.xuqm.sdk.sample.di.AppDependencies
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
data class ProfileUiState(
val userId: String = "",
@ -46,8 +47,7 @@ data class ProfileUiState(
val error: String? = null,
)
@HiltViewModel
class ProfileViewModel @Inject constructor(
class ProfileViewModel(
private val authRepository: AuthRepository,
) : ViewModel() {
@ -82,7 +82,11 @@ class ProfileViewModel @Inject constructor(
@Composable
fun ProfileScreen(
onLogout: () -> Unit,
viewModel: ProfileViewModel = hiltViewModel(),
viewModel: ProfileViewModel = viewModel(
factory = viewModelFactory {
initializer { ProfileViewModel(AppDependencies.authRepository) }
}
),
) {
val state by viewModel.state.collectAsStateWithLifecycle()

查看文件

@ -19,17 +19,16 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewModelScope
import com.xuqm.sdk.update.UpdateSDK
import com.xuqm.sdk.update.model.UpdateInfo
import dagger.hilt.android.lifecycle.HiltViewModel
import com.xuqm.sdk.sample.di.AppDependencies
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
data class UpdateUiState(
val isChecking: Boolean = false,
@ -38,8 +37,7 @@ data class UpdateUiState(
val message: String? = null,
)
@HiltViewModel
class UpdateViewModel @Inject constructor() : ViewModel() {
class UpdateViewModel : ViewModel() {
private val _state = MutableStateFlow(UpdateUiState())
val state: StateFlow<UpdateUiState> = _state
@ -67,7 +65,7 @@ class UpdateViewModel @Inject constructor() : ViewModel() {
}
@Composable
fun UpdateScreen(viewModel: UpdateViewModel = hiltViewModel()) {
fun UpdateScreen(viewModel: UpdateViewModel = viewModel()) {
val state by viewModel.state.collectAsStateWithLifecycle()
val context = LocalContext.current

查看文件

@ -1,5 +1,6 @@
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.kotlin.serialization)
}
@ -16,12 +17,19 @@ android {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
buildFeatures {
compose = true
}
}
dependencies {
api(platform(libs.androidx.compose.bom))
api(libs.bundles.network)
api(libs.kotlinx.coroutines.android)
api(libs.kotlinx.serialization.json)
api(libs.androidx.core.ktx)
api(libs.androidx.security.crypto)
api(libs.androidx.ui)
api(libs.androidx.material3)
}

查看文件

@ -2,7 +2,7 @@ package com.xuqm.sdk.auth
import android.content.Context
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import androidx.security.crypto.MasterKeys
private const val PREFS_NAME = "xuqm_sdk_secure"
private const val KEY_IM_TOKEN = "im_token"
@ -10,9 +10,9 @@ private const val KEY_IM_TOKEN = "im_token"
class TokenStore(context: Context) {
private val prefs = EncryptedSharedPreferences.create(
context,
PREFS_NAME,
MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build(),
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
context,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
)

查看文件

@ -1,7 +1,7 @@
package com.xuqm.sdk.core
internal const val BASE_URL = "https://dev.xuqinmin.com/"
internal const val WS_URL = "wss://dev.xuqinmin.com/ws/im"
const val BASE_URL = "https://dev.xuqinmin.com/"
const val WS_URL = "wss://dev.xuqinmin.com/ws/im"
data class SDKConfig(
val appId: String,

查看文件

@ -0,0 +1,33 @@
package com.xuqm.sdk.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextAlign
@Composable
fun InitialAvatar(
text: String,
modifier: Modifier = Modifier,
) {
Box(
modifier = modifier
.clip(CircleShape)
.background(MaterialTheme.colorScheme.secondaryContainer),
contentAlignment = Alignment.Center,
) {
Text(
text = text.trim().take(1).uppercase().ifBlank { "?" },
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSecondaryContainer,
textAlign = TextAlign.Center,
)
}
}

查看文件

@ -0,0 +1,23 @@
package com.xuqm.sdk.ui
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@Composable
fun SearchBarField(
value: String,
onValueChange: (String) -> Unit,
placeholder: String,
modifier: Modifier = Modifier,
) {
OutlinedTextField(
value = value,
onValueChange = onValueChange,
modifier = modifier.fillMaxWidth(),
placeholder = { Text(placeholder) },
singleLine = true,
)
}

查看文件

@ -0,0 +1,20 @@
package com.xuqm.sdk.ui
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@Composable
fun StatusHint(
text: String,
modifier: Modifier = Modifier,
isError: Boolean = false,
) {
Text(
text = text,
modifier = modifier,
style = MaterialTheme.typography.bodySmall,
color = if (isError) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.outline,
)
}

查看文件

@ -8,7 +8,7 @@ object DeviceUtils {
fun getDeviceId(context: Context): String =
Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID)
?: Build.SERIAL
?: Build.FINGERPRINT
fun getDeviceModel(): String = "${Build.MANUFACTURER} ${Build.MODEL}"

查看文件

@ -0,0 +1,15 @@
package com.xuqm.sdk.utils
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
object TimeFormatters {
private val timeOnly = SimpleDateFormat("HH:mm", Locale.getDefault())
fun formatConversationTime(timestamp: Long): String {
if (timestamp <= 0L) return ""
return timeOnly.format(Date(timestamp))
}
}

查看文件

@ -13,7 +13,6 @@ import com.xuqm.sdk.im.listener.ImEventListener
import com.xuqm.sdk.im.model.ConversationData
import com.xuqm.sdk.im.model.ImGroup
import com.xuqm.sdk.im.model.ImMessage
import com.xuqm.sdk.im.model.UserProfile
import com.xuqm.sdk.network.ApiClient
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@ -31,10 +30,15 @@ object ImSDK {
XuqmSDK.requireInit()
val res = api.login(LoginRequest(XuqmSDK.appId, userId, nickname, avatar))
val token = requireNotNull(res.data?.token) { "IM login failed: ${res.message}" }
XuqmSDK.tokenStore.saveToken(token)
currentUserId = userId
client = ImClient(WS_URL, token, XuqmSDK.appId)
client?.connect()
connectWithToken(token)
}
suspend fun loginWithToken(userId: String, token: String) =
withContext(Dispatchers.IO) {
XuqmSDK.requireInit()
currentUserId = userId
connectWithToken(token)
}
fun sendMessage(toId: String, chatType: String, msgType: String, content: String) {
@ -43,19 +47,19 @@ object ImSDK {
suspend fun fetchHistory(toId: String, page: Int = 0, size: Int = 20): List<ImMessage> =
withContext(Dispatchers.IO) {
api.fetchHistory(toId, "SINGLE", page, size).data ?: emptyList()
api.fetchHistory(toId, XuqmSDK.appId, page, size).data ?: emptyList()
}
suspend fun fetchGroupHistory(groupId: String, page: Int = 0, size: Int = 20): List<ImMessage> =
withContext(Dispatchers.IO) {
api.fetchGroupHistory(groupId, page, size).data ?: emptyList()
api.fetchGroupHistory(groupId, XuqmSDK.appId, page, size).data ?: emptyList()
}
suspend fun listGroups(): List<ImGroup> =
withContext(Dispatchers.IO) { api.listGroups().data ?: emptyList() }
withContext(Dispatchers.IO) { api.listGroups(XuqmSDK.appId).data ?: emptyList() }
suspend fun createGroup(name: String, memberIds: List<String>): ImGroup? =
withContext(Dispatchers.IO) { api.createGroup(CreateGroupRequest(name, memberIds)).data }
withContext(Dispatchers.IO) { api.createGroup(XuqmSDK.appId, CreateGroupRequest(name, memberIds)).data }
suspend fun getGroupInfo(groupId: String): ImGroup? =
withContext(Dispatchers.IO) { api.getGroupInfo(groupId).data }
@ -70,19 +74,19 @@ object ImSDK {
withContext(Dispatchers.IO) { api.removeGroupMember(groupId, userId) }
suspend fun leaveGroup(groupId: String) =
withContext(Dispatchers.IO) { api.leaveGroup(groupId) }
withContext(Dispatchers.IO) { api.removeGroupMember(groupId, currentUserId) }
suspend fun listFriends(): List<UserProfile> =
withContext(Dispatchers.IO) { api.listFriends().data ?: emptyList() }
suspend fun listFriends(): List<String> =
withContext(Dispatchers.IO) { api.listFriends(XuqmSDK.appId).data ?: emptyList() }
suspend fun addFriend(friendId: String) =
withContext(Dispatchers.IO) { api.addFriend(friendId) }
withContext(Dispatchers.IO) { api.addFriend(XuqmSDK.appId, friendId) }
suspend fun removeFriend(friendId: String) =
withContext(Dispatchers.IO) { api.removeFriend(friendId) }
withContext(Dispatchers.IO) { api.removeFriend(friendId, XuqmSDK.appId) }
suspend fun listConversations(): List<ConversationData> =
withContext(Dispatchers.IO) { api.listConversations().data ?: emptyList() }
withContext(Dispatchers.IO) { api.listConversations(XuqmSDK.appId).data ?: emptyList() }
suspend fun setConversationPinned(targetId: String, chatType: String, pinned: Boolean) =
withContext(Dispatchers.IO) {
@ -95,10 +99,7 @@ object ImSDK {
}
suspend fun markRead(targetId: String, chatType: String = "SINGLE") =
withContext(Dispatchers.IO) { api.markRead(targetId, chatType) }
suspend fun getUserProfile(userId: String): UserProfile? =
withContext(Dispatchers.IO) { api.getUserProfile(userId).data }
withContext(Dispatchers.IO) { api.markRead(targetId, XuqmSDK.appId, chatType) }
fun addListener(listener: ImEventListener) = client?.addListener(listener)
fun removeListener(listener: ImEventListener) = client?.removeListener(listener)
@ -109,4 +110,11 @@ object ImSDK {
currentUserId = ""
XuqmSDK.tokenStore.clear()
}
private fun connectWithToken(token: String) {
XuqmSDK.tokenStore.saveToken(token)
client?.disconnect()
client = ImClient(WS_URL, token, XuqmSDK.appId)
client?.connect()
}
}

查看文件

@ -3,7 +3,6 @@ package com.xuqm.sdk.im.api
import com.xuqm.sdk.im.model.ConversationData
import com.xuqm.sdk.im.model.ImGroup
import com.xuqm.sdk.im.model.ImMessage
import com.xuqm.sdk.im.model.UserProfile
import retrofit2.http.Body
import retrofit2.http.DELETE
import retrofit2.http.GET
@ -43,26 +42,30 @@ interface ImApi {
@POST("api/im/auth/login")
suspend fun login(@Body request: LoginRequest): ApiResponse<LoginResponse>
@GET("api/im/messages")
@GET("api/im/messages/history/{toId}")
suspend fun fetchHistory(
@Query("toId") toId: String,
@Query("chatType") chatType: String,
@Path("toId") toId: String,
@Query("appId") appId: String,
@Query("page") page: Int,
@Query("size") size: Int,
): ApiResponse<List<ImMessage>>
@GET("api/im/groups/{groupId}/messages")
@GET("api/im/messages/group-history/{groupId}")
suspend fun fetchGroupHistory(
@Path("groupId") groupId: String,
@Query("appId") appId: String,
@Query("page") page: Int,
@Query("size") size: Int,
): ApiResponse<List<ImMessage>>
@GET("api/im/groups")
suspend fun listGroups(): ApiResponse<List<ImGroup>>
suspend fun listGroups(@Query("appId") appId: String): ApiResponse<List<ImGroup>>
@POST("api/im/groups")
suspend fun createGroup(@Body request: CreateGroupRequest): ApiResponse<ImGroup>
suspend fun createGroup(
@Query("appId") appId: String,
@Body request: CreateGroupRequest,
): ApiResponse<ImGroup>
@GET("api/im/groups/{groupId}")
suspend fun getGroupInfo(@Path("groupId") groupId: String): ApiResponse<ImGroup>
@ -89,16 +92,22 @@ interface ImApi {
suspend fun leaveGroup(@Path("groupId") groupId: String): ApiResponse<Unit>
@GET("api/im/friends")
suspend fun listFriends(): ApiResponse<List<UserProfile>>
suspend fun listFriends(@Query("appId") appId: String): ApiResponse<List<String>>
@POST("api/im/friends/{friendId}")
suspend fun addFriend(@Path("friendId") friendId: String): ApiResponse<Unit>
@POST("api/im/friends")
suspend fun addFriend(
@Query("appId") appId: String,
@Query("friendId") friendId: String,
): ApiResponse<Unit>
@DELETE("api/im/friends/{friendId}")
suspend fun removeFriend(@Path("friendId") friendId: String): ApiResponse<Unit>
suspend fun removeFriend(
@Path("friendId") friendId: String,
@Query("appId") appId: String,
): ApiResponse<Unit>
@GET("api/im/conversations")
suspend fun listConversations(): ApiResponse<List<ConversationData>>
suspend fun listConversations(@Query("appId") appId: String): ApiResponse<List<ConversationData>>
@PUT("api/im/conversations/{targetId}/pinned")
suspend fun setConversationPinned(
@ -117,9 +126,7 @@ interface ImApi {
@PUT("api/im/conversations/{targetId}/read")
suspend fun markRead(
@Path("targetId") targetId: String,
@Query("appId") appId: String,
@Query("chatType") chatType: String,
): ApiResponse<Unit>
@GET("api/im/users/{userId}")
suspend fun getUserProfile(@Path("userId") userId: String): ApiResponse<UserProfile>
}

查看文件

@ -4,7 +4,6 @@ import android.content.Context
import com.xuqm.sdk.XuqmSDK
import com.xuqm.sdk.network.ApiClient
import com.xuqm.sdk.push.api.PushApi
import com.xuqm.sdk.push.api.RegisterDeviceRequest
import com.xuqm.sdk.utils.DeviceUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -22,14 +21,10 @@ object PushSDK {
scope.launch {
runCatching {
api.registerDevice(
RegisterDeviceRequest(
userId = userId,
appId = XuqmSDK.appId,
platform = "Android",
userId = userId,
vendor = vendor,
pushToken = "",
deviceId = deviceId,
)
token = deviceId,
)
}
}

查看文件

@ -16,8 +16,13 @@ data class RegisterDeviceRequest(
interface PushApi {
@POST("api/push/device/register")
suspend fun registerDevice(@Body request: RegisterDeviceRequest)
@POST("api/push/register")
suspend fun registerDevice(
@Query("appId") appId: String,
@Query("userId") userId: String,
@Query("vendor") vendor: String,
@Query("token") token: String,
)
@DELETE("api/push/device/unregister")
suspend fun unregisterDevice(

查看文件

@ -18,12 +18,31 @@ object UpdateSDK {
private val api: UpdateApi by lazy { ApiClient.create() }
private fun normalizeDownloadUrl(rawUrl: String?): String? {
if (rawUrl.isNullOrBlank()) return rawUrl
if (rawUrl.contains("/api/v1/updates/api/v1/rn/files/")) {
return rawUrl.replace("/api/v1/updates/api/v1/rn/files/", "/api/v1/rn/files/")
}
return runCatching {
val uri = Uri.parse(rawUrl)
if (uri.path?.startsWith("/files/apk/") == true) {
"${uri.scheme}://${uri.authority}/api/v1/updates${uri.path}"
} else {
rawUrl
}
}.getOrDefault(rawUrl)
}
suspend fun checkAppUpdate(context: Context): UpdateInfo? = withContext(Dispatchers.IO) {
XuqmSDK.requireInit()
val versionCode = context.packageManager
.getPackageInfo(context.packageName, 0).longVersionCode.toInt()
runCatching {
api.checkUpdate(XuqmSDK.appId, "ANDROID", versionCode).data
api.checkUpdate(XuqmSDK.appId, "ANDROID", versionCode).data?.let {
it.copy(downloadUrl = normalizeDownloadUrl(it.downloadUrl) ?: it.downloadUrl)
}
}.getOrNull()
}
@ -66,7 +85,9 @@ object UpdateSDK {
withContext(Dispatchers.IO) {
XuqmSDK.requireInit()
runCatching {
api.checkRnUpdate(XuqmSDK.appId, moduleId, "ANDROID", currentVersion).data
api.checkRnUpdate(XuqmSDK.appId, moduleId, "ANDROID", currentVersion).data?.let {
it.copy(downloadUrl = normalizeDownloadUrl(it.downloadUrl) ?: it.downloadUrl)
}
}.getOrNull()
}
}