新增对话

This commit is contained in:
2024-09-23 23:47:16 +08:00
parent 2b79ff319f
commit b94d8fb7b3
18 changed files with 743 additions and 51 deletions

View File

@@ -110,5 +110,7 @@ dependencies {
implementation("com.google.firebase:firebase-perf") implementation("com.google.firebase:firebase-perf")
implementation("com.google.firebase:firebase-messaging-ktx") implementation("com.google.firebase:firebase-messaging-ktx")
implementation ("cn.jiguang.sdk:jpush-google:5.4.0") implementation ("cn.jiguang.sdk:jpush-google:5.4.0")
api ("com.tencent.imsdk:imsdk-plus:8.1.6116")
} }

View File

@@ -18,4 +18,6 @@
# If you keep the line number information, uncomment this to # If you keep the line number information, uncomment this to
# hide the original source file name. # hide the original source file name.
#-renamesourcefileattribute SourceFile #-renamesourcefileattribute SourceFile
-keep class com.tencent.imsdk.** { *; }

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
@@ -7,7 +8,7 @@
<uses-permission android:name="android.permission.READ_PHONE_STATE" /> <uses-permission android:name="android.permission.READ_PHONE_STATE" />
<application <application
android:allowBackup="true" android:allowBackup="false"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/rider_pro_log" android:icon="@mipmap/rider_pro_log"
@@ -36,6 +37,7 @@
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:windowSoftInputMode="adjustResize"
android:label="@string/app_name" android:label="@string/app_name"
android:theme="@style/Theme.RiderPro"> android:theme="@style/Theme.RiderPro">
<intent-filter> <intent-filter>

View File

@@ -1,5 +1,11 @@
package com.aiosman.riderpro 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.FavouriteListViewModel
import com.aiosman.riderpro.ui.favourite.FavouriteNoticeViewModel import com.aiosman.riderpro.ui.favourite.FavouriteNoticeViewModel
import com.aiosman.riderpro.ui.follower.FollowerNoticeViewModel 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.DiscoverViewModel
import com.aiosman.riderpro.ui.index.tabs.search.SearchViewModel import com.aiosman.riderpro.ui.index.tabs.search.SearchViewModel
import com.aiosman.riderpro.ui.like.LikeNoticeViewModel 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 { object AppState {
var UserId: Int? = null 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() { fun ReloadAppState() {
// 重置动态列表页面 // 重置动态列表页面
MomentViewModel.ResetModel() MomentViewModel.ResetModel()

View File

@@ -34,10 +34,15 @@ import com.aiosman.riderpro.ui.navigateToPost
import com.aiosman.riderpro.ui.post.NewPostViewModel import com.aiosman.riderpro.ui.post.NewPostViewModel
import com.aiosman.riderpro.utils.Utils import com.aiosman.riderpro.utils.Utils
import com.google.firebase.analytics.FirebaseAnalytics 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.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
// Firebase Analytics // Firebase Analytics
private lateinit var analytics: FirebaseAnalytics private lateinit var analytics: FirebaseAnalytics
@@ -50,7 +55,7 @@ class MainActivity : ComponentActivity() {
if (isGranted) { if (isGranted) {
// FCM SDK (and your app) can post notifications. // FCM SDK (and your app) can post notifications.
} else { } else {
// TODO: Inform user that that your app will not show notifications.
} }
} }
@@ -61,17 +66,6 @@ class MainActivity : ComponentActivity() {
val accountService: AccountService = AccountServiceImpl() val accountService: AccountService = AccountServiceImpl()
try { try {
val resp = accountService.getMyAccount() 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 return true
} catch (e: Exception) { } catch (e: Exception) {
return false return false
@@ -102,11 +96,9 @@ class MainActivity : ComponentActivity() {
JPushInterface.init(this) JPushInterface.init(this)
// JPushInterface.setAlias(this, 0, "myTest")
// Log.d("MainActivity", "pushId: $pushId")
enableEdgeToEdge() enableEdgeToEdge()
// 初始化腾讯云通信 SDK
scope.launch { scope.launch {
@@ -115,9 +107,10 @@ class MainActivity : ComponentActivity() {
var startDestination = NavigationRoute.Login.route var startDestination = NavigationRoute.Login.route
// 如果有登录态,且记住登录状态,且账号有效,则初始化 FCM下一步进入首页 // 如果有登录态,且记住登录状态,且账号有效,则初始化 FCM下一步进入首页
if (AppStore.token != null && AppStore.rememberMe && isAccountValidate) { if (AppStore.token != null && AppStore.rememberMe && isAccountValidate) {
Messaging.RegistDevice(scope, this@MainActivity) AppState.initWithAccount(scope, this@MainActivity)
startDestination = NavigationRoute.Index.route startDestination = NavigationRoute.Index.route
} }
setContent { setContent {
Navigation(startDestination) { navController -> Navigation(startDestination) { navController ->
// 处理带有 postId 的通知点击 // 处理带有 postId 的通知点击

View File

@@ -2,12 +2,14 @@ package com.aiosman.riderpro.data
import com.aiosman.riderpro.AppState import com.aiosman.riderpro.AppState
import com.aiosman.riderpro.data.api.ApiClient 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.ChangePasswordRequestBody
import com.aiosman.riderpro.data.api.GoogleRegisterRequestBody import com.aiosman.riderpro.data.api.GoogleRegisterRequestBody
import com.aiosman.riderpro.data.api.LoginUserRequestBody import com.aiosman.riderpro.data.api.LoginUserRequestBody
import com.aiosman.riderpro.data.api.RegisterMessageChannelRequestBody import com.aiosman.riderpro.data.api.RegisterMessageChannelRequestBody
import com.aiosman.riderpro.data.api.RegisterRequestBody import com.aiosman.riderpro.data.api.RegisterRequestBody
import com.aiosman.riderpro.data.api.ResetPasswordRequestBody 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.UpdateNoticeRequestBody
import com.aiosman.riderpro.data.api.UpdateUserLangRequestBody import com.aiosman.riderpro.data.api.UpdateUserLangRequestBody
import com.aiosman.riderpro.entity.AccountFavouriteEntity import com.aiosman.riderpro.entity.AccountFavouriteEntity
@@ -46,6 +48,8 @@ data class AccountProfile(
val bio: String, val bio: String,
// 主页背景图 // 主页背景图
val banner: String?, val banner: String?,
// trtcUserId
val trtcUserId: String,
) { ) {
/** /**
* 转换为Entity * 转换为Entity
@@ -65,7 +69,8 @@ data class AccountProfile(
return@let "${ApiClient.BASE_SERVER}$it" return@let "${ApiClient.BASE_SERVER}$it"
} }
null null
} },
trtcUserId = trtcUserId
) )
} }
} }
@@ -355,6 +360,13 @@ interface AccountService {
* 更新用户额外信息 * 更新用户额外信息
*/ */
suspend fun updateUserExtra(language: String, timeOffset: Int, timezone: String) suspend fun updateUserExtra(language: String, timeOffset: Int, timezone: String)
/**
* 获取腾讯云TRTC签名
*/
suspend fun getMyTrtcSign(): TrtcSignResponseBody
suspend fun getAppConfig(): AppConfig
} }
class AccountServiceImpl : AccountService { class AccountServiceImpl : AccountService {
@@ -490,4 +502,15 @@ class AccountServiceImpl : AccountService {
ApiClient.api.updateUserExtra(UpdateUserLangRequestBody(language, timeOffset, timezone)) 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
}
} }

View File

@@ -107,6 +107,18 @@ data class UpdateUserLangRequestBody(
val timezone: String, 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 { interface RiderProAPI {
@POST("register") @POST("register")
suspend fun register(@Body body: RegisterRequestBody): Response<Unit> suspend fun register(@Body body: RegisterRequestBody): Response<Unit>
@@ -300,4 +312,9 @@ interface RiderProAPI {
@Body body: UpdateUserLangRequestBody @Body body: UpdateUserLangRequestBody
): Response<Unit> ): Response<Unit>
@GET("account/my/chat/sign")
suspend fun getChatSign(): Response<DataContainer<TrtcSignResponseBody>>
@GET("app/info")
suspend fun getAppConfig(): Response<DataContainer<AppConfig>>
} }

View File

@@ -59,6 +59,8 @@ data class AccountProfileEntity(
val isFollowing: Boolean, val isFollowing: Boolean,
// 主页背景图 // 主页背景图
val banner: String?, val banner: String?,
// trtcUserId
val trtcUserId: String,
) )
/** /**

View File

@@ -8,6 +8,7 @@ import com.aiosman.riderpro.R
import com.aiosman.riderpro.data.api.ApiClient import com.aiosman.riderpro.data.api.ApiClient
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
import java.util.concurrent.TimeUnit
/** /**
* 格式化时间为 xx 前 * 格式化时间为 xx 前
@@ -60,4 +61,23 @@ fun Date.formatPostTime2(): String {
val hour = calendar.get(Calendar.HOUR_OF_DAY) val hour = calendar.get(Calendar.HOUR_OF_DAY)
val minute = calendar.get(Calendar.MINUTE) val minute = calendar.get(Calendar.MINUTE)
return "$year.$month.$day $hour:$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)
}
} }

View File

@@ -29,6 +29,7 @@ import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.LocalSharedTransitionScope import com.aiosman.riderpro.LocalSharedTransitionScope
import com.aiosman.riderpro.ui.account.AccountEditScreen2 import com.aiosman.riderpro.ui.account.AccountEditScreen2
import com.aiosman.riderpro.ui.account.ResetPasswordScreen 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.comment.CommentsScreen
import com.aiosman.riderpro.ui.favourite.FavouriteListPage import com.aiosman.riderpro.ui.favourite.FavouriteListPage
import com.aiosman.riderpro.ui.favourite.FavouriteNoticeScreen import com.aiosman.riderpro.ui.favourite.FavouriteNoticeScreen
@@ -84,6 +85,7 @@ sealed class NavigationRoute(
data object FollowingList : NavigationRoute("FollowingList/{id}") data object FollowingList : NavigationRoute("FollowingList/{id}")
data object ResetPassword : NavigationRoute("ResetPassword") data object ResetPassword : NavigationRoute("ResetPassword")
data object FavouriteList : NavigationRoute("FavouriteList") data object FavouriteList : NavigationRoute("FavouriteList")
data object Chat : NavigationRoute("Chat/{id}")
} }
@@ -340,6 +342,16 @@ fun NavigationController(
FavouriteListPage() 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("{highlightCommentId}", highlightCommentId.toString())
.replace("{initImagePagerIndex}", initImagePagerIndex.toString()) .replace("{initImagePagerIndex}", initImagePagerIndex.toString())
) )
}
fun NavHostController.navigateToChat(id: String) {
navigate(
route = NavigationRoute.Chat.route
.replace("{id}", id)
)
} }

View 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 = ""
}
},
)
}
}
}

View 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
}
}

View File

@@ -30,6 +30,7 @@ import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Button
import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add 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.CustomAsyncImage
import com.aiosman.riderpro.ui.composables.MenuItem import com.aiosman.riderpro.ui.composables.MenuItem
import com.aiosman.riderpro.ui.modifiers.noRippleClickable import com.aiosman.riderpro.ui.modifiers.noRippleClickable
import com.aiosman.riderpro.ui.navigateToChat
import com.aiosman.riderpro.ui.navigateToPost import com.aiosman.riderpro.ui.navigateToPost
import com.aiosman.riderpro.ui.post.NewPostViewModel import com.aiosman.riderpro.ui.post.NewPostViewModel
import com.aiosman.riderpro.ui.post.PostViewModel import com.aiosman.riderpro.ui.post.PostViewModel
@@ -526,6 +528,28 @@ fun CommunicationOperatorGroup(
style = TextStyle(fontWeight = FontWeight.W600, fontStyle = FontStyle.Italic), 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) { if (isSelf) {

View File

@@ -156,17 +156,7 @@ fun EmailSignupScreen() {
} }
// 获取token 信息 // 获取token 信息
try { try {
val resp = accountService.getMyAccount() AppState.initWithAccount(scope, context)
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)
} catch (e: ServiceException) { } catch (e: ServiceException) {
scope.launch(Dispatchers.Main) { scope.launch(Dispatchers.Main) {
Toast.makeText(context, "Failed to get account", Toast.LENGTH_SHORT).show() Toast.makeText(context, "Failed to get account", Toast.LENGTH_SHORT).show()

View File

@@ -86,17 +86,9 @@ fun SignupScreen() {
} }
// 获取token 信息 // 获取token 信息
try { try {
val resp = accountService.getMyAccount() AppState.initWithAccount(coroutineScope, context)
val calendar: Calendar = Calendar.getInstance() } catch (e: Exception) {
val tz: TimeZone = calendar.timeZone Log.e(TAG, "Failed to init with account", e)
val offsetInMillis: Int = tz.rawOffset
accountService.updateUserExtra(
Utils.getCurrentLanguage(),
offsetInMillis / 1000,
tz.displayName
)
AppState.UserId = resp.id
Messaging.RegistDevice(coroutineScope, context)
} catch (e: ServiceException) { } catch (e: ServiceException) {
coroutineScope.launch(Dispatchers.Main) { coroutineScope.launch(Dispatchers.Main) {
Toast.makeText(context, "Failed to get account", Toast.LENGTH_SHORT) Toast.makeText(context, "Failed to get account", Toast.LENGTH_SHORT)

View File

@@ -32,6 +32,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.aiosman.riderpro.AppState
import com.aiosman.riderpro.AppStore import com.aiosman.riderpro.AppStore
import com.aiosman.riderpro.LocalNavController import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.Messaging import com.aiosman.riderpro.Messaging
@@ -83,16 +84,7 @@ fun UserAuthScreen() {
this.rememberMe = rememberMe this.rememberMe = rememberMe
saveData() saveData()
} }
accountService.getMyAccount() AppState.initWithAccount(scope, context)
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)
navController.navigate(NavigationRoute.Index.route) { navController.navigate(NavigationRoute.Index.route) {
popUpTo(NavigationRoute.Login.route) { inclusive = true } popUpTo(NavigationRoute.Login.route) { inclusive = true }
} }

View File

@@ -70,4 +70,6 @@
<string name="recover">找回</string> <string name="recover">找回</string>
<string name="reset_mail_send_success">邮件已发送!请查收您的邮箱,按照邮件中的指示重置密码。</string> <string name="reset_mail_send_success">邮件已发送!请查收您的邮箱,按照邮件中的指示重置密码。</string>
<string name="reset_mail_send_failed">邮件发送失败,请检查您的网络连接或稍后重试。</string> <string name="reset_mail_send_failed">邮件发送失败,请检查您的网络连接或稍后重试。</string>
<string name="seconds_ago">%1d秒前</string>
<string name="minutes_ago">%1d分钟前</string>
</resources> </resources>

View File

@@ -69,4 +69,6 @@
<string name="recover">Recover</string> <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_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="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> </resources>