feat(chat): 添加聊天界面和会话管理功能
- 实现了本地IM缓存功能,支持会话、消息历史和草稿的存储 - 开发了聊天界面UI组件,包含消息列表、输入框和搜索功能 - 创建了聊天相关的ViewModel,处理消息收发和状态管理 - 构建了会话列表界面,支持置顶、免打扰和删除操作 - 集成了群组功能,实现群聊管理和群设置界面 - 添加了实时消息推送和会话状态同步机制
这个提交包含在:
父节点
36f044f7b7
当前提交
59611de3c1
@ -139,6 +139,12 @@ val service = RetrofitFactory.create(MyApiService::class.java)
|
|||||||
- 联系人列表先读本地缓存,再刷新网络
|
- 联系人列表先读本地缓存,再刷新网络
|
||||||
- 聊天历史分页加载
|
- 聊天历史分页加载
|
||||||
- 当前会话本地搜索
|
- 当前会话本地搜索
|
||||||
|
- 输入草稿自动保存
|
||||||
|
- 群设置支持编辑群名和群公告
|
||||||
|
- 会话支持本地删除
|
||||||
|
- 显示总未读数
|
||||||
|
- 消息状态直接展示
|
||||||
|
- 会话置顶/免打扰/已读/草稿/删除同步服务端
|
||||||
- IM 连接状态提示
|
- IM 连接状态提示
|
||||||
- SDK 登录态恢复后自动重连
|
- SDK 登录态恢复后自动重连
|
||||||
|
|
||||||
|
|||||||
@ -13,10 +13,12 @@ class LocalImCache(context: Context) {
|
|||||||
fun loadConversations(): List<ConversationData> = readConversationList(KEY_CONVERSATIONS)
|
fun loadConversations(): List<ConversationData> = readConversationList(KEY_CONVERSATIONS)
|
||||||
|
|
||||||
fun saveConversations(conversations: List<ConversationData>) {
|
fun saveConversations(conversations: List<ConversationData>) {
|
||||||
prefs.edit().putString(KEY_CONVERSATIONS, serializeConversationList(conversations)).apply()
|
val visible = conversations.filterNot { isHiddenConversation(it.targetId, it.chatType) }
|
||||||
|
prefs.edit().putString(KEY_CONVERSATIONS, serializeConversationList(visible)).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun upsertConversation(conversation: ConversationData) {
|
fun upsertConversation(conversation: ConversationData) {
|
||||||
|
clearHiddenConversation(conversation.targetId, conversation.chatType)
|
||||||
val existing = loadConversations().firstOrNull {
|
val existing = loadConversations().firstOrNull {
|
||||||
it.targetId == conversation.targetId && it.chatType == conversation.chatType
|
it.targetId == conversation.targetId && it.chatType == conversation.chatType
|
||||||
}
|
}
|
||||||
@ -64,6 +66,29 @@ class LocalImCache(context: Context) {
|
|||||||
saveHistory(targetId, chatType, merged)
|
saveHistory(targetId, chatType, merged)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun saveDraft(targetId: String, chatType: String, draft: String) {
|
||||||
|
prefs.edit().putString(draftKey(targetId, chatType), draft).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadDraft(targetId: String, chatType: String): String =
|
||||||
|
prefs.getString(draftKey(targetId, chatType), "") ?: ""
|
||||||
|
|
||||||
|
fun clearDraft(targetId: String, chatType: String) {
|
||||||
|
prefs.edit().remove(draftKey(targetId, chatType)).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteConversation(targetId: String, chatType: String) {
|
||||||
|
val remaining = loadConversations().filterNot {
|
||||||
|
it.targetId == targetId && it.chatType == chatType
|
||||||
|
}
|
||||||
|
prefs.edit()
|
||||||
|
.putString(KEY_CONVERSATIONS, serializeConversationList(remaining))
|
||||||
|
.remove(historyKey(targetId, chatType))
|
||||||
|
.remove(draftKey(targetId, chatType))
|
||||||
|
.putStringSet(KEY_HIDDEN_CONVERSATIONS, loadHiddenConversations() + conversationKey(targetId, chatType))
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
private fun readConversationList(key: String): List<ConversationData> {
|
private fun readConversationList(key: String): List<ConversationData> {
|
||||||
val raw = prefs.getString(key, null) ?: return emptyList()
|
val raw = prefs.getString(key, null) ?: return emptyList()
|
||||||
return runCatching {
|
return runCatching {
|
||||||
@ -167,9 +192,25 @@ class LocalImCache(context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun historyKey(targetId: String, chatType: String) = "history_${chatType}_$targetId"
|
private fun historyKey(targetId: String, chatType: String) = "history_${chatType}_$targetId"
|
||||||
|
private fun draftKey(targetId: String, chatType: String) = "draft_${chatType}_$targetId"
|
||||||
|
private fun conversationKey(targetId: String, chatType: String) = "${chatType}_$targetId"
|
||||||
|
|
||||||
|
private fun loadHiddenConversations(): MutableSet<String> =
|
||||||
|
prefs.getStringSet(KEY_HIDDEN_CONVERSATIONS, emptySet())?.toMutableSet() ?: mutableSetOf()
|
||||||
|
|
||||||
|
private fun isHiddenConversation(targetId: String, chatType: String): Boolean =
|
||||||
|
loadHiddenConversations().contains(conversationKey(targetId, chatType))
|
||||||
|
|
||||||
|
private fun clearHiddenConversation(targetId: String, chatType: String) {
|
||||||
|
val hidden = loadHiddenConversations()
|
||||||
|
if (hidden.remove(conversationKey(targetId, chatType))) {
|
||||||
|
prefs.edit().putStringSet(KEY_HIDDEN_CONVERSATIONS, hidden).apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
const val PREFS_NAME = "xuqm_demo_im_cache"
|
const val PREFS_NAME = "xuqm_demo_im_cache"
|
||||||
const val KEY_CONVERSATIONS = "conversations"
|
const val KEY_CONVERSATIONS = "conversations"
|
||||||
|
const val KEY_HIDDEN_CONVERSATIONS = "hidden_conversations"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -61,12 +61,12 @@ fun ChatScreen(
|
|||||||
val messages by viewModel.messages.collectAsStateWithLifecycle()
|
val messages by viewModel.messages.collectAsStateWithLifecycle()
|
||||||
val searchQuery by viewModel.searchQuery.collectAsStateWithLifecycle()
|
val searchQuery by viewModel.searchQuery.collectAsStateWithLifecycle()
|
||||||
val searchResults by viewModel.searchResults.collectAsStateWithLifecycle()
|
val searchResults by viewModel.searchResults.collectAsStateWithLifecycle()
|
||||||
|
val draftText by viewModel.draftText.collectAsStateWithLifecycle()
|
||||||
val scrollSignal by viewModel.scrollToBottomSignal.collectAsStateWithLifecycle()
|
val scrollSignal by viewModel.scrollToBottomSignal.collectAsStateWithLifecycle()
|
||||||
val isLoadingMore by viewModel.isLoadingMore.collectAsStateWithLifecycle()
|
val isLoadingMore by viewModel.isLoadingMore.collectAsStateWithLifecycle()
|
||||||
val hasMoreHistory by viewModel.hasMoreHistory.collectAsStateWithLifecycle()
|
val hasMoreHistory by viewModel.hasMoreHistory.collectAsStateWithLifecycle()
|
||||||
val connectionState by ImSDK.connectionState.collectAsStateWithLifecycle()
|
val connectionState by ImSDK.connectionState.collectAsStateWithLifecycle()
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
var input by remember { mutableStateOf("") }
|
|
||||||
var showSearchBar by remember { mutableStateOf(false) }
|
var showSearchBar by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
LaunchedEffect(Unit) { viewModel.init(targetId, chatType) }
|
LaunchedEffect(Unit) { viewModel.init(targetId, chatType) }
|
||||||
@ -130,16 +130,16 @@ fun ChatScreen(
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = input,
|
value = draftText,
|
||||||
onValueChange = { input = it },
|
onValueChange = viewModel::updateDraft,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
placeholder = { Text("输入消息…") },
|
placeholder = { Text("输入消息…") },
|
||||||
maxLines = 4,
|
maxLines = 4,
|
||||||
shape = RoundedCornerShape(24.dp),
|
shape = RoundedCornerShape(24.dp),
|
||||||
)
|
)
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = { viewModel.sendText(input); input = "" },
|
onClick = { viewModel.sendText(draftText) },
|
||||||
enabled = input.isNotBlank(),
|
enabled = draftText.isNotBlank(),
|
||||||
) {
|
) {
|
||||||
Icon(Icons.AutoMirrored.Filled.Send, contentDescription = null)
|
Icon(Icons.AutoMirrored.Filled.Send, contentDescription = null)
|
||||||
}
|
}
|
||||||
@ -246,11 +246,19 @@ private fun MessageBubble(message: ImMessage, isOwn: Boolean) {
|
|||||||
.widthIn(max = 280.dp)
|
.widthIn(max = 280.dp)
|
||||||
.padding(horizontal = 4.dp),
|
.padding(horizontal = 4.dp),
|
||||||
) {
|
) {
|
||||||
Text(
|
Column(modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp)) {
|
||||||
text = parseContent(message),
|
Text(
|
||||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
|
text = parseContent(message),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
)
|
)
|
||||||
|
if (isOwn) {
|
||||||
|
Text(
|
||||||
|
text = statusLabel(message.status),
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = MaterialTheme.colorScheme.outline,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isOwn) {
|
if (isOwn) {
|
||||||
@ -259,6 +267,16 @@ private fun MessageBubble(message: ImMessage, isOwn: Boolean) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun statusLabel(status: String): String = when (status.uppercase()) {
|
||||||
|
"SENDING" -> "发送中"
|
||||||
|
"SENT" -> "已发送"
|
||||||
|
"DELIVERED" -> "已送达"
|
||||||
|
"READ" -> "已读"
|
||||||
|
"FAILED" -> "发送失败"
|
||||||
|
"REVOKED" -> "已撤回"
|
||||||
|
else -> status
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun AvatarPlaceholder(userId: String) {
|
private fun AvatarPlaceholder(userId: String) {
|
||||||
InitialAvatar(text = userId, modifier = Modifier.size(32.dp))
|
InitialAvatar(text = userId, modifier = Modifier.size(32.dp))
|
||||||
|
|||||||
@ -32,6 +32,9 @@ class ChatViewModel : ViewModel() {
|
|||||||
private val _searchResults = MutableStateFlow<List<ImMessage>>(emptyList())
|
private val _searchResults = MutableStateFlow<List<ImMessage>>(emptyList())
|
||||||
val searchResults: StateFlow<List<ImMessage>> = _searchResults
|
val searchResults: StateFlow<List<ImMessage>> = _searchResults
|
||||||
|
|
||||||
|
private val _draftText = MutableStateFlow("")
|
||||||
|
val draftText: StateFlow<String> = _draftText
|
||||||
|
|
||||||
val currentUserId: String get() = ImSDK.currentUserId
|
val currentUserId: String get() = ImSDK.currentUserId
|
||||||
|
|
||||||
private lateinit var targetId: String
|
private lateinit var targetId: String
|
||||||
@ -98,6 +101,7 @@ class ChatViewModel : ViewModel() {
|
|||||||
_isLoadingMore.value = false
|
_isLoadingMore.value = false
|
||||||
_searchQuery.value = ""
|
_searchQuery.value = ""
|
||||||
_searchResults.value = emptyList()
|
_searchResults.value = emptyList()
|
||||||
|
_draftText.value = cache.loadDraft(targetId, chatType)
|
||||||
ImSDK.addListener(listener)
|
ImSDK.addListener(listener)
|
||||||
loadInitialHistory()
|
loadInitialHistory()
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
@ -173,6 +177,8 @@ class ChatViewModel : ViewModel() {
|
|||||||
fun sendText(content: String) {
|
fun sendText(content: String) {
|
||||||
if (content.isBlank()) return
|
if (content.isBlank()) return
|
||||||
ImSDK.sendMessage(targetId, chatType, "TEXT", content)
|
ImSDK.sendMessage(targetId, chatType, "TEXT", content)
|
||||||
|
_draftText.value = ""
|
||||||
|
cache.clearDraft(targetId, chatType)
|
||||||
cache.upsertConversation(
|
cache.upsertConversation(
|
||||||
ConversationData(
|
ConversationData(
|
||||||
targetId = targetId,
|
targetId = targetId,
|
||||||
@ -196,6 +202,16 @@ class ChatViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateDraft(text: String) {
|
||||||
|
_draftText.value = text
|
||||||
|
if (initialized) {
|
||||||
|
cache.saveDraft(targetId, chatType, text)
|
||||||
|
viewModelScope.launch {
|
||||||
|
runCatching { ImSDK.setDraft(targetId, chatType, text) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun clearSearch() {
|
fun clearSearch() {
|
||||||
_searchQuery.value = ""
|
_searchQuery.value = ""
|
||||||
_searchResults.value = emptyList()
|
_searchResults.value = emptyList()
|
||||||
|
|||||||
@ -45,6 +45,7 @@ fun ConversationScreen(
|
|||||||
viewModel: ConversationViewModel = viewModel(),
|
viewModel: ConversationViewModel = viewModel(),
|
||||||
) {
|
) {
|
||||||
val conversations by viewModel.conversations.collectAsStateWithLifecycle()
|
val conversations by viewModel.conversations.collectAsStateWithLifecycle()
|
||||||
|
val totalUnreadCount by viewModel.totalUnreadCount.collectAsStateWithLifecycle()
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
var query by remember { mutableStateOf("") }
|
var query by remember { mutableStateOf("") }
|
||||||
val filtered = remember(conversations, query) {
|
val filtered = remember(conversations, query) {
|
||||||
@ -58,6 +59,12 @@ fun ConversationScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
|
Text(
|
||||||
|
"总未读 $totalUnreadCount",
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = MaterialTheme.colorScheme.outline,
|
||||||
|
)
|
||||||
SearchBarField(
|
SearchBarField(
|
||||||
value = query,
|
value = query,
|
||||||
onValueChange = { query = it },
|
onValueChange = { query = it },
|
||||||
@ -88,6 +95,9 @@ fun ConversationScreen(
|
|||||||
onMuteToggle = {
|
onMuteToggle = {
|
||||||
scope.launch { viewModel.setMuted(conv.targetId, conv.chatType, !conv.isMuted) }
|
scope.launch { viewModel.setMuted(conv.targetId, conv.chatType, !conv.isMuted) }
|
||||||
},
|
},
|
||||||
|
onDelete = {
|
||||||
|
viewModel.deleteConversation(conv.targetId, conv.chatType)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
HorizontalDivider(modifier = Modifier.padding(start = 72.dp))
|
HorizontalDivider(modifier = Modifier.padding(start = 72.dp))
|
||||||
}
|
}
|
||||||
@ -103,6 +113,7 @@ private fun ConversationItem(
|
|||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
onPinToggle: () -> Unit,
|
onPinToggle: () -> Unit,
|
||||||
onMuteToggle: () -> Unit,
|
onMuteToggle: () -> Unit,
|
||||||
|
onDelete: () -> Unit,
|
||||||
) {
|
) {
|
||||||
var showMenu by remember { mutableStateOf(false) }
|
var showMenu by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
@ -160,6 +171,10 @@ private fun ConversationItem(
|
|||||||
text = { Text(if (conversation.isMuted) "取消免打扰" else "免打扰") },
|
text = { Text(if (conversation.isMuted) "取消免打扰" else "免打扰") },
|
||||||
onClick = { showMenu = false; onMuteToggle() },
|
onClick = { showMenu = false; onMuteToggle() },
|
||||||
)
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("删除会话") },
|
||||||
|
onClick = { showMenu = false; onDelete() },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,6 +18,9 @@ class ConversationViewModel : 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
|
||||||
|
|
||||||
|
private val _totalUnreadCount = MutableStateFlow(0)
|
||||||
|
val totalUnreadCount: StateFlow<Int> = _totalUnreadCount
|
||||||
|
|
||||||
private val listener = object : ImEventListener {
|
private val listener = object : ImEventListener {
|
||||||
override fun onMessage(message: ImMessage) { refresh() }
|
override fun onMessage(message: ImMessage) { refresh() }
|
||||||
override fun onGroupMessage(message: ImMessage) { refresh() }
|
override fun onGroupMessage(message: ImMessage) { refresh() }
|
||||||
@ -26,6 +29,7 @@ class ConversationViewModel : ViewModel() {
|
|||||||
init {
|
init {
|
||||||
ImSDK.addListener(listener)
|
ImSDK.addListener(listener)
|
||||||
_conversations.value = cache.loadConversations().sortedByDescending { it.lastMsgTime }
|
_conversations.value = cache.loadConversations().sortedByDescending { it.lastMsgTime }
|
||||||
|
_totalUnreadCount.value = _conversations.value.sumOf { it.unreadCount }
|
||||||
refresh()
|
refresh()
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
ImSDK.connectionState.collect { state ->
|
ImSDK.connectionState.collect { state ->
|
||||||
@ -43,14 +47,27 @@ class ConversationViewModel : ViewModel() {
|
|||||||
val sorted = list.sortedByDescending { it.lastMsgTime }
|
val sorted = list.sortedByDescending { it.lastMsgTime }
|
||||||
cache.saveConversations(sorted)
|
cache.saveConversations(sorted)
|
||||||
_conversations.value = sorted
|
_conversations.value = sorted
|
||||||
|
_totalUnreadCount.value = sorted.sumOf { it.unreadCount }
|
||||||
}
|
}
|
||||||
.onFailure {
|
.onFailure {
|
||||||
val cached = cache.loadConversations().sortedByDescending { it.lastMsgTime }
|
val cached = cache.loadConversations().sortedByDescending { it.lastMsgTime }
|
||||||
if (cached.isNotEmpty()) _conversations.value = cached
|
if (cached.isNotEmpty()) {
|
||||||
|
_conversations.value = cached
|
||||||
|
_totalUnreadCount.value = cached.sumOf { it.unreadCount }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun deleteConversation(targetId: String, chatType: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
runCatching { ImSDK.deleteConversation(targetId, chatType) }
|
||||||
|
cache.deleteConversation(targetId, chatType)
|
||||||
|
_conversations.value = cache.loadConversations().sortedByDescending { it.lastMsgTime }
|
||||||
|
_totalUnreadCount.value = _conversations.value.sumOf { it.unreadCount }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun setPinned(targetId: String, chatType: String, pinned: Boolean) {
|
suspend fun setPinned(targetId: String, chatType: String, pinned: Boolean) {
|
||||||
runCatching { ImSDK.setConversationPinned(targetId, chatType, pinned) }
|
runCatching { ImSDK.setConversationPinned(targetId, chatType, pinned) }
|
||||||
refresh()
|
refresh()
|
||||||
|
|||||||
@ -155,8 +155,15 @@ fun GroupSettingsScreen(
|
|||||||
) {
|
) {
|
||||||
val group by viewModel.currentGroup.collectAsStateWithLifecycle()
|
val group by viewModel.currentGroup.collectAsStateWithLifecycle()
|
||||||
val connectionState by ImSDK.connectionState.collectAsStateWithLifecycle()
|
val connectionState by ImSDK.connectionState.collectAsStateWithLifecycle()
|
||||||
|
var showEditDialog by remember { mutableStateOf(false) }
|
||||||
|
var editName by remember { mutableStateOf("") }
|
||||||
|
var editAnnouncement by remember { mutableStateOf("") }
|
||||||
|
|
||||||
LaunchedEffect(groupId) { viewModel.loadGroupInfo(groupId) }
|
LaunchedEffect(groupId) { viewModel.loadGroupInfo(groupId) }
|
||||||
|
LaunchedEffect(group) {
|
||||||
|
editName = group?.name.orEmpty()
|
||||||
|
editAnnouncement = group?.announcement.orEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
@ -177,13 +184,20 @@ fun GroupSettingsScreen(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
) {
|
) {
|
||||||
group?.let { g ->
|
group?.let { g ->
|
||||||
|
val isOwner = g.creatorId == ImSDK.currentUserId
|
||||||
Text("群 ID: ${g.id}", style = MaterialTheme.typography.bodySmall,
|
Text("群 ID: ${g.id}", style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.outline)
|
color = MaterialTheme.colorScheme.outline)
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
Text("群公告: ${g.announcement ?: "暂无"}", style = MaterialTheme.typography.bodyMedium)
|
Text("群公告: ${g.announcement ?: "暂无"}", style = MaterialTheme.typography.bodyMedium)
|
||||||
|
if (isOwner) {
|
||||||
|
Spacer(Modifier.height(12.dp))
|
||||||
|
TextButton(onClick = { showEditDialog = true }) {
|
||||||
|
Text("编辑群资料")
|
||||||
|
}
|
||||||
|
}
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
Text("成员", style = MaterialTheme.typography.titleSmall)
|
Text("成员", style = MaterialTheme.typography.titleSmall)
|
||||||
val memberIds = g.memberIds.split(",").filter { it.isNotBlank() }
|
val memberIds = g.memberIds.split(",").filter { it.isNotBlank() }
|
||||||
@ -209,4 +223,45 @@ fun GroupSettingsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showEditDialog && group != null) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showEditDialog = false },
|
||||||
|
title = { Text("编辑群资料") },
|
||||||
|
text = {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = editName,
|
||||||
|
onValueChange = { editName = it },
|
||||||
|
label = { Text("群名称") },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = editAnnouncement,
|
||||||
|
onValueChange = { editAnnouncement = it },
|
||||||
|
label = { Text("群公告") },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
showEditDialog = false
|
||||||
|
viewModel.updateGroup(
|
||||||
|
groupId = groupId,
|
||||||
|
name = editName.trim().ifBlank { null },
|
||||||
|
announcement = editAnnouncement.trim().ifBlank { null },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
enabled = editName.isNotBlank(),
|
||||||
|
) {
|
||||||
|
Text("保存")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { showEditDialog = false }) { Text("取消") }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -116,6 +116,11 @@ object ImSDK {
|
|||||||
suspend fun listConversations(): List<ConversationData> =
|
suspend fun listConversations(): List<ConversationData> =
|
||||||
withContext(Dispatchers.IO) { api.listConversations(XuqmSDK.appId).data ?: emptyList() }
|
withContext(Dispatchers.IO) { api.listConversations(XuqmSDK.appId).data ?: emptyList() }
|
||||||
|
|
||||||
|
suspend fun getTotalUnreadCount(): Int =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
runCatching { listConversations().sumOf { it.unreadCount } }.getOrDefault(0)
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun setConversationPinned(targetId: String, chatType: String, pinned: Boolean) =
|
suspend fun setConversationPinned(targetId: String, chatType: String, pinned: Boolean) =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
api.setConversationPinned(targetId, chatType, SetPinnedRequest(pinned))
|
api.setConversationPinned(targetId, chatType, SetPinnedRequest(pinned))
|
||||||
@ -129,6 +134,12 @@ object ImSDK {
|
|||||||
suspend fun markRead(targetId: String, chatType: String = "SINGLE") =
|
suspend fun markRead(targetId: String, chatType: String = "SINGLE") =
|
||||||
withContext(Dispatchers.IO) { api.markRead(targetId, XuqmSDK.appId, chatType) }
|
withContext(Dispatchers.IO) { api.markRead(targetId, XuqmSDK.appId, chatType) }
|
||||||
|
|
||||||
|
suspend fun setDraft(targetId: String, chatType: String, draft: String) =
|
||||||
|
withContext(Dispatchers.IO) { api.setDraft(targetId, XuqmSDK.appId, chatType, draft) }
|
||||||
|
|
||||||
|
suspend fun deleteConversation(targetId: String, chatType: String) =
|
||||||
|
withContext(Dispatchers.IO) { api.deleteConversation(targetId, XuqmSDK.appId, chatType) }
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
|
|||||||
@ -129,4 +129,19 @@ interface ImApi {
|
|||||||
@Query("appId") appId: String,
|
@Query("appId") appId: String,
|
||||||
@Query("chatType") chatType: String,
|
@Query("chatType") chatType: String,
|
||||||
): ApiResponse<Unit>
|
): ApiResponse<Unit>
|
||||||
|
|
||||||
|
@PUT("api/im/conversations/{targetId}/draft")
|
||||||
|
suspend fun setDraft(
|
||||||
|
@Path("targetId") targetId: String,
|
||||||
|
@Query("appId") appId: String,
|
||||||
|
@Query("chatType") chatType: String,
|
||||||
|
@Query("draft") draft: String,
|
||||||
|
): ApiResponse<Unit>
|
||||||
|
|
||||||
|
@DELETE("api/im/conversations/{targetId}")
|
||||||
|
suspend fun deleteConversation(
|
||||||
|
@Path("targetId") targetId: String,
|
||||||
|
@Query("appId") appId: String,
|
||||||
|
@Query("chatType") chatType: String,
|
||||||
|
): ApiResponse<Unit>
|
||||||
}
|
}
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户