Feat: Add News tab and related functionality
- Added a "News" tab to the main index screen. - Implemented API parameters for fetching news-specific posts: `imageTag`, `search`, `advancedSearch`, `newsFilter`, `onlyNews`, `newsSource`, `newsLanguage`, `newsCategory`, `requireImageCache`. - Updated `Moment` data class and `MomentEntity` to include news-related fields like `isNews`, `newsTitle`, `newsUrl`, etc. - Created `News.kt` composable and `NewsViewModel.kt` to display and manage news items. - Updated `MomentLoader` to include a `newsOnly` parameter for fetching only news items. - Added Japanese translations for new index tab strings: "Worldwide", "Dynamic", "Following", "Hot", and "News". - Adjusted tab count and layout based on guest/logged-in user status to accommodate the new "News" tab.
This commit is contained in:
@@ -4,11 +4,12 @@ object ConstVars {
|
|||||||
// api 地址 - 根据构建类型自动选择
|
// api 地址 - 根据构建类型自动选择
|
||||||
// Debug: http://192.168.0.201:8088
|
// Debug: http://192.168.0.201:8088
|
||||||
// Release: https://rider-pro.aiosman.com/beta_api
|
// Release: https://rider-pro.aiosman.com/beta_api
|
||||||
val BASE_SERVER = if (BuildConfig.DEBUG) {
|
// val BASE_SERVER = if (!BuildConfig.DEBUG) {
|
||||||
"http://47.109.137.67:6363" // Debug环境
|
// "http://47.109.137.67:6363" // Debug环境
|
||||||
} else {
|
// } else {
|
||||||
"https://rider-pro.aiosman.com/beta_api" // Release环境
|
// "https://rider-pro.aiosman.com/beta_api" // Release环境
|
||||||
}
|
// }
|
||||||
|
val BASE_SERVER = "https://rider-pro.aiosman.com/beta_api"
|
||||||
|
|
||||||
const val MOMENT_LIKE_CHANNEL_ID = "moment_like"
|
const val MOMENT_LIKE_CHANNEL_ID = "moment_like"
|
||||||
const val MOMENT_LIKE_CHANNEL_NAME = "Moment Like"
|
const val MOMENT_LIKE_CHANNEL_NAME = "Moment Like"
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package com.aiosman.ravenow.data
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类翻译数据模型
|
||||||
|
*/
|
||||||
|
data class CategoryTranslation(
|
||||||
|
@SerializedName("name")
|
||||||
|
val name: String,
|
||||||
|
@SerializedName("description")
|
||||||
|
val description: String
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类翻译集合
|
||||||
|
*/
|
||||||
|
data class CategoryTranslations(
|
||||||
|
@SerializedName("ja")
|
||||||
|
val ja: CategoryTranslation? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类数据模型
|
||||||
|
*/
|
||||||
|
data class Category(
|
||||||
|
@SerializedName("id")
|
||||||
|
val id: Int,
|
||||||
|
@SerializedName("name")
|
||||||
|
val name: String,
|
||||||
|
@SerializedName("description")
|
||||||
|
val description: String,
|
||||||
|
@SerializedName("avatar")
|
||||||
|
val avatar: String,
|
||||||
|
@SerializedName("parentId")
|
||||||
|
val parentId: Int? = null,
|
||||||
|
@SerializedName("sort")
|
||||||
|
val sort: Int,
|
||||||
|
@SerializedName("isActive")
|
||||||
|
val isActive: Boolean,
|
||||||
|
@SerializedName("promptCount")
|
||||||
|
val promptCount: Int,
|
||||||
|
@SerializedName("createdAt")
|
||||||
|
val createdAt: String,
|
||||||
|
@SerializedName("updatedAt")
|
||||||
|
val updatedAt: String,
|
||||||
|
@SerializedName("translations")
|
||||||
|
val translations: CategoryTranslations? = null
|
||||||
|
)
|
||||||
@@ -6,6 +6,7 @@ import com.aiosman.ravenow.data.AccountLike
|
|||||||
import com.aiosman.ravenow.data.AccountNotice
|
import com.aiosman.ravenow.data.AccountNotice
|
||||||
import com.aiosman.ravenow.data.AccountProfile
|
import com.aiosman.ravenow.data.AccountProfile
|
||||||
import com.aiosman.ravenow.data.Agent
|
import com.aiosman.ravenow.data.Agent
|
||||||
|
import com.aiosman.ravenow.data.Category
|
||||||
import com.aiosman.ravenow.data.Comment
|
import com.aiosman.ravenow.data.Comment
|
||||||
import com.aiosman.ravenow.data.DataContainer
|
import com.aiosman.ravenow.data.DataContainer
|
||||||
import com.aiosman.ravenow.data.ListContainer
|
import com.aiosman.ravenow.data.ListContainer
|
||||||
@@ -570,19 +571,39 @@ interface RaveNowAPI {
|
|||||||
@Body body: RemoveAccountRequestBody
|
@Body body: RemoveAccountRequestBody
|
||||||
): Response<Unit>
|
): Response<Unit>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@GET("outside/prompts")
|
@GET("outside/prompts")
|
||||||
suspend fun getAgent(
|
suspend fun getAgent(
|
||||||
@Query("page") page: Int = 1,
|
@Query("page") page: Int = 1,
|
||||||
@Query("pageSize") pageSize: Int = 20,
|
@Query("pageSize") pageSize: Int = 20,
|
||||||
@Query("withWorkflow") withWorkflow: Int = 1,
|
@Query("order") order: String? = null,
|
||||||
|
@Query("orderKey") orderKey: String? = null,
|
||||||
|
@Query("createdAt") createdAt: String? = null,
|
||||||
|
@Query("updatedAt") updatedAt: String? = null,
|
||||||
|
@Query("createdStart") createdStart: String? = null,
|
||||||
|
@Query("createdEnd") createdEnd: String? = null,
|
||||||
|
@Query("updatedStart") updatedStart: String? = null,
|
||||||
|
@Query("updatedEnd") updatedEnd: String? = null,
|
||||||
|
@Query("title") title: String? = null,
|
||||||
@Query("authorId") authorId: Int? = null,
|
@Query("authorId") authorId: Int? = null,
|
||||||
|
@Query("authorOpenId") authorOpenId: String? = null,
|
||||||
|
@Query("showPrivate") showPrivate: String? = null,
|
||||||
|
@Query("explore") explore: String? = null,
|
||||||
|
@Query("desc") desc: String? = null,
|
||||||
|
@Query("withWorkflow") withWorkflow: String? = null,
|
||||||
|
@Query("hasAvatar") hasAvatar: String? = null,
|
||||||
|
@Query("random") random: String? = null,
|
||||||
|
@Query("categoryName") categoryName: String? = null,
|
||||||
|
@Query("categoryIds") categoryIds: List<Int>? = null,
|
||||||
|
@Query("uncategorized") uncategorized: String? = null,
|
||||||
): Response<DataContainer<ListContainer<Agent>>>
|
): Response<DataContainer<ListContainer<Agent>>>
|
||||||
|
|
||||||
@GET("outside/my/prompts")
|
@GET("outside/my/prompts")
|
||||||
suspend fun getMyAgent(
|
suspend fun getMyAgent(
|
||||||
@Query("page") page: Int = 1,
|
@Query("page") page: Int = 1,
|
||||||
@Query("pageSize") pageSize: Int = 20,
|
@Query("pageSize") pageSize: Int = 20,
|
||||||
@Query("withWorkflow") withWorkflow: Int = 1,
|
@Query("withWorkflow") withWorkflow: String = "1",
|
||||||
): Response<ListContainer<Agent>>
|
): Response<ListContainer<Agent>>
|
||||||
|
|
||||||
@Multipart
|
@Multipart
|
||||||
@@ -633,5 +654,12 @@ interface RaveNowAPI {
|
|||||||
@POST("outside/generate/agent-info")
|
@POST("outside/generate/agent-info")
|
||||||
suspend fun generateAgentInfo(@Body body: GenerateAgentInfoRequestBody): Response<DataContainer<GenerateAgentInfoResponseBody>>
|
suspend fun generateAgentInfo(@Body body: GenerateAgentInfoRequestBody): Response<DataContainer<GenerateAgentInfoResponseBody>>
|
||||||
|
|
||||||
|
@GET("outside/categories")
|
||||||
|
suspend fun getCategories(
|
||||||
|
@Query("page") page: Int = 1,
|
||||||
|
@Query("pageSize") pageSize: Int = 20,
|
||||||
|
@Query("lang") lang: String? = null
|
||||||
|
): Response<ListContainer<Category>>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
160
app/src/main/java/com/aiosman/ravenow/entity/Category.kt
Normal file
160
app/src/main/java/com/aiosman/ravenow/entity/Category.kt
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
package com.aiosman.ravenow.entity
|
||||||
|
|
||||||
|
import androidx.paging.PagingSource
|
||||||
|
import androidx.paging.PagingState
|
||||||
|
import com.aiosman.ravenow.AppStore
|
||||||
|
import com.aiosman.ravenow.data.Category
|
||||||
|
import com.aiosman.ravenow.data.CategoryTranslation
|
||||||
|
import com.aiosman.ravenow.data.CategoryTranslations
|
||||||
|
import com.aiosman.ravenow.data.ListContainer
|
||||||
|
import com.aiosman.ravenow.data.ServiceException
|
||||||
|
import com.aiosman.ravenow.data.api.ApiClient
|
||||||
|
import com.aiosman.ravenow.utils.Utils
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类实体类
|
||||||
|
*/
|
||||||
|
data class CategoryEntity(
|
||||||
|
val id: Int,
|
||||||
|
val name: String,
|
||||||
|
val description: String,
|
||||||
|
val avatar: String,
|
||||||
|
val parentId: Int? = null,
|
||||||
|
val sort: Int,
|
||||||
|
val isActive: Boolean,
|
||||||
|
val promptCount: Int,
|
||||||
|
val createdAt: String,
|
||||||
|
val updatedAt: String,
|
||||||
|
val translations: CategoryTranslations? = null
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* 获取本地化名称
|
||||||
|
*/
|
||||||
|
fun getLocalizedName(): String {
|
||||||
|
return when (Utils.getCurrentLanguage()) {
|
||||||
|
"ja" -> translations?.ja?.name ?: name
|
||||||
|
else -> name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取本地化描述
|
||||||
|
*/
|
||||||
|
fun getLocalizedDescription(): String {
|
||||||
|
return when (Utils.getCurrentLanguage()) {
|
||||||
|
"ja" -> translations?.ja?.description ?: description
|
||||||
|
else -> description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Category 数据模型扩展函数
|
||||||
|
*/
|
||||||
|
fun Category.toCategoryEntity(): CategoryEntity {
|
||||||
|
return CategoryEntity(
|
||||||
|
id = id,
|
||||||
|
name = name,
|
||||||
|
description = description,
|
||||||
|
avatar = if (avatar.isNotEmpty()) "${ApiClient.BASE_API_URL}/outside$avatar?token=${AppStore.token}" else "",
|
||||||
|
parentId = parentId,
|
||||||
|
sort = sort,
|
||||||
|
isActive = isActive,
|
||||||
|
promptCount = promptCount,
|
||||||
|
createdAt = createdAt,
|
||||||
|
updatedAt = updatedAt,
|
||||||
|
translations = translations
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类数据后端服务
|
||||||
|
*/
|
||||||
|
class CategoryBackend {
|
||||||
|
private val DataBatchSize = 20
|
||||||
|
|
||||||
|
suspend fun getCategories(pageNumber: Int): ListContainer<CategoryEntity>? {
|
||||||
|
try {
|
||||||
|
val resp = ApiClient.api.getCategories(
|
||||||
|
page = pageNumber,
|
||||||
|
pageSize = DataBatchSize,
|
||||||
|
lang = Utils.getCurrentLanguageTag()
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!resp.isSuccessful) {
|
||||||
|
throw ServiceException("获取分类失败: ${resp.code()}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val body = resp.body() ?: return null
|
||||||
|
|
||||||
|
return ListContainer(
|
||||||
|
total = body.total,
|
||||||
|
page = pageNumber,
|
||||||
|
pageSize = DataBatchSize,
|
||||||
|
list = body.list.map { it.toCategoryEntity() }
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw ServiceException("网络请求失败: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类数据加载器参数
|
||||||
|
*/
|
||||||
|
class CategoryLoaderExtraArgs
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类数据加载器
|
||||||
|
*/
|
||||||
|
class CategoryLoader : DataLoader<CategoryEntity, CategoryLoaderExtraArgs>() {
|
||||||
|
override suspend fun fetchData(
|
||||||
|
page: Int,
|
||||||
|
pageSize: Int,
|
||||||
|
extra: CategoryLoaderExtraArgs
|
||||||
|
): ListContainer<CategoryEntity> {
|
||||||
|
val backend = CategoryBackend()
|
||||||
|
return backend.getCategories(page) ?: ListContainer(
|
||||||
|
total = 0,
|
||||||
|
page = page,
|
||||||
|
pageSize = pageSize,
|
||||||
|
list = emptyList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类分页数据源
|
||||||
|
*/
|
||||||
|
class CategoryPagingSource(
|
||||||
|
private val backend: CategoryBackend
|
||||||
|
) : PagingSource<Int, CategoryEntity>() {
|
||||||
|
|
||||||
|
override fun getRefreshKey(state: PagingState<Int, CategoryEntity>): Int? {
|
||||||
|
return state.anchorPosition?.let { anchorPosition ->
|
||||||
|
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
|
||||||
|
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, CategoryEntity> {
|
||||||
|
return try {
|
||||||
|
val page = params.key ?: 1
|
||||||
|
val response = backend.getCategories(page)
|
||||||
|
|
||||||
|
if (response == null) {
|
||||||
|
LoadResult.Error(IOException("获取分类数据失败"))
|
||||||
|
} else {
|
||||||
|
val hasMore = response.list.size == response.pageSize
|
||||||
|
LoadResult.Page(
|
||||||
|
data = response.list,
|
||||||
|
prevKey = if (page == 1) null else page - 1,
|
||||||
|
nextKey = if (hasMore) page + 1 else null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LoadResult.Error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -253,9 +253,10 @@ fun Agent() {
|
|||||||
// )
|
// )
|
||||||
// }
|
// }
|
||||||
|
|
||||||
var selectedTabIndex by remember { mutableStateOf(0) }
|
// 使用 ViewModel 中的选中状态
|
||||||
|
val selectedTabIndex = viewModel.selectedCategoryIndex
|
||||||
|
|
||||||
// 标签页
|
// 动态标签页
|
||||||
LazyRow(
|
LazyRow(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -264,83 +265,27 @@ fun Agent() {
|
|||||||
horizontalArrangement = Arrangement.Start,
|
horizontalArrangement = Arrangement.Start,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
item {
|
viewModel.categories.forEachIndexed { index, category ->
|
||||||
CustomTabItem(
|
item {
|
||||||
text = stringResource(R.string.agent_recommend),
|
CustomTabItem(
|
||||||
isSelected = selectedTabIndex == 0,
|
text = category.getLocalizedName(),
|
||||||
onClick = {
|
isSelected = selectedTabIndex == index,
|
||||||
selectedTabIndex = 0
|
onClick = {
|
||||||
|
viewModel.selectCategory(index)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < viewModel.categories.size - 1) {
|
||||||
|
item {
|
||||||
|
TabSpacer()
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
when (selectedTabIndex) {
|
// 显示当前选中分类的 Agent 数据
|
||||||
0 -> {
|
AgentViewPagerSection(agentItems = viewModel.agentItems.take(15), viewModel)
|
||||||
AgentViewPagerSection(agentItems = viewModel.agentItems.take(15), viewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
val shuffledAgents = viewModel.agentItems.shuffled().take(15)
|
|
||||||
AgentViewPagerSection(agentItems = shuffledAgents, viewModel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 推荐聊天房间
|
// 推荐聊天房间
|
||||||
@@ -496,7 +441,7 @@ fun AgentCardSquare(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(top = avatarSize / 2 + 8.dp, start = 8.dp, end = 8.dp, bottom = 8.dp),
|
.padding(top = avatarSize / 2 + 8.dp, start = 8.dp, end = 8.dp, bottom = 48.dp), // 为底部聊天按钮留出空间
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
androidx.compose.material3.Text(
|
androidx.compose.material3.Text(
|
||||||
@@ -518,43 +463,43 @@ fun AgentCardSquare(
|
|||||||
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
|
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
|
||||||
modifier = Modifier.weight(1f, fill = false)
|
modifier = Modifier.weight(1f, fill = false)
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
// 聊天按钮,固定在底部居中,距离底部有一定边距
|
||||||
|
Box(
|
||||||
// 聊天按钮,位于底部居中
|
modifier = Modifier
|
||||||
Box(
|
.align(Alignment.BottomCenter)
|
||||||
modifier = Modifier
|
.padding(bottom = 12.dp) // 距离底部的边距
|
||||||
.width(80.dp)
|
.width(80.dp)
|
||||||
.height(32.dp)
|
.height(32.dp)
|
||||||
.background(
|
.background(
|
||||||
color = AppColors.inputBackground,
|
color = AppColors.inputBackground,
|
||||||
shape = RoundedCornerShape(8.dp)
|
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
|
|
||||||
)
|
)
|
||||||
}
|
.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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -797,7 +742,7 @@ fun ChatRoomsSection(
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
androidx.compose.material3.Text(
|
androidx.compose.material3.Text(
|
||||||
text = "推荐聊天房间",
|
text = stringResource(R.string.agent_chat_room),
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
fontWeight = androidx.compose.ui.text.font.FontWeight.W600,
|
fontWeight = androidx.compose.ui.text.font.FontWeight.W600,
|
||||||
color = AppColors.text
|
color = AppColors.text
|
||||||
@@ -845,59 +790,62 @@ fun ChatRoomCard(
|
|||||||
// 防抖状态
|
// 防抖状态
|
||||||
var lastClickTime by remember { mutableStateOf(0L) }
|
var lastClickTime by remember { mutableStateOf(0L) }
|
||||||
|
|
||||||
Column(
|
// 正方形卡片,文字重叠在底部
|
||||||
modifier = modifier,
|
Box(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
modifier = modifier
|
||||||
) {
|
.size(cardSize)
|
||||||
// 正方形卡片
|
.background(AppColors.secondaryBackground, RoundedCornerShape(12.dp))
|
||||||
Box(
|
.clickable {
|
||||||
modifier = Modifier
|
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
|
||||||
.size(cardSize)
|
// 这里可以添加进入聊天房间的逻辑
|
||||||
.background(AppColors.secondaryBackground, RoundedCornerShape(12.dp))
|
// navController.navigate(NavigationRoute.ChatRoom.route.replace("{id}", chatRoom.id))
|
||||||
.clickable {
|
}) {
|
||||||
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
|
lastClickTime = System.currentTimeMillis()
|
||||||
// 这里可以添加进入聊天房间的逻辑
|
}
|
||||||
// navController.navigate(NavigationRoute.ChatRoom.route.replace("{id}", chatRoom.id))
|
|
||||||
}) {
|
|
||||||
lastClickTime = System.currentTimeMillis()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
// 优先显示banner,如果没有banner则显示头像
|
|
||||||
val imageUrl = if (chatRoom.banner.isNotEmpty()) chatRoom.banner else chatRoom.avatar
|
|
||||||
|
|
||||||
if (imageUrl.isNotEmpty()) {
|
|
||||||
CustomAsyncImage(
|
|
||||||
imageUrl = imageUrl,
|
|
||||||
contentDescription = if (chatRoom.banner.isNotEmpty()) "房间banner" else "房间头像",
|
|
||||||
modifier = Modifier
|
|
||||||
.size(cardSize)
|
|
||||||
.clip(RoundedCornerShape(12.dp)),
|
|
||||||
contentScale = androidx.compose.ui.layout.ContentScale.Crop
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// 默认房间图标
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.mipmap.rider_pro_agent),
|
|
||||||
contentDescription = "默认房间图标",
|
|
||||||
modifier = Modifier.size(cardSize * 0.4f),
|
|
||||||
colorFilter = ColorFilter.tint(AppColors.secondaryText)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
) {
|
||||||
|
// 优先显示banner,如果没有banner则显示头像
|
||||||
|
val imageUrl = if (chatRoom.banner.isNotEmpty()) chatRoom.banner else chatRoom.avatar
|
||||||
|
|
||||||
|
if (imageUrl.isNotEmpty()) {
|
||||||
|
CustomAsyncImage(
|
||||||
|
imageUrl = imageUrl,
|
||||||
|
contentDescription = if (chatRoom.banner.isNotEmpty()) "房间banner" else "房间头像",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(cardSize)
|
||||||
|
.clip(RoundedCornerShape(12.dp)),
|
||||||
|
contentScale = androidx.compose.ui.layout.ContentScale.Crop
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// 默认房间图标
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.mipmap.rider_pro_agent),
|
||||||
|
contentDescription = "默认房间图标",
|
||||||
|
modifier = Modifier.size(cardSize * 0.4f),
|
||||||
|
colorFilter = ColorFilter.tint(AppColors.secondaryText)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
// 房间名称,重叠在底部
|
||||||
|
Box(
|
||||||
// 房间名称
|
modifier = Modifier
|
||||||
androidx.compose.material3.Text(
|
.align(Alignment.BottomCenter)
|
||||||
text = chatRoom.name,
|
.fillMaxWidth()
|
||||||
fontSize = 12.sp,
|
.background(
|
||||||
color = AppColors.text,
|
color = Color.Black.copy(alpha = 0.6f),
|
||||||
maxLines = 1,
|
shape = RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp)
|
||||||
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
|
)
|
||||||
modifier = Modifier.fillMaxWidth(),
|
.padding(horizontal = 8.dp, vertical = 6.dp)
|
||||||
textAlign = androidx.compose.ui.text.style.TextAlign.Center
|
) {
|
||||||
)
|
androidx.compose.material3.Text(
|
||||||
|
text = chatRoom.name,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = Color.White,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
textAlign = androidx.compose.ui.text.style.TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import com.aiosman.ravenow.ui.index.tabs.ai.tabs.mine.MineAgentViewModel.createG
|
|||||||
import com.aiosman.ravenow.ui.NavigationRoute
|
import com.aiosman.ravenow.ui.NavigationRoute
|
||||||
import com.aiosman.ravenow.ui.index.tabs.message.MessageListViewModel.userService
|
import com.aiosman.ravenow.ui.index.tabs.message.MessageListViewModel.userService
|
||||||
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.expolre.AgentItem
|
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.expolre.AgentItem
|
||||||
|
import com.aiosman.ravenow.entity.CategoryEntity
|
||||||
|
import com.aiosman.ravenow.entity.CategoryBackend
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
data class ChatRoom(
|
data class ChatRoom(
|
||||||
@@ -40,6 +42,12 @@ object AgentViewModel: ViewModel() {
|
|||||||
var rooms by mutableStateOf<List<Room>>(emptyList())
|
var rooms by mutableStateOf<List<Room>>(emptyList())
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
var categories by mutableStateOf<List<CategoryEntity>>(emptyList())
|
||||||
|
private set
|
||||||
|
|
||||||
|
var selectedCategoryIndex by mutableStateOf(0)
|
||||||
|
private set
|
||||||
|
|
||||||
var errorMessage by mutableStateOf<String?>(null)
|
var errorMessage by mutableStateOf<String?>(null)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
@@ -55,16 +63,32 @@ object AgentViewModel: ViewModel() {
|
|||||||
init {
|
init {
|
||||||
loadAgentData()
|
loadAgentData()
|
||||||
loadChatRooms()
|
loadChatRooms()
|
||||||
|
loadCategories()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadAgentData() {
|
private fun loadAgentData(categoryIndex: Int = 0) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
errorMessage = null
|
errorMessage = null
|
||||||
currentPage = 1
|
currentPage = 1
|
||||||
hasMoreData = true
|
hasMoreData = true
|
||||||
try {
|
try {
|
||||||
val response = apiClient.getAgent(page = currentPage, pageSize = 20, withWorkflow = 1)
|
val selectedCategory = if (categoryIndex < categories.size) categories[categoryIndex] else null
|
||||||
|
|
||||||
|
val response = if (categoryIndex == 0 || selectedCategory == null) {
|
||||||
|
// 推荐分类或无效分类,加载所有 Agent
|
||||||
|
apiClient.getAgent(page = currentPage, pageSize = 20, withWorkflow = "1", random = "true")
|
||||||
|
} else {
|
||||||
|
// 特定分类,使用 categoryName 参数
|
||||||
|
apiClient.getAgent(
|
||||||
|
page = currentPage,
|
||||||
|
pageSize = 20,
|
||||||
|
withWorkflow = "1",
|
||||||
|
categoryName = selectedCategory.name,
|
||||||
|
random = "true"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
val agents = response.body()?.data?.list ?: emptyList()
|
val agents = response.body()?.data?.list ?: emptyList()
|
||||||
agentItems = agents.map { agent ->
|
agentItems = agents.map { agent ->
|
||||||
@@ -85,9 +109,13 @@ object AgentViewModel: ViewModel() {
|
|||||||
private fun loadChatRooms() {
|
private fun loadChatRooms() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
val response = apiClient.getRooms(page = 1, pageSize = 20, isRecommended = 1, random = 1)
|
val response = apiClient.getRooms(page = 1, pageSize = 21, isRecommended = 1, random = 1) // 请求21个,确保是3的倍数
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
rooms = response.body()?.list ?: emptyList()
|
val allRooms = response.body()?.list ?: emptyList()
|
||||||
|
// 确保房间数量是3的倍数,如果不足则截取,如果超出则取前几个
|
||||||
|
val targetCount = (allRooms.size / 3) * 3 // 向下取整到最近的3的倍数
|
||||||
|
rooms = allRooms.take(targetCount)
|
||||||
|
|
||||||
// 转换为ChatRoom格式用于兼容现有UI
|
// 转换为ChatRoom格式用于兼容现有UI
|
||||||
chatRooms = rooms.map { room ->
|
chatRooms = rooms.map { room ->
|
||||||
ChatRoom(
|
ChatRoom(
|
||||||
@@ -108,6 +136,44 @@ object AgentViewModel: ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun loadCategories() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
val categoryBackend = CategoryBackend()
|
||||||
|
val response = categoryBackend.getCategories(1)
|
||||||
|
if (response != null) {
|
||||||
|
// 添加一个默认的"推荐"分类在第一位
|
||||||
|
val recommendCategory = CategoryEntity(
|
||||||
|
id = 0,
|
||||||
|
name = "推荐",
|
||||||
|
description = "推荐内容",
|
||||||
|
avatar = "",
|
||||||
|
parentId = null,
|
||||||
|
sort = 0,
|
||||||
|
isActive = true,
|
||||||
|
promptCount = 0,
|
||||||
|
createdAt = "",
|
||||||
|
updatedAt = "",
|
||||||
|
translations = null
|
||||||
|
)
|
||||||
|
categories = listOf(recommendCategory) + response.list
|
||||||
|
|
||||||
|
// 分类加载完成后,重新加载当前选中分类的 Agent 数据
|
||||||
|
if (agentItems.isEmpty()) {
|
||||||
|
loadAgentData(selectedCategoryIndex)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果请求失败,使用默认分类
|
||||||
|
categories = listOf()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// 如果网络请求失败,使用默认分类
|
||||||
|
categories = listOf()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 加载更多Agent数据
|
* 加载更多Agent数据
|
||||||
*/
|
*/
|
||||||
@@ -118,7 +184,21 @@ object AgentViewModel: ViewModel() {
|
|||||||
isLoading = true
|
isLoading = true
|
||||||
try {
|
try {
|
||||||
val nextPage = currentPage + 1
|
val nextPage = currentPage + 1
|
||||||
val response = apiClient.getAgent(page = nextPage, pageSize = 20, withWorkflow = 1)
|
val selectedCategory = if (selectedCategoryIndex < categories.size) categories[selectedCategoryIndex] else null
|
||||||
|
|
||||||
|
val response = if (selectedCategoryIndex == 0 || selectedCategory == null) {
|
||||||
|
// 推荐分类或无效分类,加载所有 Agent
|
||||||
|
apiClient.getAgent(page = nextPage, pageSize = 20, withWorkflow = "1")
|
||||||
|
} else {
|
||||||
|
// 特定分类,使用 categoryName 参数
|
||||||
|
apiClient.getAgent(
|
||||||
|
page = nextPage,
|
||||||
|
pageSize = 20,
|
||||||
|
withWorkflow = "1",
|
||||||
|
categoryName = selectedCategory.name
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
val agents = response.body()?.data?.list ?: emptyList()
|
val agents = response.body()?.data?.list ?: emptyList()
|
||||||
val newAgentItems = agents.map { agent ->
|
val newAgentItems = agents.map { agent ->
|
||||||
@@ -137,6 +217,17 @@ object AgentViewModel: ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选择分类并加载对应的 Agent 数据
|
||||||
|
*/
|
||||||
|
fun selectCategory(categoryIndex: Int) {
|
||||||
|
if (categoryIndex != selectedCategoryIndex) {
|
||||||
|
selectedCategoryIndex = categoryIndex
|
||||||
|
loadAgentData(categoryIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun createSingleChat(
|
fun createSingleChat(
|
||||||
openId: String,
|
openId: String,
|
||||||
) {
|
) {
|
||||||
@@ -195,6 +286,8 @@ object AgentViewModel: ViewModel() {
|
|||||||
*/
|
*/
|
||||||
fun ResetModel() {
|
fun ResetModel() {
|
||||||
agentItems = emptyList()
|
agentItems = emptyList()
|
||||||
|
categories = emptyList()
|
||||||
|
selectedCategoryIndex = 0
|
||||||
errorMessage = null
|
errorMessage = null
|
||||||
isRefreshing = false
|
isRefreshing = false
|
||||||
isLoading = false
|
isLoading = false
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ class ExploreViewModel : ViewModel() {
|
|||||||
isRefreshing = true
|
isRefreshing = true
|
||||||
errorMessage = null
|
errorMessage = null
|
||||||
try {
|
try {
|
||||||
val response = apiClient.getAgent(page = 1, pageSize = 20, withWorkflow = 1)
|
val response = apiClient.getAgent(page = 1, pageSize = 20, withWorkflow = "1")
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
val agents = response.body()?.data?.list ?: emptyList()
|
val agents = response.body()?.data?.list ?: emptyList()
|
||||||
agentItems = agents.map { agent ->
|
agentItems = agents.map { agent ->
|
||||||
@@ -114,7 +114,7 @@ class ExploreViewModel : ViewModel() {
|
|||||||
isLoading = true
|
isLoading = true
|
||||||
errorMessage = null
|
errorMessage = null
|
||||||
try {
|
try {
|
||||||
val response = apiClient.getAgent(page = 1, pageSize = 20, withWorkflow = 1)
|
val response = apiClient.getAgent(page = 1, pageSize = 20, withWorkflow = "1")
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
val agents = response.body()?.data?.list ?: emptyList()
|
val agents = response.body()?.data?.list ?: emptyList()
|
||||||
agentItems = agents.map { agent ->
|
agentItems = agents.map { agent ->
|
||||||
|
|||||||
@@ -63,6 +63,23 @@ object Utils {
|
|||||||
return Locale.getDefault().language
|
return Locale.getDefault().language
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前系统语言的完整标签,包含国家/地区代码
|
||||||
|
* 返回格式:en, ja, zh-CN, zh-TW 等
|
||||||
|
*/
|
||||||
|
fun getCurrentLanguageTag(): String {
|
||||||
|
val locale = Locale.getDefault()
|
||||||
|
val language = locale.language
|
||||||
|
val country = locale.country
|
||||||
|
|
||||||
|
return when {
|
||||||
|
// 中文需要区分简体和繁体
|
||||||
|
language == "zh" && country.isNotEmpty() -> "$language-$country"
|
||||||
|
// 其他语言通常只需要语言代码
|
||||||
|
else -> language
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun compressImage(context: Context, uri: Uri, maxSize: Int = 512, quality: Int = 85): File {
|
fun compressImage(context: Context, uri: Uri, maxSize: Int = 512, quality: Int = 85): File {
|
||||||
val inputStream = context.contentResolver.openInputStream(uri)
|
val inputStream = context.contentResolver.openInputStream(uri)
|
||||||
val originalBitmap = BitmapFactory.decodeStream(inputStream)
|
val originalBitmap = BitmapFactory.decodeStream(inputStream)
|
||||||
|
|||||||
@@ -33,4 +33,28 @@
|
|||||||
<string name="index_following">フォロー中</string>
|
<string name="index_following">フォロー中</string>
|
||||||
<string name="index_hot">ホット</string>
|
<string name="index_hot">ホット</string>
|
||||||
<string name="index_news">ニュース</string>
|
<string name="index_news">ニュース</string>
|
||||||
|
|
||||||
|
<!-- Main navigation -->
|
||||||
|
<string name="main_home">ホーム</string>
|
||||||
|
<string name="main_ai">エージェント</string>
|
||||||
|
<string name="main_message">メッセージ</string>
|
||||||
|
<string name="main_profile">プロフィール</string>
|
||||||
|
|
||||||
|
<!-- Agent related strings -->
|
||||||
|
<string name="agent_mine">マイ</string>
|
||||||
|
<string name="agent_hot">ホット</string>
|
||||||
|
<string name="agent_recommend">おすすめ</string>
|
||||||
|
<string name="agent_other">その他</string>
|
||||||
|
<string name="agent_find">AI発見</string>
|
||||||
|
<string name="chat">チャット</string>
|
||||||
|
<string name="agent_chat_room">チャット</string>
|
||||||
|
<string name="agent_recommended_chat_rooms">おすすめチャットルーム</string>
|
||||||
|
<string name="search">検索</string>
|
||||||
|
<string name="agent_recommend_agent">おすすめエージェント</string>
|
||||||
|
|
||||||
|
<!-- Chat related -->
|
||||||
|
<string name="chat_ai">AI</string>
|
||||||
|
<string name="chat_group">グループ</string>
|
||||||
|
<string name="chat_friend">友達</string>
|
||||||
|
<string name="hot_rooms">人気チャットルーム</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -188,7 +188,7 @@
|
|||||||
<string name="group_room_enter_success">成功加入房间</string>
|
<string name="group_room_enter_success">成功加入房间</string>
|
||||||
<string name="group_room_enter_fail">加入房间失败</string>
|
<string name="group_room_enter_fail">加入房间失败</string>
|
||||||
<string name="agent_createing">创建中...</string>
|
<string name="agent_createing">创建中...</string>
|
||||||
<string name="agent_find">发现</string>
|
<string name="agent_find">发现AI</string>
|
||||||
<string name="text_error_password_too_long">密码不能超过 %1$d 个字符</string>
|
<string name="text_error_password_too_long">密码不能超过 %1$d 个字符</string>
|
||||||
|
|
||||||
<!-- Create Bottom Sheet -->
|
<!-- Create Bottom Sheet -->
|
||||||
@@ -217,5 +217,9 @@
|
|||||||
<string name="create_agent_v2_ai_enhance_icon_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_edit_icon_desc">编辑图标</string>
|
||||||
<string name="create_agent_v2_select_avatar_desc">选择头像</string>
|
<string name="create_agent_v2_select_avatar_desc">选择头像</string>
|
||||||
|
|
||||||
|
<!-- Agent related strings -->
|
||||||
|
<string name="agent_chat_room">聊天</string>
|
||||||
|
<string name="agent_recommended_chat_rooms">推荐聊天房间</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
@@ -185,7 +185,7 @@
|
|||||||
<string name="group_room_enter_success">成功加入房间</string>
|
<string name="group_room_enter_success">成功加入房间</string>
|
||||||
<string name="group_room_enter_fail">加入房间失败</string>
|
<string name="group_room_enter_fail">加入房间失败</string>
|
||||||
<string name="agent_createing">创建中...</string>
|
<string name="agent_createing">创建中...</string>
|
||||||
<string name="agent_find">发现</string>
|
<string name="agent_find">发现AI</string>
|
||||||
<string name="text_error_password_too_long">Password cannot exceed %1$d characters</string>
|
<string name="text_error_password_too_long">Password cannot exceed %1$d characters</string>
|
||||||
|
|
||||||
<!-- Create Bottom Sheet -->
|
<!-- Create Bottom Sheet -->
|
||||||
@@ -214,5 +214,9 @@
|
|||||||
<string name="create_agent_v2_ai_enhance_icon_desc">AI Enhancement Icon</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_edit_icon_desc">Edit Icon</string>
|
||||||
<string name="create_agent_v2_select_avatar_desc">Select Avatar</string>
|
<string name="create_agent_v2_select_avatar_desc">Select Avatar</string>
|
||||||
|
|
||||||
|
<!-- Agent related strings -->
|
||||||
|
<string name="agent_chat_room">Chat</string>
|
||||||
|
<string name="agent_recommended_chat_rooms">Recommended Chat Rooms</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
Reference in New Issue
Block a user