5 Commits

Author SHA1 Message Date
8a76bd11d9 feat: 增加Google支付及优化UI与性能
- **支付功能**:
  - 在应用启动时初始化Google Play Billing Client,为应用内购买做准备。
  - 添加了`billing-ktx`依赖。

- **动态和个人主页**:
  - 动态推荐页:用户头像和昵称区域支持点击跳转到对应的个人资料页。
  - 个人资料页:优化了用户资料加载逻辑,使其同时支持通过用户ID和OpenID加载。
  - 评论功能:优化了评论交互,评论成功后才更新评论数。
  - 数据模型:调整动态图片,优先使用`smallDirectUrl`以优化加载速度。

- **AI智能体页面**:
  - 移除API请求中的`random`参数,以改善数据缓存和一致性。
  - 优化了导航到AI智能体主页的逻辑,直接传递`openId`,简化了数据请求。
  - 清理了部分未使用的代码和布局。

- **群聊列表UI**:
  - 调整了群聊列表项的布局、字体大小和颜色,优化了视觉样式。
  - 移除了列表项之间的分割线。
2025-11-19 23:51:43 +08:00
f1e91f7639 Merge branch 'main' into atm2 2025-11-18 21:46:59 +08:00
4feca77924 Merge branch 'main' into atm2
# Conflicts:
#	app/src/main/java/com/aiosman/ravenow/ui/index/tabs/message/tab/AllChatListScreen.kt
2025-11-18 01:29:34 +08:00
b12f359da1 优化会话列表的数据加载和合并逻辑
- 在 `AndroidManifest.xml` 中添加 `com.android.vending.BILLING` 权限。
- 重构 `AllChatListScreen.kt` 的数据加载机制,通过 `LaunchedEffect` 监听各个 `ViewModel` 的列表变化,并自动合并数据。
- 将原有的数据刷新和合并逻辑拆分为 `refreshAllData` 和 `combineAllData` 两个函数,提高了代码的清晰度和复用性。
- 优化了下拉刷新和初次加载的逻辑,确保在所有数据源加载完成后才更新UI状态,提升了用户体验。
2025-11-14 16:47:15 +08:00
8901792561 新增通知类消息展示及解析逻辑
- 新增 `OpenIMMessageType.kt`,用于统一管理 OpenIM 的消息类型常量,并提供判断是否为通知类型消息的辅助函数。
- 新增 `NotificationMessageHelper.kt`,用于根据不同的通知类型生成用户友好的提示文本。
- 新增 `NotificationMessageItem.kt` Composable 组件,用于在聊天界面中展示居中样式的通知消息。
- 在 `ChatItem` 实体中增加 `isNotification` 字段,以标识消息是否为通知类型。
- 更新 `MessageParser` 和 `ChatItem` 的转换逻辑,以正确解析和处理通知消息,确保其在会话列表和聊天界面中正确显示。
- 在 `GroupChatScreen`, `ChatScreen`, `ChatAiScreen` 中,根据 `isNotification` 字段调用新的 `NotificationMessageItem` 组件来渲染通知消息。
- 修正获取群组会话 ID 时可能存在的 `s` 前缀缺失问题。
2025-11-14 15:05:11 +08:00
20 changed files with 704 additions and 444 deletions

View File

@@ -17,8 +17,8 @@ android {
applicationId = "com.aiosman.ravenow" applicationId = "com.aiosman.ravenow"
minSdk = 24 minSdk = 24
targetSdk = 35 targetSdk = 35
versionCode = 1000019 versionCode = 1000021
versionName = "1.0.000.19" versionName = "1.0.000.21"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {
@@ -140,5 +140,8 @@ dependencies {
implementation(libs.androidx.room.ktx) implementation(libs.androidx.room.ktx)
ksp(libs.androidx.room.compiler) ksp(libs.androidx.room.compiler)
// Google Play Billing
implementation(libs.billing.ktx)
} }

View File

@@ -8,6 +8,7 @@
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" /> <uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="com.android.vending.BILLING" />
<uses-feature android:name="android.hardware.camera.any" android:required="false" /> <uses-feature android:name="android.hardware.camera.any" android:required="false" />
<application <application

View File

@@ -4,6 +4,10 @@ import android.app.Application
import android.content.Context import android.content.Context
import android.content.res.Configuration import android.content.res.Configuration
import android.util.Log import android.util.Log
import com.android.billingclient.api.BillingClient
import com.android.billingclient.api.BillingClientStateListener
import com.android.billingclient.api.BillingResult
import com.android.billingclient.api.PurchasesUpdatedListener
import com.google.firebase.FirebaseApp import com.google.firebase.FirebaseApp
import com.google.firebase.perf.FirebasePerformance import com.google.firebase.perf.FirebasePerformance
@@ -12,6 +16,8 @@ import com.google.firebase.perf.FirebasePerformance
*/ */
class RaveNowApplication : Application() { class RaveNowApplication : Application() {
private var billingClient: BillingClient? = null
override fun attachBaseContext(base: Context) { override fun attachBaseContext(base: Context) {
// 禁用字体缩放,固定字体大小为系统默认大小 // 禁用字体缩放,固定字体大小为系统默认大小
val configuration = Configuration(base.resources.configuration) val configuration = Configuration(base.resources.configuration)
@@ -48,6 +54,53 @@ class RaveNowApplication : Application() {
} catch (e: Exception) { } catch (e: Exception) {
Log.e("RaveNowApplication", "Firebase初始化失败在进程 $processName", e) Log.e("RaveNowApplication", "Firebase初始化失败在进程 $processName", e)
} }
// 初始化 Google Play Billing
initBillingClient()
}
/**
* 初始化 Google Play Billing Client
*/
private fun initBillingClient() {
val purchasesUpdatedListener = PurchasesUpdatedListener { billingResult, purchases ->
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) {
// 处理购买成功
Log.d("RaveNowApplication", "购买成功: ${purchases.size} 个商品")
} else if (billingResult.responseCode == BillingClient.BillingResponseCode.USER_CANCELED) {
// 用户取消购买
Log.d("RaveNowApplication", "用户取消购买")
} else {
// 处理其他错误
Log.e("RaveNowApplication", "购买失败: ${billingResult.debugMessage}")
}
}
billingClient = BillingClient.newBuilder(this)
.setListener(purchasesUpdatedListener)
.build()
billingClient?.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(billingResult: BillingResult) {
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
Log.d("RaveNowApplication", "BillingClient 初始化成功")
} else {
Log.e("RaveNowApplication", "BillingClient 初始化失败: ${billingResult.debugMessage}")
}
}
override fun onBillingServiceDisconnected() {
Log.w("RaveNowApplication", "BillingClient 连接断开,尝试重新连接")
// 可以在这里实现重连逻辑
}
})
}
/**
* 获取 BillingClient 实例
*/
fun getBillingClient(): BillingClient? {
return billingClient
} }
/** /**

View File

@@ -35,6 +35,10 @@ data class Moment(
val commentCount: Long, val commentCount: Long,
@SerializedName("time") @SerializedName("time")
val time: String?, val time: String?,
@SerializedName("createdAt")
val createdAt: String? = null,
@SerializedName("location")
val location: String? = null,
@SerializedName("isFollowed") @SerializedName("isFollowed")
val isFollowed: Boolean, val isFollowed: Boolean,
// 新闻相关字段 // 新闻相关字段
@@ -70,11 +74,11 @@ data class Moment(
"" // 如果头像为空,使用空字符串 "" // 如果头像为空,使用空字符串
}, },
nickname = user.nickName ?: "未知用户", // 如果昵称为空,使用默认值 nickname = user.nickName ?: "未知用户", // 如果昵称为空,使用默认值
location = "Worldwide", location = location ?: "Worldwide",
time = if (time != null && time.isNotEmpty()) { time = when {
ApiClient.dateFromApiString(time) createdAt != null && createdAt.isNotEmpty() -> ApiClient.dateFromApiString(createdAt)
} else { time != null && time.isNotEmpty() -> ApiClient.dateFromApiString(time)
java.util.Date() // 如果时间为空,使用当前时间作为默认值 else -> java.util.Date() // 如果时间为空,使用当前时间作为默认值
}, },
followStatus = isFollowed, followStatus = isFollowed,
momentTextContent = textContent, momentTextContent = textContent,
@@ -204,7 +208,7 @@ data class Video(
data class User( data class User(
@SerializedName("id") @SerializedName("id")
val id: Long, val id: Long,
@SerializedName("nickName") @SerializedName("nickname")
val nickName: String?, val nickName: String?,
@SerializedName("avatar") @SerializedName("avatar")
val avatar: String?, val avatar: String?,

View File

@@ -4,6 +4,7 @@ import android.content.Context
import android.icu.util.Calendar import android.icu.util.Calendar
import com.aiosman.ravenow.ConstVars import com.aiosman.ravenow.ConstVars
import com.aiosman.ravenow.exp.formatChatTime import com.aiosman.ravenow.exp.formatChatTime
import com.aiosman.ravenow.utils.NotificationMessageHelper
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
import io.openim.android.sdk.models.Message import io.openim.android.sdk.models.Message
import io.openim.android.sdk.models.PictureElem import io.openim.android.sdk.models.PictureElem
@@ -21,7 +22,8 @@ data class ChatItem(
val textDisplay: String = "", val textDisplay: String = "",
val msgId: String, // Add this property val msgId: String, // Add this property
var showTimestamp: Boolean = false, var showTimestamp: Boolean = false,
var showTimeDivider: Boolean = false var showTimeDivider: Boolean = false,
val isNotification: Boolean = false // 标识是否为通知类型消息
) { ) {
companion object { companion object {
// OpenIM 消息类型常量 // OpenIM 消息类型常量
@@ -36,6 +38,32 @@ data class ChatItem(
val calendar = Calendar.getInstance() val calendar = Calendar.getInstance()
calendar.timeInMillis = timestamp calendar.timeInMillis = timestamp
// 检查是否为通知类型消息
// 1. 检查消息类型是否为通知类型
// 2. 检查发送者ID是否为系统账户如 "imAdmin"、"administrator" 等)
val sendID = message.sendID ?: ""
val isSystemAccount = sendID == "imAdmin" || sendID == "administrator" || sendID.isEmpty()
val isNotificationType = OpenIMMessageType.isNotification(message.contentType)
val isNotification = isNotificationType || isSystemAccount
// 如果是通知类型消息,使用特殊处理
if (isNotification) {
val notificationText = NotificationMessageHelper.getNotificationText(message)
return ChatItem(
message = notificationText,
avatar = "", // 通知消息不显示头像
time = calendar.time.formatChatTime(context),
userId = sendID.ifEmpty { "system" },
nickname = "", // 通知消息不显示昵称
timestamp = timestamp,
imageList = emptyList<PictureInfo>().toMutableList(),
messageType = message.contentType,
textDisplay = notificationText,
msgId = message.clientMsgID,
isNotification = true
)
}
var faceAvatar = avatar var faceAvatar = avatar
if (faceAvatar == null) { if (faceAvatar == null) {
faceAvatar = "${ConstVars.BASE_SERVER}${message.senderFaceUrl}" faceAvatar = "${ConstVars.BASE_SERVER}${message.senderFaceUrl}"
@@ -62,7 +90,8 @@ data class ChatItem(
).toMutableList(), ).toMutableList(),
messageType = MESSAGE_TYPE_IMAGE, messageType = MESSAGE_TYPE_IMAGE,
textDisplay = "Image", textDisplay = "Image",
msgId = message.clientMsgID msgId = message.clientMsgID,
isNotification = false
) )
} }
return null return null
@@ -79,7 +108,8 @@ data class ChatItem(
imageList = emptyList<PictureInfo>().toMutableList(), imageList = emptyList<PictureInfo>().toMutableList(),
messageType = MESSAGE_TYPE_TEXT, messageType = MESSAGE_TYPE_TEXT,
textDisplay = message.textElem?.content ?: "Unsupported message type", textDisplay = message.textElem?.content ?: "Unsupported message type",
msgId = message.clientMsgID msgId = message.clientMsgID,
isNotification = false
) )
} }

View File

@@ -0,0 +1,198 @@
package com.aiosman.ravenow.entity
/**
* OpenIM 消息类型常量
* 对应 OpenIM SDK 的 ContentType 枚举值
*/
object OpenIMMessageType {
// ========== 基础消息类型 ==========
/** 文本消息 */
const val TEXT = 101
/** 图片消息 */
const val IMAGE = 102
/** 语音消息 */
const val VOICE = 103
/** 视频消息 */
const val VIDEO = 104
/** 文件消息 */
const val FILE = 105
/** @消息 */
const val AT = 106
/** 合并消息 */
const val MERGE = 107
/** 名片消息 */
const val CARD = 108
/** 位置消息 */
const val LOCATION = 109
/** 自定义消息 */
const val CUSTOM = 110
/** 输入状态 */
const val TYPING = 113
/** 引用消息 */
const val QUOTE = 114
/** 表情消息 */
const val EMOJI = 115
// ========== 通知消息类型 ==========
/** 双方成为好友通知 */
const val FRIEND_ADDED = 1201
/** 系统通知 */
const val SYSTEM_NOTIFICATION = 1400
// ========== 群通知消息类型 ==========
/** 群创建通知 */
const val GROUP_CREATED = 1501
/** 群信息改变通知 */
const val GROUP_INFO_CHANGED = 1502
/** 群成员退出通知 */
const val GROUP_MEMBER_QUIT = 1504
/** 群主更换通知 */
const val GROUP_OWNER_CHANGED = 1507
/** 群成员被踢通知 */
const val GROUP_MEMBER_KICKED = 1508
/** 邀请群成员通知 */
const val GROUP_MEMBER_INVITED = 1509
/** 群成员进群通知 */
const val GROUP_MEMBER_JOINED = 1510
/** 解散群通知 */
const val GROUP_DISMISSED = 1511
/** 群成员禁言通知 */
const val GROUP_MEMBER_MUTED = 1512
/** 取消群成员禁言通知 */
const val GROUP_MEMBER_UNMUTED = 1513
/** 群禁言通知 */
const val GROUP_MUTED = 1514
/** 取消群禁言通知 */
const val GROUP_UNMUTED = 1515
/** 群公告改变通知 */
const val GROUP_ANNOUNCEMENT_CHANGED = 1519
/** 群名称改变通知 */
const val GROUP_NAME_CHANGED = 1520
// ========== 其他通知类型 ==========
/** 阅后即焚开启或关闭通知 */
const val SNAPCHAT_TOGGLE = 1701
/** 撤回消息通知 */
const val MESSAGE_REVOKED = 2101
/**
* 获取消息类型的描述
*/
fun getDescription(type: Int): String {
return when (type) {
TEXT -> "文本消息"
IMAGE -> "图片消息"
VOICE -> "语音消息"
VIDEO -> "视频消息"
FILE -> "文件消息"
AT -> "@消息"
MERGE -> "合并消息"
CARD -> "名片消息"
LOCATION -> "位置消息"
CUSTOM -> "自定义消息"
TYPING -> "输入状态"
QUOTE -> "引用消息"
EMOJI -> "表情消息"
FRIEND_ADDED -> "双方成为好友通知"
SYSTEM_NOTIFICATION -> "系统通知"
GROUP_CREATED -> "群创建通知"
GROUP_INFO_CHANGED -> "群信息改变通知"
GROUP_MEMBER_QUIT -> "群成员退出通知"
GROUP_OWNER_CHANGED -> "群主更换通知"
GROUP_MEMBER_KICKED -> "群成员被踢通知"
GROUP_MEMBER_INVITED -> "邀请群成员通知"
GROUP_MEMBER_JOINED -> "群成员进群通知"
GROUP_DISMISSED -> "解散群通知"
GROUP_MEMBER_MUTED -> "群成员禁言通知"
GROUP_MEMBER_UNMUTED -> "取消群成员禁言通知"
GROUP_MUTED -> "群禁言通知"
GROUP_UNMUTED -> "取消群禁言通知"
GROUP_ANNOUNCEMENT_CHANGED -> "群公告改变通知"
GROUP_NAME_CHANGED -> "群名称改变通知"
SNAPCHAT_TOGGLE -> "阅后即焚开启或关闭通知"
MESSAGE_REVOKED -> "撤回消息通知"
else -> "未知消息类型($type)"
}
}
/**
* 判断是否为通知类型消息
*/
fun isNotification(type: Int): Boolean {
return type in listOf(
FRIEND_ADDED,
SYSTEM_NOTIFICATION,
GROUP_CREATED,
GROUP_INFO_CHANGED,
GROUP_MEMBER_QUIT,
GROUP_OWNER_CHANGED,
GROUP_MEMBER_KICKED,
GROUP_MEMBER_INVITED,
GROUP_MEMBER_JOINED,
GROUP_DISMISSED,
GROUP_MEMBER_MUTED,
GROUP_MEMBER_UNMUTED,
GROUP_MUTED,
GROUP_UNMUTED,
GROUP_ANNOUNCEMENT_CHANGED,
GROUP_NAME_CHANGED,
SNAPCHAT_TOGGLE,
MESSAGE_REVOKED
)
}
/**
* 判断是否为群通知类型消息
*/
fun isGroupNotification(type: Int): Boolean {
return type in listOf(
GROUP_CREATED,
GROUP_INFO_CHANGED,
GROUP_MEMBER_QUIT,
GROUP_OWNER_CHANGED,
GROUP_MEMBER_KICKED,
GROUP_MEMBER_INVITED,
GROUP_MEMBER_JOINED,
GROUP_DISMISSED,
GROUP_MEMBER_MUTED,
GROUP_MEMBER_UNMUTED,
GROUP_MUTED,
GROUP_UNMUTED,
GROUP_ANNOUNCEMENT_CHANGED,
GROUP_NAME_CHANGED
)
}
}

View File

@@ -16,6 +16,7 @@ import com.aiosman.ravenow.data.UserService
import com.aiosman.ravenow.data.UserServiceImpl import com.aiosman.ravenow.data.UserServiceImpl
import com.aiosman.ravenow.entity.AccountProfileEntity import com.aiosman.ravenow.entity.AccountProfileEntity
import com.aiosman.ravenow.entity.ChatItem import com.aiosman.ravenow.entity.ChatItem
import com.aiosman.ravenow.im.OpenIMManager
import io.openim.android.sdk.OpenIMClient import io.openim.android.sdk.OpenIMClient
import io.openim.android.sdk.enums.ConversationType import io.openim.android.sdk.enums.ConversationType
import io.openim.android.sdk.enums.ViewType import io.openim.android.sdk.enums.ViewType
@@ -100,6 +101,10 @@ abstract class BaseChatViewModel : ViewModel() {
override fun onSuccess(data: ConversationInfo) { override fun onSuccess(data: ConversationInfo) {
conversationID = data.conversationID conversationID = data.conversationID
// 如果是群组的会话id应该加上s修正,不知道是不是openIm的bug
if (data.conversationType == 2) {
conversationID = "s${conversationID}"
}
Log.d(getLogTag(), "获取会话信息成功conversationID: $conversationID") Log.d(getLogTag(), "获取会话信息成功conversationID: $conversationID")
onSuccess?.invoke() onSuccess?.invoke()
} }
@@ -324,6 +329,7 @@ abstract class BaseChatViewModel : ViewModel() {
OpenIMClient.getInstance().messageManager.getAdvancedHistoryMessageList( OpenIMClient.getInstance().messageManager.getAdvancedHistoryMessageList(
object : OnBase<AdvancedMessage> { object : OnBase<AdvancedMessage> {
override fun onSuccess(data: AdvancedMessage?) { override fun onSuccess(data: AdvancedMessage?) {
val messages = data?.messageList ?: emptyList() val messages = data?.messageList ?: emptyList()
val newChatItems = messages.mapNotNull { val newChatItems = messages.mapNotNull {
ChatItem.convertToChatItem(it, context, avatar = getMessageAvatar(it)) ChatItem.convertToChatItem(it, context, avatar = getMessageAvatar(it))

View File

@@ -504,6 +504,12 @@ fun ChatAiOtherItem(item: ChatItem) {
@Composable @Composable
fun ChatAiItem(item: ChatItem, currentUserId: String) { fun ChatAiItem(item: ChatItem, currentUserId: String) {
// 通知消息显示特殊布局
if (item.isNotification) {
NotificationMessageItem(item)
return
}
val isCurrentUser = item.userId == currentUserId val isCurrentUser = item.userId == currentUserId
if (isCurrentUser) { if (isCurrentUser) {
ChatAiSelfItem(item) ChatAiSelfItem(item)

View File

@@ -516,6 +516,12 @@ fun ChatOtherItem(item: ChatItem) {
@Composable @Composable
fun ChatItem(item: ChatItem, currentUserId: String) { fun ChatItem(item: ChatItem, currentUserId: String) {
// 通知消息显示特殊布局
if (item.isNotification) {
NotificationMessageItem(item)
return
}
val isCurrentUser = item.userId == currentUserId val isCurrentUser = item.userId == currentUserId
if (isCurrentUser) { if (isCurrentUser) {
ChatSelfItem(item) ChatSelfItem(item)

View File

@@ -310,8 +310,13 @@ fun GroupChatScreen(groupId: String,name: String,avatar: String,) {
) )
} }
// 获取上一个item的userId用于判断是否显示头像和昵称 // 获取上一个item的userId用于判断是否显示头像和昵称
// 通知消息不参与判断逻辑
val previousItem = if (index < chatList.size - 1) chatList[index + 1] else null val previousItem = if (index < chatList.size - 1) chatList[index + 1] else null
val showAvatarAndNickname = previousItem?.userId != item.userId val showAvatarAndNickname = if (item.isNotification || previousItem?.isNotification == true) {
true // 通知消息前后都显示头像和昵称
} else {
previousItem?.userId != item.userId
}
GroupChatItem( GroupChatItem(
item = item, item = item,
currentUserId = viewModel.myProfile?.trtcUserId!!, currentUserId = viewModel.myProfile?.trtcUserId!!,
@@ -528,14 +533,14 @@ fun GroupChatOtherItem(item: ChatItem, showAvatarAndNickname: Boolean = true) {
@Composable @Composable
fun GroupChatItem(item: ChatItem, currentUserId: String, showAvatarAndNickname: Boolean = true) { fun GroupChatItem(item: ChatItem, currentUserId: String, showAvatarAndNickname: Boolean = true) {
val isCurrentUser = item.userId == currentUserId // 通知消息显示特殊布局(包括系统账户发送的消息)
if (item.isNotification) {
// 管理员消息显示特殊布局 NotificationMessageItem(item)
if (item.userId == "administrator") {
GroupChatAdminItem(item)
return return
} }
val isCurrentUser = item.userId == currentUserId
// 根据是否是当前用户显示不同样式 // 根据是否是当前用户显示不同样式
when (item.userId) { when (item.userId) {
currentUserId -> GroupChatSelfItem(item) currentUserId -> GroupChatSelfItem(item)

View File

@@ -0,0 +1,58 @@
package com.aiosman.ravenow.ui.chat
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
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 com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.entity.ChatItem
/**
* 通知消息显示组件
* 参考 iOS 的 tipsView 样式,用于显示通知类型的消息
*/
@Composable
fun NotificationMessageItem(item: ChatItem) {
val AppColors = LocalAppTheme.current
// 参考 iOS: HStack { Text(...) } .padding(.vertical, 8)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
horizontalArrangement = Arrangement.Center
) {
// 参考 iOS: Text(tips)
// .font(.caption2) - 12sp
// .foregroundColor(Color.textMain) - 主文本颜色
// .padding(.vertical, 8) .padding(.horizontal, 12)
// .background(Color.background.opacity(0.2))
// .background(.ultraThinMaterial.opacity(0.3))
// .cornerRadius(12)
Text(
text = item.message,
style = TextStyle(
color = AppColors.text, // 使用主文本颜色,不是次要文本颜色
fontSize = 12.sp, // .caption2 对应 12sp
textAlign = TextAlign.Center
),
modifier = Modifier
.clip(RoundedCornerShape(12.dp)) // 圆角 12不是 8
.background(
// 参考 iOS: Color.background.opacity(0.2) + .ultraThinMaterial.opacity(0.3)
// Android 使用半透明背景色模拟毛玻璃效果
AppColors.background.copy(alpha = 0.2f)
)
.padding(vertical = 8.dp, horizontal = 12.dp), // horizontal 12不是 16
maxLines = Int.MAX_VALUE
)
}
}

View File

@@ -16,7 +16,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.systemBars
@@ -24,22 +23,18 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Icon
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -63,33 +58,24 @@ import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.NavigationRoute import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.composables.CustomAsyncImage import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.index.tabs.ai.tabs.mine.MineAgent
import com.aiosman.ravenow.ui.index.tabs.ai.tabs.hot.HotAgent
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.ui.composables.TabItem
import com.aiosman.ravenow.ui.composables.TabSpacer import com.aiosman.ravenow.ui.composables.TabSpacer
import com.aiosman.ravenow.ui.index.tabs.moment.CustomTabItem import com.aiosman.ravenow.ui.index.tabs.moment.CustomTabItem
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.expolre.AgentItem import com.aiosman.ravenow.ui.index.tabs.moment.tabs.expolre.AgentItem
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.expolre.ExploreViewModel
import com.aiosman.ravenow.utils.DebounceUtils import com.aiosman.ravenow.utils.DebounceUtils
import com.aiosman.ravenow.utils.ResourceCleanupManager
import kotlinx.coroutines.launch
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.compose.foundation.lazy.grid.items as gridItems
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import com.airbnb.lottie.compose.LottieAnimation import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieCompositionSpec import com.airbnb.lottie.compose.LottieCompositionSpec
@@ -113,10 +99,6 @@ fun Agent() {
val navigationBarPaddings = val navigationBarPaddings =
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 48.dp WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 48.dp
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues() val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
// 游客模式下只显示热门Agent正常用户显示我的Agent和热门Agent
val tabCount = if (AppStore.isGuest) 1 else 2
var pagerState = rememberPagerState { tabCount }
var scope = rememberCoroutineScope()
val viewModel: AgentViewModel = AgentViewModel val viewModel: AgentViewModel = AgentViewModel
@@ -125,16 +107,6 @@ fun Agent() {
viewModel.ensureDataLoaded() viewModel.ensureDataLoaded()
} }
// 防抖状态
var lastClickTime by remember { mutableStateOf(0L) }
// 页面退出时只清理必要的资源不清理推荐Agent数据
DisposableEffect(Unit) {
onDispose {
// 只清理子页面的资源保留推荐Agent数据
// ResourceCleanupManager.cleanupPageResources("ai")
}
}
val agentItems = viewModel.agentItems val agentItems = viewModel.agentItems
var selectedTabIndex by remember { mutableStateOf(0) } var selectedTabIndex by remember { mutableStateOf(0) }
@@ -165,7 +137,7 @@ fun Agent() {
contentDescription = "Rave AI Logo", contentDescription = "Rave AI Logo",
modifier = Modifier modifier = Modifier
.height(44.dp) .height(44.dp)
.padding(top =9.dp,bottom=9.dp) .padding(top = 9.dp, bottom = 9.dp)
.wrapContentSize(), .wrapContentSize(),
// colorFilter = ColorFilter.tint(AppColors.text) // colorFilter = ColorFilter.tint(AppColors.text)
) )
@@ -176,7 +148,7 @@ fun Agent() {
contentDescription = "search", contentDescription = "search",
modifier = Modifier modifier = Modifier
.size(44.dp) .size(44.dp)
.padding(top = 9.dp,bottom=9.dp) .padding(top = 9.dp, bottom = 9.dp)
.noRippleClickable { .noRippleClickable {
navController.navigate(NavigationRoute.Search.route) navController.navigate(NavigationRoute.Search.route)
}, },
@@ -267,11 +239,19 @@ fun Agent() {
) { ) {
when { when {
selectedTabIndex == 0 -> { selectedTabIndex == 0 -> {
AgentViewPagerSection(agentItems = viewModel.agentItems.take(15), viewModel) AgentViewPagerSection(
agentItems = viewModel.agentItems.take(15),
viewModel
)
} }
selectedTabIndex in 1..viewModel.categories.size -> { selectedTabIndex in 1..viewModel.categories.size -> {
AgentViewPagerSection(agentItems = viewModel.agentItems.take(15), viewModel) AgentViewPagerSection(
agentItems = viewModel.agentItems.take(15),
viewModel
)
} }
else -> { else -> {
val shuffledAgents = viewModel.agentItems.shuffled().take(15) val shuffledAgents = viewModel.agentItems.shuffled().take(15)
AgentViewPagerSection(agentItems = shuffledAgents, viewModel) AgentViewPagerSection(agentItems = shuffledAgents, viewModel)
@@ -329,7 +309,6 @@ fun Agent() {
} }
// 只有当热门聊天室有数据时,才展示“发现更多”区域 // 只有当热门聊天室有数据时,才展示“发现更多”区域
if (viewModel.chatRooms.isNotEmpty()) {
item { Spacer(modifier = Modifier.height(20.dp)) } item { Spacer(modifier = Modifier.height(20.dp)) }
// "发现更多" 标题 - 吸顶 // "发现更多" 标题 - 吸顶
@@ -404,58 +383,7 @@ fun Agent() {
} }
} }
} }
}
}
}
@Composable
fun AgentGridLayout(
agentItems: List<AgentItem>,
viewModel: AgentViewModel,
navController: NavHostController
) {
Column(
modifier = Modifier.fillMaxWidth()
) {
// 将agentItems按两列分组
agentItems.chunked(2).forEachIndexed { rowIndex, rowItems ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(
top = if (rowIndex == 0) 30.dp else 20.dp, // 第一行添加更多顶部间距
bottom = 20.dp
),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
// 第一列
Box(
modifier = Modifier.weight(1f)
) {
AgentCardSquare(
agentItem = rowItems[0],
viewModel = viewModel,
navController = navController
)
}
// 第二列(如果存在)
if (rowItems.size > 1) {
Box(
modifier = Modifier.weight(1f)
) {
AgentCardSquare(
agentItem = rowItems[1],
viewModel = viewModel,
navController = navController
)
}
} else {
// 如果只有一列,添加空白占位
Spacer(modifier = Modifier.weight(1f))
}
}
}
} }
} }
@@ -562,9 +490,10 @@ fun AgentCardSquare(
} }
} }
} }
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun AgentViewPagerSection(agentItems: List<AgentItem>,viewModel: AgentViewModel) { fun AgentViewPagerSection(agentItems: List<AgentItem>, viewModel: AgentViewModel) {
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
if (agentItems.isEmpty()) return if (agentItems.isEmpty()) return
@@ -711,186 +640,6 @@ fun AgentLargeCard(
} }
} }
@Composable
fun AgentPage(viewModel: AgentViewModel,agentItems: List<AgentItem>, page: Int, modifier: Modifier = Modifier,navController: NavHostController) {
Column(
modifier = modifier
.fillMaxSize()
.padding(horizontal = 0.dp)
) {
// 显示3个agent
agentItems.forEachIndexed { index, agentItem ->
AgentCard2(agentItem = agentItem, viewModel = viewModel, navController = LocalNavController.current)
if (index < agentItems.size - 1) {
Spacer(modifier = Modifier.height(8.dp))
}
}
}
}
@SuppressLint("SuspiciousIndentation")
@Composable
fun AgentCard2(viewModel: AgentViewModel,agentItem: AgentItem,navController: NavHostController) {
val AppColors = LocalAppTheme.current
// 防抖状态
var lastClickTime by remember { mutableStateOf(0L) }
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 3.dp),
verticalAlignment = Alignment.CenterVertically
) {
// 左侧头像
Box(
modifier = Modifier
.size(48.dp)
.background(Color(0x00F5F5F5), RoundedCornerShape(24.dp))
.clickable {
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
viewModel.goToProfile(agentItem.openId, navController)
}) {
lastClickTime = System.currentTimeMillis()
}
},
contentAlignment = Alignment.Center
) {
CustomAsyncImage(
imageUrl = agentItem.avatar,
contentDescription = "Agent头像",
modifier = Modifier
.size(48.dp)
.clip(RoundedCornerShape(24.dp)),
contentScale = androidx.compose.ui.layout.ContentScale.Crop,
defaultRes = R.mipmap.group_copy
)
}
Spacer(modifier = Modifier.width(12.dp))
// 中间文字内容
Column(
modifier = Modifier
.weight(1f)
.padding(end = 8.dp)
) {
// 标题
androidx.compose.material3.Text(
text = agentItem.title,
fontSize = 14.sp,
fontWeight = androidx.compose.ui.text.font.FontWeight.W600,
color = AppColors.text,
maxLines = 1,
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(8.dp))
// 描述
androidx.compose.material3.Text(
text = agentItem.desc,
fontSize = 12.sp,
color = AppColors.secondaryText,
maxLines = 1,
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis
)
}
// 右侧聊天按钮
Box(
modifier = Modifier
.size(width = 60.dp, height = 32.dp)
.background(
color = Color(0X147c7480),
shape = RoundedCornerShape(
topStart = 14.dp,
topEnd = 14.dp,
bottomStart = 0.dp,
bottomEnd = 14.dp
)
)
.clickable {
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
// 检查游客模式,如果是游客则跳转登录
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.CHAT_WITH_AGENT)) {
navController.navigate(NavigationRoute.Login.route)
} else {
viewModel.createSingleChat(agentItem.openId)
viewModel.goToChatAi(
agentItem.openId,
navController = navController
)
}
}) {
lastClickTime = System.currentTimeMillis()
}
},
contentAlignment = Alignment.Center
) {
androidx.compose.material3.Text(
text = stringResource(R.string.chat),
fontSize = 12.sp,
color = AppColors.text,
fontWeight = androidx.compose.ui.text.font.FontWeight.W500
)
}
}
}
@Composable
fun ChatRoomsSection(
chatRooms: List<ChatRoom>,
navController: NavHostController
) {
val AppColors = LocalAppTheme.current
Column(
modifier = Modifier.fillMaxWidth()
) {
// 标题
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(R.mipmap.rider_pro_hot_room),
contentDescription = "chat room",
modifier = Modifier.size(28.dp)
)
Spacer(modifier = Modifier.width(4.dp))
androidx.compose.material3.Text(
text = stringResource(R.string.hot_rooms),
fontSize = 16.sp,
fontWeight = androidx.compose.ui.text.font.FontWeight.W900,
color = AppColors.text
)
}
Column(
modifier = Modifier.fillMaxWidth()
) {
chatRooms.chunked(2).forEach { rowRooms ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
rowRooms.forEach { chatRoom ->
ChatRoomCard(
chatRoom = chatRoom,
navController = navController,
modifier = Modifier.weight(1f)
)
}
}
}
}
}
}
@Composable @Composable
fun ChatRoomCard( fun ChatRoomCard(
chatRoom: ChatRoom, chatRoom: ChatRoom,
@@ -938,7 +687,10 @@ fun ChatRoomCard(
.size(cardSize) .size(cardSize)
.background(AppColors.tabUnselectedBackground, RoundedCornerShape(12.dp)) .background(AppColors.tabUnselectedBackground, RoundedCornerShape(12.dp))
.clickable(enabled = !viewModel.isJoiningRoom) { .clickable(enabled = !viewModel.isJoiningRoom) {
if (!viewModel.isJoiningRoom && DebounceUtils.simpleDebounceClick(lastClickTime, 500L) { if (!viewModel.isJoiningRoom && DebounceUtils.simpleDebounceClick(
lastClickTime,
500L
) {
// 加入群聊房间 // 加入群聊房间
viewModel.joinRoom( viewModel.joinRoom(
id = chatRoom.id, id = chatRoom.id,
@@ -953,7 +705,8 @@ fun ChatRoomCard(
// 处理错误可以显示Toast或其他提示 // 处理错误可以显示Toast或其他提示
} }
) )
}) { }
) {
lastClickTime = System.currentTimeMillis() lastClickTime = System.currentTimeMillis()
} }
} }
@@ -967,11 +720,14 @@ fun ChatRoomCard(
modifier = Modifier modifier = Modifier
.width(cardSize) .width(cardSize)
.height(120.dp) .height(120.dp)
.clip(RoundedCornerShape( .clip(
RoundedCornerShape(
topStart = 12.dp, topStart = 12.dp,
topEnd = 12.dp, topEnd = 12.dp,
bottomStart = 0.dp, bottomStart = 0.dp,
bottomEnd = 0.dp)), bottomEnd = 0.dp
)
),
contentScale = androidx.compose.ui.layout.ContentScale.Crop, contentScale = androidx.compose.ui.layout.ContentScale.Crop,
defaultRes = R.mipmap.rider_pro_agent defaultRes = R.mipmap.rider_pro_agent
) )

View File

@@ -134,7 +134,7 @@ object AgentViewModel: ViewModel() {
pageSize = pageSize, pageSize = pageSize,
withWorkflow = 1, withWorkflow = 1,
categoryIds = listOf(categoryId), categoryIds = listOf(categoryId),
random = 1 // random = 1
) )
} else { } else {
// 获取推荐智能体使用random=1 // 获取推荐智能体使用random=1
@@ -143,7 +143,7 @@ object AgentViewModel: ViewModel() {
pageSize = pageSize, pageSize = pageSize,
withWorkflow = 1, withWorkflow = 1,
categoryIds = null, categoryIds = null,
random = 1 // random = 1
) )
} }
@@ -197,7 +197,7 @@ object AgentViewModel: ViewModel() {
page = 1, page = 1,
pageSize = 20, pageSize = 20,
isRecommended = 1, isRecommended = 1,
random = "1" // random = "1"
) )
if (response.isSuccessful) { if (response.isSuccessful) {
val allRooms = response.body()?.list ?: emptyList() val allRooms = response.body()?.list ?: emptyList()
@@ -332,18 +332,17 @@ object AgentViewModel: ViewModel() {
openId: String, openId: String,
navController: NavHostController navController: NavHostController
) { ) {
viewModelScope.launch { // 直接使用openId导航页面内的AiProfileViewModel会处理数据加载
// 避免重复请求因为AiProfileViewModel.loadProfile已经支持通过openId加载
try { try {
val profile = userService.getUserProfileByOpenId(openId)
// 从Agent列表点击进去的一定是智能体直接传递isAiAccount = true
navController.navigate( navController.navigate(
NavigationRoute.AccountProfile.route NavigationRoute.AccountProfile.route
.replace("{id}", profile.id.toString()) .replace("{id}", openId)
.replace("{isAiAccount}", "true") .replace("{isAiAccount}", "true")
) )
} catch (e: Exception) { } catch (e: Exception) {
// swallow error to avoid crash on navigation attempt failures Log.e("AgentViewModel", "Navigation failed", e)
} e.printStackTrace()
} }
} }

View File

@@ -12,7 +12,6 @@ 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
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@@ -27,6 +26,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
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 androidx.compose.ui.graphics.Color
import com.aiosman.ravenow.AppState import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.LocalAppTheme import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController import com.aiosman.ravenow.LocalNavController
@@ -153,13 +153,6 @@ fun GroupChatListScreen() {
} }
} }
) )
if (index < GroupChatListViewModel.groupChatList.size - 1) {
HorizontalDivider(
modifier = Modifier.padding(horizontal = 24.dp),
color = AppColors.divider
)
}
} }
if (GroupChatListViewModel.isLoading && GroupChatListViewModel.groupChatList.isNotEmpty()) { if (GroupChatListViewModel.isLoading && GroupChatListViewModel.groupChatList.isNotEmpty()) {
@@ -213,16 +206,16 @@ fun GroupChatItem(
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
val chatDebouncer = rememberDebouncer() val chatDebouncer = rememberDebouncer()
val avatarDebouncer = rememberDebouncer() val avatarDebouncer = rememberDebouncer()
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 12.dp) .padding(horizontal = 16.dp, vertical = 12.dp)
.noRippleClickable { .noRippleClickable {
chatDebouncer { chatDebouncer {
onChatClick(conversation) onChatClick(conversation)
} }
} },
verticalAlignment = Alignment.CenterVertically
) { ) {
Box { Box {
CustomAsyncImage( CustomAsyncImage(
@@ -242,9 +235,9 @@ fun GroupChatItem(
Column( Column(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.padding(start = 12.dp) .padding(start = 12.dp, top = 2.dp),
verticalArrangement = Arrangement.Center
) { ) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@@ -252,22 +245,22 @@ fun GroupChatItem(
) { ) {
Text( Text(
text = conversation.groupName, text = conversation.groupName,
fontSize = 16.sp, fontSize = 14.sp,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = AppColors.text, color = AppColors.text,
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(6.dp))
Text( Text(
text = conversation.lastMessageTime, text = conversation.lastMessageTime,
fontSize = 12.sp, fontSize = 11.sp,
color = AppColors.secondaryText color = AppColors.secondaryText
) )
} }
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(6.dp))
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@@ -275,29 +268,29 @@ fun GroupChatItem(
) { ) {
Text( Text(
text = "${if (conversation.isSelf) stringResource(R.string.friend_chat_me_prefix) else ""}${conversation.displayText}", text = "${if (conversation.isSelf) stringResource(R.string.friend_chat_me_prefix) else ""}${conversation.displayText}",
fontSize = 14.sp, fontSize = 12.sp,
color = AppColors.secondaryText, color = AppColors.secondaryText,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(10.dp))
if (conversation.unreadCount > 0) { if (conversation.unreadCount > 0) {
Box( Box(
modifier = Modifier modifier = Modifier
.size(if (conversation.unreadCount > 99) 24.dp else 20.dp) .size(if (conversation.unreadCount > 99) 24.dp else 20.dp)
.background( .background(
color = AppColors.main, color = Color(0xFFFF3B30),
shape = CircleShape shape = CircleShape
), ),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text( Text(
text = if (conversation.unreadCount > 99) "99+" else conversation.unreadCount.toString(), text = if (conversation.unreadCount > 99) "99+" else conversation.unreadCount.toString(),
color = AppColors.mainText, color = Color.White,
fontSize = if (conversation.unreadCount > 99) 9.sp else 10.sp, fontSize = if (conversation.unreadCount > 99) 11.sp else 12.sp,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
} }

View File

@@ -35,10 +35,16 @@ import androidx.compose.ui.text.style.TextOverflow
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.ravenow.R import com.aiosman.ravenow.R
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.entity.MomentEntity import com.aiosman.ravenow.entity.MomentEntity
import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.comment.CommentModalContent import com.aiosman.ravenow.ui.comment.CommentModalContent
import com.aiosman.ravenow.ui.composables.CustomAsyncImage import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.data.UserService
import com.aiosman.ravenow.data.UserServiceImpl
import kotlinx.coroutines.launch
import androidx.compose.runtime.rememberCoroutineScope
/** /**
* 动态推荐Item组件post_normal * 动态推荐Item组件post_normal
@@ -50,16 +56,37 @@ fun PostRecommendationItem(
moment: MomentEntity, moment: MomentEntity,
onLikeClick: ((MomentEntity) -> Unit)? = null, onLikeClick: ((MomentEntity) -> Unit)? = null,
onCommentClick: ((MomentEntity) -> Unit)? = null, onCommentClick: ((MomentEntity) -> Unit)? = null,
onCommentAdded: ((MomentEntity) -> Unit)? = null,
onFavoriteClick: ((MomentEntity) -> Unit)? = null, onFavoriteClick: ((MomentEntity) -> Unit)? = null,
onShareClick: ((MomentEntity) -> Unit)? = null, onShareClick: ((MomentEntity) -> Unit)? = null,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val context = LocalContext.current val context = LocalContext.current
val navController = LocalNavController.current
val scope = rememberCoroutineScope()
val userService: UserService = UserServiceImpl()
var showCommentModal by remember { mutableStateOf(false) } var showCommentModal by remember { mutableStateOf(false) }
var sheetState = rememberModalBottomSheetState( var sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true skipPartiallyExpanded = true
) )
// 导航到个人资料
fun navigateToProfile() {
scope.launch {
try {
val profile = userService.getUserProfile(moment.authorId.toString())
navController.navigate(
NavigationRoute.AccountProfile.route
.replace("{id}", profile.id.toString())
.replace("{isAiAccount}", if (profile.aiAccount) "true" else "false")
)
} catch (e: Exception) {
// 处理错误,避免崩溃
e.printStackTrace()
}
}
}
// 图片列表 // 图片列表
val images = moment.images val images = moment.images
val imageCount = images.size val imageCount = images.size
@@ -71,8 +98,9 @@ fun PostRecommendationItem(
) { ) {
// 图片显示区域(替代视频播放器) // 图片显示区域(替代视频播放器)
if (imageCount > 0) { if (imageCount > 0) {
// 只显示第一张图片,优先使用 thumbnailDirectUrl // 只显示第一张图片,优先使用 smallDirectUrl
val imageUrl = images[0].thumbnailDirectUrl val imageUrl = images[0].smallDirectUrl
?: images[0].thumbnailDirectUrl
?: images[0].directUrl ?: images[0].directUrl
?: images[0].url ?: images[0].url
CustomAsyncImage( CustomAsyncImage(
@@ -104,32 +132,16 @@ fun PostRecommendationItem(
.fillMaxWidth() .fillMaxWidth()
.padding(start = 16.dp, bottom = 16.dp) .padding(start = 16.dp, bottom = 16.dp)
) { ) {
// 用户头像和昵称 // 用户昵称
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(bottom = 8.dp)
) {
Box(
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.background(Color.White.copy(alpha = 0.2f))
) {
CustomAsyncImage(
imageUrl = moment.avatar,
contentDescription = "用户头像",
modifier = Modifier.fillMaxSize(),
defaultRes = R.drawable.default_avatar
)
}
Text( Text(
text = "@${moment.nickname}", text = "@${moment.nickname}",
modifier = Modifier.padding(start = 8.dp), modifier = Modifier
.padding(bottom = 8.dp)
.noRippleClickable { navigateToProfile() },
fontSize = 16.sp, fontSize = 16.sp,
color = Color.White, color = Color.White,
style = TextStyle(fontWeight = FontWeight.Bold) style = TextStyle(fontWeight = FontWeight.Bold)
) )
}
// 文字内容 // 文字内容
if (!moment.momentTextContent.isNullOrEmpty()) { if (!moment.momentTextContent.isNullOrEmpty()) {
@@ -160,7 +172,10 @@ fun PostRecommendationItem(
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
// 用户头像 // 用户头像
UserAvatar(avatarUrl = moment.avatar) UserAvatar(
avatarUrl = moment.avatar,
onClick = { navigateToProfile() }
)
// 点赞 // 点赞
VideoBtn( VideoBtn(
@@ -205,21 +220,35 @@ fun PostRecommendationItem(
containerColor = Color.White, containerColor = Color.White,
sheetState = sheetState sheetState = sheetState
) { ) {
CommentModalContent(postId = moment.id) { CommentModalContent(
// 评论添加后的回调 postId = moment.id,
commentCount = moment.commentCount,
onCommentAdded = {
onCommentAdded?.invoke(moment)
} }
)
} }
} }
} }
@Composable @Composable
private fun UserAvatar(avatarUrl: String? = null) { private fun UserAvatar(
avatarUrl: String? = null,
onClick: (() -> Unit)? = null
) {
Box( Box(
modifier = Modifier modifier = Modifier
.padding(bottom = 16.dp) .padding(bottom = 16.dp)
.size(40.dp) .size(40.dp)
.clip(CircleShape) .clip(CircleShape)
.background(Color.White.copy(alpha = 0.2f)) .background(Color.White.copy(alpha = 0.2f))
.then(
if (onClick != null) {
Modifier.noRippleClickable { onClick() }
} else {
Modifier
}
)
) { ) {
if (avatarUrl != null && avatarUrl.isNotEmpty()) { if (avatarUrl != null && avatarUrl.isNotEmpty()) {
CustomAsyncImage( CustomAsyncImage(

View File

@@ -239,11 +239,13 @@ fun RecommendScreen() {
onCommentClick = { m -> onCommentClick = { m ->
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.COMMENT_MOMENT)) { if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.COMMENT_MOMENT)) {
navController.navigate(NavigationRoute.Login.route) navController.navigate(NavigationRoute.Login.route)
} else { }
// 注意:不在这里增加评论数,应该在评论真正提交成功后再增加
},
onCommentAdded = { m ->
scope.launch { scope.launch {
RecommendViewModel.onAddComment(m.id) RecommendViewModel.onAddComment(m.id)
} }
}
}, },
onFavoriteClick = { m -> onFavoriteClick = { m ->
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) { if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) {

View File

@@ -44,12 +44,21 @@ class AiProfileViewModel : ViewModel() {
} }
try { try {
// 检查id是否是纯数字用户ID如果不是则当作openId处理
val isUserId = id.toIntOrNull() != null
if (isUserId) {
// 先通过用户ID获取基本信息获取chatAIId // 先通过用户ID获取基本信息获取chatAIId
val basicProfile = userService.getUserProfile(id) val basicProfile = userService.getUserProfile(id)
profileId = id.toInt() profileId = id.toInt()
// 使用chatAIId通过getUserProfileByOpenId获取完整信息包含creatorProfile // 使用chatAIId通过getUserProfileByOpenId获取完整信息包含creatorProfile
profile = userService.getUserProfileByOpenId(basicProfile.chatAIId) profile = userService.getUserProfileByOpenId(basicProfile.chatAIId)
} else {
// 直接通过openId获取完整信息
profile = userService.getUserProfileByOpenId(id)
profileId = profile?.id ?: 0
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e("AiProfileViewModel", "Error loading profile", e) Log.e("AiProfileViewModel", "Error loading profile", e)
e.printStackTrace() e.printStackTrace()

View File

@@ -1,6 +1,7 @@
package com.aiosman.ravenow.utils package com.aiosman.ravenow.utils
import com.aiosman.ravenow.AppState import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.entity.OpenIMMessageType
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.JsonSyntaxException import com.google.gson.JsonSyntaxException
import io.openim.android.sdk.models.Message import io.openim.android.sdk.models.Message
@@ -25,8 +26,21 @@ object MessageParser {
val gson = Gson() val gson = Gson()
val message = gson.fromJson(latestMsgJson, Message::class.java) val message = gson.fromJson(latestMsgJson, Message::class.java)
// 检查是否为通知类型消息
// 1. 检查消息类型是否为通知类型
// 2. 检查发送者ID是否为系统账户如 "imAdmin"、"administrator" 等)
val sendID = message.sendID ?: ""
val isSystemAccount = sendID == "imAdmin" || sendID == "administrator" || sendID.isEmpty()
val isNotificationType = OpenIMMessageType.isNotification(message.contentType)
val isNotification = isNotificationType || isSystemAccount
// 通知类型消息不显示"我:"前缀,设置 isSelf = false
if (isNotification) {
isSelf = false
} else {
// 判断是否是自己发送的消息 // 判断是否是自己发送的消息
isSelf = message.sendID == AppState.profile?.trtcUserId isSelf = sendID == AppState.profile?.trtcUserId
}
// 根据消息类型生成显示文本 // 根据消息类型生成显示文本
displayText = getMessageDisplayText(message) displayText = getMessageDisplayText(message)
@@ -50,6 +64,19 @@ object MessageParser {
* @return 消息的显示文本 * @return 消息的显示文本
*/ */
private fun getMessageDisplayText(message: Message): String { private fun getMessageDisplayText(message: Message): String {
// 检查是否为通知类型消息
// 1. 检查消息类型是否为通知类型
// 2. 检查发送者ID是否为系统账户如 "imAdmin"、"administrator" 等)
val sendID = message.sendID ?: ""
val isSystemAccount = sendID == "imAdmin" || sendID == "administrator" || sendID.isEmpty()
val isNotificationType = OpenIMMessageType.isNotification(message.contentType)
val isNotification = isNotificationType || isSystemAccount
if (isNotification) {
// 使用 NotificationMessageHelper 生成通知文本
return NotificationMessageHelper.getNotificationText(message)
}
return when (message.contentType) { return when (message.contentType) {
101 -> { // TEXT 101 -> { // TEXT
message.textElem?.content ?: "[文本消息]" message.textElem?.content ?: "[文本消息]"

View File

@@ -0,0 +1,73 @@
package com.aiosman.ravenow.utils
import com.aiosman.ravenow.entity.OpenIMMessageType
import io.openim.android.sdk.models.Message
/**
* 通知消息辅助工具类
* 用于生成通知消息的显示文本
*/
object NotificationMessageHelper {
/**
* 从通知消息中提取显示文本
* @param message OpenIM 消息对象
* @return 通知消息的显示文本
*/
fun getNotificationText(message: Message): String {
// 优先尝试从 textElem 中获取内容(系统通知通常使用文本消息格式)
val textContent = message.textElem?.content
if (textContent != null && textContent.isNotEmpty()) {
return textContent
}
// 如果发送者是系统账户但没有文本内容,根据消息类型生成友好的中文提示
// 根据消息类型生成友好的中文提示
return when (message.contentType) {
OpenIMMessageType.FRIEND_ADDED -> "你们已成为好友,可以开始聊天了"
OpenIMMessageType.SYSTEM_NOTIFICATION -> "系统通知"
OpenIMMessageType.GROUP_CREATED -> "群聊已创建"
OpenIMMessageType.GROUP_INFO_CHANGED -> "群信息已更新"
OpenIMMessageType.GROUP_MEMBER_QUIT -> {
val memberName = message.senderNickname ?: "成员"
"$memberName 退出了群聊"
}
OpenIMMessageType.GROUP_OWNER_CHANGED -> {
val newOwnerName = message.senderNickname ?: "成员"
"群主已更换为 $newOwnerName"
}
OpenIMMessageType.GROUP_MEMBER_KICKED -> {
val memberName = message.senderNickname ?: "成员"
"$memberName 被移出群聊"
}
OpenIMMessageType.GROUP_MEMBER_INVITED -> {
val memberName = message.senderNickname ?: "成员"
"$memberName 加入了群聊"
}
OpenIMMessageType.GROUP_MEMBER_JOINED -> {
val memberName = message.senderNickname ?: "成员"
"$memberName 加入了群聊"
}
OpenIMMessageType.GROUP_DISMISSED -> "群聊已解散"
OpenIMMessageType.GROUP_MEMBER_MUTED -> {
val memberName = message.senderNickname ?: "成员"
"$memberName 已被禁言"
}
OpenIMMessageType.GROUP_MEMBER_UNMUTED -> {
val memberName = message.senderNickname ?: "成员"
"$memberName 已解除禁言"
}
OpenIMMessageType.GROUP_MUTED -> "群聊已开启全员禁言"
OpenIMMessageType.GROUP_UNMUTED -> "群聊已关闭全员禁言"
OpenIMMessageType.GROUP_ANNOUNCEMENT_CHANGED -> "群公告已更新"
OpenIMMessageType.GROUP_NAME_CHANGED -> "群名称已更改"
OpenIMMessageType.SNAPCHAT_TOGGLE -> "阅后即焚功能已更改"
OpenIMMessageType.MESSAGE_REVOKED -> {
val senderName = message.senderNickname ?: "成员"
"$senderName 撤回了一条消息"
}
else -> OpenIMMessageType.getDescription(message.contentType)
}
}
}

View File

@@ -46,6 +46,7 @@ zoomable = "1.6.1"
camerax = "1.4.0" camerax = "1.4.0"
mlkitBarcode = "17.3.0" mlkitBarcode = "17.3.0"
room = "2.8.3" room = "2.8.3"
billing = "8.0.0"
[libraries] [libraries]
accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanistSystemuicontroller" } accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanistSystemuicontroller" }
@@ -114,6 +115,7 @@ mlkit-barcode-scanning = { module = "com.google.mlkit:barcode-scanning", version
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" } androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
billing-ktx = { module = "com.android.billingclient:billing-ktx", version.ref = "billing" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }