feat(chat): 添加聊天功能和用户认证仓库
- 新增AuthRepository用于用户认证和会话管理 - 实现聊天界面ChatScreen支持单聊和群聊 - 添加聊天视图模型ChatViewModel处理消息逻辑 - 创建群组管理界面GroupScreen支持群组操作 - 实现主界面MainScreen整合各功能模块 - 添加应用启动类XuqmSampleApp初始化配置 - 在ImSDK中增加连接状态管理功能
这个提交包含在:
父节点
c318a318a3
当前提交
64fefd1bbb
@ -4,9 +4,15 @@ 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 com.xuqm.sdk.sample.di.AppDependencies
|
import com.xuqm.sdk.sample.di.AppDependencies
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class XuqmSampleApp : Application() {
|
class XuqmSampleApp : Application() {
|
||||||
|
|
||||||
|
private val appScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
AppDependencies.init(this)
|
AppDependencies.init(this)
|
||||||
@ -15,5 +21,8 @@ class XuqmSampleApp : Application() {
|
|||||||
appId = "ak_demo_chat",
|
appId = "ak_demo_chat",
|
||||||
logLevel = if (BuildConfig.DEBUG) LogLevel.DEBUG else LogLevel.WARN,
|
logLevel = if (BuildConfig.DEBUG) LogLevel.DEBUG else LogLevel.WARN,
|
||||||
)
|
)
|
||||||
|
appScope.launch {
|
||||||
|
AppDependencies.authRepository.restoreSdkSession()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,6 +38,7 @@ class AuthRepository(context: Context) {
|
|||||||
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 getCurrentUserSig(): String? = prefs.getString("user_sig", null)
|
||||||
fun isLoggedIn(): Boolean = getDemoToken() != null
|
fun isLoggedIn(): Boolean = getDemoToken() != null
|
||||||
|
|
||||||
private fun saveSession(result: AuthResult) {
|
private fun saveSession(result: AuthResult) {
|
||||||
@ -113,6 +114,21 @@ class AuthRepository(context: Context) {
|
|||||||
runCatching { api.searchUsers(keyword = keyword).data ?: emptyList() }
|
runCatching { api.searchUsers(keyword = keyword).data ?: emptyList() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun restoreSdkSession(): Result<Unit> =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
runCatching {
|
||||||
|
val userId = getCurrentUserId()
|
||||||
|
val userSig = getCurrentUserSig()
|
||||||
|
if (userId.isNullOrBlank() || userSig.isNullOrBlank()) return@runCatching
|
||||||
|
XuqmSDK.login(
|
||||||
|
userId = userId,
|
||||||
|
userSig = userSig,
|
||||||
|
nickname = getCurrentNickname(),
|
||||||
|
avatar = getCurrentAvatar(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun logout() {
|
fun logout() {
|
||||||
XuqmSDK.logout()
|
XuqmSDK.logout()
|
||||||
prefs.edit().clear().apply()
|
prefs.edit().clear().apply()
|
||||||
|
|||||||
@ -34,6 +34,7 @@ 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.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Alignment
|
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
|
||||||
@ -41,7 +42,10 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.xuqm.sdk.im.model.ImMessage
|
import com.xuqm.sdk.im.model.ImMessage
|
||||||
|
import com.xuqm.sdk.im.ImSDK
|
||||||
import com.xuqm.sdk.ui.InitialAvatar
|
import com.xuqm.sdk.ui.InitialAvatar
|
||||||
|
import com.xuqm.sdk.sample.ui.common.ConnectionStatusBanner
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@ -54,31 +58,51 @@ fun ChatScreen(
|
|||||||
viewModel: ChatViewModel = viewModel(),
|
viewModel: ChatViewModel = viewModel(),
|
||||||
) {
|
) {
|
||||||
val messages by viewModel.messages.collectAsStateWithLifecycle()
|
val messages by viewModel.messages.collectAsStateWithLifecycle()
|
||||||
|
val scrollSignal by viewModel.scrollToBottomSignal.collectAsStateWithLifecycle()
|
||||||
|
val isLoadingMore by viewModel.isLoadingMore.collectAsStateWithLifecycle()
|
||||||
|
val hasMoreHistory by viewModel.hasMoreHistory.collectAsStateWithLifecycle()
|
||||||
|
val connectionState by ImSDK.connectionState.collectAsStateWithLifecycle()
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
var input by remember { mutableStateOf("") }
|
var input by remember { mutableStateOf("") }
|
||||||
|
|
||||||
LaunchedEffect(Unit) { viewModel.init(targetId, chatType) }
|
LaunchedEffect(Unit) { viewModel.init(targetId, chatType) }
|
||||||
LaunchedEffect(messages.size) {
|
LaunchedEffect(scrollSignal) {
|
||||||
if (messages.isNotEmpty()) listState.animateScrollToItem(0)
|
if (messages.isNotEmpty()) listState.animateScrollToItem(0)
|
||||||
}
|
}
|
||||||
|
LaunchedEffect(listState, hasMoreHistory, isLoadingMore, messages.size) {
|
||||||
|
snapshotFlow { listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0 }
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.collect { lastVisibleIndex ->
|
||||||
|
val shouldLoadMore = hasMoreHistory &&
|
||||||
|
!isLoadingMore &&
|
||||||
|
messages.isNotEmpty() &&
|
||||||
|
lastVisibleIndex >= messages.lastIndex - 2
|
||||||
|
if (shouldLoadMore) {
|
||||||
|
viewModel.loadMoreHistory()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
Column {
|
||||||
title = { Text(targetName) },
|
TopAppBar(
|
||||||
navigationIcon = {
|
title = { Text(targetName) },
|
||||||
IconButton(onClick = onNavigateBack) {
|
navigationIcon = {
|
||||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
|
IconButton(onClick = onNavigateBack) {
|
||||||
}
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
|
||||||
},
|
|
||||||
actions = {
|
|
||||||
if (chatType == "GROUP" && onGroupSettings != null) {
|
|
||||||
IconButton(onClick = onGroupSettings) {
|
|
||||||
Icon(Icons.Default.Settings, contentDescription = null)
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
actions = {
|
||||||
)
|
if (chatType == "GROUP" && onGroupSettings != null) {
|
||||||
|
IconButton(onClick = onGroupSettings) {
|
||||||
|
Icon(Icons.Default.Settings, contentDescription = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
ConnectionStatusBanner(connectionState)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
Row(
|
Row(
|
||||||
@ -118,6 +142,18 @@ fun ChatScreen(
|
|||||||
isOwn = msg.fromId == viewModel.currentUserId,
|
isOwn = msg.fromId == viewModel.currentUserId,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (isLoadingMore) {
|
||||||
|
item(key = "loading-more") {
|
||||||
|
Text(
|
||||||
|
"加载历史中...",
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 12.dp),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.outline,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,42 +14,89 @@ class ChatViewModel : 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
|
||||||
|
|
||||||
|
private val _isLoadingMore = MutableStateFlow(false)
|
||||||
|
val isLoadingMore: StateFlow<Boolean> = _isLoadingMore
|
||||||
|
|
||||||
|
private val _hasMoreHistory = MutableStateFlow(true)
|
||||||
|
val hasMoreHistory: StateFlow<Boolean> = _hasMoreHistory
|
||||||
|
|
||||||
|
private val _scrollToBottomSignal = MutableStateFlow(0L)
|
||||||
|
val scrollToBottomSignal: StateFlow<Long> = _scrollToBottomSignal
|
||||||
|
|
||||||
val currentUserId: String get() = ImSDK.currentUserId
|
val currentUserId: String get() = ImSDK.currentUserId
|
||||||
|
|
||||||
private lateinit var targetId: String
|
private lateinit var targetId: String
|
||||||
private lateinit var chatType: String
|
private lateinit var chatType: String
|
||||||
|
private var nextHistoryPage = 0
|
||||||
|
private var initialized = false
|
||||||
|
|
||||||
private val listener = object : ImEventListener {
|
private val listener = object : ImEventListener {
|
||||||
override fun onMessage(message: ImMessage) {
|
override fun onMessage(message: ImMessage) {
|
||||||
if (message.fromId == targetId || message.toId == targetId) {
|
if (isRelevant(message)) {
|
||||||
_messages.value = listOf(message) + _messages.value
|
prependMessage(message)
|
||||||
|
requestScrollToBottom()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onGroupMessage(message: ImMessage) {
|
override fun onGroupMessage(message: ImMessage) {
|
||||||
if (message.toId == targetId) {
|
if (isRelevant(message)) {
|
||||||
_messages.value = listOf(message) + _messages.value
|
prependMessage(message)
|
||||||
|
requestScrollToBottom()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun init(targetId: String, chatType: String) {
|
fun init(targetId: String, chatType: String) {
|
||||||
|
if (initialized && this.targetId == targetId && this.chatType == chatType) return
|
||||||
this.targetId = targetId
|
this.targetId = targetId
|
||||||
this.chatType = chatType
|
this.chatType = chatType
|
||||||
|
nextHistoryPage = 0
|
||||||
|
initialized = true
|
||||||
|
_messages.value = emptyList()
|
||||||
|
_hasMoreHistory.value = true
|
||||||
|
_isLoadingMore.value = false
|
||||||
ImSDK.addListener(listener)
|
ImSDK.addListener(listener)
|
||||||
loadHistory()
|
loadInitialHistory()
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
runCatching { ImSDK.markRead(targetId, chatType) }
|
runCatching { ImSDK.markRead(targetId, chatType) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadHistory() {
|
fun loadInitialHistory() {
|
||||||
|
if (!initialized) return
|
||||||
|
loadPage(replace = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadMoreHistory() {
|
||||||
|
if (!initialized || _isLoadingMore.value || !_hasMoreHistory.value) return
|
||||||
|
loadPage(replace = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadPage(replace: Boolean) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val history = if (chatType == "GROUP") {
|
_isLoadingMore.value = true
|
||||||
runCatching { ImSDK.fetchGroupHistory(targetId) }.getOrDefault(emptyList())
|
val page = if (replace) 0 else nextHistoryPage
|
||||||
} else {
|
val history = fetchHistory(page)
|
||||||
runCatching { ImSDK.fetchHistory(targetId) }.getOrDefault(emptyList())
|
if (replace) {
|
||||||
|
_messages.value = history
|
||||||
|
nextHistoryPage = 1
|
||||||
|
requestScrollToBottom()
|
||||||
|
} else if (history.isNotEmpty()) {
|
||||||
|
_messages.value = (_messages.value + history).distinctBy { it.id }
|
||||||
|
nextHistoryPage += 1
|
||||||
}
|
}
|
||||||
_messages.value = history
|
_hasMoreHistory.value = history.size >= HISTORY_PAGE_SIZE
|
||||||
|
_isLoadingMore.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun fetchHistory(page: Int): List<ImMessage> {
|
||||||
|
return if (chatType == "GROUP") {
|
||||||
|
runCatching { ImSDK.fetchGroupHistory(targetId, page = page, size = HISTORY_PAGE_SIZE) }
|
||||||
|
.getOrDefault(emptyList())
|
||||||
|
} else {
|
||||||
|
runCatching { ImSDK.fetchHistory(targetId, page = page, size = HISTORY_PAGE_SIZE) }
|
||||||
|
.getOrDefault(emptyList())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,7 +105,29 @@ class ChatViewModel : ViewModel() {
|
|||||||
ImSDK.sendMessage(targetId, chatType, "TEXT", content)
|
ImSDK.sendMessage(targetId, chatType, "TEXT", content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun prependMessage(message: ImMessage) {
|
||||||
|
if (_messages.value.any { it.id == message.id }) return
|
||||||
|
_messages.value = listOf(message) + _messages.value
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun requestScrollToBottom() {
|
||||||
|
_scrollToBottomSignal.value = _scrollToBottomSignal.value + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isRelevant(message: ImMessage): Boolean {
|
||||||
|
return if (chatType == "GROUP") {
|
||||||
|
message.chatType == "GROUP" && message.toId == targetId
|
||||||
|
} else {
|
||||||
|
message.chatType != "GROUP" && (message.fromId == targetId || message.toId == targetId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
ImSDK.removeListener(listener)
|
ImSDK.removeListener(listener)
|
||||||
|
initialized = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val HISTORY_PAGE_SIZE = 20
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,47 @@
|
|||||||
|
package com.xuqm.sdk.sample.ui.common
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.xuqm.sdk.im.model.ImConnectionState
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ConnectionStatusBanner(state: ImConnectionState) {
|
||||||
|
when (state) {
|
||||||
|
ImConnectionState.Connected -> Unit
|
||||||
|
ImConnectionState.Connecting -> StatusBanner(
|
||||||
|
text = "IM 连接中...",
|
||||||
|
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||||
|
)
|
||||||
|
is ImConnectionState.Disconnected -> StatusBanner(
|
||||||
|
text = "IM 已断开${state.reason?.let { ":$it" } ?: ""}",
|
||||||
|
containerColor = MaterialTheme.colorScheme.errorContainer,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onErrorContainer,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun StatusBanner(
|
||||||
|
text: String,
|
||||||
|
containerColor: androidx.compose.ui.graphics.Color,
|
||||||
|
contentColor: androidx.compose.ui.graphics.Color,
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
color = containerColor,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
color = contentColor,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -41,6 +41,7 @@ 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
|
||||||
|
import com.xuqm.sdk.sample.ui.common.ConnectionStatusBanner
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@ -153,19 +154,23 @@ fun GroupSettingsScreen(
|
|||||||
viewModel: GroupViewModel = viewModel(),
|
viewModel: GroupViewModel = viewModel(),
|
||||||
) {
|
) {
|
||||||
val group by viewModel.currentGroup.collectAsStateWithLifecycle()
|
val group by viewModel.currentGroup.collectAsStateWithLifecycle()
|
||||||
|
val connectionState by ImSDK.connectionState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
LaunchedEffect(groupId) { viewModel.loadGroupInfo(groupId) }
|
LaunchedEffect(groupId) { viewModel.loadGroupInfo(groupId) }
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
androidx.compose.foundation.layout.Column {
|
||||||
title = { Text(group?.name ?: "群设置") },
|
TopAppBar(
|
||||||
navigationIcon = {
|
title = { Text(group?.name ?: "群设置") },
|
||||||
IconButton(onClick = onNavigateBack) {
|
navigationIcon = {
|
||||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
|
IconButton(onClick = onNavigateBack) {
|
||||||
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
)
|
||||||
)
|
ConnectionStatusBanner(connectionState)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
) { padding ->
|
) { padding ->
|
||||||
Column(
|
Column(
|
||||||
|
|||||||
@ -25,11 +25,14 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.xuqm.sdk.im.ImSDK
|
||||||
import com.xuqm.sdk.sample.ui.contact.ContactScreen
|
import com.xuqm.sdk.sample.ui.contact.ContactScreen
|
||||||
import com.xuqm.sdk.sample.ui.conversation.ConversationScreen
|
import com.xuqm.sdk.sample.ui.conversation.ConversationScreen
|
||||||
import com.xuqm.sdk.sample.ui.group.GroupListScreen
|
import com.xuqm.sdk.sample.ui.group.GroupListScreen
|
||||||
import com.xuqm.sdk.sample.ui.profile.ProfileScreen
|
import com.xuqm.sdk.sample.ui.profile.ProfileScreen
|
||||||
import com.xuqm.sdk.sample.ui.update.UpdateScreen
|
import com.xuqm.sdk.sample.ui.update.UpdateScreen
|
||||||
|
import com.xuqm.sdk.sample.ui.common.ConnectionStatusBanner
|
||||||
|
|
||||||
private data class BottomTab(val label: String, val icon: ImageVector)
|
private data class BottomTab(val label: String, val icon: ImageVector)
|
||||||
|
|
||||||
@ -50,17 +53,21 @@ fun MainScreen(
|
|||||||
onLogout: () -> Unit,
|
onLogout: () -> Unit,
|
||||||
) {
|
) {
|
||||||
var selectedTab by remember { mutableIntStateOf(0) }
|
var selectedTab by remember { mutableIntStateOf(0) }
|
||||||
|
val connectionState by ImSDK.connectionState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
androidx.compose.foundation.layout.Column {
|
||||||
title = { Text(tabs[selectedTab].label) },
|
TopAppBar(
|
||||||
actions = {
|
title = { Text(tabs[selectedTab].label) },
|
||||||
IconButton(onClick = onOpenEnvironment) {
|
actions = {
|
||||||
Icon(Icons.Default.Settings, contentDescription = "环境设置")
|
IconButton(onClick = onOpenEnvironment) {
|
||||||
|
Icon(Icons.Default.Settings, contentDescription = "环境设置")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
)
|
||||||
)
|
ConnectionStatusBanner(connectionState)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
NavigationBar {
|
NavigationBar {
|
||||||
|
|||||||
@ -10,17 +10,35 @@ import com.xuqm.sdk.im.api.SetMutedRequest
|
|||||||
import com.xuqm.sdk.im.api.SetPinnedRequest
|
import com.xuqm.sdk.im.api.SetPinnedRequest
|
||||||
import com.xuqm.sdk.im.api.UpdateGroupRequest
|
import com.xuqm.sdk.im.api.UpdateGroupRequest
|
||||||
import com.xuqm.sdk.im.listener.ImEventListener
|
import com.xuqm.sdk.im.listener.ImEventListener
|
||||||
|
import com.xuqm.sdk.im.model.ImConnectionState
|
||||||
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.network.ApiClient
|
import com.xuqm.sdk.network.ApiClient
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
object ImSDK {
|
object ImSDK {
|
||||||
|
|
||||||
private var client: ImClient? = null
|
private var client: ImClient? = null
|
||||||
private val api: ImApi get() = ApiClient.create(ImApi::class.java, ServiceEndpointRegistry.imApiBaseUrl)
|
private val api: ImApi get() = ApiClient.create(ImApi::class.java, ServiceEndpointRegistry.imApiBaseUrl)
|
||||||
|
private val connectionListener = object : ImEventListener {
|
||||||
|
override fun onConnected() {
|
||||||
|
_connectionState.value = ImConnectionState.Connected
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDisconnected(reason: String?) {
|
||||||
|
_connectionState.value = ImConnectionState.Disconnected(reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(error: String) {
|
||||||
|
_connectionState.value = ImConnectionState.Disconnected(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private val _connectionState = MutableStateFlow<ImConnectionState>(ImConnectionState.Disconnected("未连接"))
|
||||||
|
val connectionState: StateFlow<ImConnectionState> = _connectionState
|
||||||
|
|
||||||
var currentUserId: String = ""
|
var currentUserId: String = ""
|
||||||
private set
|
private set
|
||||||
@ -131,7 +149,9 @@ object ImSDK {
|
|||||||
private fun connectWithToken(token: String) {
|
private fun connectWithToken(token: String) {
|
||||||
XuqmSDK.tokenStore.saveToken(token)
|
XuqmSDK.tokenStore.saveToken(token)
|
||||||
client?.disconnect()
|
client?.disconnect()
|
||||||
|
_connectionState.value = ImConnectionState.Connecting
|
||||||
client = ImClient(ServiceEndpointRegistry.imWsUrl, token, XuqmSDK.appId)
|
client = ImClient(ServiceEndpointRegistry.imWsUrl, token, XuqmSDK.appId)
|
||||||
|
client?.addListener(connectionListener)
|
||||||
client?.connect()
|
client?.connect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,6 +159,7 @@ object ImSDK {
|
|||||||
client?.disconnect()
|
client?.disconnect()
|
||||||
client = null
|
client = null
|
||||||
currentUserId = ""
|
currentUserId = ""
|
||||||
|
_connectionState.value = ImConnectionState.Disconnected("已断开")
|
||||||
if (clearTokenStore) {
|
if (clearTokenStore) {
|
||||||
XuqmSDK.tokenStore.clear()
|
XuqmSDK.tokenStore.clear()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,7 @@
|
|||||||
|
package com.xuqm.sdk.im.model
|
||||||
|
|
||||||
|
sealed interface ImConnectionState {
|
||||||
|
data object Connecting : ImConnectionState
|
||||||
|
data object Connected : ImConnectionState
|
||||||
|
data class Disconnected(val reason: String? = null) : ImConnectionState
|
||||||
|
}
|
||||||
正在加载...
在新工单中引用
屏蔽一个用户