You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
594 lines
23 KiB
594 lines
23 KiB
package com.example.dattingapp
|
|
import android.content.Intent
|
|
import android.os.Bundle
|
|
import androidx.activity.ComponentActivity
|
|
import androidx.activity.compose.setContent
|
|
import androidx.activity.enableEdgeToEdge
|
|
import androidx.compose.foundation.layout.fillMaxSize
|
|
import androidx.compose.foundation.layout.padding
|
|
import androidx.compose.material3.Text
|
|
import androidx.compose.runtime.Composable
|
|
import androidx.compose.runtime.remember
|
|
import androidx.compose.ui.Modifier
|
|
import com.example.dattingapp.ui.theme.DattingAPPTheme
|
|
import androidx.compose.foundation.background
|
|
import androidx.compose.foundation.layout.*
|
|
import androidx.compose.foundation.shape.CircleShape
|
|
import androidx.compose.material.icons.Icons
|
|
import androidx.compose.material.icons.filled.Favorite
|
|
import androidx.compose.material3.*
|
|
import androidx.compose.runtime.*
|
|
import androidx.compose.ui.Alignment
|
|
import androidx.compose.ui.draw.clip
|
|
import androidx.compose.ui.graphics.Color
|
|
import androidx.compose.ui.layout.ContentScale
|
|
import androidx.compose.ui.platform.LocalContext
|
|
import androidx.compose.ui.unit.dp
|
|
import coil.compose.AsyncImage
|
|
import android.util.Log
|
|
import androidx.compose.foundation.clickable
|
|
import androidx.compose.foundation.rememberScrollState
|
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
import androidx.compose.foundation.text.BasicTextField
|
|
import androidx.compose.foundation.verticalScroll
|
|
import androidx.compose.material.icons.filled.FavoriteBorder
|
|
import androidx.compose.material.icons.outlined.ChatBubbleOutline
|
|
import androidx.compose.ui.text.SpanStyle
|
|
import androidx.compose.ui.text.buildAnnotatedString
|
|
import androidx.compose.ui.text.font.FontWeight
|
|
import androidx.compose.ui.text.withStyle
|
|
import androidx.compose.ui.unit.sp
|
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
import com.example.dattingapp.dataclass.Author
|
|
import com.example.dattingapp.dataclass.ShowPostResponse
|
|
import com.example.dattingapp.homes.networkandapi.ShowPostUiState
|
|
import com.example.dattingapp.homes.networkandapi.ShowPostViewModel
|
|
import androidx.compose.material.icons.filled.Send
|
|
import androidx.compose.material3.CardDefaults
|
|
import androidx.compose.ui.res.painterResource
|
|
import androidx.compose.ui.text.style.TextAlign
|
|
import androidx.compose.ui.text.style.TextOverflow
|
|
import androidx.compose.ui.tooling.preview.Preview
|
|
import com.example.dattingapp.dataclass.CommentResponse
|
|
import com.example.dattingapp.dataclass.Location
|
|
import com.example.dattingapp.homes.networkandapi.TokenManager
|
|
import com.example.dattingapp.ui.theme.TextPostTextColor
|
|
|
|
val AccentPink = Color(0xFFD81B60)
|
|
val CommentBubbleBg = Color(0xFFF6F6F6)
|
|
|
|
class ShowPostingActivity : ComponentActivity() {
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
super.onCreate(savedInstanceState)
|
|
enableEdgeToEdge()
|
|
|
|
val postId = intent.getIntExtra("Post_ID", 0)
|
|
Log.d("Post_ID", "$postId")
|
|
|
|
setContent {
|
|
DattingAPPTheme {
|
|
val context = LocalContext.current
|
|
val showPostViewModel: ShowPostViewModel = viewModel()
|
|
val uiState by showPostViewModel.showPostUiState.collectAsState()
|
|
val comments by showPostViewModel.commentsUiState.collectAsState()
|
|
|
|
// Trigger API calls when the screen launches
|
|
LaunchedEffect(postId) {
|
|
showPostViewModel.fetchPostById(context, postId)
|
|
showPostViewModel.fetchComments(context, postId)
|
|
}
|
|
|
|
Scaffold(
|
|
modifier = Modifier.fillMaxSize()
|
|
) { innerPadding ->
|
|
Box(
|
|
modifier = Modifier
|
|
.fillMaxSize()
|
|
.padding(innerPadding),
|
|
) {
|
|
when (val state = uiState) {
|
|
is ShowPostUiState.Idle, is ShowPostUiState.Loading -> {
|
|
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
|
|
}
|
|
is ShowPostUiState.Success -> {
|
|
// Full screen scrollable container
|
|
Column(
|
|
modifier = Modifier
|
|
.fillMaxSize()
|
|
.verticalScroll(rememberScrollState())
|
|
.padding(bottom = 24.dp)
|
|
) {
|
|
PostCard(
|
|
post = state.post,
|
|
comments = comments,
|
|
)
|
|
}
|
|
}
|
|
is ShowPostUiState.Error -> {
|
|
Text(
|
|
text = state.message,
|
|
color = Color.Red,
|
|
modifier = Modifier.align(Alignment.Center)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun PostCard(
|
|
post: ShowPostResponse,
|
|
comments: List<CommentResponse>,
|
|
modifier: Modifier = Modifier
|
|
) {
|
|
Column(
|
|
modifier = modifier
|
|
.fillMaxWidth()
|
|
.padding(vertical = 8.dp)
|
|
) {
|
|
// --- 1. Author Header ---
|
|
Row(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(horizontal = 12.dp, vertical = 8.dp),
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
AsyncImage(
|
|
model = post.author?.profile_image_url,
|
|
contentDescription = "Post author image",
|
|
placeholder = painterResource(id = R.drawable.locationpermissionbg), // your existing placeholder
|
|
error = painterResource(id = R.drawable.locationpermissionbg),
|
|
modifier = Modifier
|
|
.size(40.dp)
|
|
.clip(CircleShape),
|
|
contentScale = ContentScale.Crop
|
|
)
|
|
|
|
Spacer(modifier = Modifier.width(8.dp))
|
|
Column {
|
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
val authorName = post.author?.full_name ?: "Anonymous"
|
|
val feelingText = post.feeling ?: ""
|
|
val context = LocalContext.current
|
|
|
|
// ✅ Clickable author name
|
|
Text(
|
|
text = authorName,
|
|
fontSize = 16.sp,
|
|
fontWeight = FontWeight.Bold,
|
|
modifier = Modifier.clickable {
|
|
val intent = Intent(context, FriendProfile::class.java).apply {
|
|
putExtra("USER_ID", post.author?.id) // ← send the user id
|
|
}
|
|
context.startActivity(intent)
|
|
}
|
|
)
|
|
|
|
// Rest of the line, unchanged
|
|
if (feelingText.isNotEmpty()) {
|
|
Text(
|
|
text = buildAnnotatedString {
|
|
withStyle(style = SpanStyle(fontWeight = FontWeight.Normal)) {
|
|
append(" — feeling ")
|
|
}
|
|
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
|
|
append(feelingText)
|
|
}
|
|
},
|
|
fontSize = 16.sp
|
|
)
|
|
}
|
|
}
|
|
|
|
if (!post.location?.name.isNullOrEmpty()) {
|
|
Text(
|
|
text = post.location?.name ?: "",
|
|
style = MaterialTheme.typography.bodySmall,
|
|
color = Color.Gray
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- 2. Post Text Content ---
|
|
|
|
|
|
// --- 3. Media Carousel (Images) ---
|
|
if (post.media.isNotEmpty()) {
|
|
|
|
if (!post.content.isNullOrEmpty()) {
|
|
Text(
|
|
text = post.content,
|
|
style = MaterialTheme.typography.bodyMedium,
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(horizontal = 12.dp, vertical = 4.dp)
|
|
)
|
|
}
|
|
Card(
|
|
shape = RoundedCornerShape(16.dp),
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(start = 10.dp, end = 10.dp)
|
|
.height(360.dp),
|
|
|
|
) {
|
|
AsyncImage(
|
|
model = post.media.first().url,
|
|
contentDescription = "Post Media Image",
|
|
contentScale = ContentScale.Crop,
|
|
modifier = Modifier.fillMaxSize()
|
|
)
|
|
}
|
|
|
|
}else{
|
|
|
|
Card(
|
|
shape = RoundedCornerShape(16.dp),
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(start = 10.dp, end = 10.dp)
|
|
.height(360.dp),
|
|
colors = CardDefaults.cardColors(
|
|
containerColor = Color.White // your background color
|
|
)
|
|
|
|
) {
|
|
Box(
|
|
modifier = Modifier.fillMaxSize(),
|
|
contentAlignment = Alignment.Center
|
|
) {
|
|
Text(
|
|
text = post.content.orEmpty(),
|
|
color = TextPostTextColor,
|
|
fontSize = if ((post.content?.length ?: 0) > 120) 18.sp else 24.sp,
|
|
fontWeight = FontWeight.Bold,
|
|
textAlign = TextAlign.Center,
|
|
maxLines = 8,
|
|
overflow = TextOverflow.Ellipsis,
|
|
modifier = Modifier.padding(horizontal = 32.dp)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- 4. Interaction Buttons ---
|
|
Row(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(horizontal = 8.dp, vertical = 4.dp),
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
IconButton(onClick = { /* Handle like action */ }) {
|
|
Icon(
|
|
imageVector = if (post.liked_by_me) Icons.Filled.Favorite else Icons.Default.FavoriteBorder,
|
|
contentDescription = "Like",
|
|
tint = if (post.liked_by_me) Color.Red else Color.Black
|
|
)
|
|
}
|
|
Text(text = "${post.like_count}")
|
|
|
|
Spacer(modifier = Modifier.width(16.dp))
|
|
|
|
IconButton(onClick = { /* Handle comments */ }) {
|
|
Icon(
|
|
imageVector = Icons.Outlined.ChatBubbleOutline,
|
|
contentDescription = "Comments"
|
|
)
|
|
}
|
|
Text(text = "${comments.size}")
|
|
}
|
|
|
|
// --- 5. Comment Section UI ---
|
|
CommentSection(
|
|
comments = comments,
|
|
postId = post.id,
|
|
postUserName = post.author?.full_name // see note below
|
|
)
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@Composable
|
|
fun CommentSection(
|
|
comments: List<CommentResponse>,
|
|
postId: Int,
|
|
postUserName: String?,
|
|
postUserId: Int? = null,
|
|
viewModel: ShowPostViewModel = viewModel()
|
|
) {
|
|
val context = LocalContext.current
|
|
var newCommentText by remember { mutableStateOf("") }
|
|
|
|
var menuCommentId by remember { mutableStateOf<Int?>(null) }
|
|
var editingCommentId by remember { mutableStateOf<Int?>(null) }
|
|
var editText by remember { mutableStateOf("") }
|
|
|
|
val tokenManager = remember { TokenManager.getInstance(context) }
|
|
val currentUserName = remember { tokenManager.currentUserName() }
|
|
val currentProfileImage = remember { tokenManager.currentProfileImage() }
|
|
// ✅ get the logged-in user's id once
|
|
val currentUserId = remember { tokenManager.getUserId() }
|
|
|
|
Column(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(horizontal = 12.dp, vertical = 8.dp)
|
|
) {
|
|
Text(
|
|
text = "Comments",
|
|
style = MaterialTheme.typography.titleSmall,
|
|
fontWeight = FontWeight.Bold,
|
|
modifier = Modifier.padding(bottom = 8.dp)
|
|
)
|
|
|
|
// Comment input bar
|
|
Row(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.background(CommentBubbleBg, shape = RoundedCornerShape(24.dp))
|
|
.padding(horizontal = 12.dp, vertical = 4.dp),
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
BasicTextField(
|
|
value = newCommentText,
|
|
onValueChange = { newCommentText = it },
|
|
modifier = Modifier
|
|
.weight(1f)
|
|
.padding(vertical = 8.dp),
|
|
decorationBox = { innerTextField ->
|
|
if (newCommentText.isEmpty()) {
|
|
Text(
|
|
text = "Add a comment...",
|
|
color = Color.Gray,
|
|
style = MaterialTheme.typography.bodyMedium
|
|
)
|
|
}
|
|
innerTextField()
|
|
}
|
|
)
|
|
|
|
IconButton(
|
|
onClick = {
|
|
if (newCommentText.isNotBlank()) {
|
|
viewModel.addCommentLocallyAndServer(
|
|
context = context,
|
|
postId = postId,
|
|
content = newCommentText,
|
|
currentUserName = currentUserName
|
|
)
|
|
newCommentText = ""
|
|
}
|
|
},
|
|
modifier = Modifier.size(32.dp)
|
|
) {
|
|
Icon(
|
|
imageVector = Icons.Default.Send,
|
|
contentDescription = "Send Comment",
|
|
tint = AccentPink,
|
|
modifier = Modifier.size(16.dp)
|
|
)
|
|
}
|
|
}
|
|
|
|
// List of comments
|
|
comments.reversed().forEach { comment ->
|
|
val isEditing = editingCommentId == comment.id
|
|
|
|
// ✅ Permission checks per comment
|
|
// adjust `comment.author?.id` to your actual model field (e.g. comment.user_id)
|
|
val commentAuthorId = comment.author?.id
|
|
val isCommentOwner = commentAuthorId != null && commentAuthorId == currentUserId
|
|
val isPostOwner = postUserId != null && postUserId == currentUserId
|
|
val canEdit = isCommentOwner
|
|
val canDelete = isCommentOwner || isPostOwner
|
|
val hasAnyAction = canEdit || canDelete
|
|
|
|
Log.d(
|
|
"CommentPerm",
|
|
"current=$currentUserId | commentAuthor=$commentAuthorId | postOwner=$postUserId | canEdit=$canEdit canDelete=$canDelete"
|
|
)
|
|
|
|
Row(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(vertical = 6.dp),
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
AsyncImage(
|
|
model = currentProfileImage,
|
|
contentDescription = "Post author image",
|
|
placeholder = painterResource(id = R.drawable.locationpermissionbg), // your existing placeholder
|
|
error = painterResource(id = R.drawable.locationpermissionbg),
|
|
modifier = Modifier
|
|
.size(32.dp)
|
|
.clip(CircleShape),
|
|
contentScale = ContentScale.Crop
|
|
)
|
|
|
|
Spacer(modifier = Modifier.width(8.dp))
|
|
|
|
Box(modifier = Modifier.weight(1f)) {
|
|
Column(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.clip(RoundedCornerShape(12.dp))
|
|
.background(CommentBubbleBg)
|
|
// ✅ only open the menu if the user can actually do something
|
|
.clickable(enabled = !isEditing && hasAnyAction) {
|
|
menuCommentId = comment.id
|
|
}
|
|
.padding(horizontal = 12.dp, vertical = 8.dp)
|
|
) {
|
|
Text(
|
|
text = comment.author?.full_name ?: "Anonymous",
|
|
style = MaterialTheme.typography.bodySmall,
|
|
fontWeight = FontWeight.Bold
|
|
)
|
|
Spacer(modifier = Modifier.height(2.dp))
|
|
|
|
if (isEditing) {
|
|
BasicTextField(
|
|
value = editText,
|
|
onValueChange = { editText = it },
|
|
modifier = Modifier.fillMaxWidth(),
|
|
textStyle = MaterialTheme.typography.bodyMedium
|
|
)
|
|
Row(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
horizontalArrangement = Arrangement.End
|
|
) {
|
|
TextButton(onClick = {
|
|
editingCommentId = null
|
|
editText = ""
|
|
}) { Text("Cancel") }
|
|
|
|
TextButton(onClick = {
|
|
if (editText.isNotBlank()) {
|
|
// viewModel.editComment(
|
|
// context = context,
|
|
// postId = postId,
|
|
// commentId = comment.id,
|
|
// newContent = editText
|
|
// )
|
|
editingCommentId = null
|
|
editText = ""
|
|
}
|
|
}) { Text("Save", color = AccentPink) }
|
|
}
|
|
} else {
|
|
Text(
|
|
text = comment.content,
|
|
style = MaterialTheme.typography.bodyMedium
|
|
)
|
|
}
|
|
}
|
|
|
|
DropdownMenu(
|
|
expanded = menuCommentId == comment.id,
|
|
onDismissRequest = { menuCommentId = null }
|
|
) {
|
|
// ✅ Edit: only the comment author
|
|
if (canEdit) {
|
|
DropdownMenuItem(
|
|
text = { Text("Edit") },
|
|
onClick = {
|
|
editText = comment.content
|
|
editingCommentId = comment.id
|
|
menuCommentId = null
|
|
}
|
|
)
|
|
}
|
|
// ✅ Delete: comment author OR post owner
|
|
if (canDelete) {
|
|
DropdownMenuItem(
|
|
text = { Text("Delete") },
|
|
onClick = {
|
|
menuCommentId = null
|
|
viewModel.deleteComment(
|
|
context = context,
|
|
postId = postId,
|
|
commentId = comment.id
|
|
)
|
|
}
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Spacer(modifier = Modifier.height(8.dp))
|
|
}
|
|
}
|
|
@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF)
|
|
@Composable
|
|
fun ShowPostScreenPreview() {
|
|
DattingAPPTheme {
|
|
// Mock sample data mirroring your API schema
|
|
val mockPost = ShowPostResponse(
|
|
id = 21,
|
|
content = "Exploring the city and enjoying some coffee! ☕🏙️ #Android #JetpackCompose",
|
|
postType = "image",
|
|
contentFormat = "markdown",
|
|
feeling = "happy 😊",
|
|
location = Location(
|
|
name = "Chiattagong",
|
|
latitude = 27.9671,
|
|
longitude = -175.0599
|
|
),
|
|
author = Author(
|
|
id = 49,
|
|
full_name = "Alex Rivera",
|
|
city = "Shanghai",
|
|
country = "China",
|
|
profile_image_url=null,
|
|
compatibilityPercentage = 95
|
|
),
|
|
like_count = 12,
|
|
comment_count = 2,
|
|
liked_by_me = true,
|
|
media = listOf(
|
|
com.example.dattingapp.dataclass.MediaItem(
|
|
id = 5,
|
|
mediaType = "image",
|
|
mimeType = "image/png",
|
|
sizeBytes = 199619,
|
|
position = 0,
|
|
url = "https://images.unsplash.com/photo-1541167760496-1628856ab772"
|
|
)
|
|
),
|
|
createdAt = "2026-10-13T06:06:15"
|
|
)
|
|
|
|
// Mock comments list
|
|
val mockComments = listOf(
|
|
CommentResponse(
|
|
id = 1,
|
|
postId = 21,
|
|
content = "This spot looks amazing! Enjoy your day! ☕",
|
|
author = Author(
|
|
id = 54,
|
|
full_name = "Sarah",
|
|
city = "Shanghai",
|
|
country = "China",
|
|
profile_image_url=null,
|
|
compatibilityPercentage = 100
|
|
),
|
|
createdAt = "2026-10-13T07:45:15"
|
|
),
|
|
CommentResponse(
|
|
id = 2,
|
|
postId = 21,
|
|
content = "What a wonderful view, I love it!",
|
|
author = Author(
|
|
id = 55,
|
|
full_name = "David",
|
|
city = "Beijing",
|
|
country = "China",
|
|
profile_image_url=null,
|
|
compatibilityPercentage = 90
|
|
),
|
|
createdAt = "2026-10-13T08:04:48"
|
|
)
|
|
)
|
|
|
|
// Full screen container matching activity layout
|
|
Column(
|
|
modifier = Modifier
|
|
.fillMaxSize()
|
|
.verticalScroll(rememberScrollState())
|
|
.padding(vertical = 16.dp)
|
|
) {
|
|
PostCard(
|
|
post = mockPost,
|
|
comments = mockComments,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|