新增对话
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
@@ -7,7 +8,7 @@
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:allowBackup="false"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/rider_pro_log"
|
||||
@@ -36,6 +37,7 @@
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.RiderPro">
|
||||
<intent-filter>
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
package com.aiosman.riderpro
|
||||
|
||||
import android.content.Context
|
||||
import android.icu.util.Calendar
|
||||
import android.icu.util.TimeZone
|
||||
import android.util.Log
|
||||
import com.aiosman.riderpro.data.AccountService
|
||||
import com.aiosman.riderpro.data.AccountServiceImpl
|
||||
import com.aiosman.riderpro.ui.favourite.FavouriteListViewModel
|
||||
import com.aiosman.riderpro.ui.favourite.FavouriteNoticeViewModel
|
||||
import com.aiosman.riderpro.ui.follower.FollowerNoticeViewModel
|
||||
@@ -11,10 +17,68 @@ import com.aiosman.riderpro.ui.index.tabs.profile.MyProfileViewModel
|
||||
import com.aiosman.riderpro.ui.index.tabs.search.DiscoverViewModel
|
||||
import com.aiosman.riderpro.ui.index.tabs.search.SearchViewModel
|
||||
import com.aiosman.riderpro.ui.like.LikeNoticeViewModel
|
||||
import com.aiosman.riderpro.utils.Utils
|
||||
import com.tencent.imsdk.v2.V2TIMCallback
|
||||
import com.tencent.imsdk.v2.V2TIMLogListener
|
||||
import com.tencent.imsdk.v2.V2TIMManager
|
||||
import com.tencent.imsdk.v2.V2TIMSDKConfig
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
object AppState {
|
||||
var UserId: Int? = null
|
||||
|
||||
suspend fun initWithAccount(scope: CoroutineScope, context: Context) {
|
||||
val accountService: AccountService = AccountServiceImpl()
|
||||
// 获取用户认证信息
|
||||
val resp = accountService.getMyAccount()
|
||||
// 更新必要的用户信息
|
||||
val calendar: Calendar = Calendar.getInstance()
|
||||
val tz: TimeZone = calendar.timeZone
|
||||
val offsetInMillis: Int = tz.rawOffset
|
||||
accountService.updateUserExtra(
|
||||
Utils.getCurrentLanguage(),
|
||||
// 时区偏移量单位是秒
|
||||
offsetInMillis / 1000,
|
||||
tz.displayName
|
||||
)
|
||||
// 设置当前登录用户 ID
|
||||
UserId = resp.id
|
||||
|
||||
// 注册 JPush
|
||||
Messaging.RegistDevice(scope, context)
|
||||
// 注册 Trtc
|
||||
val config = V2TIMSDKConfig()
|
||||
|
||||
config.logLevel = V2TIMSDKConfig.V2TIM_LOG_INFO
|
||||
|
||||
config.logListener = object : V2TIMLogListener() {
|
||||
override fun onLog(logLevel: Int, logContent: String) {
|
||||
Log.d("V2TIMLogListener", logContent)
|
||||
}
|
||||
}
|
||||
val appConfig = accountService.getAppConfig()
|
||||
V2TIMManager.getInstance().initSDK(context, appConfig.trtcAppId, config)
|
||||
try {
|
||||
val sign = accountService.getMyTrtcSign()
|
||||
V2TIMManager.getInstance().login(
|
||||
sign.userId,
|
||||
sign.sig,
|
||||
object : V2TIMCallback {
|
||||
override fun onError(code: Int, desc: String?) {
|
||||
Log.e("V2TIMManager", "login failed: $code, $desc")
|
||||
}
|
||||
|
||||
override fun onSuccess() {
|
||||
Log.d("V2TIMManager", "login success")
|
||||
}
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun ReloadAppState() {
|
||||
// 重置动态列表页面
|
||||
MomentViewModel.ResetModel()
|
||||
|
||||
@@ -34,10 +34,15 @@ import com.aiosman.riderpro.ui.navigateToPost
|
||||
import com.aiosman.riderpro.ui.post.NewPostViewModel
|
||||
import com.aiosman.riderpro.utils.Utils
|
||||
import com.google.firebase.analytics.FirebaseAnalytics
|
||||
import com.tencent.imsdk.v2.V2TIMCallback
|
||||
import com.tencent.imsdk.v2.V2TIMLogListener
|
||||
import com.tencent.imsdk.v2.V2TIMManager
|
||||
import com.tencent.imsdk.v2.V2TIMSDKConfig
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
// Firebase Analytics
|
||||
private lateinit var analytics: FirebaseAnalytics
|
||||
@@ -50,7 +55,7 @@ class MainActivity : ComponentActivity() {
|
||||
if (isGranted) {
|
||||
// FCM SDK (and your app) can post notifications.
|
||||
} else {
|
||||
// TODO: Inform user that that your app will not show notifications.
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,17 +66,6 @@ class MainActivity : ComponentActivity() {
|
||||
val accountService: AccountService = AccountServiceImpl()
|
||||
try {
|
||||
val resp = accountService.getMyAccount()
|
||||
val calendar: Calendar = Calendar.getInstance()
|
||||
val tz: TimeZone = calendar.timeZone
|
||||
val offsetInMillis: Int = tz.rawOffset
|
||||
accountService.updateUserExtra(
|
||||
Utils.getCurrentLanguage(),
|
||||
// 时区偏移量单位是秒
|
||||
offsetInMillis / 1000,
|
||||
tz.displayName
|
||||
)
|
||||
// 设置当前登录用户 ID
|
||||
AppState.UserId = resp.id
|
||||
return true
|
||||
} catch (e: Exception) {
|
||||
return false
|
||||
@@ -102,11 +96,9 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
JPushInterface.init(this)
|
||||
|
||||
// JPushInterface.setAlias(this, 0, "myTest")
|
||||
|
||||
// Log.d("MainActivity", "pushId: $pushId")
|
||||
enableEdgeToEdge()
|
||||
|
||||
// 初始化腾讯云通信 SDK
|
||||
|
||||
|
||||
scope.launch {
|
||||
@@ -115,9 +107,10 @@ class MainActivity : ComponentActivity() {
|
||||
var startDestination = NavigationRoute.Login.route
|
||||
// 如果有登录态,且记住登录状态,且账号有效,则初始化 FCM,下一步进入首页
|
||||
if (AppStore.token != null && AppStore.rememberMe && isAccountValidate) {
|
||||
Messaging.RegistDevice(scope, this@MainActivity)
|
||||
AppState.initWithAccount(scope, this@MainActivity)
|
||||
startDestination = NavigationRoute.Index.route
|
||||
}
|
||||
|
||||
setContent {
|
||||
Navigation(startDestination) { navController ->
|
||||
// 处理带有 postId 的通知点击
|
||||
|
||||
@@ -2,12 +2,14 @@ package com.aiosman.riderpro.data
|
||||
|
||||
import com.aiosman.riderpro.AppState
|
||||
import com.aiosman.riderpro.data.api.ApiClient
|
||||
import com.aiosman.riderpro.data.api.AppConfig
|
||||
import com.aiosman.riderpro.data.api.ChangePasswordRequestBody
|
||||
import com.aiosman.riderpro.data.api.GoogleRegisterRequestBody
|
||||
import com.aiosman.riderpro.data.api.LoginUserRequestBody
|
||||
import com.aiosman.riderpro.data.api.RegisterMessageChannelRequestBody
|
||||
import com.aiosman.riderpro.data.api.RegisterRequestBody
|
||||
import com.aiosman.riderpro.data.api.ResetPasswordRequestBody
|
||||
import com.aiosman.riderpro.data.api.TrtcSignResponseBody
|
||||
import com.aiosman.riderpro.data.api.UpdateNoticeRequestBody
|
||||
import com.aiosman.riderpro.data.api.UpdateUserLangRequestBody
|
||||
import com.aiosman.riderpro.entity.AccountFavouriteEntity
|
||||
@@ -46,6 +48,8 @@ data class AccountProfile(
|
||||
val bio: String,
|
||||
// 主页背景图
|
||||
val banner: String?,
|
||||
// trtcUserId
|
||||
val trtcUserId: String,
|
||||
) {
|
||||
/**
|
||||
* 转换为Entity
|
||||
@@ -65,7 +69,8 @@ data class AccountProfile(
|
||||
return@let "${ApiClient.BASE_SERVER}$it"
|
||||
}
|
||||
null
|
||||
}
|
||||
},
|
||||
trtcUserId = trtcUserId
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -355,6 +360,13 @@ interface AccountService {
|
||||
* 更新用户额外信息
|
||||
*/
|
||||
suspend fun updateUserExtra(language: String, timeOffset: Int, timezone: String)
|
||||
|
||||
/**
|
||||
* 获取腾讯云TRTC签名
|
||||
*/
|
||||
suspend fun getMyTrtcSign(): TrtcSignResponseBody
|
||||
|
||||
suspend fun getAppConfig(): AppConfig
|
||||
}
|
||||
|
||||
class AccountServiceImpl : AccountService {
|
||||
@@ -490,4 +502,15 @@ class AccountServiceImpl : AccountService {
|
||||
ApiClient.api.updateUserExtra(UpdateUserLangRequestBody(language, timeOffset, timezone))
|
||||
}
|
||||
|
||||
override suspend fun getMyTrtcSign(): TrtcSignResponseBody {
|
||||
val resp = ApiClient.api.getChatSign()
|
||||
val body = resp.body() ?: throw ServiceException("Failed to get trtc sign")
|
||||
return body.data
|
||||
}
|
||||
|
||||
override suspend fun getAppConfig(): AppConfig {
|
||||
val resp = ApiClient.api.getAppConfig()
|
||||
val body = resp.body() ?: throw ServiceException("Failed to get app config")
|
||||
return body.data
|
||||
}
|
||||
}
|
||||
@@ -107,6 +107,18 @@ data class UpdateUserLangRequestBody(
|
||||
val timezone: String,
|
||||
)
|
||||
|
||||
data class TrtcSignResponseBody(
|
||||
@SerializedName("sig")
|
||||
val sig: String,
|
||||
@SerializedName("userId")
|
||||
val userId: String,
|
||||
)
|
||||
|
||||
data class AppConfig(
|
||||
@SerializedName("trtcAppId")
|
||||
val trtcAppId: Int,
|
||||
)
|
||||
|
||||
interface RiderProAPI {
|
||||
@POST("register")
|
||||
suspend fun register(@Body body: RegisterRequestBody): Response<Unit>
|
||||
@@ -300,4 +312,9 @@ interface RiderProAPI {
|
||||
@Body body: UpdateUserLangRequestBody
|
||||
): Response<Unit>
|
||||
|
||||
@GET("account/my/chat/sign")
|
||||
suspend fun getChatSign(): Response<DataContainer<TrtcSignResponseBody>>
|
||||
|
||||
@GET("app/info")
|
||||
suspend fun getAppConfig(): Response<DataContainer<AppConfig>>
|
||||
}
|
||||
@@ -59,6 +59,8 @@ data class AccountProfileEntity(
|
||||
val isFollowing: Boolean,
|
||||
// 主页背景图
|
||||
val banner: String?,
|
||||
// trtcUserId
|
||||
val trtcUserId: String,
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.aiosman.riderpro.R
|
||||
import com.aiosman.riderpro.data.api.ApiClient
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* 格式化时间为 xx 前
|
||||
@@ -60,4 +61,23 @@ fun Date.formatPostTime2(): String {
|
||||
val hour = calendar.get(Calendar.HOUR_OF_DAY)
|
||||
val minute = calendar.get(Calendar.MINUTE)
|
||||
return "$year.$month.$day $hour:$minute"
|
||||
}
|
||||
|
||||
fun Date.formatChatTime(context: Context): String {
|
||||
val now = Date()
|
||||
val diffInMillis = now.time - this.time
|
||||
|
||||
val seconds = TimeUnit.MILLISECONDS.toSeconds(diffInMillis)
|
||||
val minutes = TimeUnit.MILLISECONDS.toMinutes(diffInMillis)
|
||||
val hours = TimeUnit.MILLISECONDS.toHours(diffInMillis)
|
||||
val days = TimeUnit.MILLISECONDS.toDays(diffInMillis)
|
||||
val years = days / 365
|
||||
|
||||
return when {
|
||||
seconds < 60 -> context.getString(R.string.seconds_ago, seconds)
|
||||
minutes < 60 -> context.getString(R.string.minutes_ago, minutes)
|
||||
hours < 24 -> SimpleDateFormat("HH:mm", Locale.getDefault()).format(this)
|
||||
days < 365 -> SimpleDateFormat("MM-dd HH:mm", Locale.getDefault()).format(this)
|
||||
else -> SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(this)
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import com.aiosman.riderpro.LocalNavController
|
||||
import com.aiosman.riderpro.LocalSharedTransitionScope
|
||||
import com.aiosman.riderpro.ui.account.AccountEditScreen2
|
||||
import com.aiosman.riderpro.ui.account.ResetPasswordScreen
|
||||
import com.aiosman.riderpro.ui.chat.ChatScreen
|
||||
import com.aiosman.riderpro.ui.comment.CommentsScreen
|
||||
import com.aiosman.riderpro.ui.favourite.FavouriteListPage
|
||||
import com.aiosman.riderpro.ui.favourite.FavouriteNoticeScreen
|
||||
@@ -84,6 +85,7 @@ sealed class NavigationRoute(
|
||||
data object FollowingList : NavigationRoute("FollowingList/{id}")
|
||||
data object ResetPassword : NavigationRoute("ResetPassword")
|
||||
data object FavouriteList : NavigationRoute("FavouriteList")
|
||||
data object Chat : NavigationRoute("Chat/{id}")
|
||||
}
|
||||
|
||||
|
||||
@@ -340,6 +342,16 @@ fun NavigationController(
|
||||
FavouriteListPage()
|
||||
}
|
||||
}
|
||||
composable(
|
||||
route = NavigationRoute.Chat.route,
|
||||
arguments = listOf(navArgument("id") { type = NavType.StringType })
|
||||
) {
|
||||
CompositionLocalProvider(
|
||||
LocalAnimatedContentScope provides this,
|
||||
) {
|
||||
ChatScreen(it.arguments?.getString("id")!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -381,4 +393,11 @@ fun NavHostController.navigateToPost(
|
||||
.replace("{highlightCommentId}", highlightCommentId.toString())
|
||||
.replace("{initImagePagerIndex}", initImagePagerIndex.toString())
|
||||
)
|
||||
}
|
||||
|
||||
fun NavHostController.navigateToChat(id: String) {
|
||||
navigate(
|
||||
route = NavigationRoute.Chat.route
|
||||
.replace("{id}", id)
|
||||
)
|
||||
}
|
||||
373
app/src/main/java/com/aiosman/riderpro/ui/chat/ChatScreen.kt
Normal file
373
app/src/main/java/com/aiosman/riderpro/ui/chat/ChatScreen.kt
Normal file
@@ -0,0 +1,373 @@
|
||||
package com.aiosman.riderpro.ui.chat
|
||||
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
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.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.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
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.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
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.viewmodel.compose.viewModel
|
||||
import com.aiosman.riderpro.LocalNavController
|
||||
import com.aiosman.riderpro.R
|
||||
import com.aiosman.riderpro.ui.composables.CustomAsyncImage
|
||||
import com.aiosman.riderpro.ui.composables.StatusBarSpacer
|
||||
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
@Composable
|
||||
fun ChatScreen(userId: String) {
|
||||
val navController = LocalNavController.current
|
||||
val context = LocalNavController.current.context
|
||||
val viewModel = viewModel<ChatViewModel>(
|
||||
key = "ChatViewModel_$userId",
|
||||
factory = object : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return ChatViewModel(userId) as T
|
||||
}
|
||||
}
|
||||
)
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.init(context = context)
|
||||
}
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
viewModel.UnRegistListener()
|
||||
}
|
||||
}
|
||||
val listState = rememberLazyListState()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val navigationBarHeight = with(LocalDensity.current) {
|
||||
WindowInsets.navigationBars.getBottom(this).toDp()
|
||||
}
|
||||
LaunchedEffect(listState) {
|
||||
snapshotFlow { listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index }
|
||||
.collect { index ->
|
||||
if (index == listState.layoutInfo.totalItemsCount - 1) {
|
||||
coroutineScope.launch {
|
||||
viewModel.onLoadMore(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Scaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
topBar = {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.border(1.dp, Color(0xffe5e5e5))
|
||||
.background(Color.White)
|
||||
) {
|
||||
StatusBarSpacer()
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp, horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.rider_pro_nav_back),
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.noRippleClickable {
|
||||
navController.popBackStack()
|
||||
},
|
||||
contentDescription = null
|
||||
)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Text(
|
||||
text = viewModel.userProfile?.nickName ?: "",
|
||||
modifier = Modifier.weight(1f),
|
||||
style = TextStyle(
|
||||
color = Color.Black,
|
||||
fontSize = 18.sp,
|
||||
fontWeight = androidx.compose.ui.text.font.FontWeight.Bold
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
bottomBar = {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.imePadding()
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(1.dp)
|
||||
.background(Color(0xfff7f7f7))
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
ChatInput() {
|
||||
viewModel.sendMessage(it, context)
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(navigationBarHeight)
|
||||
)
|
||||
}
|
||||
}
|
||||
) { paddingValues ->
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.background(Color(0xfff7f7f7))
|
||||
.fillMaxSize(),
|
||||
reverseLayout = true,
|
||||
verticalArrangement = Arrangement.Top
|
||||
) {
|
||||
val chatList = viewModel.getDisplayChatList()
|
||||
items(chatList.size) { index ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp)
|
||||
) {
|
||||
ChatItem(item = chatList[index], viewModel.myProfile?.trtcUserId!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatSelfItem(item: ChatItem) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp)
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.End,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = androidx.compose.ui.Alignment.End,
|
||||
) {
|
||||
Row() {
|
||||
Text(
|
||||
text = item.time,
|
||||
style = TextStyle(
|
||||
color = Color.Gray,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = item.nickname,
|
||||
style = TextStyle(
|
||||
color = Color.Black,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(Color(0xFF000000))
|
||||
.padding(vertical = 8.dp, horizontal = 16.dp),
|
||||
) {
|
||||
Text(
|
||||
text = item.message,
|
||||
style = TextStyle(
|
||||
color = Color.White,
|
||||
fontSize = 16.sp,
|
||||
),
|
||||
textAlign = TextAlign.End,
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
) {
|
||||
CustomAsyncImage(
|
||||
imageUrl = item.avatar,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentDescription = "avatar"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatOtherItem(item: ChatItem) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp)
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
) {
|
||||
CustomAsyncImage(
|
||||
imageUrl = item.avatar,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentDescription = "avatar"
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Column {
|
||||
Row() {
|
||||
Text(
|
||||
text = item.nickname,
|
||||
style = TextStyle(
|
||||
color = Color.Black,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = item.time,
|
||||
style = TextStyle(
|
||||
color = Color.Gray,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(Color(0xffFFFFFF))
|
||||
.padding(vertical = 8.dp, horizontal = 16.dp),
|
||||
) {
|
||||
Text(
|
||||
text = item.message,
|
||||
style = TextStyle(
|
||||
color = Color.Black,
|
||||
fontSize = 16.sp
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatItem(item: ChatItem, currentUserId: String) {
|
||||
val isCurrentUser = item.userId == currentUserId
|
||||
if (isCurrentUser) {
|
||||
ChatSelfItem(item)
|
||||
} else {
|
||||
ChatOtherItem(item)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatInput(
|
||||
onSend: (String) -> Unit = {}
|
||||
) {
|
||||
var text by remember { mutableStateOf("") }
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp, horizontal = 16.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.clip(androidx.compose.foundation.shape.RoundedCornerShape(16.dp))
|
||||
.background(Color(0xffe5e5e5))
|
||||
.padding(horizontal = 16.dp),
|
||||
contentAlignment = androidx.compose.ui.Alignment.CenterStart
|
||||
|
||||
) {
|
||||
BasicTextField(
|
||||
value = text,
|
||||
onValueChange = {
|
||||
text = it
|
||||
},
|
||||
textStyle = TextStyle(
|
||||
color = Color.Black,
|
||||
fontSize = 16.sp
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp)
|
||||
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Crossfade(targetState = text.isNotEmpty(), animationSpec = tween(500)) { isNotEmpty ->
|
||||
Image(
|
||||
painter = rememberUpdatedState(
|
||||
if (isNotEmpty) painterResource(id = R.drawable.rider_pro_send) else painterResource(
|
||||
id = R.drawable.rider_pro_send_disable
|
||||
)
|
||||
).value,
|
||||
contentDescription = "Send",
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.noRippleClickable {
|
||||
if (text.isNotEmpty()) {
|
||||
onSend(text)
|
||||
text = ""
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
173
app/src/main/java/com/aiosman/riderpro/ui/chat/ChatViewModel.kt
Normal file
173
app/src/main/java/com/aiosman/riderpro/ui/chat/ChatViewModel.kt
Normal file
@@ -0,0 +1,173 @@
|
||||
package com.aiosman.riderpro.ui.chat
|
||||
|
||||
import android.content.Context
|
||||
import android.icu.util.Calendar
|
||||
import android.util.Log
|
||||
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.riderpro.data.AccountService
|
||||
import com.aiosman.riderpro.data.AccountServiceImpl
|
||||
import com.aiosman.riderpro.data.UserService
|
||||
import com.aiosman.riderpro.data.UserServiceImpl
|
||||
import com.aiosman.riderpro.entity.AccountProfileEntity
|
||||
import com.aiosman.riderpro.exp.formatChatTime
|
||||
import com.tencent.imsdk.v2.V2TIMAdvancedMsgListener
|
||||
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
|
||||
|
||||
data class ChatItem(
|
||||
val message: String,
|
||||
val avatar: String,
|
||||
val time: String,
|
||||
val userId: String,
|
||||
val nickname: String
|
||||
)
|
||||
|
||||
class ChatViewModel(
|
||||
val userId: String,
|
||||
) : ViewModel() {
|
||||
var chatData by mutableStateOf<List<ChatItem>>(emptyList())
|
||||
var userProfile by mutableStateOf<AccountProfileEntity?>(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
|
||||
fun init(context: Context) {
|
||||
// 获取用户信息
|
||||
viewModelScope.launch {
|
||||
val resp = userService.getUserProfile(userId)
|
||||
userProfile = resp
|
||||
myProfile = accountService.getMyAccountProfile()
|
||||
|
||||
RegistListener(context)
|
||||
fetchHistoryMessage(context)
|
||||
}
|
||||
}
|
||||
|
||||
fun RegistListener(context: Context) {
|
||||
textMessageListener = object : V2TIMAdvancedMsgListener() {
|
||||
override fun onRecvNewMessage(msg: V2TIMMessage?) {
|
||||
super.onRecvNewMessage(msg)
|
||||
chatData = listOf(convertToChatItem(msg!!, context)) + chatData
|
||||
}
|
||||
}
|
||||
V2TIMManager.getMessageManager().addAdvancedMsgListener(textMessageListener);
|
||||
}
|
||||
|
||||
fun UnRegistListener() {
|
||||
V2TIMManager.getMessageManager().removeAdvancedMsgListener(textMessageListener);
|
||||
}
|
||||
|
||||
|
||||
fun convertToChatItem(message: V2TIMMessage, context: Context): ChatItem {
|
||||
val avatar = if (message.sender == userProfile?.trtcUserId) {
|
||||
userProfile?.avatar ?: ""
|
||||
} else {
|
||||
myProfile?.avatar ?: ""
|
||||
}
|
||||
val nickname = if (message.sender == userProfile?.trtcUserId) {
|
||||
userProfile?.nickName ?: ""
|
||||
} else {
|
||||
myProfile?.nickName ?: ""
|
||||
}
|
||||
val timestamp = message.timestamp
|
||||
val calendar = Calendar.getInstance()
|
||||
calendar.timeInMillis = timestamp * 1000
|
||||
|
||||
return ChatItem(
|
||||
message = message.textElem.text,
|
||||
avatar = avatar,
|
||||
time = calendar.time.formatChatTime(context),
|
||||
userId = message.sender,
|
||||
nickname = nickname
|
||||
)
|
||||
}
|
||||
|
||||
fun onLoadMore(context: Context) {
|
||||
if (!hasMore || isLoading) {
|
||||
return
|
||||
}
|
||||
isLoading = true
|
||||
viewModelScope.launch {
|
||||
V2TIMManager.getMessageManager().getC2CHistoryMessageList(
|
||||
userProfile?.trtcUserId!!,
|
||||
20,
|
||||
lastMessage,
|
||||
object : V2TIMValueCallback<List<V2TIMMessage>> {
|
||||
override fun onSuccess(p0: List<V2TIMMessage>?) {
|
||||
chatData = chatData + (p0 ?: emptyList()).map {
|
||||
convertToChatItem(it, context)
|
||||
}
|
||||
if ((p0?.size ?: 0) < 20) {
|
||||
hasMore = false
|
||||
}
|
||||
lastMessage = p0?.lastOrNull()
|
||||
isLoading = false
|
||||
Log.d("ChatViewModel", "fetch history message success")
|
||||
}
|
||||
override fun onError(p0: Int, p1: String?) {
|
||||
Log.e("ChatViewModel", "fetch history message error: $p1")
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun sendMessage(message: String, context: Context) {
|
||||
V2TIMManager.getInstance().sendC2CTextMessage(
|
||||
message,
|
||||
userProfile?.trtcUserId!!,
|
||||
object : V2TIMSendCallback<V2TIMMessage> {
|
||||
override fun onProgress(p0: Int) {
|
||||
|
||||
}
|
||||
override fun onError(p0: Int, p1: String?) {
|
||||
Log.e("ChatViewModel", "send message error: $p1")
|
||||
}
|
||||
|
||||
override fun onSuccess(p0: V2TIMMessage?) {
|
||||
Log.d("ChatViewModel", "send message success")
|
||||
chatData = listOf(convertToChatItem(p0!!, context)) + chatData
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun fetchHistoryMessage(context: Context) {
|
||||
V2TIMManager.getMessageManager().getC2CHistoryMessageList(
|
||||
userProfile?.trtcUserId!!,
|
||||
20,
|
||||
null,
|
||||
object : V2TIMValueCallback<List<V2TIMMessage>> {
|
||||
override fun onSuccess(p0: List<V2TIMMessage>?) {
|
||||
chatData = (p0 ?: emptyList()).map {
|
||||
convertToChatItem(it, context)
|
||||
}
|
||||
if ((p0?.size ?: 0) < 20) {
|
||||
hasMore = false
|
||||
}
|
||||
lastMessage = p0?.lastOrNull()
|
||||
Log.d("ChatViewModel", "fetch history message success")
|
||||
}
|
||||
|
||||
override fun onError(p0: Int, p1: String?) {
|
||||
Log.e("ChatViewModel", "fetch history message error: $p1")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun getDisplayChatList(): List<ChatItem> {
|
||||
return chatData
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
@@ -78,6 +79,7 @@ import com.aiosman.riderpro.ui.NavigationRoute
|
||||
import com.aiosman.riderpro.ui.composables.CustomAsyncImage
|
||||
import com.aiosman.riderpro.ui.composables.MenuItem
|
||||
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
|
||||
import com.aiosman.riderpro.ui.navigateToChat
|
||||
import com.aiosman.riderpro.ui.navigateToPost
|
||||
import com.aiosman.riderpro.ui.post.NewPostViewModel
|
||||
import com.aiosman.riderpro.ui.post.PostViewModel
|
||||
@@ -526,6 +528,28 @@ fun CommunicationOperatorGroup(
|
||||
style = TextStyle(fontWeight = FontWeight.W600, fontStyle = FontStyle.Italic),
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(width = 142.dp, height = 40.dp)
|
||||
.noRippleClickable {
|
||||
navController.navigateToChat(accountProfileEntity.id.toString())
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
painter = painterResource(id = R.mipmap.rider_pro_btn_bg_grey),
|
||||
contentDescription = ""
|
||||
)
|
||||
Text(
|
||||
text = "Chat",
|
||||
fontSize = 14.sp,
|
||||
color = Color.Black,
|
||||
fontWeight = FontWeight.Bold,
|
||||
style = TextStyle(fontWeight = FontWeight.Bold, fontStyle = FontStyle.Italic)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (isSelf) {
|
||||
|
||||
@@ -156,17 +156,7 @@ fun EmailSignupScreen() {
|
||||
}
|
||||
// 获取token 信息
|
||||
try {
|
||||
val resp = accountService.getMyAccount()
|
||||
AppState.UserId = resp.id
|
||||
val calendar: Calendar = Calendar.getInstance()
|
||||
val tz: TimeZone = calendar.timeZone
|
||||
val offsetInMillis: Int = tz.rawOffset
|
||||
accountService.updateUserExtra(
|
||||
Utils.getCurrentLanguage(),
|
||||
offsetInMillis / 1000,
|
||||
tz.displayName
|
||||
)
|
||||
Messaging.RegistDevice(scope, context)
|
||||
AppState.initWithAccount(scope, context)
|
||||
} catch (e: ServiceException) {
|
||||
scope.launch(Dispatchers.Main) {
|
||||
Toast.makeText(context, "Failed to get account", Toast.LENGTH_SHORT).show()
|
||||
|
||||
@@ -86,17 +86,9 @@ fun SignupScreen() {
|
||||
}
|
||||
// 获取token 信息
|
||||
try {
|
||||
val resp = accountService.getMyAccount()
|
||||
val calendar: Calendar = Calendar.getInstance()
|
||||
val tz: TimeZone = calendar.timeZone
|
||||
val offsetInMillis: Int = tz.rawOffset
|
||||
accountService.updateUserExtra(
|
||||
Utils.getCurrentLanguage(),
|
||||
offsetInMillis / 1000,
|
||||
tz.displayName
|
||||
)
|
||||
AppState.UserId = resp.id
|
||||
Messaging.RegistDevice(coroutineScope, context)
|
||||
AppState.initWithAccount(coroutineScope, context)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to init with account", e)
|
||||
} catch (e: ServiceException) {
|
||||
coroutineScope.launch(Dispatchers.Main) {
|
||||
Toast.makeText(context, "Failed to get account", Toast.LENGTH_SHORT)
|
||||
|
||||
@@ -32,6 +32,7 @@ import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.aiosman.riderpro.AppState
|
||||
import com.aiosman.riderpro.AppStore
|
||||
import com.aiosman.riderpro.LocalNavController
|
||||
import com.aiosman.riderpro.Messaging
|
||||
@@ -83,16 +84,7 @@ fun UserAuthScreen() {
|
||||
this.rememberMe = rememberMe
|
||||
saveData()
|
||||
}
|
||||
accountService.getMyAccount()
|
||||
val calendar: Calendar = Calendar.getInstance()
|
||||
val tz: TimeZone = calendar.timeZone
|
||||
val offsetInMillis: Int = tz.rawOffset
|
||||
accountService.updateUserExtra(
|
||||
Utils.getCurrentLanguage(),
|
||||
offsetInMillis / 1000,
|
||||
tz.displayName
|
||||
)
|
||||
Messaging.RegistDevice(scope, context)
|
||||
AppState.initWithAccount(scope, context)
|
||||
navController.navigate(NavigationRoute.Index.route) {
|
||||
popUpTo(NavigationRoute.Login.route) { inclusive = true }
|
||||
}
|
||||
|
||||
@@ -70,4 +70,6 @@
|
||||
<string name="recover">找回</string>
|
||||
<string name="reset_mail_send_success">邮件已发送!请查收您的邮箱,按照邮件中的指示重置密码。</string>
|
||||
<string name="reset_mail_send_failed">邮件发送失败,请检查您的网络连接或稍后重试。</string>
|
||||
<string name="seconds_ago">%1d秒前</string>
|
||||
<string name="minutes_ago">%1d分钟前</string>
|
||||
</resources>
|
||||
@@ -69,4 +69,6 @@
|
||||
<string name="recover">Recover</string>
|
||||
<string name="reset_mail_send_success">An email has been sent to your registered email address. Please check your inbox and follow the instructions to reset your password.</string>
|
||||
<string name="reset_mail_send_failed">Failed to send email. Please check your network connection or try again later.</string>
|
||||
<string name="seconds_ago">%1d seconds ago</string>
|
||||
<string name="minutes_ago">%1d minutes ago</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user