feat(sample): 添加示例应用的核心功能模块
- 集成依赖管理配置文件 libs.versions.toml,统一管理项目依赖版本 - 实现演示 API 接口定义,包含登录、注册、用户管理等 RESTful 端点 - 创建认证仓库 AuthRepository,处理用户会话管理和加密存储 - 开发登录和注册界面,实现用户身份验证流程 - 构建聊天界面 ChatScreen,支持消息收发和历史记录显示 - 实现联系人管理功能,包含好友搜索和添加删除操作 - 添加会话列表界面,展示最近聊天记录和未读消息提示
这个提交包含在:
父节点
6dd0fa8f49
当前提交
00f2ad04b7
12145
.gitignore
vendored
12145
.gitignore
vendored
文件差异内容过多而无法显示
加载差异
36
README.md
36
README.md
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
```
|
```
|
||||||
XuqmGroup-AndroidSDK/
|
XuqmGroup-AndroidSDK/
|
||||||
├── sdk-core/ # 核心:初始化、HTTP、Token 存储
|
├── sdk-core/ # 核心:初始化、HTTP、Token 存储、通用工具/组件
|
||||||
├── sdk-im/ # IM:WebSocket 实时通信
|
├── sdk-im/ # IM:WebSocket 实时通信
|
||||||
├── sdk-push/ # 推送:设备 Token 注册
|
├── sdk-push/ # 推送:设备 Token 注册
|
||||||
├── sdk-update/ # 版本管理:检查更新、下载安装、RN 热更新
|
├── sdk-update/ # 版本管理:检查更新、下载安装、RN 热更新
|
||||||
@ -53,21 +53,21 @@ dependencies {
|
|||||||
### 1. 初始化(Application.onCreate)
|
### 1. 初始化(Application.onCreate)
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
XuqmSDK.init(
|
XuqmSDK.initialize(
|
||||||
context = this,
|
context = this,
|
||||||
appKey = "ak_your_app_key",
|
appId = "ak_your_app_id",
|
||||||
appSecret = "as_your_app_secret",
|
|
||||||
apiBaseUrl = "https://api.xuqm.com",
|
|
||||||
imBaseUrl = "wss://im.xuqm.com",
|
|
||||||
debug = BuildConfig.DEBUG
|
debug = BuildConfig.DEBUG
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. 用户登录后设置 Token
|
### 2. 用户登录后初始化 IM
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
// 调用你的业务登录接口获得 token 后
|
// 调用业务登录接口,拿到 userSig 后再登录 IM
|
||||||
XuqmSDK.tokenStore.save(token)
|
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(租户平台获取) |
|
| `appId` | String | 应用标识(租户平台获取) |
|
||||||
| `appSecret` | String | 应用 Secret |
|
|
||||||
| `apiBaseUrl` | String | 后端 API 地址 |
|
|
||||||
| `imBaseUrl` | String | IM WebSocket 地址(wss://) |
|
|
||||||
| `debug` | Boolean | 开启日志 |
|
| `debug` | Boolean | 开启日志 |
|
||||||
|
|
||||||
### TokenStore
|
### TokenStore
|
||||||
@ -172,18 +169,9 @@ data class ImMessage(
|
|||||||
|
|
||||||
## sdk-push
|
## sdk-push
|
||||||
|
|
||||||
### 注册推送 Token
|
### 推送接入
|
||||||
|
|
||||||
在获取到厂商推送 token 后调用:
|
在 `PushSDK.initialize()` 后由 SDK 自动完成厂商初始化、设备注册与 token 上传,不再要求业务侧单独调用注册接口。
|
||||||
|
|
||||||
```kotlin
|
|
||||||
PushSDK.registerToken(
|
|
||||||
appId = "ak_xxx",
|
|
||||||
userId = "user_001",
|
|
||||||
vendor = Vendor.HUAWEI, // HUAWEI / XIAOMI / OPPO / VIVO / HONOR / APNS
|
|
||||||
token = "device_push_token"
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 与 IM 联动
|
### 与 IM 联动
|
||||||
|
|
||||||
|
|||||||
@ -3,8 +3,6 @@ plugins {
|
|||||||
alias(libs.plugins.android.library) apply false
|
alias(libs.plugins.android.library) apply false
|
||||||
alias(libs.plugins.kotlin.compose) apply false
|
alias(libs.plugins.kotlin.compose) apply false
|
||||||
alias(libs.plugins.kotlin.serialization) 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"
|
group = "com.xuqm"
|
||||||
|
|||||||
@ -21,10 +21,12 @@ junit4 = "4.13.2"
|
|||||||
androidxJunit = "1.3.0"
|
androidxJunit = "1.3.0"
|
||||||
espresso = "3.7.0"
|
espresso = "3.7.0"
|
||||||
hilt = "2.56.2"
|
hilt = "2.56.2"
|
||||||
|
ksp = "2.3.7"
|
||||||
navigationCompose = "2.9.0"
|
navigationCompose = "2.9.0"
|
||||||
securityCrypto = "1.0.0"
|
securityCrypto = "1.0.0"
|
||||||
hiltNavigationCompose = "1.2.0"
|
hiltNavigationCompose = "1.2.0"
|
||||||
viewmodelCompose = "2.10.0"
|
viewmodelCompose = "2.10.0"
|
||||||
|
material = "1.13.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
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-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
||||||
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
||||||
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
|
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-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
|
||||||
androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" }
|
androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" }
|
||||||
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
|
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" }
|
android-library = { id = "com.android.library", version.ref = "agp" }
|
||||||
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||||
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", 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" }
|
hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
alias(libs.plugins.kotlin.compose)
|
alias(libs.plugins.kotlin.compose)
|
||||||
alias(libs.plugins.kotlin.kapt)
|
|
||||||
alias(libs.plugins.hilt.android)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@ -25,12 +23,10 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_21
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
targetCompatibility = JavaVersion.VERSION_21
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions { jvmTarget = "21" }
|
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose = true
|
compose = true
|
||||||
buildConfig = true
|
buildConfig = true
|
||||||
@ -48,13 +44,10 @@ dependencies {
|
|||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
implementation(libs.androidx.lifecycle.runtime.compose)
|
implementation(libs.androidx.lifecycle.runtime.compose)
|
||||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||||
|
implementation(libs.google.material)
|
||||||
|
|
||||||
implementation(libs.androidx.navigation.compose)
|
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.compose)
|
||||||
implementation(libs.coil.network.okhttp)
|
implementation(libs.coil.network.okhttp)
|
||||||
|
|
||||||
|
|||||||
@ -4,24 +4,18 @@ import android.os.Bundle
|
|||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
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.navigation.AppNavGraph
|
||||||
import com.xuqm.sdk.sample.ui.theme.XuqmTheme
|
import com.xuqm.sdk.sample.ui.theme.XuqmTheme
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var authRepository: AuthRepository
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
setContent {
|
setContent {
|
||||||
XuqmTheme {
|
XuqmTheme {
|
||||||
AppNavGraph(authRepository = authRepository)
|
AppNavGraph(authRepository = AppDependencies.authRepository)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,16 +3,16 @@ package com.xuqm.sdk.sample
|
|||||||
import android.app.Application
|
import android.app.Application
|
||||||
import com.xuqm.sdk.XuqmSDK
|
import com.xuqm.sdk.XuqmSDK
|
||||||
import com.xuqm.sdk.core.LogLevel
|
import com.xuqm.sdk.core.LogLevel
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import com.xuqm.sdk.sample.di.AppDependencies
|
||||||
|
|
||||||
@HiltAndroidApp
|
|
||||||
class XuqmSampleApp : Application() {
|
class XuqmSampleApp : Application() {
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
AppDependencies.init(this)
|
||||||
XuqmSDK.initialize(
|
XuqmSDK.initialize(
|
||||||
context = this,
|
context = this,
|
||||||
appId = "your_app_id",
|
appId = "ak_demo_chat",
|
||||||
logLevel = if (BuildConfig.DEBUG) LogLevel.DEBUG else LogLevel.WARN,
|
logLevel = if (BuildConfig.DEBUG) LogLevel.DEBUG else LogLevel.WARN,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,46 +1,105 @@
|
|||||||
package com.xuqm.sdk.sample.data.api
|
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.Body
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
import retrofit2.http.POST
|
import retrofit2.http.POST
|
||||||
import retrofit2.http.PUT
|
import retrofit2.http.PUT
|
||||||
import retrofit2.http.Query
|
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>(
|
data class DemoResponse<T>(
|
||||||
val code: Int = 0,
|
val code: Int = 0,
|
||||||
val message: String? = null,
|
val status: String? = null,
|
||||||
val data: T? = 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 {
|
interface DemoApi {
|
||||||
|
|
||||||
@POST("api/user/login")
|
@POST("api/demo/auth/login")
|
||||||
suspend fun login(@Body request: LoginRequest): DemoResponse<LoginData>
|
suspend fun login(@Body request: LoginRequest): DemoResponse<AuthResult>
|
||||||
|
|
||||||
@POST("api/user/register")
|
@POST("api/demo/auth/register")
|
||||||
suspend fun register(@Body request: RegisterRequest): DemoResponse<LoginData>
|
suspend fun register(@Body request: RegisterRequest): DemoResponse<AuthResult>
|
||||||
|
|
||||||
@GET("api/user/profile")
|
@POST("api/demo/auth/reset-password")
|
||||||
suspend fun getProfile(): DemoResponse<UserData>
|
|
||||||
|
|
||||||
@PUT("api/user/profile")
|
|
||||||
suspend fun updateProfile(@Body request: UpdateProfileRequest): DemoResponse<UserData>
|
|
||||||
|
|
||||||
@POST("api/user/reset-password")
|
|
||||||
suspend fun resetPassword(@Body request: ResetPasswordRequest): DemoResponse<Unit>
|
suspend fun resetPassword(@Body request: ResetPasswordRequest): DemoResponse<Unit>
|
||||||
|
|
||||||
@GET("api/user/users")
|
@GET("api/demo/user/profile")
|
||||||
suspend fun searchUsers(@Query("keyword") keyword: String): DemoResponse<List<UserData>>
|
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 android.content.Context
|
||||||
import androidx.security.crypto.EncryptedSharedPreferences
|
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.im.ImSDK
|
||||||
import com.xuqm.sdk.sample.data.api.DemoApi
|
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.LoginRequest
|
||||||
import com.xuqm.sdk.sample.data.api.RegisterRequest
|
import com.xuqm.sdk.sample.data.api.RegisterRequest
|
||||||
import com.xuqm.sdk.sample.data.api.ResetPasswordRequest
|
import com.xuqm.sdk.sample.data.api.ResetPasswordRequest
|
||||||
import com.xuqm.sdk.sample.data.api.UpdateProfileRequest
|
import com.xuqm.sdk.sample.data.api.UpdateProfileRequest
|
||||||
import com.xuqm.sdk.sample.data.api.UserData
|
import com.xuqm.sdk.sample.data.api.UserData
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import javax.inject.Inject
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
@Singleton
|
class AuthRepository(context: Context) {
|
||||||
class AuthRepository @Inject constructor(
|
|
||||||
@ApplicationContext private val context: Context,
|
|
||||||
private val api: DemoApi,
|
|
||||||
) {
|
|
||||||
private val prefs = EncryptedSharedPreferences.create(
|
private val prefs = EncryptedSharedPreferences.create(
|
||||||
context,
|
|
||||||
"xuqm_demo_auth",
|
"xuqm_demo_auth",
|
||||||
MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build(),
|
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
|
||||||
|
context,
|
||||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
|
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private val api: DemoApi = DemoApiFactory.create(::getDemoToken)
|
||||||
|
|
||||||
fun getDemoToken(): String? = prefs.getString("demo_token", null)
|
fun getDemoToken(): String? = prefs.getString("demo_token", null)
|
||||||
fun getCurrentUserId(): String? = prefs.getString("user_id", null)
|
fun getCurrentUserId(): String? = prefs.getString("user_id", null)
|
||||||
fun getCurrentNickname(): String? = prefs.getString("nickname", null)
|
fun getCurrentNickname(): String? = prefs.getString("nickname", null)
|
||||||
fun getCurrentAvatar(): String? = prefs.getString("avatar", null)
|
fun getCurrentAvatar(): String? = prefs.getString("avatar", null)
|
||||||
fun isLoggedIn(): Boolean = getDemoToken() != 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()
|
prefs.edit()
|
||||||
.putString("demo_token", token)
|
.putString("demo_token", result.demoToken)
|
||||||
.putString("user_id", userId)
|
.putString("user_id", profile.userId)
|
||||||
.putString("nickname", nickname)
|
.putString("nickname", profile.nickname)
|
||||||
.putString("avatar", avatar)
|
.putString("avatar", profile.avatar)
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun login(userId: String, password: String): Result<UserData> =
|
suspend fun login(userId: String, password: String): Result<UserData> =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
runCatching {
|
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" }
|
val data = requireNotNull(res.data) { res.message ?: "Login failed" }
|
||||||
saveSession(data.token, data.userId, data.nickname, data.avatar)
|
saveSession(data)
|
||||||
ImSDK.login(data.userId, data.nickname, data.avatar)
|
data.imToken?.takeIf { it.isNotBlank() }?.let { ImSDK.loginWithToken(data.profile.userId, it) }
|
||||||
UserData(data.userId, data.nickname, data.avatar)
|
?: 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> =
|
suspend fun register(userId: String, password: String, nickname: String): Result<UserData> =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
runCatching {
|
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" }
|
val data = requireNotNull(res.data) { res.message ?: "Register failed" }
|
||||||
saveSession(data.token, data.userId, data.nickname, data.avatar)
|
saveSession(data)
|
||||||
ImSDK.login(data.userId, data.nickname, data.avatar)
|
data.imToken?.takeIf { it.isNotBlank() }?.let { ImSDK.loginWithToken(data.profile.userId, it) }
|
||||||
UserData(data.userId, data.nickname, data.avatar)
|
?: 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> =
|
suspend fun getProfile(): Result<UserData> =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
runCatching {
|
runCatching { requireNotNull(api.getProfile().data) { "Failed to get profile" } }
|
||||||
requireNotNull(api.getProfile().data) { "Failed to get profile" }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun updateProfile(nickname: String, avatar: String?): Result<UserData> =
|
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> =
|
suspend fun resetPassword(oldPassword: String, newPassword: String): Result<Unit> =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
runCatching { api.resetPassword(ResetPasswordRequest(oldPassword, newPassword)) }
|
runCatching { api.changePassword(ChangePasswordRequest(oldPassword, newPassword)) }.map { }
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun searchUsers(keyword: String): Result<List<UserData>> =
|
suspend fun searchUsers(keyword: String): Result<List<UserData>> =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
runCatching { api.searchUsers(keyword).data ?: emptyList() }
|
runCatching { api.searchUsers(keyword = keyword).data ?: emptyList() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun logout() {
|
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.KeyboardType
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
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
|
@Composable
|
||||||
fun LoginScreen(
|
fun LoginScreen(
|
||||||
onLoginSuccess: () -> Unit,
|
onLoginSuccess: () -> Unit,
|
||||||
onNavigateToRegister: () -> Unit,
|
onNavigateToRegister: () -> Unit,
|
||||||
viewModel: LoginViewModel = hiltViewModel(),
|
viewModel: LoginViewModel = viewModel(
|
||||||
|
factory = viewModelFactory {
|
||||||
|
initializer { LoginViewModel(AppDependencies.authRepository) }
|
||||||
|
}
|
||||||
|
),
|
||||||
) {
|
) {
|
||||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
|||||||
@ -3,11 +3,9 @@ package com.xuqm.sdk.sample.ui.auth
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.xuqm.sdk.sample.data.repo.AuthRepository
|
import com.xuqm.sdk.sample.data.repo.AuthRepository
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
sealed interface LoginState {
|
sealed interface LoginState {
|
||||||
data object Idle : LoginState
|
data object Idle : LoginState
|
||||||
@ -16,10 +14,7 @@ sealed interface LoginState {
|
|||||||
data class Error(val message: String) : LoginState
|
data class Error(val message: String) : LoginState
|
||||||
}
|
}
|
||||||
|
|
||||||
@HiltViewModel
|
class LoginViewModel(private val authRepository: AuthRepository) : ViewModel() {
|
||||||
class LoginViewModel @Inject constructor(
|
|
||||||
private val authRepository: AuthRepository,
|
|
||||||
) : ViewModel() {
|
|
||||||
|
|
||||||
private val _state = MutableStateFlow<LoginState>(LoginState.Idle)
|
private val _state = MutableStateFlow<LoginState>(LoginState.Idle)
|
||||||
val state: StateFlow<LoginState> = _state
|
val state: StateFlow<LoginState> = _state
|
||||||
|
|||||||
@ -29,21 +29,19 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewModelScope
|
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 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.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
class RegisterViewModel(private val authRepository: AuthRepository) : ViewModel() {
|
||||||
class RegisterViewModel @Inject constructor(
|
|
||||||
private val authRepository: AuthRepository,
|
|
||||||
) : ViewModel() {
|
|
||||||
|
|
||||||
private val _state = MutableStateFlow<LoginState>(LoginState.Idle)
|
private val _state = MutableStateFlow<LoginState>(LoginState.Idle)
|
||||||
val state: StateFlow<LoginState> = _state
|
val state: StateFlow<LoginState> = _state
|
||||||
@ -63,7 +61,11 @@ class RegisterViewModel @Inject constructor(
|
|||||||
fun RegisterScreen(
|
fun RegisterScreen(
|
||||||
onRegisterSuccess: () -> Unit,
|
onRegisterSuccess: () -> Unit,
|
||||||
onNavigateBack: () -> Unit,
|
onNavigateBack: () -> Unit,
|
||||||
viewModel: RegisterViewModel = hiltViewModel(),
|
viewModel: RegisterViewModel = viewModel(
|
||||||
|
factory = viewModelFactory {
|
||||||
|
initializer { RegisterViewModel(AppDependencies.authRepository) }
|
||||||
|
}
|
||||||
|
),
|
||||||
) {
|
) {
|
||||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
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.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
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.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.xuqm.sdk.im.model.ImMessage
|
import com.xuqm.sdk.im.model.ImMessage
|
||||||
|
import com.xuqm.sdk.ui.InitialAvatar
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@ -51,7 +51,7 @@ fun ChatScreen(
|
|||||||
targetName: String,
|
targetName: String,
|
||||||
onNavigateBack: () -> Unit,
|
onNavigateBack: () -> Unit,
|
||||||
onGroupSettings: (() -> Unit)? = null,
|
onGroupSettings: (() -> Unit)? = null,
|
||||||
viewModel: ChatViewModel = hiltViewModel(),
|
viewModel: ChatViewModel = viewModel(),
|
||||||
) {
|
) {
|
||||||
val messages by viewModel.messages.collectAsStateWithLifecycle()
|
val messages by viewModel.messages.collectAsStateWithLifecycle()
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
@ -166,14 +166,7 @@ private fun MessageBubble(message: ImMessage, isOwn: Boolean) {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun AvatarPlaceholder(userId: String) {
|
private fun AvatarPlaceholder(userId: String) {
|
||||||
Surface(
|
InitialAvatar(text = userId, modifier = Modifier.size(32.dp))
|
||||||
modifier = Modifier.size(32.dp).clip(CircleShape),
|
|
||||||
color = MaterialTheme.colorScheme.secondaryContainer,
|
|
||||||
) {
|
|
||||||
Box(contentAlignment = Alignment.Center) {
|
|
||||||
Text(userId.take(1).uppercase(), style = MaterialTheme.typography.labelSmall)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseContent(message: ImMessage): String {
|
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.ImSDK
|
||||||
import com.xuqm.sdk.im.listener.ImEventListener
|
import com.xuqm.sdk.im.listener.ImEventListener
|
||||||
import com.xuqm.sdk.im.model.ImMessage
|
import com.xuqm.sdk.im.model.ImMessage
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
class ChatViewModel : ViewModel() {
|
||||||
class ChatViewModel @Inject constructor() : ViewModel() {
|
|
||||||
|
|
||||||
private val _messages = MutableStateFlow<List<ImMessage>>(emptyList())
|
private val _messages = MutableStateFlow<List<ImMessage>>(emptyList())
|
||||||
val messages: StateFlow<List<ImMessage>> = _messages
|
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.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
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.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
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.ViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.xuqm.sdk.im.ImSDK
|
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.api.UserData
|
||||||
import com.xuqm.sdk.sample.data.repo.AuthRepository
|
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.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
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(
|
||||||
class ContactViewModel @Inject constructor(
|
|
||||||
private val authRepository: AuthRepository,
|
private val authRepository: AuthRepository,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _friends = MutableStateFlow<List<UserProfile>>(emptyList())
|
private val _friends = MutableStateFlow<List<UserData>>(emptyList())
|
||||||
val friends: StateFlow<List<UserProfile>> = _friends
|
val friends: StateFlow<List<UserData>> = _friends
|
||||||
|
|
||||||
private val _searchResults = MutableStateFlow<List<UserData>>(emptyList())
|
private val _searchResults = MutableStateFlow<List<UserData>>(emptyList())
|
||||||
val searchResults: StateFlow<List<UserData>> = _searchResults
|
val searchResults: StateFlow<List<UserData>> = _searchResults
|
||||||
@ -55,8 +49,12 @@ class ContactViewModel @Inject constructor(
|
|||||||
|
|
||||||
fun loadFriends() {
|
fun loadFriends() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
runCatching { ImSDK.listFriends() }
|
runCatching {
|
||||||
.onSuccess { _friends.value = it }
|
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
|
@Composable
|
||||||
fun ContactScreen(
|
fun ContactScreen(
|
||||||
onOpenChat: (userId: String) -> Unit,
|
onOpenChat: (userId: String) -> Unit,
|
||||||
viewModel: ContactViewModel = hiltViewModel(),
|
viewModel: ContactViewModel = viewModel(
|
||||||
|
factory = viewModelFactory {
|
||||||
|
initializer { ContactViewModel(AppDependencies.authRepository) }
|
||||||
|
}
|
||||||
|
),
|
||||||
) {
|
) {
|
||||||
val friends by viewModel.friends.collectAsStateWithLifecycle()
|
val friends by viewModel.friends.collectAsStateWithLifecycle()
|
||||||
val searchResults by viewModel.searchResults.collectAsStateWithLifecycle()
|
val searchResults by viewModel.searchResults.collectAsStateWithLifecycle()
|
||||||
var keyword by remember { mutableStateOf("") }
|
var keyword by remember { mutableStateOf("") }
|
||||||
|
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
OutlinedTextField(
|
SearchBarField(
|
||||||
value = keyword,
|
value = keyword,
|
||||||
onValueChange = { keyword = it; viewModel.search(it) },
|
onValueChange = { keyword = it; viewModel.search(it) },
|
||||||
modifier = Modifier.fillMaxWidth().padding(12.dp),
|
modifier = Modifier
|
||||||
placeholder = { Text("搜索用户 ID 或昵称") },
|
.fillMaxWidth()
|
||||||
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
|
.padding(12.dp),
|
||||||
singleLine = true,
|
placeholder = "搜索用户 ID 或昵称",
|
||||||
)
|
)
|
||||||
|
|
||||||
if (keyword.isBlank()) {
|
if (keyword.isBlank()) {
|
||||||
|
|||||||
@ -13,13 +13,11 @@ import androidx.compose.foundation.layout.size
|
|||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.material3.Badge
|
import androidx.compose.material3.Badge
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@ -32,19 +30,18 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
import com.xuqm.sdk.im.model.ConversationData
|
import com.xuqm.sdk.im.model.ConversationData
|
||||||
|
import com.xuqm.sdk.ui.InitialAvatar
|
||||||
|
import com.xuqm.sdk.utils.TimeFormatters
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Date
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ConversationScreen(
|
fun ConversationScreen(
|
||||||
onOpenChat: (targetId: String, chatType: String, targetName: String) -> Unit,
|
onOpenChat: (targetId: String, chatType: String, targetName: String) -> Unit,
|
||||||
viewModel: ConversationViewModel = hiltViewModel(),
|
viewModel: ConversationViewModel = viewModel(),
|
||||||
) {
|
) {
|
||||||
val conversations by viewModel.conversations.collectAsStateWithLifecycle()
|
val conversations by viewModel.conversations.collectAsStateWithLifecycle()
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
@ -84,17 +81,7 @@ private fun ConversationItem(
|
|||||||
.padding(horizontal = 16.dp, vertical = 10.dp),
|
.padding(horizontal = 16.dp, vertical = 10.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Surface(
|
InitialAvatar(text = conversation.targetId, modifier = Modifier.size(48.dp))
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(Modifier.width(12.dp))
|
Spacer(Modifier.width(12.dp))
|
||||||
|
|
||||||
@ -110,7 +97,7 @@ private fun ConversationItem(
|
|||||||
color = MaterialTheme.colorScheme.primary)
|
color = MaterialTheme.colorScheme.primary)
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
formatTime(conversation.lastMsgTime),
|
TimeFormatters.formatConversationTime(conversation.lastMsgTime),
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color = MaterialTheme.colorScheme.outline,
|
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.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.xuqm.sdk.im.ImSDK
|
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.ConversationData
|
||||||
import com.xuqm.sdk.im.model.ImMessage
|
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.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
class ConversationViewModel : ViewModel() {
|
||||||
class ConversationViewModel @Inject constructor() : ViewModel() {
|
|
||||||
|
|
||||||
private val _conversations = MutableStateFlow<List<ConversationData>>(emptyList())
|
private val _conversations = MutableStateFlow<List<ConversationData>>(emptyList())
|
||||||
val conversations: StateFlow<List<ConversationData>> = _conversations
|
val conversations: StateFlow<List<ConversationData>> = _conversations
|
||||||
|
|||||||
@ -37,7 +37,7 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.xuqm.sdk.im.ImSDK
|
import com.xuqm.sdk.im.ImSDK
|
||||||
import com.xuqm.sdk.im.model.ImGroup
|
import com.xuqm.sdk.im.model.ImGroup
|
||||||
@ -47,7 +47,7 @@ import com.xuqm.sdk.im.model.ImGroup
|
|||||||
fun GroupListScreen(
|
fun GroupListScreen(
|
||||||
onOpenGroupChat: (groupId: String, groupName: String) -> Unit,
|
onOpenGroupChat: (groupId: String, groupName: String) -> Unit,
|
||||||
onGroupSettings: (groupId: String) -> Unit,
|
onGroupSettings: (groupId: String) -> Unit,
|
||||||
viewModel: GroupViewModel = hiltViewModel(),
|
viewModel: GroupViewModel = viewModel(),
|
||||||
) {
|
) {
|
||||||
val groups by viewModel.groups.collectAsStateWithLifecycle()
|
val groups by viewModel.groups.collectAsStateWithLifecycle()
|
||||||
var showCreateDialog by remember { mutableStateOf(false) }
|
var showCreateDialog by remember { mutableStateOf(false) }
|
||||||
@ -150,7 +150,7 @@ private fun CreateGroupDialog(onDismiss: () -> Unit, onCreate: (String, List<Str
|
|||||||
fun GroupSettingsScreen(
|
fun GroupSettingsScreen(
|
||||||
groupId: String,
|
groupId: String,
|
||||||
onNavigateBack: () -> Unit,
|
onNavigateBack: () -> Unit,
|
||||||
viewModel: GroupViewModel = hiltViewModel(),
|
viewModel: GroupViewModel = viewModel(),
|
||||||
) {
|
) {
|
||||||
val group by viewModel.currentGroup.collectAsStateWithLifecycle()
|
val group by viewModel.currentGroup.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
|||||||
@ -4,14 +4,11 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.xuqm.sdk.im.ImSDK
|
import com.xuqm.sdk.im.ImSDK
|
||||||
import com.xuqm.sdk.im.model.ImGroup
|
import com.xuqm.sdk.im.model.ImGroup
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
class GroupViewModel : ViewModel() {
|
||||||
class GroupViewModel @Inject constructor() : ViewModel() {
|
|
||||||
|
|
||||||
private val _groups = MutableStateFlow<List<ImGroup>>(emptyList())
|
private val _groups = MutableStateFlow<List<ImGroup>>(emptyList())
|
||||||
val groups: StateFlow<List<ImGroup>> = _groups
|
val groups: StateFlow<List<ImGroup>> = _groups
|
||||||
|
|||||||
@ -27,16 +27,17 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.unit.dp
|
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.ViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.xuqm.sdk.sample.data.repo.AuthRepository
|
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.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import androidx.lifecycle.viewmodel.initializer
|
||||||
|
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||||
|
|
||||||
data class ProfileUiState(
|
data class ProfileUiState(
|
||||||
val userId: String = "",
|
val userId: String = "",
|
||||||
@ -46,8 +47,7 @@ data class ProfileUiState(
|
|||||||
val error: String? = null,
|
val error: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
@HiltViewModel
|
class ProfileViewModel(
|
||||||
class ProfileViewModel @Inject constructor(
|
|
||||||
private val authRepository: AuthRepository,
|
private val authRepository: AuthRepository,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
@ -82,7 +82,11 @@ class ProfileViewModel @Inject constructor(
|
|||||||
@Composable
|
@Composable
|
||||||
fun ProfileScreen(
|
fun ProfileScreen(
|
||||||
onLogout: () -> Unit,
|
onLogout: () -> Unit,
|
||||||
viewModel: ProfileViewModel = hiltViewModel(),
|
viewModel: ProfileViewModel = viewModel(
|
||||||
|
factory = viewModelFactory {
|
||||||
|
initializer { ProfileViewModel(AppDependencies.authRepository) }
|
||||||
|
}
|
||||||
|
),
|
||||||
) {
|
) {
|
||||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
|||||||
@ -19,17 +19,16 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.unit.dp
|
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.ViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.xuqm.sdk.update.UpdateSDK
|
import com.xuqm.sdk.update.UpdateSDK
|
||||||
import com.xuqm.sdk.update.model.UpdateInfo
|
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.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
data class UpdateUiState(
|
data class UpdateUiState(
|
||||||
val isChecking: Boolean = false,
|
val isChecking: Boolean = false,
|
||||||
@ -38,8 +37,7 @@ data class UpdateUiState(
|
|||||||
val message: String? = null,
|
val message: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
@HiltViewModel
|
class UpdateViewModel : ViewModel() {
|
||||||
class UpdateViewModel @Inject constructor() : ViewModel() {
|
|
||||||
|
|
||||||
private val _state = MutableStateFlow(UpdateUiState())
|
private val _state = MutableStateFlow(UpdateUiState())
|
||||||
val state: StateFlow<UpdateUiState> = _state
|
val state: StateFlow<UpdateUiState> = _state
|
||||||
@ -67,7 +65,7 @@ class UpdateViewModel @Inject constructor() : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun UpdateScreen(viewModel: UpdateViewModel = hiltViewModel()) {
|
fun UpdateScreen(viewModel: UpdateViewModel = viewModel()) {
|
||||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.library)
|
alias(libs.plugins.android.library)
|
||||||
|
alias(libs.plugins.kotlin.compose)
|
||||||
alias(libs.plugins.kotlin.serialization)
|
alias(libs.plugins.kotlin.serialization)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -16,12 +17,19 @@ android {
|
|||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
compose = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
api(platform(libs.androidx.compose.bom))
|
||||||
api(libs.bundles.network)
|
api(libs.bundles.network)
|
||||||
api(libs.kotlinx.coroutines.android)
|
api(libs.kotlinx.coroutines.android)
|
||||||
api(libs.kotlinx.serialization.json)
|
api(libs.kotlinx.serialization.json)
|
||||||
api(libs.androidx.core.ktx)
|
api(libs.androidx.core.ktx)
|
||||||
api(libs.androidx.security.crypto)
|
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 android.content.Context
|
||||||
import androidx.security.crypto.EncryptedSharedPreferences
|
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 PREFS_NAME = "xuqm_sdk_secure"
|
||||||
private const val KEY_IM_TOKEN = "im_token"
|
private const val KEY_IM_TOKEN = "im_token"
|
||||||
@ -10,9 +10,9 @@ private const val KEY_IM_TOKEN = "im_token"
|
|||||||
class TokenStore(context: Context) {
|
class TokenStore(context: Context) {
|
||||||
|
|
||||||
private val prefs = EncryptedSharedPreferences.create(
|
private val prefs = EncryptedSharedPreferences.create(
|
||||||
context,
|
|
||||||
PREFS_NAME,
|
PREFS_NAME,
|
||||||
MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build(),
|
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
|
||||||
|
context,
|
||||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
|
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
package com.xuqm.sdk.core
|
package com.xuqm.sdk.core
|
||||||
|
|
||||||
internal const val BASE_URL = "https://dev.xuqinmin.com/"
|
const val BASE_URL = "https://dev.xuqinmin.com/"
|
||||||
internal const val WS_URL = "wss://dev.xuqinmin.com/ws/im"
|
const val WS_URL = "wss://dev.xuqinmin.com/ws/im"
|
||||||
|
|
||||||
data class SDKConfig(
|
data class SDKConfig(
|
||||||
val appId: String,
|
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 =
|
fun getDeviceId(context: Context): String =
|
||||||
Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID)
|
Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID)
|
||||||
?: Build.SERIAL
|
?: Build.FINGERPRINT
|
||||||
|
|
||||||
fun getDeviceModel(): String = "${Build.MANUFACTURER} ${Build.MODEL}"
|
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.ConversationData
|
||||||
import com.xuqm.sdk.im.model.ImGroup
|
import com.xuqm.sdk.im.model.ImGroup
|
||||||
import com.xuqm.sdk.im.model.ImMessage
|
import com.xuqm.sdk.im.model.ImMessage
|
||||||
import com.xuqm.sdk.im.model.UserProfile
|
|
||||||
import com.xuqm.sdk.network.ApiClient
|
import com.xuqm.sdk.network.ApiClient
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@ -31,10 +30,15 @@ object ImSDK {
|
|||||||
XuqmSDK.requireInit()
|
XuqmSDK.requireInit()
|
||||||
val res = api.login(LoginRequest(XuqmSDK.appId, userId, nickname, avatar))
|
val res = api.login(LoginRequest(XuqmSDK.appId, userId, nickname, avatar))
|
||||||
val token = requireNotNull(res.data?.token) { "IM login failed: ${res.message}" }
|
val token = requireNotNull(res.data?.token) { "IM login failed: ${res.message}" }
|
||||||
XuqmSDK.tokenStore.saveToken(token)
|
|
||||||
currentUserId = userId
|
currentUserId = userId
|
||||||
client = ImClient(WS_URL, token, XuqmSDK.appId)
|
connectWithToken(token)
|
||||||
client?.connect()
|
}
|
||||||
|
|
||||||
|
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) {
|
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> =
|
suspend fun fetchHistory(toId: String, page: Int = 0, size: Int = 20): List<ImMessage> =
|
||||||
withContext(Dispatchers.IO) {
|
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> =
|
suspend fun fetchGroupHistory(groupId: String, page: Int = 0, size: Int = 20): List<ImMessage> =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
api.fetchGroupHistory(groupId, page, size).data ?: emptyList()
|
api.fetchGroupHistory(groupId, XuqmSDK.appId, page, size).data ?: emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun listGroups(): List<ImGroup> =
|
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? =
|
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? =
|
suspend fun getGroupInfo(groupId: String): ImGroup? =
|
||||||
withContext(Dispatchers.IO) { api.getGroupInfo(groupId).data }
|
withContext(Dispatchers.IO) { api.getGroupInfo(groupId).data }
|
||||||
@ -70,19 +74,19 @@ object ImSDK {
|
|||||||
withContext(Dispatchers.IO) { api.removeGroupMember(groupId, userId) }
|
withContext(Dispatchers.IO) { api.removeGroupMember(groupId, userId) }
|
||||||
|
|
||||||
suspend fun leaveGroup(groupId: String) =
|
suspend fun leaveGroup(groupId: String) =
|
||||||
withContext(Dispatchers.IO) { api.leaveGroup(groupId) }
|
withContext(Dispatchers.IO) { api.removeGroupMember(groupId, currentUserId) }
|
||||||
|
|
||||||
suspend fun listFriends(): List<UserProfile> =
|
suspend fun listFriends(): List<String> =
|
||||||
withContext(Dispatchers.IO) { api.listFriends().data ?: emptyList() }
|
withContext(Dispatchers.IO) { api.listFriends(XuqmSDK.appId).data ?: emptyList() }
|
||||||
|
|
||||||
suspend fun addFriend(friendId: String) =
|
suspend fun addFriend(friendId: String) =
|
||||||
withContext(Dispatchers.IO) { api.addFriend(friendId) }
|
withContext(Dispatchers.IO) { api.addFriend(XuqmSDK.appId, friendId) }
|
||||||
|
|
||||||
suspend fun removeFriend(friendId: String) =
|
suspend fun removeFriend(friendId: String) =
|
||||||
withContext(Dispatchers.IO) { api.removeFriend(friendId) }
|
withContext(Dispatchers.IO) { api.removeFriend(friendId, XuqmSDK.appId) }
|
||||||
|
|
||||||
suspend fun listConversations(): List<ConversationData> =
|
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) =
|
suspend fun setConversationPinned(targetId: String, chatType: String, pinned: Boolean) =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
@ -95,10 +99,7 @@ object ImSDK {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun markRead(targetId: String, chatType: String = "SINGLE") =
|
suspend fun markRead(targetId: String, chatType: String = "SINGLE") =
|
||||||
withContext(Dispatchers.IO) { api.markRead(targetId, chatType) }
|
withContext(Dispatchers.IO) { api.markRead(targetId, XuqmSDK.appId, chatType) }
|
||||||
|
|
||||||
suspend fun getUserProfile(userId: String): UserProfile? =
|
|
||||||
withContext(Dispatchers.IO) { api.getUserProfile(userId).data }
|
|
||||||
|
|
||||||
fun addListener(listener: ImEventListener) = client?.addListener(listener)
|
fun addListener(listener: ImEventListener) = client?.addListener(listener)
|
||||||
fun removeListener(listener: ImEventListener) = client?.removeListener(listener)
|
fun removeListener(listener: ImEventListener) = client?.removeListener(listener)
|
||||||
@ -109,4 +110,11 @@ object ImSDK {
|
|||||||
currentUserId = ""
|
currentUserId = ""
|
||||||
XuqmSDK.tokenStore.clear()
|
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.ConversationData
|
||||||
import com.xuqm.sdk.im.model.ImGroup
|
import com.xuqm.sdk.im.model.ImGroup
|
||||||
import com.xuqm.sdk.im.model.ImMessage
|
import com.xuqm.sdk.im.model.ImMessage
|
||||||
import com.xuqm.sdk.im.model.UserProfile
|
|
||||||
import retrofit2.http.Body
|
import retrofit2.http.Body
|
||||||
import retrofit2.http.DELETE
|
import retrofit2.http.DELETE
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
@ -43,26 +42,30 @@ interface ImApi {
|
|||||||
@POST("api/im/auth/login")
|
@POST("api/im/auth/login")
|
||||||
suspend fun login(@Body request: LoginRequest): ApiResponse<LoginResponse>
|
suspend fun login(@Body request: LoginRequest): ApiResponse<LoginResponse>
|
||||||
|
|
||||||
@GET("api/im/messages")
|
@GET("api/im/messages/history/{toId}")
|
||||||
suspend fun fetchHistory(
|
suspend fun fetchHistory(
|
||||||
@Query("toId") toId: String,
|
@Path("toId") toId: String,
|
||||||
@Query("chatType") chatType: String,
|
@Query("appId") appId: String,
|
||||||
@Query("page") page: Int,
|
@Query("page") page: Int,
|
||||||
@Query("size") size: Int,
|
@Query("size") size: Int,
|
||||||
): ApiResponse<List<ImMessage>>
|
): ApiResponse<List<ImMessage>>
|
||||||
|
|
||||||
@GET("api/im/groups/{groupId}/messages")
|
@GET("api/im/messages/group-history/{groupId}")
|
||||||
suspend fun fetchGroupHistory(
|
suspend fun fetchGroupHistory(
|
||||||
@Path("groupId") groupId: String,
|
@Path("groupId") groupId: String,
|
||||||
|
@Query("appId") appId: String,
|
||||||
@Query("page") page: Int,
|
@Query("page") page: Int,
|
||||||
@Query("size") size: Int,
|
@Query("size") size: Int,
|
||||||
): ApiResponse<List<ImMessage>>
|
): ApiResponse<List<ImMessage>>
|
||||||
|
|
||||||
@GET("api/im/groups")
|
@GET("api/im/groups")
|
||||||
suspend fun listGroups(): ApiResponse<List<ImGroup>>
|
suspend fun listGroups(@Query("appId") appId: String): ApiResponse<List<ImGroup>>
|
||||||
|
|
||||||
@POST("api/im/groups")
|
@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}")
|
@GET("api/im/groups/{groupId}")
|
||||||
suspend fun getGroupInfo(@Path("groupId") groupId: String): ApiResponse<ImGroup>
|
suspend fun getGroupInfo(@Path("groupId") groupId: String): ApiResponse<ImGroup>
|
||||||
@ -89,16 +92,22 @@ interface ImApi {
|
|||||||
suspend fun leaveGroup(@Path("groupId") groupId: String): ApiResponse<Unit>
|
suspend fun leaveGroup(@Path("groupId") groupId: String): ApiResponse<Unit>
|
||||||
|
|
||||||
@GET("api/im/friends")
|
@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}")
|
@POST("api/im/friends")
|
||||||
suspend fun addFriend(@Path("friendId") friendId: String): ApiResponse<Unit>
|
suspend fun addFriend(
|
||||||
|
@Query("appId") appId: String,
|
||||||
|
@Query("friendId") friendId: String,
|
||||||
|
): ApiResponse<Unit>
|
||||||
|
|
||||||
@DELETE("api/im/friends/{friendId}")
|
@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")
|
@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")
|
@PUT("api/im/conversations/{targetId}/pinned")
|
||||||
suspend fun setConversationPinned(
|
suspend fun setConversationPinned(
|
||||||
@ -117,9 +126,7 @@ interface ImApi {
|
|||||||
@PUT("api/im/conversations/{targetId}/read")
|
@PUT("api/im/conversations/{targetId}/read")
|
||||||
suspend fun markRead(
|
suspend fun markRead(
|
||||||
@Path("targetId") targetId: String,
|
@Path("targetId") targetId: String,
|
||||||
|
@Query("appId") appId: String,
|
||||||
@Query("chatType") chatType: String,
|
@Query("chatType") chatType: String,
|
||||||
): ApiResponse<Unit>
|
): 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.XuqmSDK
|
||||||
import com.xuqm.sdk.network.ApiClient
|
import com.xuqm.sdk.network.ApiClient
|
||||||
import com.xuqm.sdk.push.api.PushApi
|
import com.xuqm.sdk.push.api.PushApi
|
||||||
import com.xuqm.sdk.push.api.RegisterDeviceRequest
|
|
||||||
import com.xuqm.sdk.utils.DeviceUtils
|
import com.xuqm.sdk.utils.DeviceUtils
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@ -22,14 +21,10 @@ object PushSDK {
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
runCatching {
|
runCatching {
|
||||||
api.registerDevice(
|
api.registerDevice(
|
||||||
RegisterDeviceRequest(
|
appId = XuqmSDK.appId,
|
||||||
userId = userId,
|
userId = userId,
|
||||||
appId = XuqmSDK.appId,
|
vendor = vendor,
|
||||||
platform = "Android",
|
token = deviceId,
|
||||||
vendor = vendor,
|
|
||||||
pushToken = "",
|
|
||||||
deviceId = deviceId,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,8 +16,13 @@ data class RegisterDeviceRequest(
|
|||||||
|
|
||||||
interface PushApi {
|
interface PushApi {
|
||||||
|
|
||||||
@POST("api/push/device/register")
|
@POST("api/push/register")
|
||||||
suspend fun registerDevice(@Body request: RegisterDeviceRequest)
|
suspend fun registerDevice(
|
||||||
|
@Query("appId") appId: String,
|
||||||
|
@Query("userId") userId: String,
|
||||||
|
@Query("vendor") vendor: String,
|
||||||
|
@Query("token") token: String,
|
||||||
|
)
|
||||||
|
|
||||||
@DELETE("api/push/device/unregister")
|
@DELETE("api/push/device/unregister")
|
||||||
suspend fun unregisterDevice(
|
suspend fun unregisterDevice(
|
||||||
|
|||||||
@ -18,12 +18,31 @@ object UpdateSDK {
|
|||||||
|
|
||||||
private val api: UpdateApi by lazy { ApiClient.create() }
|
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) {
|
suspend fun checkAppUpdate(context: Context): UpdateInfo? = withContext(Dispatchers.IO) {
|
||||||
XuqmSDK.requireInit()
|
XuqmSDK.requireInit()
|
||||||
val versionCode = context.packageManager
|
val versionCode = context.packageManager
|
||||||
.getPackageInfo(context.packageName, 0).longVersionCode.toInt()
|
.getPackageInfo(context.packageName, 0).longVersionCode.toInt()
|
||||||
runCatching {
|
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()
|
}.getOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,7 +85,9 @@ object UpdateSDK {
|
|||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
XuqmSDK.requireInit()
|
XuqmSDK.requireInit()
|
||||||
runCatching {
|
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()
|
}.getOrNull()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户