This is a dating app
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

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,
)
}
}
}