16 Commits

Author SHA1 Message Date
68273ae166 Fix: Correct BASE_SERVER URL for debug and release builds
The BASE_SERVER constant was incorrectly assigning the release URL to debug builds and vice-versa. This commit fixes the logic to ensure the correct API endpoint is used for each build type.
2025-09-15 13:46:01 +08:00
6c7be4ba47 feat: Add string resources for Create Agent V2
This commit introduces new string resources for the "Create Agent V2" feature.
These strings are provided in English, Chinese, and Japanese to support localization.

The added strings cover various UI elements and messages within the Create Agent V2 flow, including:
- Titles and labels (e.g., "Create AI", "Name", "Description")
- User guidance and prompts (e.g., "Hello %s! What would you like to create today?", "An AI that writes poetry...")
- Action button texts (e.g., "AI Enhancement", "Manually Create AI", "Alright, that's the one")
- Status messages (e.g., "Generating...", "Creating...", "Thinking for you")
- Accessibility descriptions for icons (e.g., "AI Avatar", "Edit Icon")
2025-09-15 13:42:58 +08:00
cf25540417 Refactor: Implement V2 of Create Agent UI and logic
- Introduced `CreateAgentV2Screen` and `CreateAgentV2ViewModel` for a new agent creation experience.
- Implemented AI-powered agent info generation based on user input, including title and description.
- Added a "manual mode" for users who prefer to input agent details directly.
- Enhanced UI with gradient borders, loading animations, and improved layout.
- Integrated avatar selection and cropping using `AgentImageCropScreen`.
- Refactored `AddAgentViewModel` to support state persistence across page navigation and to store generated input text.
- Updated API client to include a longer timeout for agent info generation requests.
- Added new drawable resources for UI elements.
- Switched `Const.BASE_SERVER` to use the release URL for debug builds.
- Replaced the old `AddAgentScreen` with the new `CreateAgentV2Screen` in navigation.
2025-09-15 12:03:39 +08:00
eca85c8377 Feat: Add Create Bottom Sheet and icons
- Implemented a new `CreateBottomSheet` Composable to provide users with options to create AI, Group Chat, or Moment.
- Added new drawable resources for the create options: `ic_create_ai.xml`, `ic_create_group_chat.xml`, `ic_create_monent.xml`, and `ic_create_close.xml`.
- Integrated the `CreateBottomSheet` into the `IndexScreen`. Clicking the "+" button now opens this bottom sheet instead of directly navigating to new post creation.
- Updated `IndexViewModel` to manage the visibility state of the `CreateBottomSheet`.
- Added string resources for the Create Bottom Sheet in English, Chinese, and Japanese.
- Ensured proper navigation and tourist mode checks for each create option.
- Implemented graceful dismissal of the bottom sheet with animations.
2025-09-12 17:21:29 +08:00
f8be622ba6 Merge pull request #17 from Zhong202501/main
首页Agent卡片组件
2025-09-10 19:22:52 +08:00
f3c841779b Merge pull request #18 from Kevinlinpr/ll
修复一些未处理异常,切换到测试服务器
2025-09-10 19:19:38 +08:00
57e4614ce8 修复一些未处理异常,切换到测试服务器 2025-09-10 18:34:36 +08:00
922d6e72d6 首页Agent卡片组件 2025-09-10 18:02:58 +08:00
c41c097d41 处理最新消息显示 2025-09-10 14:03:27 +08:00
5218ca7046 重构IM viewmodel代码 2025-09-10 11:57:05 +08:00
ce6ee7bf82 imsdk 调通 2025-09-09 19:05:07 +08:00
e00deb5661 merge conflict 2025-09-09 17:57:28 +08:00
95d6522a54 fix im connect error 2025-09-09 17:53:52 +08:00
d231f3678c 首页UI 2025-09-09 16:18:35 +08:00
cd35562244 标签页调整 2025-09-09 14:41:37 +08:00
21cb512237 启动图标;动态界面调整 2025-09-08 18:06:39 +08:00
62 changed files with 2698 additions and 1291 deletions

View File

@@ -4,10 +4,10 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-09-08T06:52:32.669239Z">
<DropdownSelection timestamp="2025-09-09T09:51:06.656104400Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=/Users/liudikang/.android/avd/Pixel_8_API_30.avd" />
<DeviceId pluginId="Default" identifier="serial=192.168.0.227:45035;connection=094cb92e" />
</handle>
</Target>
</DropdownSelection>

View File

@@ -32,6 +32,9 @@ android {
}
buildTypes {
debug {
isDebuggable = true
}
release {
isMinifyEnabled = false
proguardFiles(
@@ -49,6 +52,7 @@ android {
}
buildFeatures {
compose = true
buildConfig = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.3"

View File

@@ -20,3 +20,23 @@
# hide the original source file name.
#-renamesourcefileattribute SourceFile
# OpenIM SDK ProGuard rules
-keep class io.openim.android.sdk.** { *; }
-keep class io.openim.core.** { *; }
-keepclassmembers class io.openim.android.sdk.** { *; }
-keepclassmembers class io.openim.core.** { *; }
# Keep OpenIM models and listeners
-keep class io.openim.android.sdk.models.** { *; }
-keep class io.openim.android.sdk.listener.** { *; }
-keep class io.openim.android.sdk.enums.** { *; }
# Keep OpenIM Client and managers
-keep class io.openim.android.sdk.OpenIMClient { *; }
-keep class io.openim.android.sdk.manager.** { *; }
# Prevent obfuscation of callback methods
-keepclassmembers class * implements io.openim.android.sdk.listener.** {
public *;
}

View File

@@ -13,7 +13,7 @@
android:allowBackup="false"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/rider_pro_logo_next"
android:icon="@mipmap/invalid_name"
android:label="@string/app_name"
android:roundIcon="@mipmap/rider_pro_logo_next_round"
android:supportsRtl="true"

View File

@@ -67,8 +67,13 @@ object AppState {
)
// 设置当前登录用户 ID
UserId = resp.id
var profileResult = accountService.getMyAccountProfile()
profile = profileResult
try {
var profileResult = accountService.getMyAccountProfile()
profile = profileResult
} catch (e:Exception) {
Log.e("AppState", "getMyAccountProfile Error:"+ e.message )
}
// 获取当前用户资料
// 注册 JPush
@@ -101,10 +106,11 @@ object AppState {
val initConfig = InitConfig(
"https://im.ravenow.ai/api",//SDK api地址
"wss:///im.ravenow.ai/msg_gateway",//SDK WebSocket地址
"wss://im.ravenow.ai/msg_gateway",//SDK WebSocket地址
OpenIMManager.getStorageDir(context),//SDK数据库存储目录
)
// initConfig.isLogStandardOutput = true;
// initConfig.logLevel = 6
// 使用 OpenIMManager 初始化 SDK
OpenIMManager.initSDK(context, initConfig)

View File

@@ -1,11 +1,14 @@
package com.aiosman.ravenow
object ConstVars {
// api 地址
// const val BASE_SERVER = "http://192.168.31.131:8088"
// const val BASE_SERVER = "http://192.168.142.141:8088"
// const val BASE_SERVER = "http://192.168.0.228:8088"
const val BASE_SERVER = "https://rider-pro.aiosman.com/beta_api"
// api 地址 - 根据构建类型自动选择
// Debug: http://192.168.0.201:8088
// Release: https://rider-pro.aiosman.com/beta_api
val BASE_SERVER = if (BuildConfig.DEBUG) {
"http://47.109.137.67:6363" // Debug环境
} else {
"https://rider-pro.aiosman.com/beta_api" // Release环境
}
const val MOMENT_LIKE_CHANNEL_ID = "moment_like"
const val MOMENT_LIKE_CHANNEL_NAME = "Moment Like"

View File

@@ -430,9 +430,17 @@ interface AccountService {
class AccountServiceImpl : AccountService {
override suspend fun getMyAccountProfile(): AccountProfileEntity {
// 如果已有缓存,直接返回缓存结果
AppState.profile?.let { return it }
// 第一次调用,获取数据并缓存
val resp = ApiClient.api.getMyAccount()
val body = resp.body() ?: throw ServiceException("Failed to get account")
return body.data.toAccountProfileEntity()
val profile = body.data.toAccountProfileEntity()
// 缓存结果到共享状态
AppState.profile = profile
return profile
}
override suspend fun getMyAccount(): UserAuth {

View File

@@ -91,7 +91,7 @@ interface AgentService {
pageNumber: Int,
pageSize: Int = 20,
authorId: Int? = null
): ListContainer<AgentEntity>
): ListContainer<AgentEntity>?
}

View File

@@ -13,9 +13,11 @@ import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.Date
import java.util.Locale
import java.util.concurrent.TimeUnit
fun getSafeOkHttpClient(
authInterceptor: AuthInterceptor? = null
authInterceptor: AuthInterceptor? = null,
timeoutSeconds: Long = 30
): OkHttpClient {
return OkHttpClient.Builder()
.apply {
@@ -23,6 +25,9 @@ fun getSafeOkHttpClient(
addInterceptor(it)
}
}
.connectTimeout(timeoutSeconds, TimeUnit.SECONDS)
.readTimeout(timeoutSeconds, TimeUnit.SECONDS)
.writeTimeout(timeoutSeconds, TimeUnit.SECONDS)
.build()
}
@@ -46,6 +51,7 @@ class AuthInterceptor() : Interceptor {
}
requestBuilder.addHeader("Authorization", "Bearer ${AppStore.token}")
requestBuilder.addHeader("DEVICE-OS", "Android")
val response = chain.proceed(requestBuilder.build())
return response
@@ -55,7 +61,7 @@ class AuthInterceptor() : Interceptor {
val client = Retrofit.Builder()
.baseUrl(ApiClient.RETROFIT_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(getSafeOkHttpClient())
.client(getSafeOkHttpClient(timeoutSeconds = 30))
.build()
.create(RaveNowAPI::class.java)
@@ -69,12 +75,15 @@ class AuthInterceptor() : Interceptor {
}
object ApiClient {
const val BASE_SERVER = ConstVars.BASE_SERVER
const val BASE_API_URL = "${BASE_SERVER}/api/v1"
const val RETROFIT_URL = "${BASE_API_URL}/"
val BASE_SERVER = ConstVars.BASE_SERVER
val BASE_API_URL = "${BASE_SERVER}/api/v1"
val RETROFIT_URL = "${BASE_API_URL}/"
const val TIME_FORMAT = "yyyy-MM-dd HH:mm:ss"
private val okHttpClient: OkHttpClient by lazy {
getSafeOkHttpClient(authInterceptor = AuthInterceptor())
getSafeOkHttpClient(authInterceptor = AuthInterceptor(), timeoutSeconds = 30)
}
private val longTimeoutOkHttpClient: OkHttpClient by lazy {
getSafeOkHttpClient(authInterceptor = AuthInterceptor(), timeoutSeconds = 120)
}
private val retrofit: Retrofit by lazy {
Retrofit.Builder()
@@ -83,9 +92,19 @@ object ApiClient {
.addConverterFactory(GsonConverterFactory.create())
.build()
}
private val longTimeoutRetrofit: Retrofit by lazy {
Retrofit.Builder()
.baseUrl(RETROFIT_URL)
.client(longTimeoutOkHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
val api: RaveNowAPI by lazy {
retrofit.create(RaveNowAPI::class.java)
}
val longTimeoutApi: RaveNowAPI by lazy {
longTimeoutRetrofit.create(RaveNowAPI::class.java)
}
fun formatTime(date: Date): String {
val dateFormat = SimpleDateFormat(TIME_FORMAT, Locale.getDefault())

View File

@@ -42,6 +42,20 @@ data class AgentMomentRequestBody(
val sessionId: String
)
data class GenerateAgentInfoRequestBody(
@SerializedName("descriptionText")
val descriptionText: String
)
data class GenerateAgentInfoResponseBody(
@SerializedName("title")
val title: String,
@SerializedName("description")
val description: String,
@SerializedName("content")
val content: String
)
data class SingleChatRequestBody(
@SerializedName("agentOpenId")
val agentOpenId: String? = null,
@@ -605,7 +619,8 @@ interface RaveNowAPI {
suspend fun joinRoom(@Body body: JoinGroupChatRequestBody,
): Response<DataContainer<Room>>
@POST("outside/generate/agent-info")
suspend fun generateAgentInfo(@Body body: GenerateAgentInfoRequestBody): Response<DataContainer<GenerateAgentInfoResponseBody>>
}

View File

@@ -80,9 +80,9 @@ class AgentPagingSource(
authorId = authorId
)
LoadResult.Page(
data = users.list,
data = users?.list ?: listOf(),
prevKey = if (currentPage == 1) null else currentPage - 1,
nextKey = if (users.list.isEmpty()) null else users.page + 1
nextKey = if (users?.list?.isNotEmpty() == true) users.page + 1 else null
)
} catch (exception: IOException) {
return LoadResult.Error(exception)
@@ -102,7 +102,7 @@ class AgentRemoteDataSource(
suspend fun getAgent(
pageNumber: Int,
authorId: Int? = null
): ListContainer<AgentEntity> {
): ListContainer<AgentEntity>? {
return agentService.getAgent(
pageNumber = pageNumber,
authorId = authorId
@@ -117,7 +117,7 @@ class AgentServiceImpl() : AgentService {
pageNumber: Int,
pageSize: Int,
authorId: Int?
): ListContainer<AgentEntity> {
): ListContainer<AgentEntity>? {
return agentBackend.getAgent(
pageNumber = pageNumber,
authorId = authorId
@@ -130,7 +130,7 @@ class AgentBackend {
suspend fun getAgent(
pageNumber: Int,
authorId: Int? = null
): ListContainer<AgentEntity> {
): ListContainer<AgentEntity>? {
// 如果是游客模式且获取我的AgentauthorId为null返回空列表
if (authorId == null && AppStore.isGuest) {
return ListContainer(
@@ -154,7 +154,7 @@ class AgentBackend {
)
}
val body = resp.body() ?: throw ServiceException("Failed to get agents")
val body = resp.body() ?: return null
// 处理不同的返回类型
return if (authorId != null) {

View File

@@ -39,6 +39,7 @@ import com.aiosman.ravenow.ui.account.RemoveAccountScreen
import com.aiosman.ravenow.ui.account.ResetPasswordScreen
import com.aiosman.ravenow.ui.agent.AddAgentScreen
import com.aiosman.ravenow.ui.agent.AgentImageCropScreen
import com.aiosman.ravenow.ui.agent.CreateAgentV2Screen
import com.aiosman.ravenow.ui.group.CreateGroupChatScreen
import com.aiosman.ravenow.ui.chat.ChatAiScreen
import com.aiosman.ravenow.ui.chat.ChatScreen
@@ -544,7 +545,7 @@ fun NavigationController(
composable(
route = NavigationRoute.AddAgent.route,
) {
AddAgentScreen()
CreateAgentV2Screen()
}
composable(

View File

@@ -24,6 +24,10 @@ object AddAgentViewModel : ViewModel() {
var croppedBitmap by mutableStateOf<Bitmap?>(null)
var isUpdating by mutableStateOf(false)
var isSelectingAvatar by mutableStateOf(false) // 标记是否正在选择头像
var hasExitedPage by mutableStateOf(false) // 标记是否已经完全退出页面
// 保存AI生成的输入文本避免页面重建时丢失
var generateInputText by mutableStateOf("")
suspend fun updateAgentAvatar(context: Context) {
croppedBitmap?.let {
@@ -84,5 +88,7 @@ object AddAgentViewModel : ViewModel() {
croppedBitmap = null
isUpdating = false
isSelectingAvatar = false
hasExitedPage = false
generateInputText = ""
}
}

View File

@@ -0,0 +1,648 @@
package com.aiosman.ravenow.ui.agent
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.animation.core.*
import androidx.compose.ui.geometry.Offset
import kotlin.math.cos
import kotlin.math.sin
import androidx.compose.runtime.LaunchedEffect
import kotlinx.coroutines.delay
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.composables.ActionButton
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
@Composable
fun LoadingDots(
modifier: Modifier = Modifier,
dotColor: Color = Color.Gray
) {
val infiniteTransition = rememberInfiniteTransition(label = "loading_dots")
val animationValues = (0..2).map { index ->
infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 600,
easing = EaseInOut,
delayMillis = index * 200
),
repeatMode = RepeatMode.Reverse
),
label = "dot_$index"
)
}
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
animationValues.forEach { animValue ->
Box(
modifier = Modifier
.size(6.dp)
.offset(y = (-8 * animValue.value).dp)
.background(
color = dotColor.copy(alpha = 0.5f + 0.5f * animValue.value),
shape = CircleShape
)
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CreateAgentV2Screen(
viewModel: CreateAgentV2ViewModel = remember { CreateAgentV2ViewModel() }
) {
// 页面进入时的状态管理
LaunchedEffect(Unit) {
// 总是先同步状态
viewModel.syncStateOnResume()
}
// 页面退出时的处理
DisposableEffect(Unit) {
onDispose {
// 页面退出时,标记为已退出(除非是在选择头像)
if (!viewModel.isSelectingAvatar) {
viewModel.markPageExited()
}
}
}
val appColors = LocalAppTheme.current
val navController = LocalNavController.current
val context = LocalContext.current
// 获取当前用户名,如果没有则使用默认值
val userName = AppState.profile?.nickName ?: "用户"
// 渐变边框旋转动画
val infiniteTransition = rememberInfiniteTransition(label = "gradient_rotation")
val rotationAngle by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 16000,
easing = LinearEasing
),
repeatMode = RepeatMode.Restart
),
label = "rotation_angle"
)
Column(
modifier = Modifier
.fillMaxSize()
.background(appColors.background)
) {
// 状态栏占位
StatusBarSpacer()
// 顶部标题栏
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
// 返回按钮
Image(
painter = painterResource(id = R.drawable.rider_pro_back_icon),
contentDescription = "返回",
colorFilter = ColorFilter.tint(appColors.text),
modifier = Modifier
.size(24.dp)
.noRippleClickable {
navController.navigateUp()
}
)
Spacer(modifier = Modifier.width(12.dp))
// 标题 - 左对齐
Text(
text = "创建AI",
fontSize = 18.sp,
fontWeight = FontWeight.W700,
color = appColors.text
)
}
// 主要内容区域 - 可滚动
Column(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.verticalScroll(rememberScrollState())
.padding(horizontal = 24.dp),
horizontalAlignment = Alignment.Start
) {
Spacer(modifier = Modifier.height(40.dp))
// AI头像图标
Box(
modifier = Modifier
.size(48.dp)
.background(
color = appColors.inputBackground,
shape = CircleShape
),
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(R.drawable.ic_create_head_logo),
contentDescription = "AI头像",
modifier = Modifier.size(48.dp),
)
}
Spacer(modifier = Modifier.height(32.dp))
// 问候语
Text(
text = "$userName 你好呀!今天想创建什么?",
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
color = appColors.text,
textAlign = TextAlign.Start
)
Spacer(modifier = Modifier.height(16.dp))
// 描述性文字
Text(
text = "只需要一句话你的专属AI在这里诞生",
fontSize = 14.sp,
color = appColors.secondaryText,
textAlign = TextAlign.Start
)
Spacer(modifier = Modifier.height(40.dp))
// 根据模式显示不同的UI
if (!viewModel.isManualMode) {
// AI生成模式 - 渐变边框输入框
Box(
modifier = Modifier
.fillMaxWidth()
.shadow(
elevation = 8.dp,
shape = RoundedCornerShape(16.dp),
ambientColor = Color(0xFF6246ff).copy(alpha = 0.4f),
spotColor = Color(0xFFd80264).copy(alpha = 0.4f)
)
) {
// 渐变边框
Box(
modifier = Modifier
.fillMaxWidth()
.background(
brush = Brush.linearGradient(
colors = listOf(
Color(0xFF6246ff), // 紫色
Color(0xFFd80264), // 红色
Color(0xFF6246ff), // 紫色
Color(0xFFd80264) // 红色
),
start = Offset(
x = cos(Math.toRadians(rotationAngle.toDouble())).toFloat() * 1000f,
y = sin(Math.toRadians(rotationAngle.toDouble())).toFloat() * 1000f
),
end = Offset(
x = cos(Math.toRadians(rotationAngle.toDouble() + 180)).toFloat() * 1000f,
y = sin(Math.toRadians(rotationAngle.toDouble() + 180)).toFloat() * 1000f
)
),
shape = RoundedCornerShape(16.dp)
)
.padding(1.5.dp) // 边框宽度
) {
// 内部输入框
Box(
modifier = Modifier
.fillMaxWidth()
.background(
color = appColors.background,
shape = RoundedCornerShape(15.dp)
)
.padding(8.dp)
) {
Column {
TextField(
value = viewModel.inputText,
onValueChange = {
if (!viewModel.isGenerating) {
viewModel.updateInputText(it)
}
},
placeholder = {
Text(
text = "一个会写诗的AI一个会懂你笑点的AI",
color = appColors.inputHint,
fontSize = 14.sp
)
},
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
disabledContainerColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
disabledIndicatorColor = Color.Transparent,
focusedTextColor = appColors.text,
unfocusedTextColor = appColors.text,
disabledTextColor = appColors.inputHint,
cursorColor = if (viewModel.isGenerating) Color.Transparent else appColors.main,
focusedPlaceholderColor = appColors.inputHint,
unfocusedPlaceholderColor = appColors.inputHint,
disabledPlaceholderColor = appColors.inputHint.copy(alpha = 0.5f)
),
enabled = !viewModel.isGenerating,
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 100.dp)
)
// AI美化按钮
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
TextButton(
onClick = {
if (!viewModel.isGenerating) {
viewModel.generateAgentInfo()
}
},
enabled = viewModel.canGenerate() && !viewModel.isGenerating,
colors = ButtonDefaults.textButtonColors(
contentColor = if (viewModel.isGenerating) appColors.inputHint else Color(0xFF7c46ed),
disabledContentColor = appColors.inputHint
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Image(
painter = painterResource(R.drawable.ic_create_agent_generate),
contentDescription = "AI美化图标",
modifier = Modifier.size(16.dp),
colorFilter = ColorFilter.tint(Color(0xFF7c46ed))
)
Text(
text = if (viewModel.isGenerating) "生成中..." else "AI美化",
fontSize = 12.sp,
fontWeight = FontWeight.Medium
)
}
}
}
}
}
}
}
Spacer(modifier = Modifier.height(16.dp))
// AI生成中的loading状态
if (viewModel.isGenerating) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
LoadingDots(
dotColor = appColors.main
)
Text(
text = "正在为你构思",
fontSize = 14.sp,
color = appColors.secondaryText,
fontWeight = FontWeight.Medium
)
}
}
Spacer(modifier = Modifier.height(16.dp))
}
// 手动创造AI按钮 - 只在非生成状态下显示
if (!viewModel.isGenerating) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Start
) {
OutlinedButton(
onClick = {
if (!viewModel.isGenerating) {
viewModel.enableManualMode()
}
},
enabled = !viewModel.isGenerating,
modifier = Modifier.height(40.dp),
shape = RoundedCornerShape(10.dp),
border = BorderStroke(1.dp, appColors.inputHint.copy(alpha = 0.3f)),
colors = ButtonDefaults.outlinedButtonColors(
contentColor = appColors.secondaryText,
disabledContentColor = appColors.inputHint
),
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
Icon(
Icons.Default.Edit,
contentDescription = "编辑图标",
modifier = Modifier.size(16.dp),
tint = appColors.secondaryText
)
Text(
text = "手动创造AI",
fontSize = 13.sp,
fontWeight = FontWeight.Medium
)
}
}
}
}
} else {
// 手动模式 - "一句话创造AI"按钮
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Start
) {
Box(
modifier = Modifier
.shadow(
elevation = 6.dp,
shape = RoundedCornerShape(12.dp),
ambientColor = Color(0xFF6246ff).copy(alpha = 0.3f),
spotColor = Color(0xFFd80264).copy(alpha = 0.3f)
)
) {
// 渐变边框
Box(
modifier = Modifier
.background(
brush = Brush.linearGradient(
colors = listOf(
Color(0xFF6246ff), // 紫色
Color(0xFFd80264), // 红色
Color(0xFF6246ff), // 紫色
Color(0xFFd80264) // 红色
),
start = Offset(
x = cos(Math.toRadians(rotationAngle.toDouble())).toFloat() * 1000f,
y = sin(Math.toRadians(rotationAngle.toDouble())).toFloat() * 1000f
),
end = Offset(
x = cos(Math.toRadians(rotationAngle.toDouble() + 180)).toFloat() * 1000f,
y = sin(Math.toRadians(rotationAngle.toDouble() + 180)).toFloat() * 1000f
)
),
shape = RoundedCornerShape(12.dp)
)
.padding(1.dp) // 边框宽度
) {
// 内部按钮
Box(
modifier = Modifier
.background(
color = appColors.background,
shape = RoundedCornerShape(11.dp)
)
.noRippleClickable {
viewModel.disableManualMode()
}
.padding(horizontal = 12.dp, vertical = 8.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
Icon(
Icons.Default.Edit,
contentDescription = "编辑图标",
modifier = Modifier.size(16.dp),
tint = appColors.secondaryText
)
Text(
text = "一句话创造AI",
fontSize = 13.sp,
fontWeight = FontWeight.Medium,
color = appColors.secondaryText
)
}
}
}
}
}
}
// 生成结果显示区域
if (viewModel.hasGeneratedResult()) {
Spacer(modifier = Modifier.height(32.dp))
// 头像选择组件
Box(
modifier = Modifier
.size(72.dp)
.noRippleClickable {
viewModel.setSelectingAvatar(true)
navController.navigate(NavigationRoute.AgentImageCrop.route)
},
contentAlignment = Alignment.Center
) {
if (viewModel.croppedBitmap != null) {
// 有头像时显示头像
CustomAsyncImage(
context,
viewModel.croppedBitmap,
modifier = Modifier
.size(72.dp)
.clip(CircleShape),
contentDescription = "AI头像",
contentScale = ContentScale.Crop,
placeholderRes = R.mipmap.rider_pro_agent_avatar,
showShimmer = false
)
} else {
// 没有头像时显示渐变背景和编辑图标
Box(
modifier = Modifier
.size(72.dp)
.clip(CircleShape)
.background(
brush = Brush.verticalGradient(
0f to Color(0xFF7c45ed),
0.24f to Color(0xFF7c68ef),
1f to Color(0xFF7bd8f8)
)
),
contentAlignment = Alignment.Center
) {
Icon(
Icons.Default.Edit,
contentDescription = "选择头像",
tint = Color.White,
modifier = Modifier.size(24.dp)
)
}
}
}
Spacer(modifier = Modifier.height(32.dp))
// 标题输入框
Column(
modifier = Modifier.fillMaxWidth()
) {
Text(
text = "名称",
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
color = appColors.text,
modifier = Modifier.padding(bottom = 8.dp)
)
TextField(
value = viewModel.agentTitle,
onValueChange = { viewModel.updateAgentTitle(it) },
colors = TextFieldDefaults.colors(
focusedContainerColor = appColors.inputBackground,
unfocusedContainerColor = appColors.inputBackground,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
focusedTextColor = appColors.text,
unfocusedTextColor = appColors.text
),
shape = RoundedCornerShape(12.dp),
modifier = Modifier.fillMaxWidth()
)
}
Spacer(modifier = Modifier.height(16.dp))
// 描述输入框
Column(
modifier = Modifier.fillMaxWidth()
) {
Text(
text = "描述",
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
color = appColors.text,
modifier = Modifier.padding(bottom = 8.dp)
)
TextField(
value = viewModel.agentDescription,
onValueChange = { viewModel.updateAgentDescription(it) },
colors = TextFieldDefaults.colors(
focusedContainerColor = appColors.inputBackground,
unfocusedContainerColor = appColors.inputBackground,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
focusedTextColor = appColors.text,
unfocusedTextColor = appColors.text
),
shape = RoundedCornerShape(12.dp),
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 120.dp)
)
}
Spacer(modifier = Modifier.height(32.dp))
// 错误信息显示
viewModel.errorMessage?.let { error ->
Text(
text = error,
color = Color.Red,
modifier = Modifier.padding(horizontal = 16.dp),
fontSize = 14.sp
)
Spacer(modifier = Modifier.height(16.dp))
}
// 创建AI按钮
ActionButton(
modifier = Modifier
.fillMaxWidth()
.background(
brush = Brush.linearGradient(
colors = listOf(
Color(0xFF7bd8f8),
Color(0xFF7c68ef),
Color(0xFF7c45ed)
)
),
shape = RoundedCornerShape(24.dp)
),
color = Color.White,
backgroundColor = Color.Transparent,
text = "好的,就它了",
isLoading = viewModel.isCreating,
loadingText = "创建中...",
enabled = viewModel.canCreate()
) {
viewModel.createAgent(context) {
navController.popBackStack()
}
}
}
Spacer(modifier = Modifier.height(32.dp))
}
}
}

View File

@@ -0,0 +1,238 @@
package com.aiosman.ravenow.ui.agent
import android.content.Context
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.data.api.GenerateAgentInfoRequestBody
import kotlinx.coroutines.launch
class CreateAgentV2ViewModel : ViewModel() {
// UI状态
var inputText by mutableStateOf("")
private set
var agentTitle by mutableStateOf("")
private set
var agentDescription by mutableStateOf("")
private set
var isGenerating by mutableStateOf(false)
private set
var errorMessage by mutableStateOf<String?>(null)
private set
var isCreating by mutableStateOf(false)
private set
var isManualMode by mutableStateOf(false)
private set
// 临时保存的生成结果,用于在生成过程中暂时隐藏当前结果
private var tempAgentTitle by mutableStateOf("")
private var tempAgentDescription by mutableStateOf("")
// AddAgentViewModel实例用于头像和创建逻辑
private val addAgentViewModel = AddAgentViewModel
// 获取头像相关状态
val croppedBitmap get() = addAgentViewModel.croppedBitmap
val isSelectingAvatar get() = addAgentViewModel.isSelectingAvatar
init {
// 初始化时检查是否需要恢复状态
if (addAgentViewModel.hasExitedPage) {
// 如果之前已经完全退出页面,清空所有数据
addAgentViewModel.clearData()
} else {
// 否则恢复已有状态(包括从头像选择回来的情况)
if (addAgentViewModel.name.isNotEmpty()) {
agentTitle = addAgentViewModel.name
}
if (addAgentViewModel.desc.isNotEmpty()) {
agentDescription = addAgentViewModel.desc
}
// 恢复输入文本
if (addAgentViewModel.generateInputText.isNotEmpty()) {
inputText = addAgentViewModel.generateInputText
}
}
}
fun updateInputText(text: String) {
inputText = text
addAgentViewModel.generateInputText = text // 同时保存到AddAgentViewModel
clearError()
}
fun updateAgentTitle(title: String) {
agentTitle = title
syncToAddAgentViewModel()
clearError()
}
fun updateAgentDescription(description: String) {
agentDescription = description
syncToAddAgentViewModel()
clearError()
}
private fun clearError() {
errorMessage = null
}
private fun syncToAddAgentViewModel() {
addAgentViewModel.name = agentTitle
addAgentViewModel.desc = agentDescription
}
fun setSelectingAvatar(isSelecting: Boolean) {
addAgentViewModel.isSelectingAvatar = isSelecting
}
fun markPageExited() {
addAgentViewModel.hasExitedPage = true
}
fun syncStateOnResume() {
// 如果之前在选择头像,现在回来了,重置选择状态
if (addAgentViewModel.isSelectingAvatar) {
addAgentViewModel.isSelectingAvatar = false
// 从头像选择页面回来,恢复文本状态
if (addAgentViewModel.name.isNotEmpty()) {
agentTitle = addAgentViewModel.name
}
if (addAgentViewModel.desc.isNotEmpty()) {
agentDescription = addAgentViewModel.desc
}
if (addAgentViewModel.generateInputText.isNotEmpty()) {
inputText = addAgentViewModel.generateInputText
}
}
}
fun enableManualMode() {
isManualMode = true
// 手动模式下,如果没有现有内容,初始化为空
if (agentTitle.isEmpty() && agentDescription.isEmpty()) {
agentTitle = ""
agentDescription = ""
}
}
fun disableManualMode() {
isManualMode = false
}
fun generateAgentInfo() {
if (inputText.isBlank() || isGenerating) return
viewModelScope.launch {
try {
isGenerating = true
clearError()
// 开始生成时,暂存当前结果并清空显示
tempAgentTitle = agentTitle
tempAgentDescription = agentDescription
agentTitle = ""
agentDescription = ""
val response = ApiClient.longTimeoutApi.generateAgentInfo(
GenerateAgentInfoRequestBody(inputText)
)
if (response.isSuccessful) {
val data = response.body()?.data
data?.let {
// 成功时,使用新结果
agentTitle = it.title
agentDescription = it.description
syncToAddAgentViewModel()
// 清空临时保存
tempAgentTitle = ""
tempAgentDescription = ""
}
} else {
// 失败时,恢复之前的结果
agentTitle = tempAgentTitle
agentDescription = tempAgentDescription
tempAgentTitle = ""
tempAgentDescription = ""
errorMessage = "生成失败,请重试"
}
} catch (e: Exception) {
// 异常时,恢复之前的结果
agentTitle = tempAgentTitle
agentDescription = tempAgentDescription
tempAgentTitle = ""
tempAgentDescription = ""
errorMessage = "网络错误: ${e.message}"
} finally {
isGenerating = false
}
}
}
fun createAgent(context: Context, onSuccess: () -> Unit) {
if (isCreating) return
viewModelScope.launch {
try {
isCreating = true
clearError()
// 验证输入
val validationError = addAgentViewModel.validate()
if (validationError != null) {
errorMessage = validationError
return@launch
}
// 调用创建智能体API
val result = addAgentViewModel.createAgent(context)
if (result != null) {
// 创建成功,清空数据
clearData()
onSuccess()
} else {
errorMessage = "创建失败,请重试"
}
} catch (e: Exception) {
errorMessage = "创建智能体失败: ${e.message}"
} finally {
isCreating = false
}
}
}
fun clearData() {
inputText = ""
agentTitle = ""
agentDescription = ""
errorMessage = null
isGenerating = false
isCreating = false
addAgentViewModel.clearData()
}
// 检查是否可以创建
fun canCreate(): Boolean {
return !isCreating && agentTitle.isNotBlank() && agentDescription.isNotBlank()
}
// 检查是否可以生成
fun canGenerate(): Boolean {
return !isGenerating && inputText.isNotBlank()
}
// 检查是否有生成结果或处于手动模式
fun hasGeneratedResult(): Boolean {
return agentTitle.isNotEmpty() || agentDescription.isNotEmpty() || isManualMode
}
}

View File

@@ -0,0 +1,377 @@
package com.aiosman.ravenow.ui.chat
import android.content.Context
import android.net.Uri
import android.provider.MediaStore
import android.util.Log
import android.webkit.MimeTypeMap
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aiosman.ravenow.data.AccountService
import com.aiosman.ravenow.data.AccountServiceImpl
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 io.openim.android.sdk.OpenIMClient
import io.openim.android.sdk.enums.ConversationType
import io.openim.android.sdk.enums.ViewType
import io.openim.android.sdk.listener.OnAdvanceMsgListener
import io.openim.android.sdk.listener.OnBase
import io.openim.android.sdk.listener.OnMsgSendCallback
import io.openim.android.sdk.models.*
import kotlinx.coroutines.launch
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
/**
* 聊天ViewModel基类包含所有聊天功能的通用实现
* 子类需要实现抽象方法来处理特定的聊天类型(单聊/群聊)
*/
abstract class BaseChatViewModel : ViewModel() {
// 通用状态属性
var chatData by mutableStateOf<List<ChatItem>>(emptyList())
var myProfile by mutableStateOf<AccountProfileEntity?>(null)
var hasMore by mutableStateOf(true)
var isLoading by mutableStateOf(false)
var lastMessage: Message? = null
val showTimestampMap = mutableMapOf<String, Boolean>()
var goToNew by mutableStateOf(false)
var conversationID: String = "" // 会话ID通过getOneConversation初始化
// 通用服务
val userService: UserService = UserServiceImpl()
val accountService: AccountService = AccountServiceImpl()
var textMessageListener: OnAdvanceMsgListener? = null
val fetchHistorySize = 20
/**
* 初始化方法,子类需要实现具体的初始化逻辑
*/
abstract fun init(context: Context)
/**
* 获取日志标签,子类需要实现
*/
abstract fun getLogTag(): String
/**
* 获取会话参数,子类需要实现
* @return Triple(targetId, conversationType, isSingleChat)
*/
abstract fun getConversationParams(): Triple<String, Int, Boolean>
/**
* 处理接收到的新消息,子类可以重写以添加特定逻辑
*/
open fun handleNewMessage(message: Message, context: Context): Boolean {
return false // 默认不处理,子类重写
}
/**
* 获取发送消息时的接收者ID子类需要实现
*/
abstract fun getReceiverInfo(): Pair<String?, String?> // (recvID, groupID)
/**
* 发送消息成功后的额外处理,子类可以重写
*/
open fun onMessageSentSuccess(message: String, sentMessage: Message?) {
// 默认无额外处理,子类可以重写
}
/**
* 获取会话信息并初始化conversationID
*/
fun getOneConversation(onSuccess: (() -> Unit)? = null) {
val (targetId, conversationType, isSingleChat) = getConversationParams()
OpenIMClient.getInstance().conversationManager.getOneConversation(
object : OnBase<ConversationInfo> {
override fun onError(code: Int, error: String) {
Log.e(getLogTag(), "getOneConversation error: $error")
}
override fun onSuccess(data: ConversationInfo) {
conversationID = data.conversationID
Log.d(getLogTag(), "获取会话信息成功conversationID: $conversationID")
onSuccess?.invoke()
}
},
targetId,
conversationType
)
}
/**
* 注册消息监听器
*/
fun RegistListener(context: Context) {
// 检查 OpenIM 是否已登录
if (!com.aiosman.ravenow.AppState.enableChat) {
Log.w(getLogTag(), "OpenIM 未登录,跳过注册消息监听器")
return
}
textMessageListener = object : OnAdvanceMsgListener {
override fun onRecvNewMessage(msg: Message?) {
msg?.let { message ->
if (handleNewMessage(message, context)) {
val chatItem = ChatItem.convertToChatItem(message, context, avatar = getMessageAvatar(message))
chatItem?.let {
chatData = listOf(it) + chatData
goToNew = true
Log.i(getLogTag(), "收到来自 ${message.sendID} 的消息,更新聊天列表")
}
}
}
}
}
OpenIMClient.getInstance().messageManager.setAdvancedMsgListener(textMessageListener)
}
/**
* 获取消息头像,子类可以重写
*/
open fun getMessageAvatar(message: Message): String? {
return null
}
/**
* 取消注册消息监听器
*/
fun UnRegistListener() {
textMessageListener = null
}
/**
* 清除未读消息
*/
fun clearUnRead() {
if (conversationID.isEmpty()) {
Log.w(getLogTag(), "conversationID为空无法清除未读消息")
return
}
OpenIMClient.getInstance().messageManager.markConversationMessageAsRead(
conversationID,
object : OnBase<String> {
override fun onSuccess(data: String?) {
Log.i("openim", "清除未读消息成功")
}
override fun onError(code: Int, error: String?) {
Log.i("openim", "清除未读消息失败, code:$code, error:$error")
}
}
)
}
/**
* 加载更多历史消息
*/
fun onLoadMore(context: Context) {
if (!hasMore || isLoading) {
return
}
loadHistoryMessages(context, isLoadMore = true)
}
/**
* 发送文本消息
*/
fun sendMessage(message: String, context: Context) {
// 检查 OpenIM 是否已登录
if (!com.aiosman.ravenow.AppState.enableChat) {
Log.w(getLogTag(), "OpenIM 未登录,无法发送消息")
return
}
val textMessage = OpenIMClient.getInstance().messageManager.createTextMessage(message)
val (recvID, groupID) = getReceiverInfo()
OpenIMClient.getInstance().messageManager.sendMessage(
object : OnMsgSendCallback {
override fun onProgress(progress: Long) {
// 发送进度
}
override fun onError(code: Int, error: String?) {
Log.e(getLogTag(), "发送消息失败: $error")
}
override fun onSuccess(data: Message?) {
Log.d(getLogTag(), "发送消息成功")
onMessageSentSuccess(message, data)
data?.let { sentMessage ->
val chatItem = ChatItem.convertToChatItem(sentMessage, context, avatar = myProfile?.avatar)
chatItem?.let {
chatData = listOf(it) + chatData
goToNew = true
}
}
}
},
textMessage,
recvID,
groupID,
OfflinePushInfo()
)
}
/**
* 发送图片消息
*/
fun sendImageMessage(imageUri: Uri, context: Context) {
val tempFile = createTempFile(context, imageUri)
val imagePath = tempFile?.path
if (imagePath != null) {
val imageMessage = OpenIMClient.getInstance().messageManager.createImageMessageFromFullPath(imagePath)
val (recvID, groupID) = getReceiverInfo()
OpenIMClient.getInstance().messageManager.sendMessage(
object : OnMsgSendCallback {
override fun onProgress(progress: Long) {
Log.d(getLogTag(), "发送图片消息进度: $progress")
}
override fun onError(code: Int, error: String?) {
Log.e(getLogTag(), "发送图片消息失败: $error")
}
override fun onSuccess(data: Message?) {
Log.d(getLogTag(), "发送图片消息成功")
data?.let { sentMessage ->
val chatItem = ChatItem.convertToChatItem(sentMessage, context, avatar = myProfile?.avatar)
chatItem?.let {
chatData = listOf(it) + chatData
goToNew = true
}
}
}
},
imageMessage,
recvID,
groupID,
OfflinePushInfo()
)
}
}
/**
* 创建临时文件
*/
fun createTempFile(context: Context, uri: Uri): File? {
return try {
val projection = arrayOf(MediaStore.Images.Media.DATA)
val cursor = context.contentResolver.query(uri, projection, null, null, null)
cursor?.use {
if (it.moveToFirst()) {
val columnIndex = it.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
val filePath = it.getString(columnIndex)
val inputStream: InputStream? = context.contentResolver.openInputStream(uri)
val mimeType = context.contentResolver.getType(uri)
val extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType)
val tempFile =
File.createTempFile("temp_image", ".$extension", context.cacheDir)
val outputStream = FileOutputStream(tempFile)
inputStream?.use { input ->
outputStream.use { output ->
input.copyTo(output)
}
}
tempFile
} else {
null
}
}
} catch (e: Exception) {
e.printStackTrace()
null
}
}
/**
* 获取历史消息
*/
fun fetchHistoryMessage(context: Context) {
loadHistoryMessages(context, isLoadMore = false)
}
/**
* 加载历史消息的通用方法
* @param context 上下文
* @param isLoadMore 是否是加载更多true追加到现有数据false替换现有数据
*/
private fun loadHistoryMessages(context: Context, isLoadMore: Boolean) {
if (conversationID.isEmpty()) {
Log.w(getLogTag(), "conversationID为空无法${if (isLoadMore) "加载更多" else "获取"}历史消息")
return
}
if (isLoadMore) {
isLoading = true
}
viewModelScope.launch {
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))
}.reversed() // 反转顺序,使最新消息在前面
// 根据是否是加载更多来决定数据处理方式
chatData = if (isLoadMore) {
chatData + newChatItems // 追加到现有数据
} else {
newChatItems // 替换现有数据
}
if (messages.size < fetchHistorySize) {
hasMore = false
}
lastMessage = messages.firstOrNull()
if (isLoadMore) {
isLoading = false
}
Log.d(getLogTag(), "${if (isLoadMore) "加载更多" else "获取"}历史消息成功")
}
override fun onError(code: Int, error: String?) {
Log.e(getLogTag(), "${if (isLoadMore) "加载更多" else "获取"}历史消息失败: $error")
if (isLoadMore) {
isLoading = false
}
}
},
conversationID,
if (isLoadMore) lastMessage else null, // 首次加载不传lastMessage
fetchHistorySize,
ViewType.History
)
}
}
/**
* 获取显示的聊天列表
*/
fun getDisplayChatList(): List<ChatItem> {
val list = chatData
// 更新每条消息的时间戳显示状态
for (item in list) {
item.showTimestamp = showTimestampMap.getOrDefault(item.msgId, false)
}
return list
}
}

View File

@@ -1,57 +1,27 @@
package com.aiosman.ravenow.ui.chat
import android.content.Context
import android.net.Uri
import android.provider.MediaStore
import android.util.Log
import android.webkit.MimeTypeMap
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavHostController
import com.aiosman.ravenow.ChatState
import com.aiosman.ravenow.data.AccountService
import com.aiosman.ravenow.data.AccountServiceImpl
import com.aiosman.ravenow.data.UserService
import com.aiosman.ravenow.data.UserServiceImpl
import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.data.api.SendChatAiRequestBody
import com.aiosman.ravenow.data.api.SingleChatRequestBody
import com.aiosman.ravenow.entity.AccountProfileEntity
import com.aiosman.ravenow.entity.ChatItem
import com.aiosman.ravenow.entity.ChatNotification
import com.aiosman.ravenow.ui.navigateToChatAi
// OpenIM SDK 导入
import io.openim.android.sdk.OpenIMClient
import io.openim.android.sdk.enums.ViewType
import io.openim.android.sdk.listener.OnAdvanceMsgListener
import io.openim.android.sdk.listener.OnBase
import io.openim.android.sdk.listener.OnMsgSendCallback
import io.openim.android.sdk.enums.ConversationType
import io.openim.android.sdk.models.*
import kotlinx.coroutines.launch
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
class ChatAiViewModel(
val userId: String,
) : ViewModel() {
var chatData by mutableStateOf<List<ChatItem>>(emptyList())
) : BaseChatViewModel() {
var userProfile by mutableStateOf<AccountProfileEntity?>(null)
var myProfile by mutableStateOf<AccountProfileEntity?>(null)
val userService: UserService = UserServiceImpl()
val accountService: AccountService = AccountServiceImpl()
var textMessageListener: OnAdvanceMsgListener? = null
var hasMore by mutableStateOf(true)
var isLoading by mutableStateOf(false)
var lastMessage: Message? = null
val showTimestampMap = mutableMapOf<String, Boolean>() // Add this map
var chatNotification by mutableStateOf<ChatNotification?>(null)
var goToNew by mutableStateOf(false)
fun init(context: Context) {
override fun init(context: Context) {
// 获取用户信息
viewModelScope.launch {
val resp = userService.getUserProfile(userId)
@@ -59,150 +29,55 @@ class ChatAiViewModel(
myProfile = accountService.getMyAccountProfile()
RegistListener(context)
fetchHistoryMessage(context)
// 获取会话信息,然后加载历史消息
getOneConversation {
fetchHistoryMessage(context)
}
// 获取通知信息
val notiStrategy = ChatState.getStrategyByTargetTrtcId(resp.trtcUserId)
chatNotification = notiStrategy
}
}
override fun getConversationParams(): Triple<String, Int, Boolean> {
return Triple(userProfile?.trtcUserId ?: userId, ConversationType.SINGLE_CHAT, true)
}
fun RegistListener(context: Context) {
// 检查 OpenIM 是否已登录
if (!com.aiosman.ravenow.AppState.enableChat) {
android.util.Log.w("ChatAiViewModel", "OpenIM 未登录,跳过注册消息监听器")
return
}
override fun getLogTag(): String {
return "ChatAiViewModel"
}
override fun handleNewMessage(message: Message, context: Context): Boolean {
// 只处理当前聊天对象的消息
val currentChatUserId = userProfile?.trtcUserId
val currentUserId = com.aiosman.ravenow.AppState.profile?.trtcUserId
textMessageListener = object : OnAdvanceMsgListener {
override fun onRecvNewMessage(msg: Message?) {
msg?.let { message ->
// 只处理当前聊天对象的消息
val currentChatUserId = userProfile?.trtcUserId
val currentUserId = com.aiosman.ravenow.AppState.profile?.trtcUserId
if (currentChatUserId != null && currentUserId != null) {
// 检查消息是否来自当前聊天对象,且不是自己发送的消息
if ((message.sendID == currentChatUserId || message.sendID == currentUserId) &&
message.sendID != currentUserId) {
val chatItem = ChatItem.convertToChatItem(message, context, avatar = userProfile?.avatar)
chatItem?.let {
chatData = listOf(it) + chatData
goToNew = true
android.util.Log.i("ChatAiViewModel", "收到来自 ${message.sendID} 的消息更新AI聊天列表")
}
}
}
}
}
if (currentChatUserId != null && currentUserId != null) {
// 检查消息是否来自当前聊天对象,且不是自己发送的消息
return (message.sendID == currentChatUserId || message.sendID == currentUserId) &&
message.sendID != currentUserId
}
OpenIMClient.getInstance().messageManager.setAdvancedMsgListener(textMessageListener)
return false
}
fun UnRegistListener() {
// OpenIM SDK 不需要显式移除监听器,只需要设置为 null
textMessageListener = null
override fun getReceiverInfo(): Pair<String?, String?> {
return Pair(userProfile?.trtcUserId, null) // (recvID, groupID)
}
fun clearUnRead() {
val conversationID = "single_${userProfile?.trtcUserId}"
OpenIMClient.getInstance().messageManager.markConversationMessageAsRead(
conversationID,
object : OnBase<String> {
override fun onSuccess(data: String?) {
Log.i("openim", "clear unread success")
}
override fun onError(code: Int, error: String?) {
Log.i("openim", "clear unread failure, code:$code, error:$error")
}
}
)
}
fun onLoadMore(context: Context) {
if (!hasMore || isLoading) {
return
}
isLoading = true
viewModelScope.launch {
val conversationID = "single_${userProfile?.trtcUserId!!}"
// val options = OpenIMClient.getInstance().messageManager.getAdvancedHistoryMessageList() .apply {
// conversationID = conversationID
// count = 20
// lastMinSeq = lastMessage?.seq ?: 0
// }
OpenIMClient.getInstance().messageManager.getAdvancedHistoryMessageList(
object : OnBase<AdvancedMessage> {
override fun onSuccess(data: AdvancedMessage?) {
val messages = data?.messageList ?: emptyList()
chatData = chatData + messages.map {
var avatar = userProfile?.avatar
if (it.sendID == com.aiosman.ravenow.AppState.profile?.trtcUserId) {
avatar = myProfile?.avatar
}
ChatItem.convertToChatItem(it, context, avatar)
}.filterNotNull()
if (messages.size < 20) {
hasMore = false
}
lastMessage = messages.lastOrNull()
isLoading = false
Log.d("ChatAiViewModel", "fetch history message success")
}
override fun onError(code: Int, error: String?) {
Log.e("ChatAiViewModel", "fetch history message error: $error")
isLoading = false
}
},
conversationID,
lastMessage,
20,
ViewType.History
)
override fun getMessageAvatar(message: Message): String? {
return if (message.sendID == com.aiosman.ravenow.AppState.profile?.trtcUserId) {
myProfile?.avatar
} else {
userProfile?.avatar
}
}
fun sendMessage(message: String, context: Context) {
// 检查 OpenIM 是否已登录
if (!com.aiosman.ravenow.AppState.enableChat) {
android.util.Log.w("ChatAiViewModel", "OpenIM 未登录,无法发送消息")
return
}
val textMessage = OpenIMClient.getInstance().messageManager.createTextMessage(message)
OpenIMClient.getInstance().messageManager.sendMessage(
object : OnMsgSendCallback {
override fun onProgress(progress: Long) {
// 发送进度
}
override fun onError(code: Int, error: String?) {
Log.e("ChatAiViewModel", "send message error: $error")
}
override fun onSuccess(data: Message?) {
Log.d("ChatAiViewModel", "send message success")
sendChatAiMessage(myProfile?.trtcUserId!!, userProfile?.trtcUserId!!, message)
createGroup2ChatAi(userProfile?.trtcUserId!!, "ai_group")
data?.let { sentMessage ->
val chatItem = ChatItem.convertToChatItem(sentMessage, context, avatar = myProfile?.avatar)
chatItem?.let {
chatData = listOf(it) + chatData
goToNew = true
}
}
}
},
textMessage,
userProfile?.trtcUserId!!, // recvID
null, // groupID
null // offlinePushInfo
)
override fun onMessageSentSuccess(message: String, sentMessage: Message?) {
// AI聊天特有的处理逻辑
sendChatAiMessage(myProfile?.trtcUserId!!, userProfile?.trtcUserId!!, message)
createGroup2ChatAi(userProfile?.trtcUserId!!, "ai_group")
}
fun createGroup2ChatAi(
trtcUserId: String,
@@ -212,104 +87,6 @@ class ChatAiViewModel(
Log.d("ChatAiViewModel", "OpenIM 不支持会话分组功能")
}
fun sendImageMessage(imageUri: Uri, context: Context) {
val tempFile = createTempFile(context, imageUri)
val imagePath = tempFile?.path
if (imagePath != null) {
val imageMessage = OpenIMClient.getInstance().messageManager.createImageMessageFromFullPath(imagePath)
OpenIMClient.getInstance().messageManager.sendMessage(
object : OnMsgSendCallback {
override fun onProgress(progress: Long) {
Log.d("ChatAiViewModel", "send image message progress: $progress")
}
override fun onError(code: Int, error: String?) {
Log.e("ChatAiViewModel", "send image message error: $error")
}
override fun onSuccess(data: Message?) {
Log.d("ChatAiViewModel", "send image message success")
data?.let { sentMessage ->
val chatItem = ChatItem.convertToChatItem(sentMessage, context, avatar = myProfile?.avatar)
chatItem?.let {
chatData = listOf(it) + chatData
goToNew = true
}
}
}
},
imageMessage,
userProfile?.trtcUserId!!, // recvID
null, // groupID
null // offlinePushInfo
)
}
}
fun createTempFile(context: Context, uri: Uri): File? {
return try {
val projection = arrayOf(MediaStore.Images.Media.DATA)
val cursor = context.contentResolver.query(uri, projection, null, null, null)
cursor?.use {
if (it.moveToFirst()) {
val columnIndex = it.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
val filePath = it.getString(columnIndex)
val inputStream: InputStream? = context.contentResolver.openInputStream(uri)
val mimeType = context.contentResolver.getType(uri)
val extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType)
val tempFile =
File.createTempFile("temp_image", ".$extension", context.cacheDir)
val outputStream = FileOutputStream(tempFile)
inputStream?.use { input ->
outputStream.use { output ->
input.copyTo(output)
}
}
tempFile
} else {
null
}
}
} catch (e: Exception) {
e.printStackTrace()
null
}
}
fun fetchHistoryMessage(context: Context) {
val conversationID = "single_${userProfile?.trtcUserId!!}"
OpenIMClient.getInstance().messageManager.getAdvancedHistoryMessageList(
object : OnBase<AdvancedMessage> {
override fun onSuccess(data: AdvancedMessage?) {
val messages = data?.messageList ?: emptyList()
chatData = messages.mapNotNull {
var avatar = userProfile?.avatar
if (it.sendID == com.aiosman.ravenow.AppState.profile?.trtcUserId) {
avatar = myProfile?.avatar
}
ChatItem.convertToChatItem(it, context, avatar)
}
if (messages.size < 20) {
hasMore = false
}
lastMessage = messages.lastOrNull()
Log.d("ChatAiViewModel", "fetch history message success")
}
override fun onError(code: Int, error: String?) {
Log.e("ChatAiViewModel", "fetch history message error: $error")
}
},
conversationID,
lastMessage,
20,
ViewType.History
)
}
fun sendChatAiMessage(
fromTrtcUserId: String,
toTrtcUserId: String,
@@ -318,16 +95,6 @@ class ChatAiViewModel(
viewModelScope.launch {
val response = ApiClient.api.sendChatAiMessage(SendChatAiRequestBody(fromTrtcUserId = fromTrtcUserId,toTrtcUserId = toTrtcUserId,message = message))
}
}
fun getDisplayChatList(): List<ChatItem> {
val list = chatData
// Update showTimestamp for each message
for (item in list) {
item.showTimestamp = showTimestampMap.getOrDefault(item.msgId, false)
}
return list
}
suspend fun updateNotificationStrategy(strategy: String) {

View File

@@ -1,53 +1,24 @@
package com.aiosman.ravenow.ui.chat
import android.content.Context
import android.net.Uri
import android.provider.MediaStore
import android.util.Log
import android.webkit.MimeTypeMap
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aiosman.ravenow.ChatState
import com.aiosman.ravenow.data.AccountService
import com.aiosman.ravenow.data.AccountServiceImpl
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.entity.ChatNotification
// OpenIM SDK 导入
import io.openim.android.sdk.OpenIMClient
import io.openim.android.sdk.enums.MessageType
import io.openim.android.sdk.enums.ViewType
import io.openim.android.sdk.listener.OnAdvanceMsgListener
import io.openim.android.sdk.listener.OnBase
import io.openim.android.sdk.listener.OnMsgSendCallback
import io.openim.android.sdk.models.*
import io.openim.android.sdk.enums.ConversationType
import io.openim.android.sdk.models.Message
import kotlinx.coroutines.launch
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
class ChatViewModel(
val userId: String,
) : ViewModel() {
var chatData by mutableStateOf<List<ChatItem>>(emptyList())
) : BaseChatViewModel() {
var userProfile by mutableStateOf<AccountProfileEntity?>(null)
var myProfile by mutableStateOf<AccountProfileEntity?>(null)
val userService: UserService = UserServiceImpl()
val accountService: AccountService = AccountServiceImpl()
var textMessageListener: OnAdvanceMsgListener? = null
var hasMore by mutableStateOf(true)
var isLoading by mutableStateOf(false)
var lastMessage: Message? = null
val showTimestampMap = mutableMapOf<String, Boolean>() // Add this map
var chatNotification by mutableStateOf<ChatNotification?>(null)
var goToNew by mutableStateOf(false)
fun init(context: Context) {
override fun init(context: Context) {
// 获取用户信息
viewModelScope.launch {
val resp = userService.getUserProfile(userId)
@@ -55,251 +26,49 @@ class ChatViewModel(
myProfile = accountService.getMyAccountProfile()
RegistListener(context)
fetchHistoryMessage(context)
// 获取会话信息,然后加载历史消息
getOneConversation {
fetchHistoryMessage(context)
}
// 获取通知信息
val notiStrategy = ChatState.getStrategyByTargetTrtcId(resp.trtcUserId)
chatNotification = notiStrategy
}
}
override fun getConversationParams(): Triple<String, Int, Boolean> {
return Triple(userProfile?.trtcUserId ?: userId, ConversationType.SINGLE_CHAT, true)
}
fun RegistListener(context: Context) {
// 检查 OpenIM 是否已登录
if (!com.aiosman.ravenow.AppState.enableChat) {
android.util.Log.w("ChatViewModel", "OpenIM 未登录,跳过注册消息监听器")
return
}
override fun getLogTag(): String {
return "ChatViewModel"
}
override fun handleNewMessage(message: Message, context: Context): Boolean {
// 只处理当前聊天对象的消息
val currentChatUserId = userProfile?.trtcUserId
val currentUserId = com.aiosman.ravenow.AppState.profile?.trtcUserId
textMessageListener = object : OnAdvanceMsgListener {
override fun onRecvNewMessage(msg: Message?) {
msg?.let { message ->
// 只处理当前聊天对象的消息
val currentChatUserId = userProfile?.trtcUserId
val currentUserId = com.aiosman.ravenow.AppState.profile?.trtcUserId
if (currentChatUserId != null && currentUserId != null) {
// 检查消息是否来自当前聊天对象,且不是自己发送的消息
if ((message.sendID == currentChatUserId || message.sendID == currentUserId) &&
message.sendID != currentUserId) {
val chatItem = ChatItem.convertToChatItem(message, context, avatar = userProfile?.avatar)
chatItem?.let {
chatData = listOf(it) + chatData
goToNew = true
android.util.Log.i("ChatViewModel", "收到来自 ${message.sendID} 的消息,更新聊天列表")
}
}
}
}
}
if (currentChatUserId != null && currentUserId != null) {
// 检查消息是否来自当前聊天对象,且不是自己发送的消息
return (message.sendID == currentChatUserId || message.sendID == currentUserId) &&
message.sendID != currentUserId
}
OpenIMClient.getInstance().messageManager.setAdvancedMsgListener(textMessageListener)
return false
}
fun UnRegistListener() {
// OpenIM SDK 不需要显式移除监听器,只需要设置为 null
textMessageListener = null
override fun getReceiverInfo(): Pair<String?, String?> {
return Pair(userProfile?.trtcUserId, null) // (recvID, groupID)
}
fun clearUnRead() {
val conversationID = "single_${userProfile?.trtcUserId}"
OpenIMClient.getInstance().messageManager.markConversationMessageAsRead(
conversationID,
object : OnBase<String> {
override fun onSuccess(data: String?) {
Log.i("openim", "clear unread success")
}
override fun onError(code: Int, error: String?) {
Log.i("openim", "clear unread failure, code:$code, error:$error")
}
}
)
}
fun onLoadMore(context: Context) {
if (!hasMore || isLoading) {
return
override fun getMessageAvatar(message: Message): String? {
return if (message.sendID == com.aiosman.ravenow.AppState.profile?.trtcUserId) {
myProfile?.avatar
} else {
userProfile?.avatar
}
isLoading = true
viewModelScope.launch {
val conversationID = "single_${userProfile?.trtcUserId!!}"
OpenIMClient.getInstance().messageManager.getAdvancedHistoryMessageList(
object : OnBase<AdvancedMessage> {
override fun onSuccess(data: AdvancedMessage?) {
val messages = data?.messageList ?: emptyList()
chatData = chatData + messages.map {
var avatar = userProfile?.avatar
if (it.sendID == com.aiosman.ravenow.AppState.profile?.trtcUserId) {
avatar = myProfile?.avatar
}
ChatItem.convertToChatItem(it, context, avatar)
}.filterNotNull()
if (messages.size < 20) {
hasMore = false
}
lastMessage = messages.lastOrNull()
isLoading = false
Log.d("ChatViewModel", "fetch history message success")
}
override fun onError(code: Int, error: String?) {
Log.e("ChatViewModel", "fetch history message error: $error")
isLoading = false
}
},
conversationID,
lastMessage,
20,
ViewType.History
)
}
}
fun sendMessage(message: String, context: Context) {
// 检查 OpenIM 是否已登录
if (!com.aiosman.ravenow.AppState.enableChat) {
android.util.Log.w("ChatViewModel", "OpenIM 未登录,无法发送消息")
return
}
val textMessage = OpenIMClient.getInstance().messageManager.createTextMessage(message)
OpenIMClient.getInstance().messageManager.sendMessage(
object : OnMsgSendCallback {
override fun onProgress(progress: Long) {
// 发送进度
}
override fun onError(code: Int, error: String?) {
Log.e("ChatViewModel", "send message error: $error")
}
override fun onSuccess(data: Message?) {
Log.d("ChatViewModel", "send message success")
data?.let { sentMessage ->
val chatItem = ChatItem.convertToChatItem(sentMessage, context, avatar = myProfile?.avatar)
chatItem?.let {
chatData = listOf(it) + chatData
goToNew = true
}
}
}
},
textMessage,
userProfile?.trtcUserId!!, // recvID
null, // groupID
null // offlinePushInfo
)
}
fun sendImageMessage(imageUri: Uri, context: Context) {
val tempFile = createTempFile(context, imageUri)
val imagePath = tempFile?.path
if (imagePath != null) {
val imageMessage = OpenIMClient.getInstance().messageManager.createImageMessageFromFullPath(imagePath)
OpenIMClient.getInstance().messageManager.sendMessage(
object : OnMsgSendCallback {
override fun onProgress(progress: Long) {
Log.d("ChatViewModel", "send image message progress: $progress")
}
override fun onError(code: Int, error: String?) {
Log.e("ChatViewModel", "send image message error: $error")
}
override fun onSuccess(data: Message?) {
Log.d("ChatViewModel", "send image message success")
data?.let { sentMessage ->
val chatItem = ChatItem.convertToChatItem(sentMessage, context, avatar = myProfile?.avatar)
chatItem?.let {
chatData = listOf(it) + chatData
goToNew = true
}
}
}
},
imageMessage,
userProfile?.trtcUserId!!, // recvID
null, // groupID
null // offlinePushInfo
)
}
}
fun createTempFile(context: Context, uri: Uri): File? {
return try {
val projection = arrayOf(MediaStore.Images.Media.DATA)
val cursor = context.contentResolver.query(uri, projection, null, null, null)
cursor?.use {
if (it.moveToFirst()) {
val columnIndex = it.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
val filePath = it.getString(columnIndex)
val inputStream: InputStream? = context.contentResolver.openInputStream(uri)
val mimeType = context.contentResolver.getType(uri)
val extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType)
val tempFile =
File.createTempFile("temp_image", ".$extension", context.cacheDir)
val outputStream = FileOutputStream(tempFile)
inputStream?.use { input ->
outputStream.use { output ->
input.copyTo(output)
}
}
tempFile
} else {
null
}
}
} catch (e: Exception) {
e.printStackTrace()
null
}
}
fun fetchHistoryMessage(context: Context) {
val conversationID = "single_${userProfile?.trtcUserId!!}"
OpenIMClient.getInstance().messageManager.getAdvancedHistoryMessageList(
object : OnBase<AdvancedMessage> {
override fun onSuccess(data: AdvancedMessage?) {
val messages = data?.messageList ?: emptyList()
chatData = messages.mapNotNull {
var avatar = userProfile?.avatar
if (it.sendID == com.aiosman.ravenow.AppState.profile?.trtcUserId) {
avatar = myProfile?.avatar
}
ChatItem.convertToChatItem(it, context, avatar)
}
if (messages.size < 20) {
hasMore = false
}
lastMessage = messages.lastOrNull()
Log.d("ChatViewModel", "fetch history message success")
}
override fun onError(code: Int, error: String?) {
Log.e("ChatViewModel", "fetch history message error: $error")
}
},
conversationID,
null,
20,
ViewType.History
)
}
fun getDisplayChatList(): List<ChatItem> {
val list = chatData
// Update showTimestamp for each message
for (item in list) {
item.showTimestamp = showTimestampMap.getOrDefault(item.msgId, false)
}
return list
}
suspend fun updateNotificationStrategy(strategy: String) {

View File

@@ -1,55 +1,23 @@
package com.aiosman.ravenow.ui.chat
import android.content.Context
import android.net.Uri
import android.provider.MediaStore
import android.util.Log
import android.webkit.MimeTypeMap
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aiosman.ravenow.ChatState
import com.aiosman.ravenow.data.AccountService
import com.aiosman.ravenow.data.AccountServiceImpl
import com.aiosman.ravenow.data.UserService
import com.aiosman.ravenow.data.UserServiceImpl
import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.data.api.GroupChatRequestBody
import com.aiosman.ravenow.data.api.SendChatAiRequestBody
import com.aiosman.ravenow.data.api.SingleChatRequestBody
import com.aiosman.ravenow.entity.AccountProfileEntity
import com.aiosman.ravenow.entity.ChatItem
import com.aiosman.ravenow.entity.ChatNotification
// OpenIM SDK 导入
import io.openim.android.sdk.OpenIMClient
import io.openim.android.sdk.enums.ViewType
import io.openim.android.sdk.listener.OnAdvanceMsgListener
import io.openim.android.sdk.listener.OnBase
import io.openim.android.sdk.listener.OnMsgSendCallback
import io.openim.android.sdk.enums.ConversationType
import io.openim.android.sdk.models.*
import kotlinx.coroutines.launch
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
class GroupChatViewModel(
val groupId: String,
val name: String,
val avatar: String,
) : ViewModel() {
var chatData by mutableStateOf<List<ChatItem>>(emptyList())
) : BaseChatViewModel() {
var groupInfo by mutableStateOf<GroupInfo?>(null)
var myProfile by mutableStateOf<AccountProfileEntity?>(null)
val userService: UserService = UserServiceImpl()
val accountService: AccountService = AccountServiceImpl()
var textMessageListener: OnAdvanceMsgListener? = null
var hasMore by mutableStateOf(true)
var isLoading by mutableStateOf(false)
var lastMessage: Message? = null
val showTimestampMap = mutableMapOf<String, Boolean>()
var goToNew by mutableStateOf(false)
// 群聊特有属性
var memberCount by mutableStateOf(0)
@@ -64,13 +32,17 @@ class GroupChatViewModel(
val ownerId: String
)
fun init(context: Context) {
override fun init(context: Context) {
viewModelScope.launch {
try {
getGroupInfo()
myProfile = accountService.getMyAccountProfile()
RegistListener(context)
fetchHistoryMessage(context)
// 获取会话信息,然后加载历史消息
getOneConversation {
fetchHistoryMessage(context)
}
} catch (e: Exception) {
Log.e("GroupChatViewModel", "初始化失败: ${e.message}")
}
@@ -91,120 +63,36 @@ class GroupChatViewModel(
memberCount = groupInfo?.memberCount ?: 0
}
fun RegistListener(context: Context) {
// 检查 OpenIM 是否已登录
if (!com.aiosman.ravenow.AppState.enableChat) {
android.util.Log.w("GroupChatViewModel", "OpenIM 未登录,跳过注册消息监听器")
return
}
textMessageListener = object : OnAdvanceMsgListener {
override fun onRecvNewMessage(msg: Message?) {
msg?.let {
// 检查是否是当前群聊的消息
if (it.groupID == groupId) {
val chatItem = ChatItem.convertToChatItem(msg, context, avatar = null)
chatItem?.let {
chatData = listOf(it) + chatData
goToNew = true
}
}
}
}
}
OpenIMClient.getInstance().messageManager.setAdvancedMsgListener(textMessageListener)
override fun getConversationParams(): Triple<String, Int, Boolean> {
// 根据群组类型决定ConversationType这里假设是普通群聊
return Triple(groupId, ConversationType.GROUP_CHAT, false)
}
fun UnRegistListener() {
// OpenIM SDK 不需要显式移除监听器,只需要设置为 null
textMessageListener = null
override fun getLogTag(): String {
return "GroupChatViewModel"
}
fun clearUnRead() {
val conversationID = "group_${groupId}"
OpenIMClient.getInstance().messageManager.markConversationMessageAsRead(
conversationID,
object : OnBase<String> {
override fun onSuccess(data: String?) {
Log.i("openim", "清除群聊未读消息成功")
}
override fun onError(code: Int, error: String?) {
Log.i("openim", "清除群聊未读消息失败, code:$code, error:$error")
}
}
)
override fun handleNewMessage(message: Message, context: Context): Boolean {
// 检查是否是当前群聊的消息
return message.groupID == groupId
}
fun onLoadMore(context: Context) {
if (!hasMore || isLoading) return
isLoading = true
viewModelScope.launch {
val conversationID = "group_${groupId}"
OpenIMClient.getInstance().messageManager.getAdvancedHistoryMessageList(
object : OnBase<AdvancedMessage> {
override fun onSuccess(data: AdvancedMessage?) {
val messages = data?.messageList ?: emptyList()
chatData = chatData + messages.map {
ChatItem.convertToChatItem(it, context, avatar = null)
}.filterNotNull()
if (messages.size < 20) {
hasMore = false
}
lastMessage = messages.lastOrNull()
isLoading = false
}
override fun getReceiverInfo(): Pair<String?, String?> {
return Pair(null, groupId) // (recvID, groupID)
}
override fun onError(code: Int, error: String?) {
Log.e("GroupChatViewModel", "获取群聊历史消息失败: $error")
isLoading = false
}
},
conversationID,
lastMessage,
20,
ViewType.History
)
override fun getMessageAvatar(message: Message): String? {
// 群聊中如果是自己发送的消息显示自己的头像否则为null由ChatItem处理
return if (message.sendID == com.aiosman.ravenow.AppState.profile?.trtcUserId) {
myProfile?.avatar
} else {
null
}
}
fun sendMessage(message: String, context: Context) {
// 检查 OpenIM 是否已登录
if (!com.aiosman.ravenow.AppState.enableChat) {
android.util.Log.w("GroupChatViewModel", "OpenIM 未登录,无法发送消息")
return
}
val textMessage = OpenIMClient.getInstance().messageManager.createTextMessage(message)
OpenIMClient.getInstance().messageManager.sendMessage(
object : OnMsgSendCallback {
override fun onProgress(progress: Long) {
// 发送进度
}
override fun onError(code: Int, error: String?) {
Log.e("GroupChatViewModel", "发送群聊消息失败: $error")
}
override fun onSuccess(data: Message?) {
sendChatAiMessage(message = message, trtcGroupId = groupId)
data?.let { sentMessage ->
val chatItem = ChatItem.convertToChatItem(sentMessage, context, avatar = myProfile?.avatar)
chatItem?.let {
chatData = listOf(it) + chatData
goToNew = true
}
}
}
},
textMessage,
null, // recvID (群聊为 null)
groupId, // groupID
null // offlinePushInfo
)
override fun onMessageSentSuccess(message: String, sentMessage: Message?) {
// 群聊特有的处理逻辑
sendChatAiMessage(message = message, trtcGroupId = groupId)
}
@@ -216,107 +104,6 @@ class GroupChatViewModel(
viewModelScope.launch {
val response = ApiClient.api.sendChatAiMessage(SendChatAiRequestBody(trtcGroupId = trtcGroupId,message = message))
}
}
fun sendImageMessage(imageUri: Uri, context: Context) {
val tempFile = createTempFile(context, imageUri)
val imagePath = tempFile?.path
if (imagePath != null) {
val imageMessage = OpenIMClient.getInstance().messageManager.createImageMessageFromFullPath(imagePath)
OpenIMClient.getInstance().messageManager.sendMessage(
object : OnMsgSendCallback {
override fun onProgress(progress: Long) {
Log.d("GroupChatViewModel", "发送群聊图片消息进度: $progress")
}
override fun onError(code: Int, error: String?) {
Log.e("GroupChatViewModel", "发送群聊图片消息失败: $error")
}
override fun onSuccess(data: Message?) {
data?.let { sentMessage ->
val chatItem = ChatItem.convertToChatItem(sentMessage, context, avatar = myProfile?.avatar)
chatItem?.let {
chatData = listOf(it) + chatData
goToNew = true
}
}
}
},
imageMessage,
null, // recvID (群聊为 null)
groupId, // groupID
null // offlinePushInfo
)
}
}
fun createTempFile(context: Context, uri: Uri): File? {
return try {
val projection = arrayOf(MediaStore.Images.Media.DATA)
val cursor = context.contentResolver.query(uri, projection, null, null, null)
cursor?.use {
if (it.moveToFirst()) {
val columnIndex = it.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
val filePath = it.getString(columnIndex)
val inputStream: InputStream? = context.contentResolver.openInputStream(uri)
val mimeType = context.contentResolver.getType(uri)
val extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType)
val tempFile =
File.createTempFile("temp_image", ".$extension", context.cacheDir)
val outputStream = FileOutputStream(tempFile)
inputStream?.use { input ->
outputStream.use { output ->
input.copyTo(output)
}
}
tempFile
} else {
null
}
}
} catch (e: Exception) {
e.printStackTrace()
null
}
}
fun fetchHistoryMessage(context: Context) {
val conversationID = "group_${groupId}"
OpenIMClient.getInstance().messageManager.getAdvancedHistoryMessageList(
object : OnBase<AdvancedMessage> {
override fun onSuccess(data: AdvancedMessage?) {
val messages = data?.messageList ?: emptyList()
chatData = messages.mapNotNull {
ChatItem.convertToChatItem(it, context, avatar = null)
}
if (messages.size < 20) {
hasMore = false
}
lastMessage = messages.lastOrNull()
}
override fun onError(code: Int, error: String?) {
Log.e("GroupChatViewModel", "获取群聊历史消息失败: $error")
}
},
conversationID,
null,
20,
ViewType.History
)
}
fun getDisplayChatList(): List<ChatItem> {
val list = chatData
for (item in list) {
item.showTimestamp = showTimestampMap.getOrDefault(item.msgId, false)
}
return list
}
}

View File

@@ -160,7 +160,7 @@ fun EditCommentBottomModal(
painter = painterResource(id = R.mipmap.rider_pro_moment_post),
contentDescription = "Send",
modifier = Modifier
.size(25.dp)
.size(20.dp)
.align(Alignment.Top)
.noRippleClickable {
if (text.isNotEmpty()) {
@@ -168,7 +168,7 @@ fun EditCommentBottomModal(
text = ""
}
},
tint = if (isNotEmpty) AppColors.main else AppColors.nonActive
tint = if (isNotEmpty) Color.Unspecified else AppColors.nonActive
)
}
}

View File

@@ -37,12 +37,12 @@ fun TabItem(
) {
Text(
text = text,
fontSize = 14.sp,
fontSize = 15.sp,
color = if (isSelected) AppColors.tabSelectedText else AppColors.tabUnselectedText,
modifier = Modifier
.clip(RoundedCornerShape(8.dp))
.clip(RoundedCornerShape(12.dp))
.background(if (isSelected) AppColors.tabSelectedBackground else AppColors.tabUnselectedBackground)
.padding(horizontal = 11.dp, vertical = 4.dp)
.padding(horizontal = 16.dp, vertical = 8.dp)
)
}
}

View File

@@ -23,8 +23,6 @@ import com.aiosman.ravenow.exp.formatChatTime
import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.navigateToChat
import com.aiosman.ravenow.utils.TrtcHelper
// 临时兼容层 - TODO: 完成 OpenIM 迁移后删除
import com.aiosman.ravenow.compat.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch

View File

@@ -0,0 +1,150 @@
package com.aiosman.ravenow.ui.index
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.BottomSheetDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.SheetState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.res.stringResource
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CreateBottomSheet(
sheetState: SheetState,
onDismiss: () -> Unit,
onAiClick: () -> Unit,
onGroupChatClick: () -> Unit,
onMomentClick: () -> Unit
) {
val appColors = LocalAppTheme.current
ModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = sheetState,
windowInsets = BottomSheetDefaults.windowInsets,
containerColor = appColors.background,
dragHandle = null,
shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(top = 24.dp, bottom = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// 标题
Text(
text = stringResource(R.string.create_title),
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
color = appColors.text,
modifier = Modifier.padding(bottom = 32.dp)
)
// 三个创建选项
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
// 群聊选项
CreateOption(
icon = R.drawable.ic_create_group_chat,
label = stringResource(R.string.create_group_chat_option),
onClick = onGroupChatClick
)
// 动态选项
CreateOption(
icon = R.drawable.ic_create_monent,
label = stringResource(R.string.create_moment),
onClick = onMomentClick
)
// AI选项
CreateOption(
icon = R.drawable.ic_create_ai,
label = stringResource(R.string.create_ai),
onClick = onAiClick
)
}
Spacer(modifier = Modifier.height(40.dp))
// 关闭按钮
Box(
modifier = Modifier
.size(32.dp)
.clip(CircleShape)
.noRippleClickable { onDismiss() },
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(R.drawable.ic_create_close),
contentDescription = stringResource(R.string.create_close),
modifier = Modifier.size(32.dp)
)
}
Spacer(modifier = Modifier.height(16.dp))
}
}
}
@Composable
private fun CreateOption(
icon: Int,
label: String,
onClick: () -> Unit
) {
val appColors = LocalAppTheme.current
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.noRippleClickable { onClick() }
) {
// 直接使用图标,不要背景
Image(
painter = painterResource(icon),
contentDescription = label,
modifier = Modifier.size(72.dp)
)
Spacer(modifier = Modifier.height(12.dp))
// 文字标签
Text(
text = label,
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
color = appColors.text
)
}
}

View File

@@ -25,6 +25,7 @@ import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.NavigationBar
@@ -34,6 +35,7 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
@@ -78,9 +80,10 @@ import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.ui.post.NewPostViewModel
import com.aiosman.ravenow.utils.ResourceCleanupManager
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class)
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
@Composable
fun IndexScreen() {
val AppColors = LocalAppTheme.current
@@ -91,6 +94,7 @@ fun IndexScreen() {
val navController = LocalNavController.current
val item = listOf(
NavigationItem.Home,
//NavigationItem.Dynamic,
NavigationItem.Ai,
NavigationItem.Add,
NavigationItem.Notification,
@@ -100,8 +104,9 @@ fun IndexScreen() {
val pagerState = rememberPagerState(pageCount = { item.size })
val coroutineScope = rememberCoroutineScope()
val drawerState = rememberDrawerState(DrawerValue.Closed)
val bottomSheetState = rememberModalBottomSheetState()
val context = LocalContext.current
// 注意:不要在离开 Index 路由时全量清理资源,以免返回后列表被重置
LaunchedEffect(Unit) {
systemUiController.setNavigationBarColor(Color.Transparent)
@@ -291,11 +296,11 @@ fun IndexScreen() {
navController.navigate(NavigationRoute.Login.route)
return@noRippleClickable
}
NewPostViewModel.asNewPost()
navController.navigate(NavigationRoute.NewPost.route)
// 显示创建底部弹窗
model.showCreateBottomSheet = true
return@noRippleClickable
}
// 检查消息tab的游客模式
if (it.route === NavigationItem.Notification.route) {
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.VIEW_MESSAGES)) {
@@ -303,7 +308,7 @@ fun IndexScreen() {
return@noRippleClickable
}
}
// 检查我的tab的游客模式
if (it.route === NavigationItem.Profile.route) {
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.VIEW_PROFILE)) {
@@ -311,7 +316,7 @@ fun IndexScreen() {
return@noRippleClickable
}
}
coroutineScope.launch {
pagerState.scrollToPage(idx)
}
@@ -378,8 +383,8 @@ fun IndexScreen() {
userScrollEnabled = false
) { page ->
when (page) {
0 -> Home()
1 -> Agent()
0 -> Agent()
1 -> Home()
2 -> Add()
3 -> Notifications()
4 -> Profile()
@@ -388,6 +393,56 @@ fun IndexScreen() {
}
}
}
// 创建底部弹窗
if (model.showCreateBottomSheet) {
CreateBottomSheet(
sheetState = bottomSheetState,
onDismiss = {
// 使用协程来优雅地关闭弹窗
coroutineScope.launch {
bottomSheetState.hide()
model.showCreateBottomSheet = false
}
},
onAiClick = {
// 使用协程来优雅地关闭弹窗并导航
coroutineScope.launch {
bottomSheetState.hide() // 触发关闭动画
model.showCreateBottomSheet = false
// 检查游客模式,如果是游客则跳转登录
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.CREATE_AGENT)) {
navController.navigate(NavigationRoute.Login.route)
} else {
navController.navigate(NavigationRoute.AddAgent.route)
}
}
},
onGroupChatClick = {
// 使用协程来优雅地关闭弹窗并导航
coroutineScope.launch {
bottomSheetState.hide() // 触发关闭动画
model.showCreateBottomSheet = false
// 检查游客模式,如果是游客则跳转登录
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.JOIN_GROUP_CHAT)) {
navController.navigate(NavigationRoute.Login.route)
} else {
navController.navigate(NavigationRoute.CreateGroupChat.route)
}
}
},
onMomentClick = {
// 使用协程来优雅地关闭弹窗并导航
coroutineScope.launch {
bottomSheetState.hide() // 触发关闭动画
model.showCreateBottomSheet = false
// 导航到动态创建页面
NewPostViewModel.asNewPost()
navController.navigate(NavigationRoute.NewPost.route)
}
}
)
}
}
}
@@ -396,13 +451,13 @@ fun IndexScreen() {
fun Home() {
val systemUiController = rememberSystemUiController()
val context = LocalContext.current
LaunchedEffect(AppState.darkMode) {
systemUiController.setStatusBarColor(Color.Transparent, darkIcons = !AppState.darkMode)
}
// 注意:避免在离开 Home 时清理动态资源,防止返回详情后触发重新加载
Column(
modifier = Modifier
.fillMaxSize(),
@@ -418,18 +473,18 @@ fun Home() {
fun Street() {
val systemUiController = rememberSystemUiController()
val context = LocalContext.current
LaunchedEffect(AppState.darkMode) {
systemUiController.setStatusBarColor(Color.Transparent, darkIcons = !AppState.darkMode)
}
// 页面退出时清理搜索相关资源
DisposableEffect(Unit) {
onDispose {
ResourceCleanupManager.cleanupPageResources("search")
}
}
Column(
modifier = Modifier
.fillMaxSize(),
@@ -478,18 +533,18 @@ fun Video() {
fun Profile() {
val systemUiController = rememberSystemUiController()
val context = LocalContext.current
LaunchedEffect(AppState.darkMode) {
systemUiController.setStatusBarColor(Color.Transparent, !AppState.darkMode)
}
// 页面退出时清理个人资料相关资源
DisposableEffect(Unit) {
onDispose {
ResourceCleanupManager.cleanupPageResources("profile")
}
}
Column(
modifier = Modifier
.fillMaxSize(),
@@ -504,18 +559,18 @@ fun Profile() {
fun Notifications() {
val systemUiController = rememberSystemUiController()
val context = LocalContext.current
LaunchedEffect(AppState.darkMode) {
systemUiController.setStatusBarColor(Color.Transparent, !AppState.darkMode)
}
// 页面退出时清理消息相关资源
DisposableEffect(Unit) {
onDispose {
ResourceCleanupManager.cleanupPageResources("message")
}
}
Column(
modifier = Modifier
.fillMaxSize(),

View File

@@ -9,9 +9,12 @@ object IndexViewModel:ViewModel() {
var tabIndex by mutableStateOf(0)
var openDrawer by mutableStateOf(false)
var showCreateBottomSheet by mutableStateOf(false)
fun ResetModel(){
tabIndex = 0
showCreateBottomSheet = false
}
}

View File

@@ -22,10 +22,15 @@ sealed class NavigationItem(
)
data object Ai : NavigationItem("Ai",
icon = { R.drawable.rider_pro_nav_ai },
selectedIcon = { R.mipmap.rider_pro_nav_ai_hl },
label = { stringResource(R.string.main_ai) }
icon = { R.mipmap.bars_x_buttons_discover_bold},
selectedIcon = { R.mipmap.dynamic_hl },
label = { stringResource(R.string.index_dynamic) }
)
// data object Ai : NavigationItem("Ai",
// icon = { R.drawable.rider_pro_nav_ai },
// selectedIcon = { R.mipmap.rider_pro_nav_ai_hl },
// label = { stringResource(R.string.main_ai) }
// )
data object Add : NavigationItem("Add",
icon = { R.drawable.ic_nav_add },

View File

@@ -16,6 +16,7 @@ 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
@@ -43,6 +44,7 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
@@ -60,11 +62,18 @@ 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.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.lazy.grid.items
@OptIn( ExperimentalFoundationApi::class)
@Composable
@@ -80,7 +89,8 @@ fun Agent() {
var scope = rememberCoroutineScope()
val viewModel: AgentViewModel = viewModel()
val scrollState = rememberScrollState()
// 确保推荐Agent数据已加载
LaunchedEffect(Unit) {
viewModel.ensureDataLoaded()
@@ -100,8 +110,9 @@ fun Agent() {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState)
.padding(
top = statusBarPaddingValues.calculateTopPadding()+18.dp,
top = statusBarPaddingValues.calculateTopPadding(),
bottom = navigationBarPaddings,
start = 16.dp,
end = 16.dp
@@ -111,95 +122,202 @@ fun Agent() {
Row(
modifier = Modifier
.wrapContentHeight()
.height(44.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {
// 搜索框
Row(
androidx.compose.material3.Text(
text = "Rave AI",
fontSize = 20.sp,
fontWeight = FontWeight.W900,
color = AppColors.text,
modifier = Modifier
.height(36.dp)
.weight(1f)
.clip(shape = RoundedCornerShape(8.dp))
.background(AppColors.inputBackground)
.padding(horizontal = 8.dp, vertical = 0.dp)
.noRippleClickable {
// 搜索框点击事件
},
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(id = R.drawable.rider_pro_nav_search),
contentDescription = null,
tint = AppColors.inputHint
)
Box {
Text(
text = stringResource(R.string.search),
modifier = Modifier.padding(start = 8.dp),
color = AppColors.inputHint,
fontSize = 17.sp
)
}
}
Spacer(modifier = Modifier.width(16.dp))
// 新增
Icon(
.align(Alignment.CenterVertically)
)
Spacer(modifier = Modifier.weight(1f))
Image(
painter = painterResource(id = R.drawable.rider_pro_nav_search),
contentDescription = "search",
modifier = Modifier
.size(36.dp)
.size(24.dp)
.noRippleClickable {
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
// 检查游客模式,如果是游客则跳转登录
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.CREATE_AGENT)) {
navController.navigate(NavigationRoute.Login.route)
} else {
// 导航到添加智能体页面
navController.navigate(
NavigationRoute.AddAgent.route
)
}
}) {
lastClickTime = System.currentTimeMillis()
}
navController.navigate(NavigationRoute.Search.route)
},
painter = painterResource(id = R.drawable.rider_pro_new_post_add_pic),
contentDescription = null,
tint = AppColors.text
colorFilter = ColorFilter.tint(AppColors.text)
)
}
// 推荐Agent
Spacer(modifier = Modifier.height(15.dp))
// // 搜索框
// Row(
// modifier = Modifier
// .height(36.dp)
// .weight(1f)
// .clip(shape = RoundedCornerShape(8.dp))
// .background(AppColors.inputBackground)
// .padding(horizontal = 8.dp, vertical = 0.dp)
// .noRippleClickable {
// // 搜索框点击事件
// },
// verticalAlignment = Alignment.CenterVertically
// ) {
// Icon(
// painter = painterResource(id = R.drawable.rider_pro_nav_search),
// contentDescription = null,
// tint = AppColors.inputHint
// )
// Box {
// Text(
// text = stringResource(R.string.search),
// modifier = Modifier.padding(start = 8.dp),
// color = AppColors.inputHint,
// fontSize = 17.sp
// )
// }
// }
// Spacer(modifier = Modifier.width(16.dp))
// // 创建智能体
// Icon(
// modifier = Modifier
// .size(36.dp)
// .noRippleClickable {
// if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
// // 检查游客模式,如果是游客则跳转登录
// if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.CREATE_AGENT)) {
// navController.navigate(NavigationRoute.Login.route)
// } else {
// // 导航到添加智能体页面
// navController.navigate(
// NavigationRoute.AddAgent.route
// )
// }
// }) {
// lastClickTime = System.currentTimeMillis()
// }
// },
// painter = painterResource(id = R.drawable.rider_pro_new_post_add_pic),
// contentDescription = null,
// tint = AppColors.text
// )
Column(
modifier = Modifier
.fillMaxWidth()
.height(260.dp)
.padding(vertical = 8.dp)
) {
// 标题
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(bottom = 12.dp)
) {
Image(
painter = painterResource(R.mipmap.rider_pro_agent2),
contentDescription = "agent",
modifier = Modifier.size(28.dp),
// // 标题
// Row(
// verticalAlignment = Alignment.CenterVertically,
// modifier = Modifier.padding(bottom = 12.dp)
// ) {
// Image(
// painter = painterResource(R.mipmap.rider_pro_agent2),
// contentDescription = "agent",
// modifier = Modifier.size(28.dp),
//
// )
// Spacer(modifier = Modifier.width(4.dp))
// androidx.compose.material3.Text(
// text = stringResource(R.string.agent_recommend_agent),
// fontSize = 16.sp,
// fontWeight = androidx.compose.ui.text.font.FontWeight.W600,
// color = AppColors.text
// )
// }
var selectedTabIndex by remember { mutableStateOf(0) }
// 标签页
LazyRow(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(bottom = 16.dp),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {
item {
CustomTabItem(
text = stringResource(R.string.agent_recommend),
isSelected = selectedTabIndex == 0,
onClick = {
selectedTabIndex = 0
}
)
Spacer(modifier = Modifier.width(4.dp))
androidx.compose.material3.Text(
text = stringResource(R.string.agent_recommend_agent),
fontSize = 16.sp,
fontWeight = androidx.compose.ui.text.font.FontWeight.W600,
color = AppColors.text
)
}
item {
TabSpacer()
}
item {
CustomTabItem(
text = "scenes",
isSelected = selectedTabIndex == 1,
onClick = {
selectedTabIndex = 1
}
)
}
item {
TabSpacer()
}
item {
CustomTabItem(
text = "voices",
isSelected = selectedTabIndex == 2,
onClick = {
selectedTabIndex = 2
}
)
}
item {
TabSpacer()
}
item {
CustomTabItem(
text = "anime",
isSelected = selectedTabIndex == 3,
onClick = {
selectedTabIndex = 3
}
)
}
item {
TabSpacer()
}
item {
CustomTabItem(
text = "assist",
isSelected = selectedTabIndex == 4,
onClick = {
selectedTabIndex = 4
}
)
}
}
// Agent ViewPager
AgentViewPagerSection(agentItems = viewModel.agentItems.take(9),viewModel)
when (selectedTabIndex) {
0 -> {
AgentViewPagerSection(agentItems = viewModel.agentItems.take(15), viewModel)
}
else -> {
val shuffledAgents = viewModel.agentItems.shuffled().take(15)
AgentViewPagerSection(agentItems = shuffledAgents, viewModel)
}
}
}
Spacer(modifier = Modifier.height(0.dp))
Row(
modifier = Modifier
.fillMaxWidth()
@@ -221,99 +339,157 @@ fun Agent() {
fontWeight = androidx.compose.ui.text.font.FontWeight.W600,
color = AppColors.text
)
Spacer(modifier = Modifier.weight(1f))
// 只有非游客用户才显示"我的Agent"tab
if (!AppStore.isGuest) {
TabItem(
text = stringResource(R.string.agent_mine),
isSelected = pagerState.currentPage == 0,
onClick = {
if (DebounceUtils.simpleDebounceClick(lastClickTime, 300L) {
scope.launch {
pagerState.animateScrollToPage(0)
}
}) {
lastClickTime = System.currentTimeMillis()
}
}
)
TabSpacer()
}
TabItem(
text = stringResource(R.string.agent_hot),
isSelected = if (AppStore.isGuest) pagerState.currentPage == 0 else pagerState.currentPage == 1,
onClick = {
if (DebounceUtils.simpleDebounceClick(lastClickTime, 300L) {
scope.launch {
val targetPage = if (AppStore.isGuest) 0 else 1
pagerState.animateScrollToPage(targetPage)
}
}) {
lastClickTime = System.currentTimeMillis()
}
}
)
/*TabSpacer()
TabItem(
text = stringResource(R.string.agent_recommend),
isSelected = pagerState.currentPage == 2,
onClick = {
scope.launch {
pagerState.animateScrollToPage(2)
}
}
)
TabSpacer()
TabItem(
text = stringResource(R.string.agent_other),
isSelected = pagerState.currentPage == 3,
onClick = {
scope.launch {
pagerState.animateScrollToPage(3)
}
}
)*/
}
Spacer(modifier = Modifier.height(16.dp))
HorizontalPager(
state = pagerState,
Spacer(modifier = Modifier.height(50.dp))
Column(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
beyondBoundsPageCount = 1 // 预加载相邻页面,避免切换时重新加载
.weight(1f)
) {
if (AppStore.isGuest) {
// 游客模式下只显示热门Agent
when (it) {
0 -> {
HotAgent()
}
}
} else {
// 正常用户显示我的Agent和热门Agent
when (it) {
0 -> {
MineAgent()
}
1 -> {
HotAgent()
}
val agentItems = viewModel.agentItems.take(15)
LazyVerticalGrid(
columns = GridCells.Fixed(2),
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalArrangement = Arrangement.spacedBy(50.dp)
) {
items(agentItems) { agentItem ->
AgentCardSquare(
agentItem = agentItem,
viewModel = viewModel,
navController = LocalNavController.current
)
}
}
}
}
}
@SuppressLint("SuspiciousIndentation")
@Composable
fun AgentCardSquare(agentItem: AgentItem, viewModel: AgentViewModel, navController: NavHostController) {
val AppColors = LocalAppTheme.current
val cardHeight = 180.dp
val avatarSize = cardHeight / 3 // 头像大小为方块高度的三分之一
// 防抖状态
var lastClickTime by remember { mutableStateOf(0L) }
Box(
modifier = Modifier
.fillMaxWidth()
.height(cardHeight)
.background(Color(0xFFE0E0E0), RoundedCornerShape(12.dp)) // 灰色背景
.clickable {
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
viewModel.goToProfile(agentItem.openId, navController)
}) {
lastClickTime = System.currentTimeMillis()
}
},
contentAlignment = Alignment.TopCenter
) {
// 头像,位于方块上方居中,部分悬于方块外部
Box(
modifier = Modifier
.offset(y = -avatarSize / 2)
.size(avatarSize)
.background(Color.White, RoundedCornerShape(avatarSize / 2))
.clip(RoundedCornerShape(avatarSize / 2)),
contentAlignment = Alignment.Center
) {
if (agentItem.avatar.isNotEmpty()) {
CustomAsyncImage(
imageUrl = agentItem.avatar,
contentDescription = "Agent头像",
modifier = Modifier
.size(avatarSize)
.clip(RoundedCornerShape(avatarSize / 2)),
contentScale = androidx.compose.ui.layout.ContentScale.Crop
)
} else {
Image(
painter = painterResource(R.mipmap.rider_pro_agent),
contentDescription = "默认头像",
modifier = Modifier.size(avatarSize / 2),
colorFilter = ColorFilter.tint(AppColors.secondaryText)
)
}
}
// 内容区域(名称和描述)
Column(
modifier = Modifier
.fillMaxWidth()
.padding(top = avatarSize / 2 + 8.dp, start = 8.dp, end = 8.dp, bottom = 8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
androidx.compose.material3.Text(
text = agentItem.title,
fontSize = 16.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 = 14.sp,
color = AppColors.secondaryText,
maxLines = 2,
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
modifier = Modifier.weight(1f, fill = false)
)
Spacer(modifier = Modifier.height(8.dp))
// 聊天按钮,位于底部居中
Box(
modifier = Modifier
.width(80.dp)
.height(32.dp)
.background(
color = Color(0X147c7480),
shape = RoundedCornerShape(8.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
)
}
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun AgentViewPagerSection(agentItems: List<AgentItem>,viewModel: AgentViewModel) {
val AppColors = LocalAppTheme.current
// 每页显示3个agent
val itemsPerPage = 3
// 每页显示5个agent
val itemsPerPage = 5
val totalPages = (agentItems.size + itemsPerPage - 1) / itemsPerPage
if (totalPages > 0) {
@@ -323,7 +499,7 @@ fun AgentViewPagerSection(agentItems: List<AgentItem>,viewModel: AgentViewModel)
// Agent内容
Box(
modifier = Modifier
.height(180.dp)
.height(300.dp)
) {
HorizontalPager(
state = pagerState,
@@ -344,7 +520,8 @@ fun AgentViewPagerSection(agentItems: List<AgentItem>,viewModel: AgentViewModel)
viewModel = viewModel,
agentItems = agentItems.drop(page * itemsPerPage).take(itemsPerPage),
page = page,
modifier = Modifier.height(180.dp)
modifier = Modifier
.height(300.dp)
.graphicsLayer {
scaleX = scale
scaleY = scale
@@ -368,7 +545,9 @@ fun AgentViewPagerSection(agentItems: List<AgentItem>,viewModel: AgentViewModel)
.padding(horizontal = 4.dp)
.size(3.dp)
.background(
color = if (pagerState.currentPage == index) AppColors.text else AppColors.secondaryText.copy(alpha = 0.3f),
color = if (pagerState.currentPage == index) AppColors.text else AppColors.secondaryText.copy(
alpha = 0.3f
),
shape = androidx.compose.foundation.shape.CircleShape
)
)
@@ -416,8 +595,8 @@ fun AgentCard2(viewModel: AgentViewModel,agentItem: AgentItem,navController: Nav
.background(Color(0xFFF5F5F5), RoundedCornerShape(24.dp))
.clickable {
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
viewModel.goToProfile(agentItem.openId, navController)
}) {
viewModel.goToProfile(agentItem.openId, navController)
}) {
lastClickTime = System.currentTimeMillis()
}
},
@@ -482,14 +661,17 @@ fun AgentCard2(viewModel: AgentViewModel,agentItem: AgentItem,navController: Nav
)
.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)
}
}) {
// 检查游客模式,如果是游客则跳转登录
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()
}
},

View File

@@ -140,143 +140,183 @@ fun NotificationsScreen() {
Column(
modifier = Modifier.fillMaxSize(),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 15.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
Row(
modifier = Modifier
.size(24.dp)
)
Column(
modifier = Modifier
.weight(1f)
.align(Alignment.CenterVertically),
horizontalAlignment = Alignment.CenterHorizontally
.fillMaxWidth()
.height(44.dp)
.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(R.string.main_message),
fontSize = 17.sp,
fontWeight = FontWeight.W700,
fontSize = 20.sp,
fontWeight = FontWeight.W900,
color = AppColors.text
)
}
Image(
painter = painterResource(id = R.drawable.rider_pro_group),
contentDescription = "add",
modifier = Modifier
.size(24.dp)
.noRippleClickable {
debouncer {
navController.navigate(NavigationRoute.CreateGroupChat.route)
}
},
colorFilter = ColorFilter.tint(AppColors.text)
)
Spacer(modifier = Modifier.weight(1f))
}
// 搜索栏//
Box(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp)
.height(40.dp)
.noRippleClickable {
},
contentAlignment = Alignment.CenterStart
) {
Row(
modifier = Modifier
.height(36.dp)
.fillMaxWidth()
.clip(shape = RoundedCornerShape(8.dp))
.background(AppColors.inputBackground)
.padding(horizontal = 8.dp, vertical = 0.dp)
.noRippleClickable {
// 搜索框点击事件
},
verticalAlignment = Alignment.CenterVertically
) {
Icon(
Image(
painter = painterResource(id = R.drawable.rider_pro_nav_search),
contentDescription = null,
tint = AppColors.inputHint
contentDescription = "search",
modifier = Modifier
.size(24.dp)
.noRippleClickable {
// TODO: 实现搜索功能
},
colorFilter = ColorFilter.tint(AppColors.text)
)
Spacer(modifier = Modifier.width(16.dp))
Box {
androidx.compose.material.Text(
text = stringResource(R.string.search),
modifier = Modifier.padding(start = 8.dp),
color = AppColors.inputHint,
fontSize = 17.sp
Image(
painter = painterResource(id = R.drawable.rider_pro_notification),
contentDescription = "notifications",
modifier = Modifier
.size(24.dp)
.noRippleClickable {
// TODO: 实现通知功能
},
colorFilter = ColorFilter.tint(AppColors.text)
)
}
}
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
) {
val likeDebouncer = rememberDebouncer()
val followDebouncer = rememberDebouncer()
val commentDebouncer = rememberDebouncer()
// 通知红点
val totalNoticeCount = MessageListViewModel.likeNoticeCount +
MessageListViewModel.followNoticeCount +
MessageListViewModel.commentNoticeCount +
MessageListViewModel.favouriteNoticeCount
NotificationIndicator(
MessageListViewModel.likeNoticeCount,
R.mipmap.rider_pro_like,
stringResource(R.string.like_upper),
Color(0xFFFAFD5D)
) {
likeDebouncer {
if (MessageListViewModel.likeNoticeCount > 0) {
// 刷新点赞消息列表
LikeNoticeViewModel.isFirstLoad = true
// 清除点赞消息数量
MessageListViewModel.clearLikeNoticeCount()
if (totalNoticeCount > 0) {
Box(
modifier = Modifier
.size(8.dp)
.background(
color = Color(0xFFFF3B30),
shape = CircleShape
)
.align(Alignment.TopEnd)
.offset(x = 8.dp, y = (-4).dp)
)
}
navController.navigate(NavigationRoute.Likes.route)
}
}
NotificationIndicator(
MessageListViewModel.followNoticeCount,
R.mipmap.rider_pro_followers,
stringResource(R.string.followers_upper),
Color(0xFFF470FE)
) {
followDebouncer {
if (MessageListViewModel.followNoticeCount > 0) {
// 刷新关注消息列表
FollowerNoticeViewModel.isFirstLoad = true
MessageListViewModel.clearFollowNoticeCount()
}
navController.navigate(NavigationRoute.Followers.route)
}
}
NotificationIndicator(
MessageListViewModel.commentNoticeCount,
R.mipmap.rider_pro_comment,
stringResource(R.string.comment).uppercase(),
Color(0xFF6246FF)
) {
commentDebouncer {
navController.navigate(NavigationRoute.CommentNoticeScreen.route)
}
}
}
//创建群聊//
// Image(
// painter = painterResource(id = R.drawable.rider_pro_group),
// contentDescription = "add",
// modifier = Modifier
// .size(24.dp)
// .noRippleClickable {
// debouncer {
// navController.navigate(NavigationRoute.CreateGroupChat.route)
// }
// },
// colorFilter = ColorFilter.tint(AppColors.text)
// )
// // 搜索栏//
// Box(
// modifier = Modifier
// .fillMaxWidth()
// .padding(horizontal = 16.dp, vertical = 8.dp)
// .height(40.dp)
//
// .noRippleClickable {
// },
// contentAlignment = Alignment.CenterStart
// )
// {
// Row(
// modifier = Modifier
// .height(36.dp)
// .fillMaxWidth()
// .clip(shape = RoundedCornerShape(8.dp))
// .background(AppColors.inputBackground)
// .padding(horizontal = 8.dp, vertical = 0.dp)
// .noRippleClickable {
// // 搜索框点击事件
// },
// verticalAlignment = Alignment.CenterVertically
// ) {
// Icon(
// painter = painterResource(id = R.drawable.rider_pro_nav_search),
// contentDescription = null,
// tint = AppColors.inputHint
// )
// Box {
// androidx.compose.material.Text(
// text = stringResource(R.string.search),
// modifier = Modifier.padding(start = 8.dp),
// color = AppColors.inputHint,
// fontSize = 17.sp
// )
// }
// }
// }
//赞、粉丝、评论//
// Row(
// modifier = Modifier
// .fillMaxWidth()
// .padding(horizontal = 16.dp),
// horizontalArrangement = Arrangement.SpaceBetween,
// ) {
// val likeDebouncer = rememberDebouncer()
// val followDebouncer = rememberDebouncer()
// val commentDebouncer = rememberDebouncer()
//
// NotificationIndicator(
// MessageListViewModel.likeNoticeCount,
// R.mipmap.rider_pro_like,
// stringResource(R.string.like_upper),
// Color(0xFFFAFD5D)
// ) {
// likeDebouncer {
// if (MessageListViewModel.likeNoticeCount > 0) {
// // 刷新点赞消息列表
// LikeNoticeViewModel.isFirstLoad = true
// // 清除点赞消息数量
// MessageListViewModel.clearLikeNoticeCount()
// }
// navController.navigate(NavigationRoute.Likes.route)
// }
// }
// NotificationIndicator(
// MessageListViewModel.followNoticeCount,
// R.mipmap.rider_pro_followers,
// stringResource(R.string.followers_upper),
// Color(0xFFF470FE)
// ) {
// followDebouncer {
// if (MessageListViewModel.followNoticeCount > 0) {
// // 刷新关注消息列表
// FollowerNoticeViewModel.isFirstLoad = true
// MessageListViewModel.clearFollowNoticeCount()
// }
// navController.navigate(NavigationRoute.Followers.route)
// }
// }
// NotificationIndicator(
// MessageListViewModel.commentNoticeCount,
// R.mipmap.rider_pro_comment,
// stringResource(R.string.comment).uppercase(),
// Color(0xFF6246FF)
// ) {
// commentDebouncer {
// navController.navigate(NavigationRoute.CommentNoticeScreen.route)
// }
// }
// }
Spacer(modifier = Modifier.height(23.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(start = 16.dp,bottom = 16.dp),
// center the tabs
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.Bottom
) {

View File

@@ -24,8 +24,10 @@ import com.aiosman.ravenow.ui.navigateToChatAi
import io.openim.android.sdk.OpenIMClient
import io.openim.android.sdk.listener.OnBase
import io.openim.android.sdk.models.ConversationInfo
import io.openim.android.sdk.models.Message
import kotlinx.coroutines.launch
import kotlin.coroutines.suspendCoroutine
import com.aiosman.ravenow.utils.MessageParser
data class AgentConversation(
val id: String,
@@ -43,39 +45,19 @@ data class AgentConversation(
val lastMessage = Calendar.getInstance().apply {
timeInMillis = conversation.latestMsgSendTime
}
var displayText = conversation.latestMsg?: ""
// when (conversation.latestMsg) {
// 101 -> { // TEXT
// displayText = conversation.latestMsg?: ""
// }
// 102 -> { // IMAGE
// displayText = "[图片]"
// }
// 103 -> { // AUDIO
// displayText = "[语音]"
// }
// 104 -> { // VIDEO
// displayText = "[视频]"
// }
// 105 -> { // FILE
// displayText = "[文件]"
// }
// else -> {
// displayText = "[消息]"
// }
// }
// 解析最新消息
val (displayText, isSelf) = MessageParser.parseLatestMessage(conversation.latestMsg)
return AgentConversation(
id = conversation.conversationID,
nickname = conversation.showName ?: "",
lastMessage = conversation.latestMsg ?: "",
lastMessage = displayText, // 使用解析后的显示文本
lastMessageTime = lastMessage.time.formatChatTime(context),
avatar = "${ApiClient.BASE_API_URL+"/"}${conversation.faceURL}"+"?token="+"${AppStore.token}".replace("storage/avatars/", "/avatar/"),
unreadCount = conversation.unreadCount,
trtcUserId = conversation.userID ?: "",
displayText = displayText,
// TODO: openim latestMsg
isSelf = false,
// isSelf = conversation.latestMsg?.sendID == AppState.profile?.trtcUserId
isSelf = isSelf // 使用解析后的发送者信息
)
}
}

View File

@@ -21,8 +21,10 @@ import com.aiosman.ravenow.data.api.ApiClient
import io.openim.android.sdk.OpenIMClient
import io.openim.android.sdk.listener.OnBase
import io.openim.android.sdk.models.ConversationInfo
import io.openim.android.sdk.models.Message
import kotlinx.coroutines.launch
import kotlin.coroutines.suspendCoroutine
import com.aiosman.ravenow.utils.MessageParser
data class FriendConversation(
val id: String,
@@ -40,17 +42,20 @@ data class FriendConversation(
val lastMessage = Calendar.getInstance().apply {
timeInMillis = conversation.latestMsgSendTime
}
var displayText = conversation.latestMsg
// 解析最新消息
val (displayText, isSelf) = MessageParser.parseLatestMessage(conversation.latestMsg)
return FriendConversation(
id = conversation.conversationID,
nickname = conversation.showName ?: "",
lastMessage = conversation.latestMsg ?: "",
lastMessage = displayText, // 使用解析后的显示文本
lastMessageTime = lastMessage.time.formatChatTime(context),
avatar = "${ApiClient.BASE_API_URL+"/"}${conversation.faceURL}"+"?token="+"${AppStore.token}".replace("storage/avatars/", "/avatar/"),
unreadCount = conversation.unreadCount,
trtcUserId = conversation.userID ?: "",
displayText = displayText,
isSelf = false
isSelf = isSelf // 使用解析后的发送者信息
)
}
}

View File

@@ -26,8 +26,10 @@ import io.openim.android.sdk.listener.OnAdvanceMsgListener
import io.openim.android.sdk.listener.OnBase
import io.openim.android.sdk.listener.OnConversationListener
import io.openim.android.sdk.models.ConversationInfo
import io.openim.android.sdk.models.Message
import kotlinx.coroutines.launch
import kotlin.coroutines.suspendCoroutine
import com.aiosman.ravenow.utils.MessageParser
data class GroupConversation(
val id: String,
@@ -46,12 +48,15 @@ data class GroupConversation(
val lastMessage = Calendar.getInstance().apply {
timeInMillis = conversation.latestMsgSendTime
}
// 解析最新消息
val (displayText, isSelf) = MessageParser.parseLatestMessage(conversation.latestMsg)
return GroupConversation(
id = conversation.conversationID,
groupId = conversation.groupID ?: "",
groupName = conversation.showName ?: "",
lastMessage = conversation.latestMsg?: "",
lastMessage = displayText, // 使用解析后的显示文本
lastMessageTime = lastMessage.time.formatChatTime(context),
avatar = if (conversation.faceURL.isNullOrEmpty()) {
// 将 groupId 转换为 Base64
@@ -64,10 +69,9 @@ data class GroupConversation(
"${ApiClient.BASE_API_URL+"/outside/rooms/avatar/"}${conversation.faceURL}"+"?token="+"${AppStore.token}"
},
unreadCount = conversation.unreadCount,
displayText = conversation.latestMsg?: "",
isSelf = false,
// TODO openim get grouplist
memberCount = 0
displayText = displayText,
isSelf = isSelf, // 使用解析后的发送者信息
memberCount = 0 // TODO: 获取群组成员数量
)
}
}

View File

@@ -30,9 +30,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
@@ -51,6 +48,12 @@ import com.aiosman.ravenow.ui.index.tabs.moment.tabs.timeline.TimelineMomentsLis
import com.aiosman.ravenow.ui.index.tabs.search.SearchViewModel
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import kotlinx.coroutines.launch
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.ColorFilter
import com.aiosman.ravenow.ui.composables.TabItem
import com.aiosman.ravenow.ui.composables.TabSpacer
import com.aiosman.ravenow.ui.composables.rememberDebouncer
/**
* 动态列表
@@ -63,8 +66,8 @@ fun MomentsList() {
val navigationBarPaddings =
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 48.dp
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
// 游客模式下不显示timeline只显示3个tabExplore、Dynamic、Hot
val tabCount = if (AppStore.isGuest) 3 else 4
// 游客模式下不显示timeline只显示2个tabDynamic、Hot // 游客模式下不显示timeline只显示3个tabExplore、Dynamic、Hot
val tabCount = if (AppStore.isGuest) 2 else 3 // val tabCount = if (AppStore.isGuest) 3 else 4
var pagerState = rememberPagerState { tabCount }
var scope = rememberCoroutineScope()
Column(
@@ -79,154 +82,143 @@ fun MomentsList() {
Row(
modifier = Modifier
.fillMaxWidth()
.height(44.dp),
.height(44.dp)
.padding(horizontal = 16.dp),
// center the tabs
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {
Column(
//原探索//
// Column(
// modifier = Modifier
// .noRippleClickable {
// scope.launch {
// pagerState.animateScrollToPage(0)
// }
// }.padding(start = 16.dp),
// verticalArrangement = Arrangement.Center,
// horizontalAlignment = Alignment.CenterHorizontally
//
// ) {
// Text(
// text = stringResource(R.string.index_worldwide),
// fontSize = if (pagerState.currentPage == 0)18.sp else 16.sp,
// color = if (pagerState.currentPage == 0) AppColors.text else AppColors.nonActiveText,
// fontWeight = FontWeight.W600)
// Spacer(modifier = Modifier.height(4.dp))
//
// Image(
// painter = painterResource(
// if (pagerState.currentPage == 0) R.mipmap.tab_indicator_selected
// else R.drawable.tab_indicator_unselected
// ),
// contentDescription = "tab indicator",
// modifier = Modifier
// .width(34.dp)
// .height(4.dp)
// )
//
// }
// Spacer(modifier = Modifier.width(16.dp))
Text(
text = stringResource(R.string.moment),
fontSize = 20.sp,
fontWeight = FontWeight.W900,
color = AppColors.text,
modifier = Modifier
.noRippleClickable {
scope.launch {
pagerState.animateScrollToPage(0)
}
}.padding(start = 16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
.align(Alignment.CenterVertically)
)
) {
Text(
text = stringResource(R.string.index_worldwide),
fontSize = if (pagerState.currentPage == 0)18.sp else 16.sp,
color = if (pagerState.currentPage == 0) AppColors.text else AppColors.nonActiveText,
fontWeight = FontWeight.W600)
Spacer(modifier = Modifier.height(4.dp))
Spacer(modifier = Modifier.weight(1f))
Image(
painter = painterResource(
if (pagerState.currentPage == 0) R.mipmap.tab_indicator_selected
else R.drawable.tab_indicator_unselected
),
contentDescription = "tab indicator",
modifier = Modifier
.width(34.dp)
.height(4.dp)
)
}
Spacer(modifier = Modifier.width(16.dp))
Column(
Image(
painter = painterResource(id = R.drawable.rider_pro_nav_search),
contentDescription = "search",
modifier = Modifier
.size(24.dp)
.noRippleClickable {
scope.launch {
pagerState.animateScrollToPage(1)
}
navController.navigate(NavigationRoute.Search.route)
},
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = stringResource(R.string.index_dynamic),
fontSize = if (pagerState.currentPage == 1)18.sp else 16.sp,
color = if (pagerState.currentPage == 1) AppColors.text else AppColors.nonActiveText,
fontWeight = FontWeight.W600)
Spacer(modifier = Modifier.height(4.dp))
colorFilter = ColorFilter.tint(AppColors.text)
)
}
Image(
painter = painterResource(
if (pagerState.currentPage == 1) R.mipmap.tab_indicator_selected
else R.drawable.tab_indicator_unselected
),
contentDescription = "tab indicator",
modifier = Modifier
.width(34.dp)
.height(4.dp)
Spacer(modifier = Modifier.height(23.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(start = 16.dp, bottom = 16.dp),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.Bottom
) {
val tabDebouncer = rememberDebouncer()
// 新探索标签
Box {
CustomTabItem(
text = stringResource(R.string.index_worldwide),
isSelected = pagerState.currentPage == 0,
onClick = {
tabDebouncer {
scope.launch {
pagerState.animateScrollToPage(0)
}
}
}
)
}
TabSpacer()
// 只有非游客用户才显示"关注"tab
if (!AppStore.isGuest) {
//关注tab
Spacer(modifier = Modifier.width(16.dp))
Column(
modifier = Modifier
.noRippleClickable {
scope.launch {
pagerState.animateScrollToPage(2)
}
},
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
Box {
CustomTabItem(
text = stringResource(R.string.index_following),
fontSize = if (pagerState.currentPage == 2)18.sp else 16.sp,
color = if (pagerState.currentPage == 2) AppColors.text else AppColors.nonActiveText,
fontWeight = FontWeight.W600)
Spacer(modifier = Modifier.height(4.dp))
Image(
painter = painterResource(
if (pagerState.currentPage == 2) R.mipmap.tab_indicator_selected
else R.drawable.tab_indicator_unselected
),
contentDescription = "tab indicator",
modifier = Modifier
.width(34.dp)
.height(4.dp)
isSelected = pagerState.currentPage == 1,
onClick = {
tabDebouncer {
scope.launch {
pagerState.animateScrollToPage(1)
}
}
}
)
}
}
//热门tab
Spacer(modifier = Modifier.width(16.dp))
Column(
modifier = Modifier
.noRippleClickable {
scope.launch {
val targetPage = if (AppStore.isGuest) 2 else 3
pagerState.animateScrollToPage(targetPage)
}
},
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = stringResource(R.string.index_hot),
fontSize = if ((AppStore.isGuest && pagerState.currentPage == 2) || (!AppStore.isGuest && pagerState.currentPage == 3)) 18.sp else 16.sp,
color = if ((AppStore.isGuest && pagerState.currentPage == 2) || (!AppStore.isGuest && pagerState.currentPage == 3)) AppColors.text else AppColors.nonActiveText,
fontWeight = FontWeight.W600)
Spacer(modifier = Modifier.height(4.dp))
Image(
painter = painterResource(
if ((AppStore.isGuest && pagerState.currentPage == 2) || (!AppStore.isGuest && pagerState.currentPage == 3)) R.mipmap.tab_indicator_selected
else R.drawable.tab_indicator_unselected
),
contentDescription = "tab indicator",
modifier = Modifier
.width(34.dp)
.height(4.dp)
)
TabSpacer()
}
//搜索按钮
Column(
modifier = Modifier
.padding(bottom = 8.dp, end = 16.dp)
.fillMaxWidth(),
horizontalAlignment = Alignment.End
) {
Icon(
ImageVector.vectorResource(R.drawable.rider_pro_nav_search),
contentDescription = "Clickable Icon",
tint = AppColors.text,
modifier = Modifier.size(24.dp)
.noRippleClickable {
navController.navigate(NavigationRoute.Search.route)
// 热门标签
Box {
CustomTabItem(
text = stringResource(R.string.index_hot),
isSelected = pagerState.currentPage == 2,
onClick = {
tabDebouncer {
scope.launch {
pagerState.animateScrollToPage(2)
}
}
}
)
)
}
} else {
// 热门标签 (游客模式)
Box {
CustomTabItem(
text = stringResource(R.string.index_hot),
isSelected = pagerState.currentPage == 1,
onClick = {
tabDebouncer {
scope.launch {
pagerState.animateScrollToPage(1)
}
}
}
)
}
}
}
@@ -237,31 +229,25 @@ fun MomentsList() {
.weight(1f)
) {
if (AppStore.isGuest) {
// 游客模式:Explore(0), Dynamic(1), Hot(2)
// 游客模式Dynamic(0), Hot(1)
when (it) {
0 -> {
Explore()
}
1 -> {
Dynamic()
}
2 -> {
1 -> {
HotMomentsList()
}
}
} else {
// 正常用户:Explore(0), Dynamic(1), Timeline(2), Hot(3)
// 正常用户Dynamic(0), Timeline(1), Hot(2)
when (it) {
0 -> {
Explore()
}
1 -> {
Dynamic()
}
2 -> {
1 -> {
TimelineMomentsList()
}
3 -> {
2 -> {
HotMomentsList()
}
}
@@ -269,5 +255,30 @@ fun MomentsList() {
}
}
}
@Composable
fun CustomTabItem(
text: String,
isSelected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
val AppColors = LocalAppTheme.current
Column(
modifier = modifier
.noRippleClickable { onClick() },
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = text,
fontSize = 15.sp,
color = if (isSelected) AppColors.tabSelectedText else AppColors.tabUnselectedText,
modifier = Modifier
.clip(RoundedCornerShape(12.dp))
.background(if (isSelected) AppColors.tabSelectedBackground else AppColors.tabUnselectedBackground)
.padding(horizontal = 16.dp, vertical = 8.dp)
)
}
}

View File

@@ -93,7 +93,7 @@ fun GalleryItem(
Text(
text = "故事还没开始",
fontSize = 12.sp,
fontSize = 16.sp,
color = AppColors.text,
fontWeight = FontWeight.W600
)
@@ -150,7 +150,7 @@ fun GalleryGrid(
Text(
text = "故事还没开始",
fontSize = 12.sp,
fontSize = 16.sp,
color = AppColors.text,
fontWeight = FontWeight.W600
)

View File

@@ -275,7 +275,7 @@ fun LoginPage() {
) {
Spacer(modifier = Modifier.weight(1f))
Image(
painter = painterResource(id = R.mipmap.rider_pro_color_logo_next),
painter = painterResource(id = R.mipmap.invalid_name),
contentDescription = "Rave Now",
modifier = Modifier
.size(52.dp)

View File

@@ -96,7 +96,7 @@ fun NewPostScreen() {
var isAiEnabled by remember { mutableStateOf(false) }
var isRotating by remember { mutableStateOf(false) }
var isRequesting by remember { mutableStateOf(false) }
val keyboardController = LocalSoftwareKeyboardController.current // 添加这行
val keyboardController = LocalSoftwareKeyboardController.current
val model = NewPostViewModel
val systemUiController = rememberSystemUiController()

View File

@@ -0,0 +1,69 @@
package com.aiosman.ravenow.utils
import com.aiosman.ravenow.AppState
import com.google.gson.Gson
import com.google.gson.JsonSyntaxException
import io.openim.android.sdk.models.Message
/**
* OpenIM 消息解析工具类
* 用于解析 ConversationInfo 中的 latestMsg JSON 字符串
*/
object MessageParser {
/**
* 解析最新消息的显示文本和发送者信息
* @param latestMsgJson 最新消息的JSON字符串
* @return Pair<displayText, isSelf> 显示文本和是否是自己发送的消息
*/
fun parseLatestMessage(latestMsgJson: String?): Pair<String, Boolean> {
var displayText = ""
var isSelf = false
try {
if (!latestMsgJson.isNullOrEmpty()) {
val gson = Gson()
val message = gson.fromJson(latestMsgJson, Message::class.java)
// 判断是否是自己发送的消息
isSelf = message.sendID == AppState.profile?.trtcUserId
// 根据消息类型生成显示文本
displayText = getMessageDisplayText(message)
} else {
displayText = "[暂无消息]"
}
} catch (e: JsonSyntaxException) {
// JSON 解析失败,使用原始文本
displayText = latestMsgJson ?: "[消息解析失败]"
} catch (e: Exception) {
// 其他异常
displayText = "[消息]"
}
return Pair(displayText, isSelf)
}
/**
* 根据消息类型获取显示文本
* @param message OpenIM Message 对象
* @return 消息的显示文本
*/
private fun getMessageDisplayText(message: Message): String {
return when (message.contentType) {
101 -> { // TEXT
message.textElem?.content ?: "[文本消息]"
}
102 -> "[图片]" // IMAGE
103 -> "[语音]" // AUDIO
104 -> "[视频]" // VIDEO
105 -> "[文件]" // FILE
106 -> "[位置]" // LOCATION
107 -> "[自定义消息]" // CUSTOM
108 -> "[合并消息]" // MERGE
109 -> "[名片]" // CARD
110 -> "[引用消息]" // QUOTE
else -> "[消息]"
}
}
}

View File

@@ -0,0 +1,18 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:pathData="m9.098,4.33 l1.475,-1.475a0.643,0.643 0,0 1,0.909 0l1.663,1.663a0.643,0.643 0,0 1,0 0.91L11.67,6.901M9.098,4.33l-6.243,6.242a0.643,0.643 0,0 0,-0.188 0.455v1.663c0,0.355 0.288,0.643 0.643,0.643h1.663c0.17,0 0.334,-0.067 0.455,-0.188l6.242,-6.243M9.098,4.33l2.572,2.572"
android:strokeLineJoin="round"
android:strokeWidth="1.333"
android:fillColor="#00000000"
android:strokeColor="#7C45ED"
android:fillType="evenOdd"
android:strokeLineCap="round"/>
<path
android:pathData="M3.608,6.333c0.13,0 0.202,-0.07 0.215,-0.208 0.056,-0.434 0.114,-0.775 0.172,-1.022 0.06,-0.247 0.152,-0.434 0.28,-0.56 0.128,-0.126 0.318,-0.221 0.57,-0.286 0.252,-0.066 0.599,-0.133 1.042,-0.202 0.152,-0.026 0.228,-0.1 0.228,-0.222 0,-0.06 -0.02,-0.11 -0.059,-0.146a0.304,0.304 0,0 0,-0.143 -0.075,15.533 15.533,0 0,1 -1.048,-0.225c-0.256,-0.067 -0.45,-0.161 -0.583,-0.283 -0.132,-0.121 -0.228,-0.304 -0.287,-0.547a10.18,10.18 0,0 1,-0.172 -1.015c-0.013,-0.14 -0.085,-0.209 -0.215,-0.209a0.221,0.221 0,0 0,-0.153 0.056,0.208 0.208,0 0,0 -0.068,0.146 9.948,9.948 0,0 1,-0.176 1.039c-0.06,0.25 -0.155,0.438 -0.283,0.566 -0.128,0.128 -0.32,0.224 -0.576,0.286 -0.256,0.063 -0.608,0.125 -1.055,0.186a0.251,0.251 0,0 0,-0.143 0.072,0.203 0.203,0 0,0 -0.059,0.15c0,0.06 0.02,0.109 0.059,0.146 0.039,0.037 0.086,0.062 0.143,0.075 0.447,0.082 0.799,0.158 1.055,0.228 0.256,0.069 0.448,0.166 0.576,0.29 0.128,0.123 0.221,0.306 0.28,0.55 0.058,0.242 0.118,0.581 0.179,1.015a0.202,0.202 0,0 0,0.068 0.14,0.221 0.221,0 0,0 0.153,0.055zM12.516,14.702c0.086,0 0.139,-0.05 0.156,-0.15 0.056,-0.308 0.108,-0.552 0.156,-0.732a0.963,0.963 0,0 1,0.208 -0.417,0.879 0.879,0 0,1 0.41,-0.225c0.183,-0.052 0.439,-0.106 0.769,-0.162 0.1,-0.018 0.15,-0.072 0.15,-0.163 0,-0.091 -0.05,-0.146 -0.15,-0.163a6.959,6.959 0,0 1,-0.768 -0.166,0.919 0.919,0 0,1 -0.41,-0.224 0.937,0.937 0,0 1,-0.209 -0.414c-0.048,-0.18 -0.1,-0.426 -0.156,-0.739 -0.017,-0.095 -0.07,-0.143 -0.156,-0.143 -0.091,0 -0.146,0.048 -0.163,0.143a9.897,9.897 0,0 1,-0.156 0.74,0.937 0.937,0 0,1 -0.209,0.413 0.919,0.919 0,0 1,-0.41 0.224c-0.182,0.054 -0.436,0.11 -0.762,0.166 -0.1,0.017 -0.15,0.072 -0.15,0.163 0,0.091 0.05,0.145 0.15,0.163 0.326,0.056 0.58,0.11 0.762,0.162a0.879,0.879 0,0 1,0.41 0.225c0.091,0.098 0.16,0.237 0.209,0.417 0.047,0.18 0.1,0.424 0.156,0.732 0.009,0.044 0.026,0.08 0.052,0.108a0.143,0.143 0,0 0,0.11 0.042z"
android:fillColor="#7C45ED"
android:fillType="evenOdd"/>
</vector>

View File

@@ -0,0 +1,19 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="72dp"
android:height="72dp"
android:viewportWidth="72"
android:viewportHeight="72">
<path
android:pathData="M36,36m-36,0a36,36 0,1 1,72 0a36,36 0,1 1,-72 0"
android:fillColor="#8FBFFA"
android:fillAlpha="0.08"
android:fillType="evenOdd"/>
<path
android:pathData="M36.804,31.654h-0.108c-2.5,0 -5.222,0 -7.559,0.409a4.115,4.115 0,0 0,-3.3 3.202c-0.337,1.56 -0.337,2.86 -0.337,5.224v0.175c0,2.365 0,3.665 0.338,5.225a4.115,4.115 0,0 0,3.3 3.2c2.336,0.411 5.058,0.411 7.558,0.411h0.108c2.5,0 5.222,0 7.559,-0.41a4.115,4.115 0,0 0,3.3 -3.2C48,44.328 48,43.03 48,40.663v-0.175c0,-2.364 0,-3.663 -0.338,-5.224a4.115,4.115 0,0 0,-3.3 -3.202c-2.336,-0.409 -5.058,-0.409 -7.558,-0.409z"
android:fillColor="#8FBFFA"
android:fillType="evenOdd"/>
<path
android:pathData="M36.75,22.5c-1.06,0 -2.031,0.302 -2.738,1.006 -0.704,0.704 -1.004,1.673 -1.004,2.735 0,1.058 0.302,2.028 1.006,2.732 0.35,0.35 0.765,0.601 1.228,0.764v1.921c0.49,-0.004 0.977,-0.004 1.454,-0.004h0.108c0.48,0 0.967,0 1.456,0.004v-1.92c0.461,-0.16 0.881,-0.421 1.228,-0.765 0.704,-0.704 1.006,-1.674 1.006,-2.734s-0.302,-2.03 -1.006,-2.733c-0.705,-0.704 -1.677,-1.006 -2.738,-1.006zM32.682,35.919c0.694,0 1.258,0.563 1.258,1.257v1.213a1.258,1.258 0,0 1,-2.516 0v-1.213c0,-0.694 0.564,-1.257 1.258,-1.257zM40.818,35.919c0.694,0 1.258,0.563 1.258,1.257v1.213a1.258,1.258 0,0 1,-2.516 0v-1.213c0,-0.694 0.564,-1.257 1.258,-1.257zM32.193,41.933a1.259,1.259 0,0 1,1.7 0.48l0.013,0.019c0.16,0.227 0.354,0.43 0.573,0.601 0.435,0.342 1.154,0.718 2.271,0.718 1.115,0 1.836,-0.378 2.273,-0.718a2.87,2.87 0,0 0,0.571 -0.603l0.012,-0.016A1.259,1.259 0,0 1,41.8 43.64l-1.1,-0.606 1.1,0.608v0.004l-0.004,0.006 -0.008,0.014 -0.022,0.036 -0.066,0.105a5.392,5.392 0,0 1,-1.128 1.202c-0.82,0.646 -2.077,1.256 -3.824,1.256 -1.743,0 -3,-0.61 -3.822,-1.256a5.392,5.392 0,0 1,-1.128 -1.202,2.99 2.99,0 0,1 -0.064,-0.105l-0.02,-0.036 -0.01,-0.014 -0.002,-0.006 -0.002,-0.002c0,-0.002 0,-0.004 1.1,-0.61l-1.102,0.608a1.256,1.256 0,0 1,0.495 -1.71z"
android:fillColor="#2859C5"
android:fillType="evenOdd"/>
</vector>

View File

@@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="33dp"
android:viewportWidth="32"
android:viewportHeight="33">
<path
android:pathData="M15.478,0.75L16.522,0.75A15.478,15.478 0,0 1,32 16.228L32,17.522A15.478,15.478 0,0 1,16.522 33L15.478,33A15.478,15.478 0,0 1,0 17.522L0,16.228A15.478,15.478 0,0 1,15.478 0.75z"
android:fillColor="#7C7480"
android:fillAlpha="0.08"
android:fillType="evenOdd"/>
<path
android:pathData="M20.782,12.155a1.29,1.29 0,0 1,0 1.824l-2.889,2.889 2.86,2.859a1.29,1.29 0,0 1,0 1.824l-0.077,0.076a1.29,1.29 0,0 1,-1.824 0l-2.859,-2.86 -2.889,2.89a1.29,1.29 0,0 1,-1.824 0l-0.062,-0.062a1.29,1.29 0,0 1,0 -1.824l2.89,-2.89 -2.86,-2.858a1.29,1.29 0,0 1,0 -1.824l0.076,-0.076a1.29,1.29 0,0 1,1.824 0l2.86,2.859 2.888,-2.889a1.29,1.29 0,0 1,1.824 0l0.062,0.062z"
android:fillColor="#918E93"
android:fillType="evenOdd"/>
</vector>

View File

@@ -0,0 +1,28 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="72dp"
android:height="72dp"
android:viewportWidth="72"
android:viewportHeight="72">
<path
android:pathData="M36,36m-36,0a36,36 0,1 1,72 0a36,36 0,1 1,-72 0"
android:fillColor="#FC0"
android:fillAlpha="0.08"
android:fillType="evenOdd"/>
<path
android:pathData="M31.206,26.187c1.179,-1.192 2.856,-1.826 4.61,-1.57l8.182,1.196c3.012,0.44 5.11,3.336 4.687,6.468l-1.533,11.34 -1.75,-0.255"
android:strokeWidth="2.147"
android:fillColor="#00000000"
android:strokeColor="#FECE51"
android:fillType="evenOdd"
android:strokeLineCap="round"/>
<path
android:pathData="m28.156,30.17 l7.756,-1.134a5.726,5.726 0,0 1,6.502 4.899l0.766,5.662a5.726,5.726 0,0 1,-4.847 6.433L24.85,48l-1.54,-11.398a5.726,5.726 0,0 1,4.846 -6.433z"
android:strokeWidth="2.147"
android:fillColor="#FECE51"
android:strokeColor="#FECE51"
android:fillType="evenOdd"/>
<path
android:pathData="m27.74,39.562 l11.024,-0.06 -0.015,2.861 -11.024,0.06z"
android:fillColor="#D86002"
android:fillType="evenOdd"/>
</vector>

View File

@@ -0,0 +1,36 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:pathData="M24,24m-24,0a24,24 0,1 1,48 0a24,24 0,1 1,-48 0"
android:fillColor="#5E5CE6"
android:fillAlpha="0.1"
android:fillType="evenOdd"/>
<path
android:pathData="M29.132,11.3c0.861,0.075 1.609,0.484 2.124,1.082a3.019,3.019 50,0 1,-0.378 4.325c-0.448,0.365 -0.987,0.6 -1.56,0.68l-0.02,0.002 0.057,0.035c1.649,0.997 2.914,2.401 3.789,4.009l0.041,0.076c1.115,2.087 1.576,4.508 1.374,6.807 -0.237,2.703 -1.392,4.636 -3.188,5.873a3.07,3.07 50,0 1,0.025 1.749c-0.168,0.58 -0.476,0.756 -0.578,0.791 -0.176,0.059 -1.855,-0.393 -2.957,-1.133 -1.289,0.249 -2.726,0.304 -4.271,0.168 -3.173,-0.276 -6.14,-1.296 -8.154,-3.002 -1.84,-1.559 -2.906,-3.682 -2.672,-6.345 0.261,-2.968 1.606,-5.945 3.885,-7.972 1.978,-1.761 4.657,-2.815 7.951,-2.529 0.608,0.053 1.187,0.148 1.738,0.28l0.039,0.009 -0.01,-0.013a3.013,3.013 50,0 1,-0.627 -2.061l0.004,-0.051c0.074,-0.845 0.492,-1.579 1.109,-2.083a3.163,3.163 50,0 1,2.281 -0.698z"
android:fillType="nonZero">
<aapt:attr name="android:fillColor">
<gradient
android:startX="23.669"
android:startY="11.286"
android:endX="23.669"
android:endY="36.734"
android:type="linear">
<item android:offset="0" android:color="#FF7C45ED"/>
<item android:offset="0.236" android:color="#FF7C68EF"/>
<item android:offset="1" android:color="#FF7BD8F8"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M17.51,22.904L17.736,22.924A0.912,0.912 50,0 1,18.565 23.912L18.419,25.584A0.912,0.912 50,0 1,17.431 26.413L17.205,26.393A0.912,0.912 50,0 1,16.376 25.405L16.522,23.733A0.912,0.912 50,0 1,17.51 22.904z"
android:fillColor="#FFF"
android:fillType="nonZero"/>
<path
android:pathData="M22.163,23.341L22.389,23.361A0.912,0.912 50,0 1,23.218 24.349L23.072,26.021A0.912,0.912 50,0 1,22.083 26.85L21.857,26.83A0.912,0.912 50,0 1,21.028 25.842L21.175,24.17A0.912,0.912 50,0 1,22.163 23.341z"
android:fillColor="#FFF"
android:fillType="nonZero"/>
</vector>

View File

@@ -0,0 +1,21 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="72dp"
android:height="72dp"
android:viewportWidth="72"
android:viewportHeight="72">
<path
android:pathData="M36,36m-36,0a36,36 0,1 1,72 0a36,36 0,1 1,-72 0"
android:fillColor="#3DC779"
android:fillAlpha="0.08"
android:fillType="evenOdd"/>
<path
android:pathData="M31.555,24.228a0.964,0.964 0,0 0,-0.845 0.498l-1.958,3.548 -0.636,0.054 -0.154,0.012a5,5 0,0 0,-4.504 4.233c-0.246,1.664 -0.476,3.433 -0.476,5.253 0,1.82 0.232,3.59 0.476,5.256a5,5 0,0 0,4.504 4.231c2.64,0.228 5.33,0.459 8.038,0.459s5.398,-0.231 8.038,-0.459a5,5 0,0 0,4.503 -4.231c0.247,-1.667 0.477,-3.433 0.477,-5.256 0,-1.82 -0.232,-3.587 -0.477,-5.253a5,5 0,0 0,-4.503 -4.233l-0.127,-0.01 -0.663,-0.058 -1.958,-3.546a0.964,0.964 0,0 0,-0.845 -0.498h-8.89z"
android:fillColor="#61CD8C"
android:fillType="evenOdd"/>
<path
android:pathData="M36,33.345c1.253,0 2.26,0.306 2.934,0.98 0.674,0.674 0.98,1.68 0.98,2.932 0,1.253 -0.306,2.26 -0.98,2.934 -0.674,0.674 -1.681,0.98 -2.935,0.98 -1.252,0.002 -2.26,-0.305 -2.933,-0.978 -0.674,-0.674 -0.98,-1.681 -0.98,-2.934 0,-1.253 0.306,-2.26 0.98,-2.934 0.674,-0.674 1.68,-0.98 2.934,-0.98z"
android:strokeWidth="2.25"
android:fillColor="#61CD8C"
android:fillType="nonZero"
android:strokeColor="#FFF"/>
</vector>

View File

@@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:pathData="m9.185,2 l2.37,2.341 -6.518,6.44h-2.37V8.438zM2.667,13.707h10.667"
android:strokeLineJoin="round"
android:strokeWidth="1.185"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#000"
android:strokeLineCap="round"/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 689 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 417 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 707 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 935 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 984 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Create Bottom Sheet -->
<string name="create_title">作成</string>
<string name="create_ai">AI</string>
<string name="create_group_chat_option">グループチャット</string>
<string name="create_moment">モーメント</string>
<string name="create_close">閉じる</string>
<!-- Create Agent V2 -->
<string name="create_agent_v2_back">戻る</string>
<string name="create_agent_v2_title">AI作成</string>
<string name="create_agent_v2_greeting">%s さん、こんにちは!今日は何を作りたいですか?</string>
<string name="create_agent_v2_description">たった一言で、あなた専用のAIがここに誕生します</string>
<string name="create_agent_v2_input_hint">詩を書けるAI、あなたの笑いのツボがわかるAI</string>
<string name="create_agent_v2_ai_enhance">AI美化</string>
<string name="create_agent_v2_generating">生成中...</string>
<string name="create_agent_v2_manual_create">手動でAI作成</string>
<string name="create_agent_v2_one_sentence_create">一言でAI作成</string>
<string name="create_agent_v2_thinking">あなたのために構想中</string>
<string name="create_agent_v2_name_label">名前</string>
<string name="create_agent_v2_description_label">説明</string>
<string name="create_agent_v2_create_button">よし、これで決まり</string>
<string name="create_agent_v2_creating">作成中...</string>
<string name="create_agent_v2_ai_avatar_desc">AIアバター</string>
<string name="create_agent_v2_ai_enhance_icon_desc">AI美化アイコン</string>
<string name="create_agent_v2_edit_icon_desc">編集アイコン</string>
<string name="create_agent_v2_select_avatar_desc">アバター選択</string>
</resources>

View File

@@ -190,5 +190,32 @@
<string name="agent_createing">创建中...</string>
<string name="agent_find">发现</string>
<string name="text_error_password_too_long">密码不能超过 %1$d 个字符</string>
<!-- Create Bottom Sheet -->
<string name="create_title">创建</string>
<string name="create_ai">AI</string>
<string name="create_group_chat_option">群聊</string>
<string name="create_moment">动态</string>
<string name="create_close">关闭</string>
<!-- Create Agent V2 -->
<string name="create_agent_v2_back">返回</string>
<string name="create_agent_v2_title">创建AI</string>
<string name="create_agent_v2_greeting">%s 你好呀!今天想创建什么?</string>
<string name="create_agent_v2_description">只需要一句话你的专属AI在这里诞生</string>
<string name="create_agent_v2_input_hint">一个会写诗的AI一个会懂你笑点的AI</string>
<string name="create_agent_v2_ai_enhance">AI美化</string>
<string name="create_agent_v2_generating">生成中...</string>
<string name="create_agent_v2_manual_create">手动创造AI</string>
<string name="create_agent_v2_one_sentence_create">一句话创造AI</string>
<string name="create_agent_v2_thinking">正在为你构思</string>
<string name="create_agent_v2_name_label">名称</string>
<string name="create_agent_v2_description_label">描述</string>
<string name="create_agent_v2_create_button">好的,就它了</string>
<string name="create_agent_v2_creating">创建中...</string>
<string name="create_agent_v2_ai_avatar_desc">AI头像</string>
<string name="create_agent_v2_ai_enhance_icon_desc">AI美化图标</string>
<string name="create_agent_v2_edit_icon_desc">编辑图标</string>
<string name="create_agent_v2_select_avatar_desc">选择头像</string>
</resources>

View File

@@ -186,5 +186,32 @@
<string name="agent_createing">创建中...</string>
<string name="agent_find">发现</string>
<string name="text_error_password_too_long">Password cannot exceed %1$d characters</string>
<!-- Create Bottom Sheet -->
<string name="create_title">Create</string>
<string name="create_ai">AI</string>
<string name="create_group_chat_option">Group Chat</string>
<string name="create_moment">Moment</string>
<string name="create_close">Close</string>
<!-- Create Agent V2 -->
<string name="create_agent_v2_back">Back</string>
<string name="create_agent_v2_title">Create AI</string>
<string name="create_agent_v2_greeting">Hello %s! What would you like to create today?</string>
<string name="create_agent_v2_description">Just one sentence, and your exclusive AI will be born here</string>
<string name="create_agent_v2_input_hint">An AI that writes poetry, an AI that understands your humor</string>
<string name="create_agent_v2_ai_enhance">AI Enhancement</string>
<string name="create_agent_v2_generating">Generating...</string>
<string name="create_agent_v2_manual_create">Manually Create AI</string>
<string name="create_agent_v2_one_sentence_create">Create AI with One Sentence</string>
<string name="create_agent_v2_thinking">Thinking for you</string>
<string name="create_agent_v2_name_label">Name</string>
<string name="create_agent_v2_description_label">Description</string>
<string name="create_agent_v2_create_button">Alright, that\'s the one</string>
<string name="create_agent_v2_creating">Creating...</string>
<string name="create_agent_v2_ai_avatar_desc">AI Avatar</string>
<string name="create_agent_v2_ai_enhance_icon_desc">AI Enhancement Icon</string>
<string name="create_agent_v2_edit_icon_desc">Edit Icon</string>
<string name="create_agent_v2_select_avatar_desc">Select Avatar</string>
</resources>

View File

@@ -5,7 +5,8 @@ animation = "1.7.0-beta05"
coil = "2.7.0"
composeImageBlurhash = "3.0.2"
converterGson = "2.11.0"
coreSdk = "3.8.3"
imSdk = "3.8.3.2"
imcoreSdk = "3.8.3-patch10"
coreSplashscreen = "1.0.1"
credentialsPlayServicesAuth = "1.2.2"
eventbus = "3.3.1"
@@ -42,7 +43,6 @@ zoomable = "1.6.1"
[libraries]
accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanistSystemuicontroller" }
im-sdk = { module = "io.openim:android-sdk", version.ref = "coreSdk" }
androidx-animation = { module = "androidx.compose.animation:animation", version.ref = "animation" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" }
@@ -60,7 +60,6 @@ coil = { module = "io.coil-kt:coil", version.ref = "coil" }
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
compose-image-blurhash = { module = "com.github.orlando-dev-code:compose-image-blurhash", version.ref = "composeImageBlurhash" }
converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "converterGson" }
im-core-sdk = { module = "io.openim:core-sdk", version.ref = "coreSdk" }
eventbus = { module = "org.greenrobot:eventbus", version.ref = "eventbus" }
firebase-analytics = { module = "com.google.firebase:firebase-analytics" }
firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" }
@@ -84,6 +83,8 @@ androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-man
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-material3-android = { group = "androidx.compose.material3", name = "material3-android", version.ref = "material3Android" }
im-sdk = { module = "io.openim:android-sdk", version.ref = "imSdk" }
im-core-sdk = { module = "io.openim:core-sdk", version.ref = "imcoreSdk" }
jwtdecode = { module = "com.auth0.android:jwtdecode", version.ref = "jwtdecode" }
kotlin-faker = { module = "io.github.serpro69:kotlin-faker", version.ref = "kotlinFaker" }
maps-compose = { module = "com.google.maps.android:maps-compose", version.ref = "mapsCompose" }