增加动态举报

This commit is contained in:
2024-12-06 22:08:57 +08:00
parent 76e7bbb84a
commit 50fb1874e7
8 changed files with 428 additions and 50 deletions

View File

@@ -37,5 +37,7 @@ object ConstVars {
const val DICT_KEY_GOOGLE_LOGIN_CLIENT_ID = "google_login_client_id"
// trtc功能开启
const val DICT_KEY_ENABLE_TRTC = "enable_chat"
// 举报选项
const val DICT_KEY_REPORT_OPTIONS = "report_reasons"
}

View File

@@ -0,0 +1,50 @@
package com.aiosman.ravenow.data
import android.util.Log
import com.aiosman.ravenow.ConstVars
import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.data.api.CreateReportRequestBody
import com.aiosman.ravenow.entity.ReportReasons
import com.google.gson.Gson
import com.google.gson.annotations.SerializedName
data class ReportReasonList(
@SerializedName("reasons") var reasons: ArrayList<ReportReasons>
)
interface CommonService {
suspend fun getReportReasons(): ReportReasonList
suspend fun createReport(
reportReasonId: Int,
reportType: String,
reportId: Int,
)
}
class CommonServiceImpl : CommonService {
private val dictService: DictService = DictServiceImpl()
override suspend fun getReportReasons(): ReportReasonList {
val dictItem = dictService.getDictByKey(ConstVars.DICT_KEY_REPORT_OPTIONS)
val rawJson: String = dictItem.value as? String ?: throw Exception("parse report reasons error")
val gson = Gson()
val list = gson.fromJson(rawJson, ReportReasonList::class.java)
return list
}
override suspend fun createReport(
reportReasonId: Int,
reportType: String,
reportId: Int,
) {
ApiClient.api.createReport(
CreateReportRequestBody(
reportType = reportType,
reportId = reportId,
reason = reportReasonId,
extra = "",
base64Images = emptyList()
)
)
}
}

View File

@@ -186,6 +186,21 @@ data class UpdateChatNotificationRequestBody(
val strategy: String,
)
data class CreateReportRequestBody(
@SerializedName("reportType")
val reportType: String,
@SerializedName("reportId")
val reportId: Int,
@SerializedName("reason")
val reason: Int,
@SerializedName("extra")
val extra: String,
@SerializedName("base64Images")
val base64Images: List<String>,
)
interface RaveNowAPI {
@POST("register")
suspend fun register(@Body body: RegisterRequestBody): Response<Unit>
@@ -429,5 +444,10 @@ interface RaveNowAPI {
suspend fun updateChatNotification(
@Body body: UpdateChatNotificationRequestBody
): Response<DataContainer<ChatNotification>>
@POST("reports")
suspend fun createReport(
@Body body: CreateReportRequestBody
): Response<Unit>
}

View File

@@ -0,0 +1,19 @@
package com.aiosman.ravenow.entity
import android.content.Context
import com.google.gson.annotations.SerializedName
data class ReportReasons(
@SerializedName("id") var id: Int,
@SerializedName("text") var text: Map<String, String>
) {
fun getReasonText(context:Context): String? {
val language = context.resources.configuration.locale.language
val langMapping = mapOf(
"zh" to "zh",
"en" to "en"
)
val useLang = langMapping[language] ?: "en"
return text[useLang] ?: text["en"]
}
}

View File

@@ -11,12 +11,11 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import com.aiosman.ravenow.data.MomentService
import com.aiosman.ravenow.entity.MomentServiceImpl
import com.aiosman.ravenow.data.UploadImage
import com.aiosman.ravenow.entity.MomentEntity
import com.aiosman.ravenow.entity.MomentServiceImpl
import com.aiosman.ravenow.event.MomentAddEvent
import com.aiosman.ravenow.exp.rotate
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.timeline.TimelineMomentViewModel
import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
import com.aiosman.ravenow.ui.modification.Modification
import com.aiosman.ravenow.utils.FileUtil
@@ -36,7 +35,7 @@ data class ImageItem(
val file: File
) {
companion object {
suspend fun fromUri(context: Context,uri: String): ImageItem? {
fun fromUri(context: Context,uri: String): ImageItem? {
// 保存图片文件到临时文件夹
context.contentResolver.openInputStream(Uri.parse(uri))?.use { inputStream ->
val tempFileName = UUID.randomUUID().toString()
@@ -65,10 +64,10 @@ data class ImageItem(
// 清理临时文件
tempFile.delete()
return ImageItem(
Uri.fromFile(bitmapFile).toString(),
savedBitmapFilename,
bitmap,
bitmapFile
uri = Uri.fromFile(bitmapFile).toString(),
id = savedBitmapFilename,
bitmap = bitmap,
file = bitmapFile
)
} catch (e: IOException) {
Log.e("NewPost", "Failed to save bitmap to file", e)
@@ -79,7 +78,21 @@ data class ImageItem(
}
}
}
data class DraftImageItem(
val uri: String,
val id: String,
val filename: String
){
companion object {
fun fromImageItem(imageItem: ImageItem): DraftImageItem {
return DraftImageItem(imageItem.uri, imageItem.id, imageItem.file.name)
}
}
}
data class Draft(
val textContent: String,
val imageList: List<DraftImageItem>
)
object NewPostViewModel : ViewModel() {
var momentService: MomentService = MomentServiceImpl()
var textContent by mutableStateOf("")
@@ -89,6 +102,13 @@ object NewPostViewModel : ViewModel() {
var relPostId by mutableStateOf<Int?>(null)
var relMoment by mutableStateOf<MomentEntity?>(null)
var currentPhotoUri: Uri? = null
var draft: Draft? = null
// watch textContent change and save draft
// fun saveDraft() {
// draft = Draft(textContent, imageList.map {
// DraftImageItem(it.uri, it.id, it.bitmap)
// })
// }
fun asNewPost() {
textContent = ""
searchPlaceAddressResult = null

View File

@@ -36,8 +36,10 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerDefaults
import androidx.compose.foundation.pager.rememberPagerState
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.CircularProgressIndicator
import androidx.compose.material.LinearProgressIndicator
import androidx.compose.material3.BasicAlertDialog
@@ -75,6 +77,7 @@ import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
@@ -87,16 +90,24 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.paging.LoadState
import androidx.paging.compose.collectAsLazyPagingItems
import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.ConstVars
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R
import com.aiosman.ravenow.data.CommonService
import com.aiosman.ravenow.data.CommonServiceImpl
import com.aiosman.ravenow.data.DictService
import com.aiosman.ravenow.data.DictServiceImpl
import com.aiosman.ravenow.data.ReportReasonList
import com.aiosman.ravenow.entity.CommentEntity
import com.aiosman.ravenow.entity.MomentEntity
import com.aiosman.ravenow.entity.MomentImageEntity
import com.aiosman.ravenow.entity.ReportReasons
import com.aiosman.ravenow.exp.formatPostTime
import com.aiosman.ravenow.exp.timeAgo
import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
import com.aiosman.ravenow.ui.composables.ActionButton
import com.aiosman.ravenow.ui.composables.AnimatedFavouriteIcon
import com.aiosman.ravenow.ui.composables.AnimatedLikeIcon
import com.aiosman.ravenow.ui.composables.BottomNavigationPlaceholder
@@ -107,6 +118,7 @@ import com.aiosman.ravenow.ui.composables.FollowButton
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.utils.FileUtil.saveImageToGallery
import com.google.gson.Gson
import kotlinx.coroutines.launch
import net.engawapg.lib.zoomable.rememberZoomState
import net.engawapg.lib.zoomable.zoomable
@@ -138,6 +150,7 @@ fun PostScreen(
var editCommentModalState = rememberModalBottomSheetState(
skipPartiallyExpanded = true
)
var showReportDialog by remember { mutableStateOf(false) }
val AppColors = LocalAppTheme.current
LaunchedEffect(Unit) {
@@ -250,6 +263,27 @@ fun PostScreen(
}
}
}
if (showReportDialog && viewModel.moment != null) {
ModalBottomSheet(
onDismissRequest = {
showReportDialog = false
},
containerColor = AppColors.background,
sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true
),
dragHandle = {},
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
) {
ReportModal(
momentId = viewModel.moment!!.id,
onClose = {
showReportDialog = false
}
)
}
}
Scaffold(
modifier = Modifier.fillMaxSize(),
bottomBar = {
@@ -280,7 +314,6 @@ fun PostScreen(
momentEntity = viewModel.moment
)
}
}
) {
it
@@ -332,6 +365,9 @@ fun PostScreen(
viewModel.deleteMoment {
navController.navigateUp()
}
},
onReportClick = {
showReportDialog = true
}
)
LazyColumn(
@@ -643,7 +679,8 @@ fun Header(
userId: Int?,
isFollowing: Boolean,
onFollowClick: () -> Unit,
onDeleteClick: () -> Unit = {}
onDeleteClick: () -> Unit = {},
onReportClick: () -> Unit = {}
) {
val AppColors = LocalAppTheme.current
val navController = LocalNavController.current
@@ -666,6 +703,10 @@ fun Header(
onDeleteClick = {
onDeleteClick()
expanded = false
},
onReportClick = {
onReportClick()
expanded = false
}
)
}
@@ -729,21 +770,21 @@ fun Header(
)
Spacer(modifier = Modifier.width(8.dp))
}
if (AppState.UserId == userId) {
Box {
Image(
modifier = Modifier
.height(20.dp)
.padding(end = 8.dp)
.noRippleClickable {
expanded = true
},
painter = painterResource(id = R.drawable.rider_pro_more_horizon),
contentDescription = "",
colorFilter = ColorFilter.tint(AppColors.text)
)
}
Box {
Image(
modifier = Modifier
.height(20.dp)
.padding(end = 8.dp)
.noRippleClickable {
expanded = true
},
painter = painterResource(id = R.drawable.rider_pro_more_horizon),
contentDescription = "",
colorFilter = ColorFilter.tint(AppColors.text)
)
}
}
}
@@ -1321,7 +1362,9 @@ fun PostBottomBar(
@Composable
fun PostMenuModal(
onDeleteClick: () -> Unit = {}
onDeleteClick: () -> Unit = {},
onReportClick: () -> Unit = {},
momentEntity: MomentEntity? = null
) {
val AppColors = LocalAppTheme.current
@@ -1337,37 +1380,70 @@ fun PostMenuModal(
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Column(
modifier = Modifier,
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
modifier = Modifier
.clip(CircleShape)
.noRippleClickable {
onDeleteClick()
}
momentEntity?.let {
Column(
modifier = Modifier.padding(end = 16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(id = R.drawable.rider_pro_moment_delete),
contentDescription = "",
modifier = Modifier.size(24.dp),
colorFilter = ColorFilter.tint(
AppColors.text
Box(
modifier = Modifier
.clip(CircleShape)
.noRippleClickable {
onDeleteClick()
}
) {
Image(
painter = painterResource(id = R.drawable.rider_pro_moment_delete),
contentDescription = "",
modifier = Modifier.size(24.dp),
colorFilter = ColorFilter.tint(
AppColors.text
)
)
}
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.delete),
fontSize = 11.sp,
fontWeight = FontWeight.Bold,
color = AppColors.text
)
}
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.delete),
fontSize = 11.sp,
fontWeight = FontWeight.Bold,
color = AppColors.text
)
}
}
Column(
modifier = Modifier.padding(end = 16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
modifier = Modifier
.clip(CircleShape)
.noRippleClickable {
onReportClick()
}
) {
Image(
painter = painterResource(id = R.drawable.rider_pro_moment_delete),
contentDescription = "",
modifier = Modifier.size(24.dp),
colorFilter = ColorFilter.tint(
AppColors.text
)
)
}
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.report),
fontSize = 11.sp,
fontWeight = FontWeight.Bold,
color = AppColors.text
)
}
}
}
@@ -1597,4 +1673,183 @@ fun OrderSelectionComponent(
}
}
}
@Composable
fun ReportModal(
momentId: Int,
onClose: () -> Unit = {}
) {
val AppColors = LocalAppTheme.current
val commonService : CommonService = CommonServiceImpl()
val scope = rememberCoroutineScope()
val context = LocalContext.current
var reasonMapping by remember { mutableStateOf(mutableMapOf<Int,String>()) }
var result:Boolean? by remember { mutableStateOf(null) }
fun loadReportOptions() {
scope.launch {
val reportOptions = commonService.getReportReasons()
val newReasonMapping :MutableMap<Int,String> = mutableMapOf()
reportOptions.reasons.forEach { option ->
option.getReasonText(context)?.let {
newReasonMapping[option.id] = it
}
}
reasonMapping = newReasonMapping
}
}
fun createReport(code:Int) {
scope.launch {
try {
commonService.createReport(
reportReasonId = code,
reportType = "post",
reportId = momentId
)
result = true
}catch (e:Exception) {
e.printStackTrace()
result = false
}
}
}
LaunchedEffect(Unit) {
loadReportOptions()
}
Column(
modifier = Modifier
.fillMaxWidth()
.background(AppColors.background)
.padding(start = 24.dp, end = 24.dp, bottom = 64.dp)
) {
Box(
modifier = Modifier.fillMaxWidth().padding(top = 16.dp, bottom = 16.dp),
contentAlignment = Alignment.Center
) {
Text(
stringResource(R.string.report),
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
color = AppColors.text
)
}
// divider
Box(
modifier = Modifier
.fillMaxWidth()
.height(1.dp)
.background(AppColors.divider)
) {
}
if (result == null) {
Column(
modifier = Modifier.fillMaxWidth().padding(top = 24.dp, bottom = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
stringResource(R.string.report_title),
fontSize = 20.sp,
color = AppColors.text,
textAlign = TextAlign.Center,
fontWeight = FontWeight.Bold,
)
Spacer(modifier = Modifier.height(8.dp))
Text(
stringResource(R.string.report_description),
fontSize = 14.sp,
color = AppColors.secondaryText,
textAlign = TextAlign.Center
)
}
// report options,scroll list
Column(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.verticalScroll(rememberScrollState())
) {
reasonMapping.forEach { (id, reason) ->
Column(
modifier = Modifier.noRippleClickable {
createReport(id)
}
) {
// divider
Box(
modifier = Modifier
.fillMaxWidth()
.height(1.dp)
.background(AppColors.divider)
) {
}
Row(
modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier.weight(1f),
text = reason,
fontSize = 14.sp,
color = AppColors.text
)
// right icon
Icon(
painter = painterResource(id = R.drawable.rider_pro_nav_next),
contentDescription = "",
modifier = Modifier.size(24.dp),
tint = AppColors.text
)
}
}
}
}
}else{
Column(
modifier = Modifier.fillMaxWidth().weight(1f)
) {
Box(
modifier = Modifier.fillMaxWidth().padding(top = 24.dp, bottom = 24.dp),
contentAlignment = Alignment.Center
) {
if (result == true) {
Text(
stringResource(R.string.report_success_desc),
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
color = AppColors.text
)
}
if (result == false) {
Text(
stringResource(R.string.report_fail_desc),
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
color = AppColors.text
)
}
}
Spacer(
modifier = Modifier.weight(1f)
)
ActionButton(
text = stringResource(R.string.close),
click = {
onClose()
}
)
}
}
}
}