Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a76bd11d9 | |||
| f1e91f7639 | |||
| 4feca77924 | |||
| b12f359da1 | |||
| 8901792561 |
@@ -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)
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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?,
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,14 +533,14 @@ 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) {
|
||||
currentUserId -> GroupChatSelfItem(item)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,7 +309,6 @@ fun Agent() {
|
||||
}
|
||||
|
||||
// 只有当热门聊天室有数据时,才展示“发现更多”区域
|
||||
if (viewModel.chatRooms.isNotEmpty()) {
|
||||
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)
|
||||
@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(
|
||||
.clip(
|
||||
RoundedCornerShape(
|
||||
topStart = 12.dp,
|
||||
topEnd = 12.dp,
|
||||
bottomStart = 0.dp,
|
||||
bottomEnd = 0.dp)),
|
||||
bottomEnd = 0.dp
|
||||
)
|
||||
),
|
||||
contentScale = androidx.compose.ui.layout.ContentScale.Crop,
|
||||
defaultRes = R.mipmap.rider_pro_agent
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
// 直接使用openId导航,页面内的AiProfileViewModel会处理数据加载
|
||||
// 避免重复请求,因为AiProfileViewModel.loadProfile已经支持通过openId加载
|
||||
try {
|
||||
val profile = userService.getUserProfileByOpenId(openId)
|
||||
// 从Agent列表点击进去的一定是智能体,直接传递isAiAccount = true
|
||||
navController.navigate(
|
||||
NavigationRoute.AccountProfile.route
|
||||
.replace("{id}", profile.id.toString())
|
||||
.replace("{id}", openId)
|
||||
.replace("{isAiAccount}", "true")
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
// swallow error to avoid crash on navigation attempt failures
|
||||
}
|
||||
Log.e("AgentViewModel", "Navigation failed", e)
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
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(
|
||||
|
||||
@@ -239,11 +239,13 @@ fun RecommendScreen() {
|
||||
onCommentClick = { m ->
|
||||
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.COMMENT_MOMENT)) {
|
||||
navController.navigate(NavigationRoute.Login.route)
|
||||
} else {
|
||||
}
|
||||
// 注意:不在这里增加评论数,应该在评论真正提交成功后再增加
|
||||
},
|
||||
onCommentAdded = { m ->
|
||||
scope.launch {
|
||||
RecommendViewModel.onAddComment(m.id)
|
||||
}
|
||||
}
|
||||
},
|
||||
onFavoriteClick = { m ->
|
||||
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) {
|
||||
|
||||
@@ -44,12 +44,21 @@ class AiProfileViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查id是否是纯数字(用户ID),如果不是则当作openId处理
|
||||
val isUserId = id.toIntOrNull() != null
|
||||
|
||||
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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
// 检查是否为通知类型消息
|
||||
// 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)
|
||||
@@ -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 ?: "[文本消息]"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" }
|
||||
|
||||
Reference in New Issue
Block a user