UI调整,群聊天室开发
This commit is contained in:
@@ -35,6 +35,7 @@ import com.aiosman.ravenow.ui.agent.AddAgentScreen
|
|||||||
import com.aiosman.ravenow.ui.group.CreateGroupChatScreen
|
import com.aiosman.ravenow.ui.group.CreateGroupChatScreen
|
||||||
import com.aiosman.ravenow.ui.chat.ChatAiScreen
|
import com.aiosman.ravenow.ui.chat.ChatAiScreen
|
||||||
import com.aiosman.ravenow.ui.chat.ChatScreen
|
import com.aiosman.ravenow.ui.chat.ChatScreen
|
||||||
|
import com.aiosman.ravenow.ui.chat.GroupChatScreen
|
||||||
import com.aiosman.ravenow.ui.comment.CommentsScreen
|
import com.aiosman.ravenow.ui.comment.CommentsScreen
|
||||||
import com.aiosman.ravenow.ui.comment.notice.CommentNoticeScreen
|
import com.aiosman.ravenow.ui.comment.notice.CommentNoticeScreen
|
||||||
import com.aiosman.ravenow.ui.crop.ImageCropScreen
|
import com.aiosman.ravenow.ui.crop.ImageCropScreen
|
||||||
@@ -94,6 +95,7 @@ sealed class NavigationRoute(
|
|||||||
data object FavouriteList : NavigationRoute("FavouriteList")
|
data object FavouriteList : NavigationRoute("FavouriteList")
|
||||||
data object Chat : NavigationRoute("Chat/{id}")
|
data object Chat : NavigationRoute("Chat/{id}")
|
||||||
data object ChatAi : NavigationRoute("ChatAi/{id}")
|
data object ChatAi : NavigationRoute("ChatAi/{id}")
|
||||||
|
data object ChatGroup : NavigationRoute("ChatGroup/{id}")
|
||||||
data object CommentNoticeScreen : NavigationRoute("CommentNoticeScreen")
|
data object CommentNoticeScreen : NavigationRoute("CommentNoticeScreen")
|
||||||
data object ImageCrop : NavigationRoute("ImageCrop")
|
data object ImageCrop : NavigationRoute("ImageCrop")
|
||||||
data object AccountSetting : NavigationRoute("AccountSetting")
|
data object AccountSetting : NavigationRoute("AccountSetting")
|
||||||
@@ -385,6 +387,20 @@ fun NavigationController(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
composable(
|
||||||
|
route = NavigationRoute.ChatGroup.route,
|
||||||
|
arguments = listOf(navArgument("id") { type = NavType.StringType })
|
||||||
|
) {
|
||||||
|
val encodedId = it.arguments?.getString("id")
|
||||||
|
val decodedId = encodedId?.let { java.net.URLDecoder.decode(it, "UTF-8") }
|
||||||
|
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalAnimatedContentScope provides this,
|
||||||
|
) {
|
||||||
|
GroupChatScreen(decodedId?:"")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
composable(route = NavigationRoute.CommentNoticeScreen.route) {
|
composable(route = NavigationRoute.CommentNoticeScreen.route) {
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
@@ -494,6 +510,14 @@ fun NavHostController.navigateToChatAi(id: String) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun NavHostController.navigateToGroupChat(id: String) {
|
||||||
|
val encodedId = java.net.URLEncoder.encode(id, "UTF-8")
|
||||||
|
navigate(
|
||||||
|
route = NavigationRoute.ChatGroup.route
|
||||||
|
.replace("{id}", encodedId)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
fun NavHostController.goTo(
|
fun NavHostController.goTo(
|
||||||
|
|||||||
683
app/src/main/java/com/aiosman/ravenow/ui/chat/GroupChatScreen.kt
Normal file
683
app/src/main/java/com/aiosman/ravenow/ui/chat/GroupChatScreen.kt
Normal file
@@ -0,0 +1,683 @@
|
|||||||
|
package com.aiosman.ravenow.ui.chat
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.animation.Crossfade
|
||||||
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.ime
|
||||||
|
import androidx.compose.foundation.layout.imePadding
|
||||||
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.Scaffold
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
|
import androidx.compose.ui.platform.SoftwareKeyboardController
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
|
import com.aiosman.ravenow.LocalNavController
|
||||||
|
import com.aiosman.ravenow.R
|
||||||
|
import com.aiosman.ravenow.entity.ChatItem
|
||||||
|
import com.aiosman.ravenow.exp.formatChatTime
|
||||||
|
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||||
|
import com.aiosman.ravenow.ui.composables.DropdownMenu
|
||||||
|
import com.aiosman.ravenow.ui.composables.MenuItem
|
||||||
|
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
||||||
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
|
import com.tencent.imsdk.v2.V2TIMMessage
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GroupChatScreen(groupId: String) {
|
||||||
|
var isMenuExpanded by remember { mutableStateOf(false) }
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
val context = LocalNavController.current.context
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
var goToNewCount by remember { mutableStateOf(0) }
|
||||||
|
|
||||||
|
val viewModel = viewModel<GroupChatViewModel>(
|
||||||
|
key = "GroupChatViewModel_$groupId",
|
||||||
|
factory = object : ViewModelProvider.Factory {
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
|
return GroupChatViewModel(groupId) as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
viewModel.init(context = context)
|
||||||
|
}
|
||||||
|
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose {
|
||||||
|
viewModel.UnRegistListener()
|
||||||
|
viewModel.clearUnRead()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
var inBottom by remember { mutableStateOf(true) }
|
||||||
|
|
||||||
|
LaunchedEffect(listState) {
|
||||||
|
snapshotFlow { listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index }
|
||||||
|
.collect { index ->
|
||||||
|
if (index == listState.layoutInfo.totalItemsCount - 1) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
viewModel.onLoadMore(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(listState) {
|
||||||
|
snapshotFlow { listState.layoutInfo.visibleItemsInfo.firstOrNull()?.index }
|
||||||
|
.collect { index ->
|
||||||
|
inBottom = index == 0
|
||||||
|
if (index == 0) {
|
||||||
|
goToNewCount = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(viewModel.goToNew) {
|
||||||
|
if (viewModel.goToNew) {
|
||||||
|
if (inBottom) {
|
||||||
|
listState.scrollToItem(0)
|
||||||
|
} else {
|
||||||
|
goToNewCount++
|
||||||
|
}
|
||||||
|
viewModel.goToNew = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
topBar = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(AppColors.background)
|
||||||
|
) {
|
||||||
|
StatusBarSpacer()
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 16.dp, horizontal = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.Start,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.rider_pro_back_icon),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(28.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
navController.navigateUp()
|
||||||
|
},
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(AppColors.text)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
|
||||||
|
if (viewModel.groupAvatar.isNotEmpty()) {
|
||||||
|
CustomAsyncImage(
|
||||||
|
imageUrl = viewModel.groupAvatar,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(40.dp)
|
||||||
|
.clip(RoundedCornerShape(8.dp)),
|
||||||
|
contentDescription = "群聊头像"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(40.dp)
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.background(AppColors.decentBackground),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = viewModel.groupName.take(1),
|
||||||
|
style = TextStyle(
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = androidx.compose.ui.text.font.FontWeight.Bold
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = viewModel.groupName,
|
||||||
|
style = TextStyle(
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 18.sp,
|
||||||
|
fontWeight = androidx.compose.ui.text.font.FontWeight.Bold
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "${viewModel.memberCount}人",
|
||||||
|
style = TextStyle(
|
||||||
|
color = AppColors.secondaryText,
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
Box {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.rider_pro_more_horizon),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(28.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
isMenuExpanded = true
|
||||||
|
},
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(AppColors.text)
|
||||||
|
)
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = isMenuExpanded,
|
||||||
|
onDismissRequest = {
|
||||||
|
isMenuExpanded = false
|
||||||
|
},
|
||||||
|
menuItems = listOf(
|
||||||
|
MenuItem(
|
||||||
|
title = if (viewModel.notificationStrategy == "mute") "取消静音" else "静音",
|
||||||
|
icon = if (viewModel.notificationStrategy == "mute") R.drawable.rider_pro_notice_mute else R.drawable.rider_pro_notice_active,
|
||||||
|
) {
|
||||||
|
isMenuExpanded = false
|
||||||
|
viewModel.viewModelScope.launch {
|
||||||
|
if (viewModel.notificationStrategy == "mute") {
|
||||||
|
viewModel.updateNotificationStrategy("active")
|
||||||
|
} else {
|
||||||
|
viewModel.updateNotificationStrategy("mute")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MenuItem(
|
||||||
|
title = "群成员",
|
||||||
|
icon = R.drawable.rider_pro_more_horizon,
|
||||||
|
) {
|
||||||
|
isMenuExpanded = false
|
||||||
|
viewModel.getGroupMembers()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bottomBar = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.imePadding()
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(1.dp)
|
||||||
|
.background(AppColors.decentBackground)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
GroupChatInput(
|
||||||
|
onSendImage = { uri ->
|
||||||
|
uri?.let {
|
||||||
|
viewModel.sendImageMessage(it, context)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) { message ->
|
||||||
|
viewModel.sendMessage(message, context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(AppColors.decentBackground)
|
||||||
|
.padding(paddingValues)
|
||||||
|
) {
|
||||||
|
LazyColumn(
|
||||||
|
state = listState,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
reverseLayout = true,
|
||||||
|
verticalArrangement = Arrangement.Top
|
||||||
|
) {
|
||||||
|
val chatList = groupMessagesByTime(viewModel.getDisplayChatList(), viewModel)
|
||||||
|
items(chatList.size, key = { index -> chatList[index].msgId }) { index ->
|
||||||
|
val item = chatList[index]
|
||||||
|
if (item.showTimeDivider) {
|
||||||
|
val calendar = java.util.Calendar.getInstance()
|
||||||
|
calendar.timeInMillis = item.timestamp
|
||||||
|
Text(
|
||||||
|
text = calendar.time.formatChatTime(context),
|
||||||
|
style = TextStyle(
|
||||||
|
color = AppColors.secondaryText,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
GroupChatItem(item = item, viewModel.myProfile?.trtcUserId!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (goToNewCount > 0) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
.padding(bottom = 16.dp, end = 16.dp)
|
||||||
|
.shadow(4.dp, shape = RoundedCornerShape(16.dp))
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.background(AppColors.background)
|
||||||
|
.padding(8.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
coroutineScope.launch {
|
||||||
|
listState.scrollToItem(0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "${goToNewCount} 条新消息",
|
||||||
|
style = TextStyle(
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GroupChatSelfItem(item: ChatItem) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.End,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = androidx.compose.ui.Alignment.End,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = item.nickname,
|
||||||
|
style = TextStyle(
|
||||||
|
color = Color.Gray,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
),
|
||||||
|
modifier = Modifier.padding(bottom = 2.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.widthIn(
|
||||||
|
min = 20.dp,
|
||||||
|
max = (if (item.messageType == V2TIMMessage.V2TIM_ELEM_TYPE_TEXT) 250.dp else 150.dp)
|
||||||
|
)
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.background(Color(0xFF000000))
|
||||||
|
.padding(
|
||||||
|
vertical = (if (item.messageType == V2TIMMessage.V2TIM_ELEM_TYPE_TEXT) 8.dp else 0.dp),
|
||||||
|
horizontal = (if (item.messageType == V2TIMMessage.V2TIM_ELEM_TYPE_TEXT) 16.dp else 0.dp)
|
||||||
|
)
|
||||||
|
.padding(bottom = (if (item.messageType == V2TIMMessage.V2TIM_ELEM_TYPE_TEXT) 3.dp else 0.dp))
|
||||||
|
) {
|
||||||
|
when (item.messageType) {
|
||||||
|
V2TIMMessage.V2TIM_ELEM_TYPE_TEXT -> {
|
||||||
|
Text(
|
||||||
|
text = item.message,
|
||||||
|
style = TextStyle(
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
),
|
||||||
|
textAlign = TextAlign.Start
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
V2TIMMessage.V2TIM_ELEM_TYPE_IMAGE -> {
|
||||||
|
CustomAsyncImage(
|
||||||
|
imageUrl = item.imageList[1].url,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentDescription = "image"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
Text(
|
||||||
|
text = "不支持的消息类型",
|
||||||
|
style = TextStyle(
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(40.dp)
|
||||||
|
.clip(RoundedCornerShape(40.dp))
|
||||||
|
) {
|
||||||
|
CustomAsyncImage(
|
||||||
|
imageUrl = item.avatar,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentDescription = "avatar"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GroupChatOtherItem(item: ChatItem) {
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.Start,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(40.dp)
|
||||||
|
.clip(RoundedCornerShape(40.dp))
|
||||||
|
) {
|
||||||
|
CustomAsyncImage(
|
||||||
|
imageUrl = item.avatar,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentDescription = "avatar"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = item.nickname,
|
||||||
|
style = TextStyle(
|
||||||
|
color = AppColors.secondaryText,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
),
|
||||||
|
modifier = Modifier.padding(bottom = 2.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.widthIn(
|
||||||
|
min = 20.dp,
|
||||||
|
max = (if (item.messageType == V2TIMMessage.V2TIM_ELEM_TYPE_TEXT) 250.dp else 150.dp)
|
||||||
|
)
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.background(AppColors.background)
|
||||||
|
.padding(
|
||||||
|
vertical = (if (item.messageType == V2TIMMessage.V2TIM_ELEM_TYPE_TEXT) 8.dp else 0.dp),
|
||||||
|
horizontal = (if (item.messageType == V2TIMMessage.V2TIM_ELEM_TYPE_TEXT) 16.dp else 0.dp)
|
||||||
|
)
|
||||||
|
.padding(bottom = (if (item.messageType == V2TIMMessage.V2TIM_ELEM_TYPE_TEXT) 3.dp else 0.dp))
|
||||||
|
) {
|
||||||
|
when (item.messageType) {
|
||||||
|
V2TIMMessage.V2TIM_ELEM_TYPE_TEXT -> {
|
||||||
|
Text(
|
||||||
|
text = item.message,
|
||||||
|
style = TextStyle(
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
),
|
||||||
|
textAlign = TextAlign.Start
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
V2TIMMessage.V2TIM_ELEM_TYPE_IMAGE -> {
|
||||||
|
CustomAsyncImage(
|
||||||
|
imageUrl = item.imageList[1].url,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentDescription = "image"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
Text(
|
||||||
|
text = "不支持的消息类型",
|
||||||
|
style = TextStyle(
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GroupChatItem(item: ChatItem, currentUserId: String) {
|
||||||
|
val isCurrentUser = item.userId == currentUserId
|
||||||
|
if (isCurrentUser) {
|
||||||
|
GroupChatSelfItem(item)
|
||||||
|
} else {
|
||||||
|
GroupChatOtherItem(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GroupChatInput(
|
||||||
|
onSendImage: (Uri?) -> Unit = {},
|
||||||
|
onSend: (String) -> Unit = {},
|
||||||
|
) {
|
||||||
|
val navigationBarHeight = with(LocalDensity.current) {
|
||||||
|
WindowInsets.navigationBars.getBottom(this).toDp()
|
||||||
|
}
|
||||||
|
var keyboardController by remember { mutableStateOf<SoftwareKeyboardController?>(null) }
|
||||||
|
var isKeyboardOpen by remember { mutableStateOf(false) }
|
||||||
|
var text by remember { mutableStateOf("") }
|
||||||
|
val appColors = LocalAppTheme.current
|
||||||
|
val inputBarHeight by animateDpAsState(
|
||||||
|
targetValue = if (isKeyboardOpen) 8.dp else (navigationBarHeight + 8.dp),
|
||||||
|
animationSpec = tween(
|
||||||
|
durationMillis = 300,
|
||||||
|
easing = androidx.compose.animation.core.LinearEasing
|
||||||
|
), label = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
LaunchedEffect(isKeyboardOpen) {
|
||||||
|
inputBarHeight
|
||||||
|
}
|
||||||
|
val focusManager = LocalFocusManager.current
|
||||||
|
val windowInsets = WindowInsets.ime
|
||||||
|
val density = LocalDensity.current
|
||||||
|
val softwareKeyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
val currentDensity by rememberUpdatedState(density)
|
||||||
|
|
||||||
|
LaunchedEffect(windowInsets.getBottom(currentDensity)) {
|
||||||
|
if (windowInsets.getBottom(currentDensity) <= 0) {
|
||||||
|
focusManager.clearFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val imagePickUpLauncher = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.StartActivityForResult()
|
||||||
|
) {
|
||||||
|
if (it.resultCode == Activity.RESULT_OK) {
|
||||||
|
val uri = it.data?.data
|
||||||
|
onSendImage(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.padding(bottom = inputBarHeight)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.background(appColors.background)
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
contentAlignment = Alignment.CenterStart,
|
||||||
|
) {
|
||||||
|
BasicTextField(
|
||||||
|
value = text,
|
||||||
|
onValueChange = {
|
||||||
|
text = it
|
||||||
|
},
|
||||||
|
textStyle = TextStyle(
|
||||||
|
color = appColors.text,
|
||||||
|
fontSize = 16.sp
|
||||||
|
),
|
||||||
|
cursorBrush = SolidColor(appColors.text),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 8.dp)
|
||||||
|
.onFocusChanged { focusState ->
|
||||||
|
isKeyboardOpen = focusState.isFocused
|
||||||
|
}
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
awaitPointerEventScope {
|
||||||
|
keyboardController = softwareKeyboardController
|
||||||
|
awaitFirstDown().also {
|
||||||
|
keyboardController?.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||||
|
keyboardActions = KeyboardActions(
|
||||||
|
onDone = {
|
||||||
|
keyboardController?.hide()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.rider_pro_camera),
|
||||||
|
contentDescription = "发送图片",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(30.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
imagePickUpLauncher.launch(
|
||||||
|
Intent.createChooser(
|
||||||
|
Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||||
|
type = "image/*"
|
||||||
|
},
|
||||||
|
"选择图片"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
tint = appColors.chatActionColor
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Crossfade(
|
||||||
|
targetState = text.isNotEmpty(), animationSpec = tween(500),
|
||||||
|
label = ""
|
||||||
|
) { isNotEmpty ->
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.rider_pro_video_share),
|
||||||
|
contentDescription = "发送消息",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(32.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
if (text.isNotEmpty()) {
|
||||||
|
onSend(text)
|
||||||
|
text = ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tint = if (isNotEmpty) appColors.main else appColors.chatActionColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun groupMessagesByTime(chatList: List<ChatItem>, viewModel: GroupChatViewModel): List<ChatItem> {
|
||||||
|
for (i in chatList.indices) {
|
||||||
|
if (i == 0) {
|
||||||
|
viewModel.showTimestampMap[chatList[i].msgId] = false
|
||||||
|
chatList[i].showTimeDivider = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val currentMessage = chatList[i]
|
||||||
|
val timeDiff = currentMessage.timestamp - chatList[i - 1].timestamp
|
||||||
|
if (-timeDiff > 30 * 60 * 1000) {
|
||||||
|
viewModel.showTimestampMap[currentMessage.msgId] = true
|
||||||
|
currentMessage.showTimeDivider = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return chatList
|
||||||
|
}
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
package com.aiosman.ravenow.ui.chat
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.util.Log
|
||||||
|
import android.webkit.MimeTypeMap
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.aiosman.ravenow.ChatState
|
||||||
|
import com.aiosman.ravenow.data.AccountService
|
||||||
|
import com.aiosman.ravenow.data.AccountServiceImpl
|
||||||
|
import com.aiosman.ravenow.data.UserService
|
||||||
|
import com.aiosman.ravenow.data.UserServiceImpl
|
||||||
|
import com.aiosman.ravenow.entity.AccountProfileEntity
|
||||||
|
import com.aiosman.ravenow.entity.ChatItem
|
||||||
|
import com.aiosman.ravenow.entity.ChatNotification
|
||||||
|
import com.tencent.imsdk.v2.V2TIMAdvancedMsgListener
|
||||||
|
import com.tencent.imsdk.v2.V2TIMCallback
|
||||||
|
import com.tencent.imsdk.v2.V2TIMManager
|
||||||
|
import com.tencent.imsdk.v2.V2TIMMessage
|
||||||
|
import com.tencent.imsdk.v2.V2TIMSendCallback
|
||||||
|
import com.tencent.imsdk.v2.V2TIMValueCallback
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
class GroupChatViewModel(
|
||||||
|
val groupId: String,
|
||||||
|
) : ViewModel() {
|
||||||
|
var chatData by mutableStateOf<List<ChatItem>>(emptyList())
|
||||||
|
var groupInfo by mutableStateOf<GroupInfo?>(null)
|
||||||
|
var myProfile by mutableStateOf<AccountProfileEntity?>(null)
|
||||||
|
val userService: UserService = UserServiceImpl()
|
||||||
|
val accountService: AccountService = AccountServiceImpl()
|
||||||
|
var textMessageListener: V2TIMAdvancedMsgListener? = null
|
||||||
|
var hasMore by mutableStateOf(true)
|
||||||
|
var isLoading by mutableStateOf(false)
|
||||||
|
var lastMessage: V2TIMMessage? = null
|
||||||
|
val showTimestampMap = mutableMapOf<String, Boolean>()
|
||||||
|
var chatNotification by mutableStateOf<ChatNotification?>(null)
|
||||||
|
var goToNew by mutableStateOf(false)
|
||||||
|
|
||||||
|
// 群聊特有属性
|
||||||
|
var memberCount by mutableStateOf(0)
|
||||||
|
var groupName by mutableStateOf("")
|
||||||
|
var groupAvatar by mutableStateOf("")
|
||||||
|
|
||||||
|
data class GroupInfo(
|
||||||
|
val groupId: String,
|
||||||
|
val groupName: String,
|
||||||
|
val groupAvatar: String,
|
||||||
|
val memberCount: Int,
|
||||||
|
val ownerId: String
|
||||||
|
)
|
||||||
|
|
||||||
|
fun init(context: Context) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
getGroupInfo()
|
||||||
|
myProfile = accountService.getMyAccountProfile()
|
||||||
|
RegistListener(context)
|
||||||
|
fetchHistoryMessage(context)
|
||||||
|
val notiStrategy = ChatState.getStrategyByTargetTrtcId(groupId)
|
||||||
|
chatNotification = notiStrategy
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("GroupChatViewModel", "初始化失败: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getGroupInfo() {
|
||||||
|
// 简化群组信息获取,使用默认信息
|
||||||
|
groupInfo = GroupInfo(
|
||||||
|
groupId = groupId,
|
||||||
|
groupName = "群聊 $groupId",
|
||||||
|
groupAvatar = "",
|
||||||
|
memberCount = 0,
|
||||||
|
ownerId = ""
|
||||||
|
)
|
||||||
|
groupName = groupInfo?.groupName ?: ""
|
||||||
|
groupAvatar = groupInfo?.groupAvatar ?: ""
|
||||||
|
memberCount = groupInfo?.memberCount ?: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fun RegistListener(context: Context) {
|
||||||
|
textMessageListener = object : V2TIMAdvancedMsgListener() {
|
||||||
|
override fun onRecvNewMessage(msg: V2TIMMessage?) {
|
||||||
|
super.onRecvNewMessage(msg)
|
||||||
|
msg?.let {
|
||||||
|
if (it.groupID == groupId) {
|
||||||
|
val chatItem = ChatItem.convertToChatItem(msg, context, avatar = null)
|
||||||
|
chatItem?.let {
|
||||||
|
chatData = listOf(it) + chatData
|
||||||
|
goToNew = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
V2TIMManager.getMessageManager().addAdvancedMsgListener(textMessageListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun UnRegistListener() {
|
||||||
|
V2TIMManager.getMessageManager().removeAdvancedMsgListener(textMessageListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearUnRead() {
|
||||||
|
val conversationID = "group_${groupId}"
|
||||||
|
V2TIMManager.getConversationManager()
|
||||||
|
.cleanConversationUnreadMessageCount(conversationID, 0, 0, object : V2TIMCallback {
|
||||||
|
override fun onSuccess() {
|
||||||
|
Log.i("imsdk", "清除群聊未读消息成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(code: Int, desc: String) {
|
||||||
|
Log.i("imsdk", "清除群聊未读消息失败, code:$code, desc:$desc")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onLoadMore(context: Context) {
|
||||||
|
if (!hasMore || isLoading) return
|
||||||
|
isLoading = true
|
||||||
|
viewModelScope.launch {
|
||||||
|
V2TIMManager.getMessageManager().getGroupHistoryMessageList(
|
||||||
|
groupId,
|
||||||
|
20,
|
||||||
|
lastMessage,
|
||||||
|
object : V2TIMValueCallback<List<V2TIMMessage>> {
|
||||||
|
override fun onSuccess(p0: List<V2TIMMessage>?) {
|
||||||
|
chatData = chatData + (p0 ?: emptyList()).map {
|
||||||
|
ChatItem.convertToChatItem(it, context, avatar = null)
|
||||||
|
}.filterNotNull()
|
||||||
|
if ((p0?.size ?: 0) < 20) {
|
||||||
|
hasMore = false
|
||||||
|
}
|
||||||
|
lastMessage = p0?.lastOrNull()
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(p0: Int, p1: String?) {
|
||||||
|
Log.e("GroupChatViewModel", "获取群聊历史消息失败: $p1")
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sendMessage(message: String, context: Context) {
|
||||||
|
val v2TIMMessage = V2TIMManager.getMessageManager().createTextMessage(message)
|
||||||
|
V2TIMManager.getMessageManager().sendMessage(
|
||||||
|
v2TIMMessage,
|
||||||
|
groupId,
|
||||||
|
null,
|
||||||
|
V2TIMMessage.V2TIM_PRIORITY_NORMAL,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
object : V2TIMSendCallback<V2TIMMessage> {
|
||||||
|
override fun onProgress(p0: Int) {}
|
||||||
|
override fun onError(p0: Int, p1: String?) {
|
||||||
|
Log.e("GroupChatViewModel", "发送群聊消息失败: $p1")
|
||||||
|
}
|
||||||
|
override fun onSuccess(p0: V2TIMMessage?) {
|
||||||
|
val chatItem = ChatItem.convertToChatItem(p0!!, context, avatar = myProfile?.avatar)
|
||||||
|
chatItem?.let {
|
||||||
|
chatData = listOf(it) + chatData
|
||||||
|
goToNew = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sendImageMessage(imageUri: Uri, context: Context) {
|
||||||
|
val tempFile = createTempFile(context, imageUri)
|
||||||
|
val imagePath = tempFile?.path
|
||||||
|
if (imagePath != null) {
|
||||||
|
val v2TIMMessage = V2TIMManager.getMessageManager().createImageMessage(imagePath)
|
||||||
|
V2TIMManager.getMessageManager().sendMessage(
|
||||||
|
v2TIMMessage,
|
||||||
|
groupId,
|
||||||
|
null,
|
||||||
|
V2TIMMessage.V2TIM_PRIORITY_NORMAL,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
object : V2TIMSendCallback<V2TIMMessage> {
|
||||||
|
override fun onProgress(p0: Int) {}
|
||||||
|
override fun onError(p0: Int, p1: String?) {
|
||||||
|
Log.e("GroupChatViewModel", "发送群聊图片消息失败: $p1")
|
||||||
|
}
|
||||||
|
override fun onSuccess(p0: V2TIMMessage?) {
|
||||||
|
val chatItem = ChatItem.convertToChatItem(p0!!, context, avatar = myProfile?.avatar)
|
||||||
|
chatItem?.let {
|
||||||
|
chatData = listOf(it) + chatData
|
||||||
|
goToNew = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createTempFile(context: Context, uri: Uri): File? {
|
||||||
|
return try {
|
||||||
|
val projection = arrayOf(MediaStore.Images.Media.DATA)
|
||||||
|
val cursor = context.contentResolver.query(uri, projection, null, null, null)
|
||||||
|
cursor?.use {
|
||||||
|
if (it.moveToFirst()) {
|
||||||
|
val columnIndex = it.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
|
||||||
|
val filePath = it.getString(columnIndex)
|
||||||
|
val inputStream: InputStream? = context.contentResolver.openInputStream(uri)
|
||||||
|
val mimeType = context.contentResolver.getType(uri)
|
||||||
|
val extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType)
|
||||||
|
val tempFile =
|
||||||
|
File.createTempFile("temp_image", ".$extension", context.cacheDir)
|
||||||
|
val outputStream = FileOutputStream(tempFile)
|
||||||
|
|
||||||
|
inputStream?.use { input ->
|
||||||
|
outputStream.use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tempFile
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fetchHistoryMessage(context: Context) {
|
||||||
|
V2TIMManager.getMessageManager().getGroupHistoryMessageList(
|
||||||
|
groupId,
|
||||||
|
20,
|
||||||
|
null,
|
||||||
|
object : V2TIMValueCallback<List<V2TIMMessage>> {
|
||||||
|
override fun onSuccess(p0: List<V2TIMMessage>?) {
|
||||||
|
chatData = (p0 ?: emptyList()).mapNotNull {
|
||||||
|
ChatItem.convertToChatItem(it, context, avatar = null)
|
||||||
|
}
|
||||||
|
if ((p0?.size ?: 0) < 20) {
|
||||||
|
hasMore = false
|
||||||
|
}
|
||||||
|
lastMessage = p0?.lastOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(p0: Int, p1: String?) {
|
||||||
|
Log.e("GroupChatViewModel", "获取群聊历史消息失败: $p1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDisplayChatList(): List<ChatItem> {
|
||||||
|
val list = chatData
|
||||||
|
for (item in list) {
|
||||||
|
item.showTimestamp = showTimestampMap.getOrDefault(item.msgId, false)
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateNotificationStrategy(strategy: String) {
|
||||||
|
val result = ChatState.updateChatNotification(groupId.hashCode(), strategy)
|
||||||
|
chatNotification = result
|
||||||
|
}
|
||||||
|
|
||||||
|
val notificationStrategy get() = chatNotification?.strategy ?: "default"
|
||||||
|
|
||||||
|
// 群聊特有功能
|
||||||
|
fun getGroupMembers() {
|
||||||
|
// 简化群成员获取,暂时只记录日志
|
||||||
|
Log.d("GroupChatViewModel", "获取群成员功能待实现")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ import com.aiosman.ravenow.data.UserServiceImpl
|
|||||||
import com.aiosman.ravenow.exp.formatChatTime
|
import com.aiosman.ravenow.exp.formatChatTime
|
||||||
import com.aiosman.ravenow.ui.NavigationRoute
|
import com.aiosman.ravenow.ui.NavigationRoute
|
||||||
import com.aiosman.ravenow.ui.navigateToChat
|
import com.aiosman.ravenow.ui.navigateToChat
|
||||||
|
import com.aiosman.ravenow.ui.navigateToGroupChat
|
||||||
import com.tencent.imsdk.v2.V2TIMConversation
|
import com.tencent.imsdk.v2.V2TIMConversation
|
||||||
import com.tencent.imsdk.v2.V2TIMConversationListFilter
|
import com.tencent.imsdk.v2.V2TIMConversationListFilter
|
||||||
import com.tencent.imsdk.v2.V2TIMConversationResult
|
import com.tencent.imsdk.v2.V2TIMConversationResult
|
||||||
@@ -168,7 +169,7 @@ object GroupChatListViewModel : ViewModel() {
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
// 群聊直接使用群ID进行导航
|
// 群聊直接使用群ID进行导航
|
||||||
//navController.navigateToChat(conversation.groupId)
|
navController.navigateToGroupChat(conversation.groupId)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
error = ""
|
error = ""
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.expolre
|
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.expolre
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
@@ -25,6 +26,7 @@ import androidx.compose.foundation.layout.wrapContentSize
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
|
import androidx.compose.material.Icon
|
||||||
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
||||||
import androidx.compose.material.pullrefresh.pullRefresh
|
import androidx.compose.material.pullrefresh.pullRefresh
|
||||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||||
@@ -58,6 +60,7 @@ import com.aiosman.ravenow.data.Agent
|
|||||||
import com.aiosman.ravenow.data.api.ApiClient
|
import com.aiosman.ravenow.data.api.ApiClient
|
||||||
import com.aiosman.ravenow.ui.NavigationRoute
|
import com.aiosman.ravenow.ui.NavigationRoute
|
||||||
import com.aiosman.ravenow.ui.composables.AgentCard
|
import com.aiosman.ravenow.ui.composables.AgentCard
|
||||||
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
import com.aiosman.ravenow.ui.post.NewPostViewModel
|
import com.aiosman.ravenow.ui.post.NewPostViewModel
|
||||||
|
|
||||||
// Banner数据类
|
// Banner数据类
|
||||||
@@ -74,9 +77,9 @@ data class BannerItem(
|
|||||||
fun fromRoom(room: Room): BannerItem {
|
fun fromRoom(room: Room): BannerItem {
|
||||||
return BannerItem(
|
return BannerItem(
|
||||||
id = room.id,
|
id = room.id,
|
||||||
title = room.name ?: "聊天室",
|
title = room.name ,
|
||||||
subtitle = room.description ?: "欢迎加入我们的聊天室",
|
subtitle = room.description ,
|
||||||
imageUrl = "${ApiClient.BASE_API_URL+"/outside"}${room.creator.profile.avatar}"+"?token="+"${AppStore.token}" ?: "",
|
imageUrl = "${ApiClient.RETROFIT_URL}${room.creator.profile.avatar}"+"?token="+"${AppStore.token}" ?: "",
|
||||||
backgroundImageUrl = "${ApiClient.BASE_API_URL+"/outside"}${room.recommendBanner}"+"?token="+"${AppStore.token}" ?: "",
|
backgroundImageUrl = "${ApiClient.BASE_API_URL+"/outside"}${room.recommendBanner}"+"?token="+"${AppStore.token}" ?: "",
|
||||||
userCount = room.userCount,
|
userCount = room.userCount,
|
||||||
agentName = room.creator.profile.nickname
|
agentName = room.creator.profile.nickname
|
||||||
@@ -138,6 +141,7 @@ fun Explore() {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@SuppressLint("SuspiciousIndentation")
|
||||||
@Composable
|
@Composable
|
||||||
fun AgentCard2(agentItem: AgentItem) {
|
fun AgentCard2(agentItem: AgentItem) {
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
@@ -192,14 +196,14 @@ fun Explore() {
|
|||||||
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis
|
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
// 描述
|
// 描述
|
||||||
Text(
|
Text(
|
||||||
text = agentItem.desc,
|
text = agentItem.desc,
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
color = AppColors.secondaryText,
|
color = AppColors.secondaryText,
|
||||||
maxLines = 2,
|
maxLines = 1,
|
||||||
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis
|
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -316,17 +320,19 @@ fun Explore() {
|
|||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
// 左侧图标
|
// 左侧图标
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(24.dp)
|
.size(24.dp),
|
||||||
.padding(start = 16.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.rider_pro_group_chat),
|
|
||||||
contentDescription = "chat",
|
|
||||||
modifier = Modifier.size(12.dp),
|
|
||||||
|
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.rider_pro_group_chat),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp),
|
||||||
|
tint = AppColors.text
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,13 +368,13 @@ fun Explore() {
|
|||||||
horizontalArrangement = androidx.compose.foundation.layout.Arrangement.spacedBy(8.dp)
|
horizontalArrangement = androidx.compose.foundation.layout.Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
repeat(3) { columnIndex ->
|
repeat(3) { columnIndex ->
|
||||||
val itemIndex = rowIndex * 3 + columnIndex
|
val itemIndex = rowIndex * 2 + columnIndex
|
||||||
if (itemIndex < roomItems.size) {
|
if (itemIndex < roomItems.size) {
|
||||||
val roomItem = roomItems[itemIndex]
|
val roomItem = roomItems[itemIndex]
|
||||||
HotChatRoomGridItem(roomItem = roomItem)
|
HotChatRoomGridItem(roomItem = roomItem)
|
||||||
} else {
|
} else {
|
||||||
// 填充空白占位
|
// 填充空白占位
|
||||||
Spacer(modifier = Modifier.width(80.dp))
|
//Spacer(modifier = Modifier.width(80.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -393,9 +399,9 @@ fun Explore() {
|
|||||||
contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 0.dp),
|
contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 0.dp),
|
||||||
horizontalArrangement = androidx.compose.foundation.layout.Arrangement.spacedBy(6.dp)
|
horizontalArrangement = androidx.compose.foundation.layout.Arrangement.spacedBy(6.dp)
|
||||||
) {
|
) {
|
||||||
repeat(gridCount) { gridIndex ->
|
repeat(1) { gridIndex ->
|
||||||
item {
|
item {
|
||||||
HotChatRoomGrid(roomItems = roomItems.drop(gridIndex * totalItems).take(totalItems))
|
HotChatRoomGrid(roomItems = viewModel.bannerItems.take(8))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -473,6 +479,8 @@ fun Explore() {
|
|||||||
Text(
|
Text(
|
||||||
text = bannerItem.title,
|
text = bannerItem.title,
|
||||||
fontSize = 24.sp,
|
fontSize = 24.sp,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
|
||||||
fontWeight = androidx.compose.ui.text.font.FontWeight.Bold,
|
fontWeight = androidx.compose.ui.text.font.FontWeight.Bold,
|
||||||
color = Color.White,
|
color = Color.White,
|
||||||
modifier = Modifier.padding(start = 16.dp)
|
modifier = Modifier.padding(start = 16.dp)
|
||||||
@@ -484,12 +492,13 @@ fun Explore() {
|
|||||||
color = Color.White.copy(alpha = 0.9f),
|
color = Color.White.copy(alpha = 0.9f),
|
||||||
maxLines = 2,
|
maxLines = 2,
|
||||||
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
|
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
|
||||||
modifier = Modifier.padding(start = 16.dp)
|
modifier = Modifier.padding(start = 16.dp, end = 16.dp)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth().background(brush = androidx.compose.ui.graphics.Brush.verticalGradient(
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
.background(brush = androidx.compose.ui.graphics.Brush.verticalGradient(
|
||||||
colors = listOf(
|
colors = listOf(
|
||||||
Color(0x00000000), // 底部颜色(透明)
|
Color(0x00000000), // 底部颜色(透明)
|
||||||
Color(0x33000000), // 顶部颜色
|
Color(0x33000000), // 顶部颜色
|
||||||
@@ -499,81 +508,75 @@ fun Explore() {
|
|||||||
horizontalArrangement = androidx.compose.foundation.layout.Arrangement.SpaceBetween,
|
horizontalArrangement = androidx.compose.foundation.layout.Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
// Agent信息
|
Row( modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically){
|
||||||
|
// 左侧头像
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.wrapContentWidth()
|
.size(24.dp)
|
||||||
.padding(8.dp),
|
.background(
|
||||||
|
Color.White.copy(alpha = 0.2f),
|
||||||
|
RoundedCornerShape(16.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
) {
|
||||||
|
CustomAsyncImage(
|
||||||
|
context = context,
|
||||||
|
imageUrl = bannerItem.imageUrl,
|
||||||
|
contentDescription = "agent Image",
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
.clip(RoundedCornerShape(24.dp)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 中间信息区域(占比1)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(horizontal = 8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = bannerItem.agentName,
|
||||||
|
fontSize = 10.sp,
|
||||||
|
color = Color(0xfff5f5f5).copy(alpha = 0.6f),
|
||||||
|
fontWeight = androidx.compose.ui.text.font.FontWeight.W500
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = bannerItem.subtitle,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = Color.White,
|
||||||
|
maxLines = 1,
|
||||||
|
fontWeight = androidx.compose.ui.text.font.FontWeight.W400,
|
||||||
|
modifier = Modifier.padding(top = 2.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 右侧进入按钮
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(69.dp)
|
||||||
|
.height(29.dp)
|
||||||
|
.background(
|
||||||
|
color = Color(0x7dffffff),
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
.clickable {
|
||||||
|
// 进入房间逻辑
|
||||||
|
},
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Row(
|
Text(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
text = "进入",
|
||||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp)
|
fontSize = 14.sp,
|
||||||
) {
|
color = Color.White,
|
||||||
Box(
|
fontWeight = androidx.compose.ui.text.font.FontWeight.W600
|
||||||
modifier = Modifier
|
)
|
||||||
.size(24.dp)
|
|
||||||
.background(
|
|
||||||
Color.White.copy(alpha = 0.2f),
|
|
||||||
RoundedCornerShape(16.dp)
|
|
||||||
).blur(6.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
CustomAsyncImage(
|
|
||||||
context = context,
|
|
||||||
imageUrl = bannerItem.imageUrl,
|
|
||||||
contentDescription = "agent Image",
|
|
||||||
contentScale = ContentScale.Crop,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Column {
|
|
||||||
Text(
|
|
||||||
text = bannerItem.agentName,
|
|
||||||
fontSize = 10.sp,
|
|
||||||
color = Color(0xfff5f5f5).copy(alpha = 0.6f),
|
|
||||||
fontWeight = androidx.compose.ui.text.font.FontWeight.W500
|
|
||||||
)
|
|
||||||
// 新增的文字行
|
|
||||||
Text(
|
|
||||||
text = bannerItem.subtitle,
|
|
||||||
fontSize = 12.sp,
|
|
||||||
color = Color.White,
|
|
||||||
fontWeight = androidx.compose.ui.text.font.FontWeight.W400,
|
|
||||||
modifier = Modifier.padding(top = 2.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Column (
|
|
||||||
modifier = Modifier
|
|
||||||
.wrapContentWidth()
|
|
||||||
.padding(end = 16.dp)
|
|
||||||
){
|
|
||||||
// 进入按钮
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.width(69.dp)
|
|
||||||
.height(29.dp)
|
|
||||||
.background(
|
|
||||||
color = Color(0x7dffffff),
|
|
||||||
shape = RoundedCornerShape(8.dp)
|
|
||||||
)
|
|
||||||
.clickable {
|
|
||||||
// 进入房间逻辑
|
|
||||||
},
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "进入",
|
|
||||||
fontSize = 14.sp,
|
|
||||||
color = Color.White,
|
|
||||||
fontWeight = androidx.compose.ui.text.font.FontWeight.W600
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -810,12 +813,11 @@ fun BannerSection(bannerItems: List<BannerItem>) {
|
|||||||
painter = painterResource(R.mipmap.rider_pro_fire2),
|
painter = painterResource(R.mipmap.rider_pro_fire2),
|
||||||
contentDescription = "fire",
|
contentDescription = "fire",
|
||||||
modifier = Modifier.size(28.dp),
|
modifier = Modifier.size(28.dp),
|
||||||
colorFilter = ColorFilter.tint(Color(0xFFEF4444))
|
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "正在高能对话中",
|
text = "正在高能对话中",
|
||||||
fontSize = 20.sp,
|
fontSize = 16.sp,
|
||||||
fontWeight = androidx.compose.ui.text.font.FontWeight.W600,
|
fontWeight = androidx.compose.ui.text.font.FontWeight.W600,
|
||||||
color = AppColors.text
|
color = AppColors.text
|
||||||
)
|
)
|
||||||
@@ -854,7 +856,7 @@ fun BannerSection(bannerItems: List<BannerItem>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Agent ViewPager
|
// Agent ViewPager
|
||||||
AgentViewPagerSection(agentItems = viewModel.agentItems)
|
AgentViewPagerSection(agentItems = viewModel.agentItems.take(9))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -874,7 +876,6 @@ fun BannerSection(bannerItems: List<BannerItem>) {
|
|||||||
painter = painterResource(R.mipmap.rider_pro_hot_room),
|
painter = painterResource(R.mipmap.rider_pro_hot_room),
|
||||||
contentDescription = "chat room",
|
contentDescription = "chat room",
|
||||||
modifier = Modifier.size(24.dp),
|
modifier = Modifier.size(24.dp),
|
||||||
colorFilter = ColorFilter.tint(Color(0xFFFFA500))
|
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ fun HotMomentsList() {
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.pullRefresh(state)
|
.pullRefresh(state)
|
||||||
|
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxWidth().background(
|
modifier = Modifier.fillMaxWidth().background(
|
||||||
@@ -80,6 +81,7 @@ fun HotMomentsList() {
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
|
.padding(2.dp)
|
||||||
) {
|
) {
|
||||||
DiscoverView()
|
DiscoverView()
|
||||||
PullRefreshIndicator(refreshing, state, Modifier.align(Alignment.TopCenter))
|
PullRefreshIndicator(refreshing, state, Modifier.align(Alignment.TopCenter))
|
||||||
@@ -107,7 +109,6 @@ fun DiscoverView() {
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.aspectRatio(1f)
|
.aspectRatio(1f)
|
||||||
.padding(2.dp)
|
.padding(2.dp)
|
||||||
.clip(RoundedCornerShape(8.dp))
|
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
navController.navigateToPost(
|
navController.navigateToPost(
|
||||||
id = momentItem.id,
|
id = momentItem.id,
|
||||||
|
|||||||
@@ -5,19 +5,24 @@
|
|||||||
android:viewportWidth="24"
|
android:viewportWidth="24"
|
||||||
android:viewportHeight="24">
|
android:viewportHeight="24">
|
||||||
|
|
||||||
<path
|
<group
|
||||||
android:fillType="evenOdd"
|
android:translateX="3"
|
||||||
android:strokeColor="#110C13"
|
android:translateY="4">
|
||||||
android:strokeWidth="1.432"
|
<path
|
||||||
android:strokeLineCap="round"
|
android:fillType="evenOdd"
|
||||||
android:pathData="M8.804 5.458a3.58 3.58 0 0 1 3.074-1.046l5.454 0.797 c2.008 0.293 3.407 2.224 3.125 4.312l-1.022 7.56-1.167-0.17" />
|
android:strokeColor="#110C13"
|
||||||
<path
|
android:strokeWidth="1.5"
|
||||||
android:fillColor="#110C13"
|
android:strokeLineCap="round"
|
||||||
android:fillType="evenOdd"
|
android:pathData="M5.14899667,2.26161678 C5.81639662,1.36500291 6.86507369,0.791250947 8.04718047,0.797664676 L13.5589051,0.827875965 C15.588266,0.838979528 17.2423894,2.5558848 17.2534929,4.66269122 L17.2937025,12.2921122 L16.1141693,12.2856584" />
|
||||||
android:pathData="M6.494 14.375 7.348-0.04-0.01 1.907-7.349 0.04 z" />
|
<path
|
||||||
<path
|
android:fillColor="#110C13"
|
||||||
android:fillType="evenOdd"
|
android:fillType="evenOdd"
|
||||||
android:strokeColor="#110C13"
|
android:strokeWidth="1"
|
||||||
android:strokeWidth="1.432"
|
android:pathData="M 3.49352162 10.3748471 L 10.8424878 10.3346375 L 10.8324354 12.2419928 L 3.48346923 12.2822023 Z" />
|
||||||
android:pathData="M6.77 8.113 5.171-0.756a3.818 3.818 0 0 1 4.335 3.266l0.51 3.775a3.818 3.818 0 0 1-3.23 4.289L4.565 20 3.54 12.402a3.818 3.818 0 0 1 3.23-4.29z" />
|
<path
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:strokeColor="#110C13"
|
||||||
|
android:strokeWidth="1.5"
|
||||||
|
android:pathData="M4.53948169,3.69210555 L9.7647742,3.66351559 C11.8731096,3.65197992 13.5916046,5.35177192 13.6031402,7.46010728 C13.603215,7.47377603 13.6032164,7.48744509 13.6031444,7.50111385 L13.5830711,11.3098388 C13.5720438,13.4021803 11.8788145,15.0957501 9.78647531,15.1071983 L0.702475953,15.156901 L0.702475953,15.156901 L0.742885864,7.48946504 C0.753913192,5.39712355 2.44714246,3.7035537 4.53948169,3.69210555 Z" />
|
||||||
|
</group>
|
||||||
</vector>
|
</vector>
|
||||||
BIN
app/src/main/res/mipmap-xhdpi/ic_agent.png
Normal file
BIN
app/src/main/res/mipmap-xhdpi/ic_agent.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
Reference in New Issue
Block a user