5 Commits
main ... atm2

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"
minSdk = 24
targetSdk = 35
versionCode = 1000019
versionName = "1.0.000.19"
versionCode = 1000021
versionName = "1.0.000.21"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -140,5 +140,8 @@ dependencies {
implementation(libs.androidx.room.ktx)
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.READ_PHONE_STATE" />
<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" />
<application

View File

@@ -4,6 +4,10 @@ import android.app.Application
import android.content.Context
import android.content.res.Configuration
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.perf.FirebasePerformance
@@ -12,6 +16,8 @@ import com.google.firebase.perf.FirebasePerformance
*/
class RaveNowApplication : Application() {
private var billingClient: BillingClient? = null
override fun attachBaseContext(base: Context) {
// 禁用字体缩放,固定字体大小为系统默认大小
val configuration = Configuration(base.resources.configuration)
@@ -48,6 +54,53 @@ class RaveNowApplication : Application() {
} catch (e: Exception) {
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,
@SerializedName("time")
val time: String?,
@SerializedName("createdAt")
val createdAt: String? = null,
@SerializedName("location")
val location: String? = null,
@SerializedName("isFollowed")
val isFollowed: Boolean,
// 新闻相关字段
@@ -70,11 +74,11 @@ data class Moment(
"" // 如果头像为空,使用空字符串
},
nickname = user.nickName ?: "未知用户", // 如果昵称为空,使用默认值
location = "Worldwide",
time = if (time != null && time.isNotEmpty()) {
ApiClient.dateFromApiString(time)
} else {
java.util.Date() // 如果时间为空,使用当前时间作为默认值
location = location ?: "Worldwide",
time = when {
createdAt != null && createdAt.isNotEmpty() -> ApiClient.dateFromApiString(createdAt)
time != null && time.isNotEmpty() -> ApiClient.dateFromApiString(time)
else -> java.util.Date() // 如果时间为空,使用当前时间作为默认值
},
followStatus = isFollowed,
momentTextContent = textContent,
@@ -204,7 +208,7 @@ data class Video(
data class User(
@SerializedName("id")
val id: Long,
@SerializedName("nickName")
@SerializedName("nickname")
val nickName: String?,
@SerializedName("avatar")
val avatar: String?,

View File

@@ -4,6 +4,7 @@ import android.content.Context
import android.icu.util.Calendar
import com.aiosman.ravenow.ConstVars
import com.aiosman.ravenow.exp.formatChatTime
import com.aiosman.ravenow.utils.NotificationMessageHelper
import com.google.gson.annotations.SerializedName
import io.openim.android.sdk.models.Message
import io.openim.android.sdk.models.PictureElem
@@ -21,7 +22,8 @@ data class ChatItem(
val textDisplay: String = "",
val msgId: String, // Add this property
var showTimestamp: Boolean = false,
var showTimeDivider: Boolean = false
var showTimeDivider: Boolean = false,
val isNotification: Boolean = false // 标识是否为通知类型消息
) {
companion object {
// OpenIM 消息类型常量
@@ -36,6 +38,32 @@ data class ChatItem(
val calendar = Calendar.getInstance()
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
if (faceAvatar == null) {
faceAvatar = "${ConstVars.BASE_SERVER}${message.senderFaceUrl}"
@@ -62,7 +90,8 @@ data class ChatItem(
).toMutableList(),
messageType = MESSAGE_TYPE_IMAGE,
textDisplay = "Image",
msgId = message.clientMsgID
msgId = message.clientMsgID,
isNotification = false
)
}
return null
@@ -79,7 +108,8 @@ data class ChatItem(
imageList = emptyList<PictureInfo>().toMutableList(),
messageType = MESSAGE_TYPE_TEXT,
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.entity.AccountProfileEntity
import com.aiosman.ravenow.entity.ChatItem
import com.aiosman.ravenow.im.OpenIMManager
import io.openim.android.sdk.OpenIMClient
import io.openim.android.sdk.enums.ConversationType
import io.openim.android.sdk.enums.ViewType
@@ -100,6 +101,10 @@ abstract class BaseChatViewModel : ViewModel() {
override fun onSuccess(data: ConversationInfo) {
conversationID = data.conversationID
// 如果是群组的会话id应该加上s修正,不知道是不是openIm的bug
if (data.conversationType == 2) {
conversationID = "s${conversationID}"
}
Log.d(getLogTag(), "获取会话信息成功conversationID: $conversationID")
onSuccess?.invoke()
}
@@ -324,6 +329,7 @@ abstract class BaseChatViewModel : ViewModel() {
OpenIMClient.getInstance().messageManager.getAdvancedHistoryMessageList(
object : OnBase<AdvancedMessage> {
override fun onSuccess(data: AdvancedMessage?) {
val messages = data?.messageList ?: emptyList()
val newChatItems = messages.mapNotNull {
ChatItem.convertToChatItem(it, context, avatar = getMessageAvatar(it))

View File

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

View File

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

View File

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

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.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
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.wrapContentSize
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -63,33 +58,24 @@ import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.NavigationRoute
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.composables.TabItem
import com.aiosman.ravenow.ui.composables.TabSpacer
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.ExploreViewModel
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.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
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.window.Dialog
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.graphics.Brush
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.ui.platform.LocalConfiguration
import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieCompositionSpec
@@ -113,10 +99,6 @@ fun Agent() {
val navigationBarPaddings =
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 48.dp
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
@@ -125,16 +107,6 @@ fun Agent() {
viewModel.ensureDataLoaded()
}
// 防抖状态
var lastClickTime by remember { mutableStateOf(0L) }
// 页面退出时只清理必要的资源不清理推荐Agent数据
DisposableEffect(Unit) {
onDispose {
// 只清理子页面的资源保留推荐Agent数据
// ResourceCleanupManager.cleanupPageResources("ai")
}
}
val agentItems = viewModel.agentItems
var selectedTabIndex by remember { mutableStateOf(0) }
@@ -165,7 +137,7 @@ fun Agent() {
contentDescription = "Rave AI Logo",
modifier = Modifier
.height(44.dp)
.padding(top =9.dp,bottom=9.dp)
.padding(top = 9.dp, bottom = 9.dp)
.wrapContentSize(),
// colorFilter = ColorFilter.tint(AppColors.text)
)
@@ -176,7 +148,7 @@ fun Agent() {
contentDescription = "search",
modifier = Modifier
.size(44.dp)
.padding(top = 9.dp,bottom=9.dp)
.padding(top = 9.dp, bottom = 9.dp)
.noRippleClickable {
navController.navigate(NavigationRoute.Search.route)
},
@@ -267,11 +239,19 @@ fun Agent() {
) {
when {
selectedTabIndex == 0 -> {
AgentViewPagerSection(agentItems = viewModel.agentItems.take(15), viewModel)
AgentViewPagerSection(
agentItems = viewModel.agentItems.take(15),
viewModel
)
}
selectedTabIndex in 1..viewModel.categories.size -> {
AgentViewPagerSection(agentItems = viewModel.agentItems.take(15), viewModel)
AgentViewPagerSection(
agentItems = viewModel.agentItems.take(15),
viewModel
)
}
else -> {
val shuffledAgents = viewModel.agentItems.shuffled().take(15)
AgentViewPagerSection(agentItems = shuffledAgents, viewModel)
@@ -329,133 +309,81 @@ fun Agent() {
}
// 只有当热门聊天室有数据时,才展示“发现更多”区域
if (viewModel.chatRooms.isNotEmpty()) {
item { Spacer(modifier = Modifier.height(20.dp)) }
item { Spacer(modifier = Modifier.height(20.dp)) }
// "发现更多" 标题 - 吸顶
stickyHeader(key = "discover_more") {
Row(
modifier = Modifier
.fillMaxWidth()
.background(AppColors.background)
.padding(top = 8.dp, bottom = 12.dp),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.Bottom
) {
Image(
painter = painterResource(R.mipmap.bars_x_buttons_home_n_copy_2),
contentDescription = "agent",
modifier = Modifier.size(28.dp)
)
Spacer(modifier = Modifier.width(4.dp))
androidx.compose.material3.Text(
text = stringResource(R.string.agent_find),
fontSize = 16.sp,
fontWeight = androidx.compose.ui.text.font.FontWeight.W900,
color = AppColors.text
)
}
// "发现更多" 标题 - 吸顶
stickyHeader(key = "discover_more") {
Row(
modifier = Modifier
.fillMaxWidth()
.background(AppColors.background)
.padding(top = 8.dp, bottom = 12.dp),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.Bottom
) {
Image(
painter = painterResource(R.mipmap.bars_x_buttons_home_n_copy_2),
contentDescription = "agent",
modifier = Modifier.size(28.dp)
)
Spacer(modifier = Modifier.width(4.dp))
androidx.compose.material3.Text(
text = stringResource(R.string.agent_find),
fontSize = 16.sp,
fontWeight = androidx.compose.ui.text.font.FontWeight.W900,
color = AppColors.text
)
}
}
// Agent网格 - 使用行式布局
items(
items = agentItems.chunked(2),
key = { row -> row.firstOrNull()?.openId ?: "" }
) { rowItems ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
rowItems.forEach { agentItem ->
Box(
modifier = Modifier.weight(1f)
) {
AgentCardSquare(
agentItem = agentItem,
viewModel = viewModel,
navController = LocalNavController.current
)
}
}
// 如果这一行只有一个item添加一个空的占位符
if (rowItems.size == 1) {
Spacer(modifier = Modifier.weight(1f))
}
}
}
// 加载更多指示器(仅在展示"发现更多"时显示)
if (viewModel.isLoadingMore) {
item {
// Agent网格 - 使用行式布局
items(
items = agentItems.chunked(2),
key = { row -> row.firstOrNull()?.openId ?: "" }
) { rowItems ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
rowItems.forEach { agentItem ->
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 24.dp),
contentAlignment = Alignment.Center
modifier = Modifier.weight(1f)
) {
LottieAnimation(
composition = rememberLottieComposition(LottieCompositionSpec.Asset("star_Loader.lottie")).value,
iterations = LottieConstants.IterateForever,
modifier = Modifier.size(80.dp)
AgentCardSquare(
agentItem = agentItem,
viewModel = viewModel,
navController = LocalNavController.current
)
}
}
// 如果这一行只有一个item添加一个空的占位符
if (rowItems.size == 1) {
Spacer(modifier = Modifier.weight(1f))
}
}
}
}
}
}
@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) {
// 加载更多指示器(仅在展示"发现更多"时显示)
if (viewModel.isLoadingMore) {
item {
Box(
modifier = Modifier.weight(1f)
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 24.dp),
contentAlignment = Alignment.Center
) {
AgentCardSquare(
agentItem = rowItems[1],
viewModel = viewModel,
navController = navController
LottieAnimation(
composition = rememberLottieComposition(LottieCompositionSpec.Asset("star_Loader.lottie")).value,
iterations = LottieConstants.IterateForever,
modifier = Modifier.size(80.dp)
)
}
} else {
// 如果只有一列,添加空白占位
Spacer(modifier = Modifier.weight(1f))
}
}
}
}
}
@@ -486,11 +414,11 @@ fun AgentCardSquare(
}
) {
// 背景大图
CustomAsyncImage(
imageUrl = agentItem.avatar,
CustomAsyncImage(
imageUrl = agentItem.avatar,
contentDescription = agentItem.title,
modifier = Modifier.fillMaxSize(),
contentScale = androidx.compose.ui.layout.ContentScale.Crop,
contentScale = androidx.compose.ui.layout.ContentScale.Crop,
defaultRes = R.mipmap.rider_pro_agent
)
@@ -507,27 +435,27 @@ fun AgentCardSquare(
)
.padding(12.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
Column(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 40.dp) // 为底部聊天按钮预留空间
) {
androidx.compose.material3.Text(
text = agentItem.title,
) {
androidx.compose.material3.Text(
text = agentItem.title,
color = Color.White,
fontSize = 14.sp,
fontSize = 14.sp,
fontWeight = androidx.compose.ui.text.font.FontWeight.W700,
maxLines = 1,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
)
Spacer(modifier = Modifier.height(4.dp))
androidx.compose.material3.Text(
text = agentItem.desc,
androidx.compose.material3.Text(
text = agentItem.desc,
color = Color.White.copy(alpha = 0.92f),
fontSize = 11.sp,
maxLines = 2,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
)
}
}
@@ -562,9 +490,10 @@ fun AgentCardSquare(
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun AgentViewPagerSection(agentItems: List<AgentItem>,viewModel: AgentViewModel) {
fun AgentViewPagerSection(agentItems: List<AgentItem>, viewModel: AgentViewModel) {
val AppColors = LocalAppTheme.current
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
fun ChatRoomCard(
chatRoom: ChatRoom,
@@ -938,7 +687,10 @@ fun ChatRoomCard(
.size(cardSize)
.background(AppColors.tabUnselectedBackground, RoundedCornerShape(12.dp))
.clickable(enabled = !viewModel.isJoiningRoom) {
if (!viewModel.isJoiningRoom && DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
if (!viewModel.isJoiningRoom && DebounceUtils.simpleDebounceClick(
lastClickTime,
500L
) {
// 加入群聊房间
viewModel.joinRoom(
id = chatRoom.id,
@@ -953,7 +705,8 @@ fun ChatRoomCard(
// 处理错误可以显示Toast或其他提示
}
)
}) {
}
) {
lastClickTime = System.currentTimeMillis()
}
}
@@ -967,11 +720,14 @@ fun ChatRoomCard(
modifier = Modifier
.width(cardSize)
.height(120.dp)
.clip(RoundedCornerShape(
topStart = 12.dp,
topEnd = 12.dp,
bottomStart = 0.dp,
bottomEnd = 0.dp)),
.clip(
RoundedCornerShape(
topStart = 12.dp,
topEnd = 12.dp,
bottomStart = 0.dp,
bottomEnd = 0.dp
)
),
contentScale = androidx.compose.ui.layout.ContentScale.Crop,
defaultRes = R.mipmap.rider_pro_agent
)

View File

@@ -134,7 +134,7 @@ object AgentViewModel: ViewModel() {
pageSize = pageSize,
withWorkflow = 1,
categoryIds = listOf(categoryId),
random = 1
// random = 1
)
} else {
// 获取推荐智能体使用random=1
@@ -143,7 +143,7 @@ object AgentViewModel: ViewModel() {
pageSize = pageSize,
withWorkflow = 1,
categoryIds = null,
random = 1
// random = 1
)
}
@@ -197,7 +197,7 @@ object AgentViewModel: ViewModel() {
page = 1,
pageSize = 20,
isRecommended = 1,
random = "1"
// random = "1"
)
if (response.isSuccessful) {
val allRooms = response.body()?.list ?: emptyList()
@@ -332,18 +332,17 @@ object AgentViewModel: ViewModel() {
openId: String,
navController: NavHostController
) {
viewModelScope.launch {
try {
val profile = userService.getUserProfileByOpenId(openId)
// 从Agent列表点击进去的一定是智能体直接传递isAiAccount = true
navController.navigate(
NavigationRoute.AccountProfile.route
.replace("{id}", profile.id.toString())
.replace("{isAiAccount}", "true")
)
} catch (e: Exception) {
// swallow error to avoid crash on navigation attempt failures
}
// 直接使用openId导航页面内的AiProfileViewModel会处理数据加载
// 避免重复请求因为AiProfileViewModel.loadProfile已经支持通过openId加载
try {
navController.navigate(
NavigationRoute.AccountProfile.route
.replace("{id}", openId)
.replace("{isAiAccount}", "true")
)
} catch (e: Exception) {
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.rememberPullRefreshState
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
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.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.graphics.Color
import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.LocalAppTheme
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()) {
@@ -213,16 +206,16 @@ fun GroupChatItem(
val AppColors = LocalAppTheme.current
val chatDebouncer = rememberDebouncer()
val avatarDebouncer = rememberDebouncer()
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 12.dp)
.padding(horizontal = 16.dp, vertical = 12.dp)
.noRippleClickable {
chatDebouncer {
onChatClick(conversation)
}
}
},
verticalAlignment = Alignment.CenterVertically
) {
Box {
CustomAsyncImage(
@@ -242,9 +235,9 @@ fun GroupChatItem(
Column(
modifier = Modifier
.weight(1f)
.padding(start = 12.dp)
.padding(start = 12.dp, top = 2.dp),
verticalArrangement = Arrangement.Center
) {
Row(
modifier = Modifier.fillMaxWidth(),
@@ -252,22 +245,22 @@ fun GroupChatItem(
) {
Text(
text = conversation.groupName,
fontSize = 16.sp,
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
color = AppColors.text,
modifier = Modifier.weight(1f)
)
Spacer(modifier = Modifier.width(8.dp))
Spacer(modifier = Modifier.width(6.dp))
Text(
text = conversation.lastMessageTime,
fontSize = 12.sp,
fontSize = 11.sp,
color = AppColors.secondaryText
)
}
Spacer(modifier = Modifier.height(4.dp))
Spacer(modifier = Modifier.height(6.dp))
Row(
modifier = Modifier.fillMaxWidth(),
@@ -275,29 +268,29 @@ fun GroupChatItem(
) {
Text(
text = "${if (conversation.isSelf) stringResource(R.string.friend_chat_me_prefix) else ""}${conversation.displayText}",
fontSize = 14.sp,
fontSize = 12.sp,
color = AppColors.secondaryText,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f)
)
Spacer(modifier = Modifier.width(8.dp))
Spacer(modifier = Modifier.width(10.dp))
if (conversation.unreadCount > 0) {
Box(
modifier = Modifier
.size(if (conversation.unreadCount > 99) 24.dp else 20.dp)
.background(
color = AppColors.main,
color = Color(0xFFFF3B30),
shape = CircleShape
),
contentAlignment = Alignment.Center
) {
Text(
text = if (conversation.unreadCount > 99) "99+" else conversation.unreadCount.toString(),
color = AppColors.mainText,
fontSize = if (conversation.unreadCount > 99) 9.sp else 10.sp,
color = Color.White,
fontSize = if (conversation.unreadCount > 99) 11.sp else 12.sp,
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.sp
import com.aiosman.ravenow.R
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.entity.MomentEntity
import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.comment.CommentModalContent
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
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
@@ -50,16 +56,37 @@ fun PostRecommendationItem(
moment: MomentEntity,
onLikeClick: ((MomentEntity) -> Unit)? = null,
onCommentClick: ((MomentEntity) -> Unit)? = null,
onCommentAdded: ((MomentEntity) -> Unit)? = null,
onFavoriteClick: ((MomentEntity) -> Unit)? = null,
onShareClick: ((MomentEntity) -> Unit)? = null,
modifier: Modifier = Modifier
) {
val context = LocalContext.current
val navController = LocalNavController.current
val scope = rememberCoroutineScope()
val userService: UserService = UserServiceImpl()
var showCommentModal by remember { mutableStateOf(false) }
var sheetState = rememberModalBottomSheetState(
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 imageCount = images.size
@@ -71,8 +98,9 @@ fun PostRecommendationItem(
) {
// 图片显示区域(替代视频播放器)
if (imageCount > 0) {
// 只显示第一张图片,优先使用 thumbnailDirectUrl
val imageUrl = images[0].thumbnailDirectUrl
// 只显示第一张图片,优先使用 smallDirectUrl
val imageUrl = images[0].smallDirectUrl
?: images[0].thumbnailDirectUrl
?: images[0].directUrl
?: images[0].url
CustomAsyncImage(
@@ -104,32 +132,16 @@ fun PostRecommendationItem(
.fillMaxWidth()
.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 = "@${moment.nickname}",
modifier = Modifier.padding(start = 8.dp),
fontSize = 16.sp,
color = Color.White,
style = TextStyle(fontWeight = FontWeight.Bold)
)
}
// 用户昵称
Text(
text = "@${moment.nickname}",
modifier = Modifier
.padding(bottom = 8.dp)
.noRippleClickable { navigateToProfile() },
fontSize = 16.sp,
color = Color.White,
style = TextStyle(fontWeight = FontWeight.Bold)
)
// 文字内容
if (!moment.momentTextContent.isNullOrEmpty()) {
@@ -160,7 +172,10 @@ fun PostRecommendationItem(
horizontalAlignment = Alignment.CenterHorizontally
) {
// 用户头像
UserAvatar(avatarUrl = moment.avatar)
UserAvatar(
avatarUrl = moment.avatar,
onClick = { navigateToProfile() }
)
// 点赞
VideoBtn(
@@ -205,21 +220,35 @@ fun PostRecommendationItem(
containerColor = Color.White,
sheetState = sheetState
) {
CommentModalContent(postId = moment.id) {
// 评论添加后的回调
}
CommentModalContent(
postId = moment.id,
commentCount = moment.commentCount,
onCommentAdded = {
onCommentAdded?.invoke(moment)
}
)
}
}
}
@Composable
private fun UserAvatar(avatarUrl: String? = null) {
private fun UserAvatar(
avatarUrl: String? = null,
onClick: (() -> Unit)? = null
) {
Box(
modifier = Modifier
.padding(bottom = 16.dp)
.size(40.dp)
.clip(CircleShape)
.background(Color.White.copy(alpha = 0.2f))
.then(
if (onClick != null) {
Modifier.noRippleClickable { onClick() }
} else {
Modifier
}
)
) {
if (avatarUrl != null && avatarUrl.isNotEmpty()) {
CustomAsyncImage(

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
package com.aiosman.ravenow.utils
import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.entity.OpenIMMessageType
import com.google.gson.Gson
import com.google.gson.JsonSyntaxException
import io.openim.android.sdk.models.Message
@@ -25,8 +26,21 @@ object MessageParser {
val gson = Gson()
val message = gson.fromJson(latestMsgJson, Message::class.java)
// 判断是否是自己发送的消息
isSelf = message.sendID == AppState.profile?.trtcUserId
// 检查是否为通知类型消息
// 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 = sendID == AppState.profile?.trtcUserId
}
// 根据消息类型生成显示文本
displayText = getMessageDisplayText(message)
@@ -50,6 +64,19 @@ object MessageParser {
* @return 消息的显示文本
*/
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) {
101 -> { // TEXT
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"
mlkitBarcode = "17.3.0"
room = "2.8.3"
billing = "8.0.0"
[libraries]
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-ktx = { module = "androidx.room:room-ktx", 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]
android-application = { id = "com.android.application", version.ref = "agp" }
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }