Browse Source

feed update

master
osman160128 1 week ago
parent
commit
7873619494
  1. 246
      app/src/main/java/com/example/dattingapp/ShowPostingActivity.kt
  2. 99
      app/src/main/java/com/example/dattingapp/homes/ExploreScreen.kt
  3. 155
      app/src/main/java/com/example/dattingapp/homes/MeetScreen.kt
  4. 59
      app/src/main/java/com/example/dattingapp/homes/MyProfile.kt
  5. 17
      app/src/main/java/com/example/dattingapp/homes/networkandapi/HomeApiService.kt
  6. 142
      app/src/main/java/com/example/dattingapp/homes/networkandapi/HomeDataClass.kt
  7. 133
      app/src/main/java/com/example/dattingapp/homes/networkandapi/HomeViewModelClass.kt

246
app/src/main/java/com/example/dattingapp/ShowPostingActivity.kt

@ -1,57 +1,57 @@
package com.example.dattingapp package com.example.dattingapp
import android.app.Activity
import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge 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.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.FavoriteBorder
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Send
import androidx.compose.material.icons.outlined.ChatBubbleOutline
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.res.painterResource
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.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight 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.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.AsyncImage
import com.example.dattingapp.dataclass.Author import com.example.dattingapp.dataclass.Author
import com.example.dattingapp.dataclass.CommentResponse
import com.example.dattingapp.dataclass.Location
import com.example.dattingapp.dataclass.ShowPostResponse import com.example.dattingapp.dataclass.ShowPostResponse
import com.example.dattingapp.homes.networkandapi.PostAllLikerResponse
import com.example.dattingapp.homes.networkandapi.ShowPostUiState import com.example.dattingapp.homes.networkandapi.ShowPostUiState
import com.example.dattingapp.homes.networkandapi.ShowPostViewModel 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.homes.networkandapi.TokenManager
import com.example.dattingapp.homes.networkandapi.rememberPostInteraction
import com.example.dattingapp.ui.theme.DattingAPPTheme
import com.example.dattingapp.ui.theme.TextPostTextColor import com.example.dattingapp.ui.theme.TextPostTextColor
val AccentPink = Color(0xFFD81B60) val AccentPink = Color(0xFFD81B60)
@ -71,11 +71,12 @@ class ShowPostingActivity : ComponentActivity() {
val showPostViewModel: ShowPostViewModel = viewModel() val showPostViewModel: ShowPostViewModel = viewModel()
val uiState by showPostViewModel.showPostUiState.collectAsState() val uiState by showPostViewModel.showPostUiState.collectAsState()
val comments by showPostViewModel.commentsUiState.collectAsState() val comments by showPostViewModel.commentsUiState.collectAsState()
val postLikers by showPostViewModel.postLikers.collectAsState()
// Trigger API calls when the screen launches
LaunchedEffect(postId) { LaunchedEffect(postId) {
showPostViewModel.fetchPostById(context, postId) showPostViewModel.fetchPostById(context, postId)
showPostViewModel.fetchComments(context, postId) showPostViewModel.fetchComments(context, postId)
showPostViewModel.fetchPostLikers(context, postId)
} }
Scaffold( Scaffold(
@ -91,7 +92,6 @@ class ShowPostingActivity : ComponentActivity() {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
} }
is ShowPostUiState.Success -> { is ShowPostUiState.Success -> {
// Full screen scrollable container
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@ -99,8 +99,11 @@ class ShowPostingActivity : ComponentActivity() {
.padding(bottom = 24.dp) .padding(bottom = 24.dp)
) { ) {
PostCard( PostCard(
context = context,
showPostViewModel = showPostViewModel,
post = state.post, post = state.post,
comments = comments, comments = comments,
postLikers = postLikers
) )
} }
} }
@ -121,10 +124,26 @@ class ShowPostingActivity : ComponentActivity() {
@Composable @Composable
fun PostCard( fun PostCard(
context: Context,
showPostViewModel: ShowPostViewModel,
post: ShowPostResponse, post: ShowPostResponse,
comments: List<CommentResponse>, comments: List<CommentResponse>,
postLikers: List<PostAllLikerResponse>,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
// Live like/comment values shared with the feed screen
val live = rememberPostInteraction(post.id)
val likedByMe = live?.likedByMe ?: post.liked_by_me
val likeCount = live?.likeCount ?: post.like_count
val commentCount = live?.commentCount ?: comments.size
// Owner check + menu state
val tokenManager = remember { TokenManager.getInstance(context) }
val currentUserId = remember { tokenManager.getUserId() }
val isPostOwner = post.author?.id != null && post.author?.id == currentUserId
var showPostMenu by remember { mutableStateOf(false) }
var showDeleteDialog by remember { mutableStateOf(false) }
Column( Column(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
@ -140,7 +159,7 @@ fun PostCard(
AsyncImage( AsyncImage(
model = post.author?.profile_image_url, model = post.author?.profile_image_url,
contentDescription = "Post author image", contentDescription = "Post author image",
placeholder = painterResource(id = R.drawable.locationpermissionbg), // your existing placeholder placeholder = painterResource(id = R.drawable.locationpermissionbg),
error = painterResource(id = R.drawable.locationpermissionbg), error = painterResource(id = R.drawable.locationpermissionbg),
modifier = Modifier modifier = Modifier
.size(40.dp) .size(40.dp)
@ -149,26 +168,24 @@ fun PostCard(
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Column {
Column(modifier = Modifier.weight(1f)) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
val authorName = post.author?.full_name ?: "Anonymous" val authorName = post.author?.full_name ?: "Anonymous"
val feelingText = post.feeling ?: "" val feelingText = post.feeling ?: ""
val context = LocalContext.current
// ✅ Clickable author name
Text( Text(
text = authorName, text = authorName,
fontSize = 16.sp, fontSize = 16.sp,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
modifier = Modifier.clickable { modifier = Modifier.clickable {
val intent = Intent(context, FriendProfile::class.java).apply { val intent = Intent(context, FriendProfile::class.java).apply {
putExtra("USER_ID", post.author?.id) // ← send the user id putExtra("USER_ID", post.author?.id)
} }
context.startActivity(intent) context.startActivity(intent)
} }
) )
// Rest of the line, unchanged
if (feelingText.isNotEmpty()) { if (feelingText.isNotEmpty()) {
Text( Text(
text = buildAnnotatedString { text = buildAnnotatedString {
@ -192,12 +209,40 @@ fun PostCard(
) )
} }
} }
}
// --- 2. Post Text Content ---
// Three-dot menu: only for the post owner
if (isPostOwner) {
Box {
IconButton(onClick = { showPostMenu = true }) {
Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = "Post options"
)
}
DropdownMenu(
expanded = showPostMenu,
onDismissRequest = { showPostMenu = false }
) {
DropdownMenuItem(
text = { Text("Edit") },
onClick = {
showPostMenu = false
// TODO: edit post (empty for now)
}
)
DropdownMenuItem(
text = { Text("Delete", color = Color.Red) },
onClick = {
showPostMenu = false
showDeleteDialog = true
}
)
}
}
}
}
// --- 3. Media Carousel (Images) --- // --- 2. Media / text content ---
if (post.media.isNotEmpty()) { if (post.media.isNotEmpty()) {
if (!post.content.isNullOrEmpty()) { if (!post.content.isNullOrEmpty()) {
@ -215,7 +260,6 @@ fun PostCard(
.fillMaxWidth() .fillMaxWidth()
.padding(start = 10.dp, end = 10.dp) .padding(start = 10.dp, end = 10.dp)
.height(360.dp), .height(360.dp),
) { ) {
AsyncImage( AsyncImage(
model = post.media.first().url, model = post.media.first().url,
@ -225,7 +269,7 @@ fun PostCard(
) )
} }
}else{ } else {
Card( Card(
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
@ -233,10 +277,7 @@ fun PostCard(
.fillMaxWidth() .fillMaxWidth()
.padding(start = 10.dp, end = 10.dp) .padding(start = 10.dp, end = 10.dp)
.height(360.dp), .height(360.dp),
colors = CardDefaults.cardColors( colors = CardDefaults.cardColors(containerColor = Color.White)
containerColor = Color.White // your background color
)
) { ) {
Box( Box(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
@ -256,21 +297,23 @@ fun PostCard(
} }
} }
// --- 4. Interaction Buttons --- // --- 3. Interaction Buttons ---
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 4.dp), .padding(horizontal = 8.dp, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
IconButton(onClick = { /* Handle like action */ }) { IconButton(onClick = {
showPostViewModel.toggleLikeLocallyAndServer(context, post.id)
}) {
Icon( Icon(
imageVector = if (post.liked_by_me) Icons.Filled.Favorite else Icons.Default.FavoriteBorder, imageVector = if (likedByMe) Icons.Filled.Favorite else Icons.Default.FavoriteBorder,
contentDescription = "Like", contentDescription = "Like",
tint = if (post.liked_by_me) Color.Red else Color.Black tint = if (likedByMe) Color.Red else Color.Black
) )
} }
Text(text = "${post.like_count}") Text(text = "$likeCount")
Spacer(modifier = Modifier.width(16.dp)) Spacer(modifier = Modifier.width(16.dp))
@ -280,19 +323,71 @@ fun PostCard(
contentDescription = "Comments" contentDescription = "Comments"
) )
} }
Text(text = "${comments.size}") Text(text = "$commentCount")
}
// --- 4. Likers Summary Text ---
val currentUserName = tokenManager.currentUserName() ?: "You"
val displayLikersText = when {
postLikers.isNotEmpty() -> {
val primaryLikerName = postLikers.first().fullName
val otherCount = postLikers.size - 1
if (otherCount > 0) {
"$primaryLikerName and $otherCount other liked this"
} else {
"$primaryLikerName liked this"
}
}
likedByMe && likeCount > 0 -> {
if (likeCount > 1) {
"$currentUserName and ${likeCount - 1} other liked this"
} else {
"$currentUserName liked this"
}
}
else -> null
}
if (displayLikersText != null) {
Text(
text = displayLikersText,
style = MaterialTheme.typography.bodySmall,
fontWeight = FontWeight.Bold,
color = Color.Gray,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 2.dp)
)
} }
// --- 5. Comment Section UI --- // --- 5. Comment Section UI ---
CommentSection( CommentSection(
comments = comments, comments = comments,
postId = post.id, postId = post.id,
postUserName = post.author?.full_name // see note below postUserName = post.author?.full_name,
postUserId = post.author?.id
) )
} }
}
// --- Delete confirmation dialog ---
if (showDeleteDialog) {
AlertDialog(
onDismissRequest = { showDeleteDialog = false },
title = { Text("Delete post?") },
text = { Text("This can't be undone.") },
confirmButton = {
TextButton(onClick = {
showDeleteDialog = false
showPostViewModel.deletePost(context, post.id) {
(context as? Activity)?.finish() // close screen after delete
}
}) { Text("Delete", color = Color.Red) }
},
dismissButton = {
TextButton(onClick = { showDeleteDialog = false }) { Text("Cancel") }
}
)
}
}
@Composable @Composable
fun CommentSection( fun CommentSection(
@ -311,8 +406,6 @@ fun CommentSection(
val tokenManager = remember { TokenManager.getInstance(context) } val tokenManager = remember { TokenManager.getInstance(context) }
val currentUserName = remember { tokenManager.currentUserName() } val currentUserName = remember { tokenManager.currentUserName() }
val currentProfileImage = remember { tokenManager.currentProfileImage() }
// ✅ get the logged-in user's id once
val currentUserId = remember { tokenManager.getUserId() } val currentUserId = remember { tokenManager.getUserId() }
Column( Column(
@ -380,8 +473,6 @@ fun CommentSection(
comments.reversed().forEach { comment -> comments.reversed().forEach { comment ->
val isEditing = editingCommentId == comment.id 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 commentAuthorId = comment.author?.id
val isCommentOwner = commentAuthorId != null && commentAuthorId == currentUserId val isCommentOwner = commentAuthorId != null && commentAuthorId == currentUserId
val isPostOwner = postUserId != null && postUserId == currentUserId val isPostOwner = postUserId != null && postUserId == currentUserId
@ -389,11 +480,6 @@ fun CommentSection(
val canDelete = isCommentOwner || isPostOwner val canDelete = isCommentOwner || isPostOwner
val hasAnyAction = canEdit || canDelete val hasAnyAction = canEdit || canDelete
Log.d(
"CommentPerm",
"current=$currentUserId | commentAuthor=$commentAuthorId | postOwner=$postUserId | canEdit=$canEdit canDelete=$canDelete"
)
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -401,9 +487,9 @@ fun CommentSection(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
AsyncImage( AsyncImage(
model = currentProfileImage, model = comment.author?.profile_image_url,
contentDescription = "Post author image", contentDescription = "Comment author image",
placeholder = painterResource(id = R.drawable.locationpermissionbg), // your existing placeholder placeholder = painterResource(id = R.drawable.locationpermissionbg),
error = painterResource(id = R.drawable.locationpermissionbg), error = painterResource(id = R.drawable.locationpermissionbg),
modifier = Modifier modifier = Modifier
.size(32.dp) .size(32.dp)
@ -419,7 +505,6 @@ fun CommentSection(
.fillMaxWidth() .fillMaxWidth()
.clip(RoundedCornerShape(12.dp)) .clip(RoundedCornerShape(12.dp))
.background(CommentBubbleBg) .background(CommentBubbleBg)
// ✅ only open the menu if the user can actually do something
.clickable(enabled = !isEditing && hasAnyAction) { .clickable(enabled = !isEditing && hasAnyAction) {
menuCommentId = comment.id menuCommentId = comment.id
} }
@ -450,12 +535,7 @@ fun CommentSection(
TextButton(onClick = { TextButton(onClick = {
if (editText.isNotBlank()) { if (editText.isNotBlank()) {
// viewModel.editComment( // viewModel.editComment(...) not implemented yet
// context = context,
// postId = postId,
// commentId = comment.id,
// newContent = editText
// )
editingCommentId = null editingCommentId = null
editText = "" editText = ""
} }
@ -473,7 +553,6 @@ fun CommentSection(
expanded = menuCommentId == comment.id, expanded = menuCommentId == comment.id,
onDismissRequest = { menuCommentId = null } onDismissRequest = { menuCommentId = null }
) { ) {
// ✅ Edit: only the comment author
if (canEdit) { if (canEdit) {
DropdownMenuItem( DropdownMenuItem(
text = { Text("Edit") }, text = { Text("Edit") },
@ -484,7 +563,6 @@ fun CommentSection(
} }
) )
} }
// ✅ Delete: comment author OR post owner
if (canDelete) { if (canDelete) {
DropdownMenuItem( DropdownMenuItem(
text = { Text("Delete") }, text = { Text("Delete") },
@ -506,11 +584,14 @@ fun CommentSection(
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
} }
} }
@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF) @Preview(showBackground = true, backgroundColor = 0xFFFFFFFF)
@Composable @Composable
fun ShowPostScreenPreview() { fun ShowPostScreenPreview() {
DattingAPPTheme { DattingAPPTheme {
// Mock sample data mirroring your API schema val context = LocalContext.current
val showPostViewModel: ShowPostViewModel = viewModel()
val mockPost = ShowPostResponse( val mockPost = ShowPostResponse(
id = 21, id = 21,
content = "Exploring the city and enjoying some coffee! ☕🏙️ #Android #JetpackCompose", content = "Exploring the city and enjoying some coffee! ☕🏙️ #Android #JetpackCompose",
@ -527,7 +608,7 @@ fun ShowPostScreenPreview() {
full_name = "Alex Rivera", full_name = "Alex Rivera",
city = "Shanghai", city = "Shanghai",
country = "China", country = "China",
profile_image_url=null, profile_image_url = null,
compatibilityPercentage = 95 compatibilityPercentage = 95
), ),
like_count = 12, like_count = 12,
@ -546,7 +627,6 @@ fun ShowPostScreenPreview() {
createdAt = "2026-10-13T06:06:15" createdAt = "2026-10-13T06:06:15"
) )
// Mock comments list
val mockComments = listOf( val mockComments = listOf(
CommentResponse( CommentResponse(
id = 1, id = 1,
@ -557,7 +637,7 @@ fun ShowPostScreenPreview() {
full_name = "Sarah", full_name = "Sarah",
city = "Shanghai", city = "Shanghai",
country = "China", country = "China",
profile_image_url=null, profile_image_url = null,
compatibilityPercentage = 100 compatibilityPercentage = 100
), ),
createdAt = "2026-10-13T07:45:15" createdAt = "2026-10-13T07:45:15"
@ -571,14 +651,19 @@ fun ShowPostScreenPreview() {
full_name = "David", full_name = "David",
city = "Beijing", city = "Beijing",
country = "China", country = "China",
profile_image_url=null, profile_image_url = null,
compatibilityPercentage = 90 compatibilityPercentage = 90
), ),
createdAt = "2026-10-13T08:04:48" createdAt = "2026-10-13T08:04:48"
) )
) )
// Full screen container matching activity layout val mockLikers = listOf(
PostAllLikerResponse(id = 1, fullName = "Parvej"),
PostAllLikerResponse(id = 2, fullName = "Sarah"),
PostAllLikerResponse(id = 3, fullName = "David")
)
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@ -586,8 +671,11 @@ fun ShowPostScreenPreview() {
.padding(vertical = 16.dp) .padding(vertical = 16.dp)
) { ) {
PostCard( PostCard(
context = context,
showPostViewModel = showPostViewModel,
post = mockPost, post = mockPost,
comments = mockComments, comments = mockComments,
postLikers = mockLikers
) )
} }
} }

99
app/src/main/java/com/example/dattingapp/homes/ExploreScreen.kt

@ -6,7 +6,6 @@ import androidx.media3.common.MediaItem as ExoMediaItem
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.indication
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
@ -23,11 +22,8 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
@ -39,6 +35,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.media3.common.Player import androidx.media3.common.Player
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.PlayerView import androidx.media3.ui.PlayerView
@ -49,16 +46,15 @@ import com.example.dattingapp.dataclass.Author
import com.example.dattingapp.dataclass.Location import com.example.dattingapp.dataclass.Location
import com.example.dattingapp.dataclass.MediaItem import com.example.dattingapp.dataclass.MediaItem
import com.example.dattingapp.dataclass.ShowPostResponse import com.example.dattingapp.dataclass.ShowPostResponse
import com.example.dattingapp.homes.networkandapi.PostResponse import com.example.dattingapp.homes.networkandapi.PostInteractionStore // NEW
import com.example.dattingapp.homes.networkandapi.rememberPostInteraction // NEW
import com.example.dattingapp.homes.networkandapi.ShowPostViewModel
import com.example.dattingapp.homes.networkandapi.TokenManager import com.example.dattingapp.homes.networkandapi.TokenManager
import com.example.dattingapp.homes.profile.UserResponse
import com.example.dattingapp.ui.theme.DattingAPPTheme import com.example.dattingapp.ui.theme.DattingAPPTheme
import com.example.dattingapp.ui.theme.OverlayShadow import com.example.dattingapp.ui.theme.OverlayShadow
import com.example.dattingapp.ui.theme.TextPostTextColor import com.example.dattingapp.ui.theme.TextPostTextColor
import com.example.dattingapp.ui.theme.lightChipBg import com.example.dattingapp.ui.theme.lightChipBg
import com.example.dattingapp.ui.theme.primaryColor import com.example.dattingapp.ui.theme.primaryColor
import kotlinx.serialization.SerialName
// Data Models // Data Models
data class VibeCategory( data class VibeCategory(
@ -67,7 +63,6 @@ data class VibeCategory(
val isSelected: Boolean = false val isSelected: Boolean = false
) )
data class UserMatch( data class UserMatch(
val id: String, val id: String,
val name: String, val name: String,
@ -77,11 +72,18 @@ data class UserMatch(
@Composable @Composable
fun VibeCheckHomeScreen( fun VibeCheckHomeScreen(
feedResponse: List<ShowPostResponse> = emptyList(), // Change from MyProfileData? to MyProfileResponse? feedResponse: List<ShowPostResponse> = emptyList(),
isLoading: Boolean, isLoading: Boolean,
errorMessage: String?, errorMessage: String?,
onRetry: () -> Unit = {}, onRetry: () -> Unit = {},
) { ) {
// NEW: put feed data into the shared store (ignored for likes while a like is pending)
LaunchedEffect(feedResponse) {
feedResponse.forEach {
PostInteractionStore.setFromServer(it.id, it.liked_by_me, it.like_count, it.comment_count)
}
}
Scaffold( Scaffold(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
topBar = { topBar = {
@ -99,14 +101,6 @@ fun VibeCheckHomeScreen(
SearchBarSection() SearchBarSection()
} }
// item {
// VibeFilterChipsSection(
// selectedColor = primaryColor,
// unselectedBg = lightChipBg
// )
// }
// One card per post from the API (replaces both hardcoded cards)
items(feedResponse, key = { it.id }) { post -> items(feedResponse, key = { it.id }) { post ->
FeaturedEventCard( FeaturedEventCard(
post = post, post = post,
@ -114,7 +108,6 @@ fun VibeCheckHomeScreen(
) )
} }
item { item {
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
} }
@ -149,7 +142,7 @@ private fun VibeCheckTopAppBar(
AsyncImage( AsyncImage(
model = currentProfileImage, model = currentProfileImage,
contentDescription = "Post author image", contentDescription = "Post author image",
placeholder = painterResource(id = com.example.dattingapp.R.drawable.locationpermissionbg), // your existing placeholder placeholder = painterResource(id = R.drawable.locationpermissionbg),
error = painterResource(id = R.drawable.locationpermissionbg), error = painterResource(id = R.drawable.locationpermissionbg),
modifier = Modifier modifier = Modifier
.size(40.dp) .size(40.dp)
@ -160,7 +153,7 @@ private fun VibeCheckTopAppBar(
Spacer(modifier = Modifier.width(2.dp)) Spacer(modifier = Modifier.width(2.dp))
Text( Text(
text = currentUserName?:"", text = currentUserName ?: "",
color = primaryColor, color = primaryColor,
fontSize = 20.sp, fontSize = 20.sp,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
@ -195,9 +188,7 @@ private fun SearchBarSection() {
) )
}, },
trailingIcon = { trailingIcon = {
Row( Row(verticalAlignment = Alignment.CenterVertically) {
verticalAlignment = Alignment.CenterVertically
) {
Icon( Icon(
imageVector = Icons.Outlined.Image, imageVector = Icons.Outlined.Image,
contentDescription = null, contentDescription = null,
@ -225,8 +216,8 @@ private fun SearchBarSection() {
.fillMaxWidth() .fillMaxWidth()
.fillMaxHeight() .fillMaxHeight()
) )
} }
@Composable @Composable
private fun VibeFilterChipsSection( private fun VibeFilterChipsSection(
selectedColor: Color, selectedColor: Color,
@ -271,14 +262,20 @@ private fun VibeFilterChipsSection(
@Composable @Composable
private fun FeaturedEventCard( private fun FeaturedEventCard(
post: ShowPostResponse, post: ShowPostResponse,
badgeColor: Color badgeColor: Color,
showPostViewModel: ShowPostViewModel = viewModel()
) { ) {
val context = LocalContext.current val context = LocalContext.current
// NEW: live like/comment values shared with the details screen
val live = rememberPostInteraction(post.id)
val likedByMe = live?.likedByMe ?: post.liked_by_me
val likeCount = live?.likeCount ?: post.like_count
val commentCount = live?.commentCount ?: post.comment_count
val hasMedia = post.media.isNotEmpty() val hasMedia = post.media.isNotEmpty()
val tags = listOfNotNull(post.feeling, post.location?.name) val tags = listOfNotNull(post.feeling, post.location?.name)
// White over photos, dark over the white text-only card
val contentColor = if (hasMedia) Color.White else TextPostTextColor val contentColor = if (hasMedia) Color.White else TextPostTextColor
val shadowStyle = if (hasMedia) TextStyle(shadow = OverlayShadow) else TextStyle.Default val shadowStyle = if (hasMedia) TextStyle(shadow = OverlayShadow) else TextStyle.Default
@ -304,7 +301,7 @@ private fun FeaturedEventCard(
if (hasMedia) { if (hasMedia) {
PostMediaPager(media = post.media, modifier = Modifier.fillMaxSize()) PostMediaPager(media = post.media, modifier = Modifier.fillMaxSize())
// 2. Top scrim (keeps the badge readable) // 2. Top scrim
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -317,7 +314,7 @@ private fun FeaturedEventCard(
) )
) )
// 3. Bottom scrim (keeps author, text and tags readable) // 3. Bottom scrim
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -344,11 +341,13 @@ private fun FeaturedEventCard(
) )
} }
// 4. Badge (it has its own colored background, so white text is fine) // 4. Badge
Surface( Surface(
color = badgeColor, color = badgeColor,
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(12.dp),
modifier = Modifier.padding(12.dp).align(Alignment.TopStart) modifier = Modifier
.padding(12.dp)
.align(Alignment.TopStart)
) { ) {
Row( Row(
modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp), modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp),
@ -374,7 +373,7 @@ private fun FeaturedEventCard(
} }
} }
// 5. Like / comment, on a dark pill when over a photo // 5. Like / comment pillar
Column( Column(
modifier = Modifier modifier = Modifier
.align(Alignment.BottomEnd) .align(Alignment.BottomEnd)
@ -382,20 +381,22 @@ private fun FeaturedEventCard(
.clip(RoundedCornerShape(24.dp)) .clip(RoundedCornerShape(24.dp))
.padding(vertical = 4.dp, horizontal = 2.dp), .padding(vertical = 4.dp, horizontal = 2.dp),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp) // space BETWEEN like and comment groups verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
// Like + count // Like + count
Column(horizontalAlignment = Alignment.CenterHorizontally) { Column(horizontalAlignment = Alignment.CenterHorizontally) {
IconButton(onClick = {
showPostViewModel.toggleLikeLocallyAndServer(context, post.id)
}) {
Icon( Icon(
imageVector = if (post.liked_by_me) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder, imageVector = if (likedByMe) Icons.Default.Favorite else Icons.Outlined.FavoriteBorder,
contentDescription = "Like", contentDescription = "Like",
tint = if (post.liked_by_me) Color.Red else contentColor, tint = if (likedByMe) Color.Red else contentColor,
modifier = Modifier modifier = Modifier.size(28.dp)
.size(28.dp)
.clickable { }
) )
}
Text( Text(
text = "${post.like_count}", text = "$likeCount",
color = contentColor, color = contentColor,
fontSize = 12.sp, fontSize = 12.sp,
style = shadowStyle style = shadowStyle
@ -408,12 +409,10 @@ private fun FeaturedEventCard(
imageVector = Icons.Outlined.ChatBubbleOutline, imageVector = Icons.Outlined.ChatBubbleOutline,
contentDescription = "Comment", contentDescription = "Comment",
tint = contentColor, tint = contentColor,
modifier = Modifier modifier = Modifier.size(28.dp)
.size(28.dp)
.clickable { }
) )
Text( Text(
text = "${post.comment_count}", text = "$commentCount",
color = contentColor, color = contentColor,
fontSize = 12.sp, fontSize = 12.sp,
style = shadowStyle style = shadowStyle
@ -422,8 +421,11 @@ private fun FeaturedEventCard(
} }
// 6. Bottom info // 6. Bottom info
Column(modifier = Modifier.align(Alignment.BottomStart) Column(
.padding(start = 16.dp, top = 16.dp, bottom = 16.dp, end = 72.dp)) { modifier = Modifier
.align(Alignment.BottomStart)
.padding(start = 16.dp, top = 16.dp, bottom = 16.dp, end = 72.dp)
) {
Text( Text(
text = post.author?.full_name ?: "Anonymous", text = post.author?.full_name ?: "Anonymous",
color = contentColor, color = contentColor,
@ -468,6 +470,7 @@ private fun FeaturedEventCard(
} }
} }
} }
@Composable @Composable
private fun PostMediaPager(media: List<MediaItem>, modifier: Modifier = Modifier) { private fun PostMediaPager(media: List<MediaItem>, modifier: Modifier = Modifier) {
val pagerState = rememberPagerState(pageCount = { media.size }) val pagerState = rememberPagerState(pageCount = { media.size })
@ -490,12 +493,13 @@ private fun PostMediaPager(media: List<MediaItem>, modifier: Modifier = Modifier
} }
} }
// "1/3" counter, only when there is more than one item
if (media.size > 1) { if (media.size > 1) {
Surface( Surface(
color = Color.Black.copy(alpha = 0.5f), color = Color.Black.copy(alpha = 0.5f),
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(12.dp),
modifier = Modifier.align(Alignment.TopEnd).padding(12.dp) modifier = Modifier
.align(Alignment.TopEnd)
.padding(12.dp)
) { ) {
Text( Text(
text = "${pagerState.currentPage + 1}/${media.size}", text = "${pagerState.currentPage + 1}/${media.size}",
@ -521,7 +525,6 @@ private fun PostVideoPlayer(url: String, isActive: Boolean) {
} }
} }
// Pause when the user swipes to another page
LaunchedEffect(isActive) { LaunchedEffect(isActive) {
if (!isActive) player.pause() if (!isActive) player.pause()
} }

155
app/src/main/java/com/example/dattingapp/homes/MeetScreen.kt

@ -7,6 +7,7 @@ import androidx.compose.animation.core.Animatable
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
@ -23,9 +24,12 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -40,9 +44,16 @@ import com.example.dattingapp.homes.networkandapi.SwipeViewModel
import com.example.dattingapp.homes.networkandapi.TokenManager import com.example.dattingapp.homes.networkandapi.TokenManager
import com.example.dattingapp.ui.theme.BrandPink import com.example.dattingapp.ui.theme.BrandPink
import com.example.dattingapp.ui.theme.BrandPurple import com.example.dattingapp.ui.theme.BrandPurple
import kotlinx.coroutines.launch
import java.time.LocalDate import java.time.LocalDate
import java.time.Period import java.time.Period
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import kotlin.math.abs
// Direction strings sent to the backend. CHECK "superlike" matches what your API expects.
private const val DIR_LIKE = "like"
private const val DIR_PASS = "pass"
private const val DIR_SUPER_LIKE = "superlike"
@Composable @Composable
fun MeetDiscoveryScreen( fun MeetDiscoveryScreen(
@ -138,16 +149,17 @@ fun MeetDiscoveryScreen(
ProfileCardItem( ProfileCardItem(
profile = currentProfile, profile = currentProfile,
onSuperLike = { onSuperLike = {
handleSwipeAction(context, DIR_SUPER_LIKE, currentProfile, swipeViewModel)
onSuperLikeClick(currentProfile) onSuperLikeClick(currentProfile)
currentIndex++ currentIndex++
}, },
onReject = { onReject = {
handleSwipeAction(context,direction="pass", currentProfile, swipeViewModel) handleSwipeAction(context, DIR_PASS, currentProfile, swipeViewModel)
onRejectClick(currentProfile) onRejectClick(currentProfile)
currentIndex++ currentIndex++
}, },
onLike = { onLike = {
handleSwipeAction(context,direction="like", currentProfile, swipeViewModel) handleSwipeAction(context, DIR_LIKE, currentProfile, swipeViewModel)
onLikeClick(currentProfile) onLikeClick(currentProfile)
currentIndex++ currentIndex++
} }
@ -175,10 +187,15 @@ private fun ProfileCardItem(
onLike: () -> Unit onLike: () -> Unit
) { ) {
val context = LocalContext.current val context = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
// Reset drag offset whenever the profile changes
// Drag offsets. Reset whenever the profile changes.
val offsetX = remember(profile) { Animatable(0f) } val offsetX = remember(profile) { Animatable(0f) }
val offsetY = remember(profile) { Animatable(0f) }
// Distance (in px) the card must be dragged before it counts as a swipe
val threshold = with(LocalDensity.current) { 120.dp.toPx() }
val flyDistance = with(LocalDensity.current) { 700.dp.toPx() }
val photoUrl = remember(profile) { val photoUrl = remember(profile) {
profile.media profile.media
@ -196,9 +213,69 @@ private fun ProfileCardItem(
listOfNotNull(dist, loc.takeIf { it.isNotBlank() }).joinToString(" • ") listOfNotNull(dist, loc.takeIf { it.isNotBlank() }).joinToString(" • ")
} }
// How strongly each stamp should show (0f..1f), based on the current drag
val x = offsetX.value
val y = offsetY.value
val verticalDominant = abs(y) > abs(x)
val likeAlpha = if (!verticalDominant) (x / threshold).coerceIn(0f, 1f) else 0f
val nopeAlpha = if (!verticalDominant) (-x / threshold).coerceIn(0f, 1f) else 0f
val superAlpha = if (verticalDominant) (-y / threshold).coerceIn(0f, 1f) else 0f
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
// pointerInput comes BEFORE graphicsLayer so drag deltas are not
// measured in the moved/rotated coordinates (that would cause jitter)
.pointerInput(profile) {
detectDragGestures(
onDrag = { change, dragAmount ->
change.consume()
scope.launch {
offsetX.snapTo(offsetX.value + dragAmount.x)
offsetY.snapTo(offsetY.value + dragAmount.y)
}
},
onDragEnd = {
scope.launch {
val dx = offsetX.value
val dy = offsetY.value
when {
// UP -> super like
dy < -threshold && abs(dy) > abs(dx) -> {
offsetY.animateTo(-flyDistance)
onSuperLike()
}
// RIGHT -> like
dx > threshold -> {
offsetX.animateTo(flyDistance)
onLike()
}
// LEFT -> reject
dx < -threshold -> {
offsetX.animateTo(-flyDistance)
onReject()
}
// Not far enough: snap back
else -> {
launch { offsetX.animateTo(0f) }
launch { offsetY.animateTo(0f) }
}
}
}
},
onDragCancel = {
scope.launch {
launch { offsetX.animateTo(0f) }
launch { offsetY.animateTo(0f) }
}
}
)
}
.graphicsLayer {
translationX = offsetX.value
translationY = offsetY.value
rotationZ = offsetX.value / 40f // slight tilt while dragging sideways
}
.clip(RoundedCornerShape(20.dp)) .clip(RoundedCornerShape(20.dp))
) { ) {
val token = remember(context) { val token = remember(context) {
@ -209,7 +286,7 @@ private fun ProfileCardItem(
if (!photoUrl.isNullOrBlank()) { if (!photoUrl.isNullOrBlank()) {
AsyncImage( AsyncImage(
model = ImageRequest.Builder(context) model = ImageRequest.Builder(context)
.data(photoUrl) // ← guaranteed non-null here .data(photoUrl)
.apply { .apply {
if (!token.isNullOrBlank()) { if (!token.isNullOrBlank()) {
val authHeader = val authHeader =
@ -230,7 +307,6 @@ private fun ProfileCardItem(
} }
) )
} else { } else {
// No URL at all → show the default directly, Coil never runs
Image( Image(
painter = defaultPainter, painter = defaultPainter,
contentDescription = "Profile Image", contentDescription = "Profile Image",
@ -251,6 +327,31 @@ private fun ProfileCardItem(
) )
) )
// SWIPE STAMPS (fade in as you drag)
SwipeStamp(
text = "LIKE",
color = Color(0xFF2ECC71),
alpha = likeAlpha,
modifier = Modifier
.align(Alignment.TopStart)
.padding(24.dp)
)
SwipeStamp(
text = "NOPE",
color = Color(0xFFE74C3C),
alpha = nopeAlpha,
modifier = Modifier
.align(Alignment.TopEnd)
.padding(24.dp)
)
SwipeStamp(
text = "SUPER LIKE",
color = Color(0xFF3498DB),
alpha = superAlpha,
modifier = Modifier
.align(Alignment.Center)
)
// ACTION BUTTONS (Right Side) // ACTION BUTTONS (Right Side)
Column( Column(
modifier = Modifier modifier = Modifier
@ -323,12 +424,10 @@ private fun ProfileCardItem(
.fillMaxWidth() .fillMaxWidth()
.clickable { .clickable {
val intent = Intent(context, FriendProfile::class.java).apply { val intent = Intent(context, FriendProfile::class.java).apply {
putExtra("USER_ID",profile.id ) putExtra("USER_ID", profile.id)
} }
context.startActivity(intent) context.startActivity(intent)
}, },
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
@ -403,7 +502,32 @@ private fun ProfileCardItem(
} }
} }
//Helper to calculate age from birth date string (e.g., "1999-05-15") // The LIKE / NOPE / SUPER LIKE label that fades in while dragging
@Composable
private fun SwipeStamp(
text: String,
color: Color,
alpha: Float,
modifier: Modifier = Modifier
) {
if (alpha <= 0f) return
Surface(
modifier = modifier.graphicsLayer { this.alpha = alpha },
color = Color.Transparent,
shape = RoundedCornerShape(8.dp),
border = androidx.compose.foundation.BorderStroke(3.dp, color)
) {
Text(
text = text,
color = color,
fontSize = 28.sp,
fontWeight = FontWeight.ExtraBold,
modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp)
)
}
}
// Helper to calculate age from birth date string (e.g., "1999-05-15")
private fun calculateAge(birthDateStr: String?): Int? { private fun calculateAge(birthDateStr: String?): Int? {
if (birthDateStr.isNullOrBlank()) return null if (birthDateStr.isNullOrBlank()) return null
return try { return try {
@ -415,20 +539,17 @@ private fun calculateAge(birthDateStr: String?): Int? {
} }
} }
// Helper method for reject swipe network call // Sends the swipe (like / pass / superlike) to the server
private fun handleSwipeAction( private fun handleSwipeAction(
context: Context, context: Context,
direction:String, direction: String,
profile: MeetUserProfile, profile: MeetUserProfile,
swipeViewModel: SwipeViewModel swipeViewModel: SwipeViewModel
) { ) {
val targetUserId = profile.id val swipeRequest = SwipeRequest(profile.id, direction)
val swipeRequest = SwipeRequest(targetUserId, direction)
swipeViewModel.sendSwipeResponse(context, swipeRequest) swipeViewModel.sendSwipeResponse(context, swipeRequest)
} }
// ========================================== // ==========================================
// PREVIEWS & MOCK DATA // PREVIEWS & MOCK DATA
// ========================================== // ==========================================
@ -443,7 +564,7 @@ private val mockProfiles = listOf(
distanceKm = 4.2, distanceKm = 4.2,
bio = "Coffee enthusiast, UI designer, and full-time dog lover 🐶✨ Looking for someone to explore new coffee shops with!", bio = "Coffee enthusiast, UI designer, and full-time dog lover 🐶✨ Looking for someone to explore new coffee shops with!",
compatibilityPercentage = 92, compatibilityPercentage = 92,
media = null, // Triggers fallback default Person icon media = null,
gender = "Male", gender = "Male",
videoUrl = null videoUrl = null
), ),

59
app/src/main/java/com/example/dattingapp/homes/MyProfile.kt

@ -11,12 +11,10 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.TextSnippet
import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.MusicNote import androidx.compose.material.icons.filled.MusicNote
import androidx.compose.material.icons.filled.TextSnippet import androidx.compose.material.icons.filled.TextSnippet
import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
@ -35,15 +33,14 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.example.dattingapp.LikeANdMatchActivity import com.example.dattingapp.LikeANdMatchActivity
import com.example.dattingapp.MasonryPostItem
import com.example.dattingapp.R import com.example.dattingapp.R
import com.example.dattingapp.ShowPostingActivity import com.example.dattingapp.ShowPostingActivity
import com.example.dattingapp.StaggeredFriendPostsGrid import com.example.dattingapp.homes.networkandapi.PostInteractionStore // NEW
import com.example.dattingapp.homes.profile.Author import com.example.dattingapp.homes.profile.Author
import com.example.dattingapp.homes.profile.Media import com.example.dattingapp.homes.profile.Media
import com.example.dattingapp.homes.profile.UserResponse
import com.example.dattingapp.homes.profile.Post import com.example.dattingapp.homes.profile.Post
import com.example.dattingapp.homes.profile.ProfileData import com.example.dattingapp.homes.profile.ProfileData
import com.example.dattingapp.homes.profile.UserResponse
import com.example.dattingapp.ui.theme.BrandMagenta import com.example.dattingapp.ui.theme.BrandMagenta
import com.example.dattingapp.ui.theme.BrandPurple import com.example.dattingapp.ui.theme.BrandPurple
import com.example.dattingapp.ui.theme.DattingAPPTheme import com.example.dattingapp.ui.theme.DattingAPPTheme
@ -84,6 +81,10 @@ fun MyProfileScreen(
val profile = profileResponse?.profile val profile = profileResponse?.profile
val age = calculateAge(profile?.birth_date) val age = calculateAge(profile?.birth_date)
// NEW: posts deleted anywhere in the app
val deletedIds by PostInteractionStore.deletedIds.collectAsState()
val visiblePosts = (profileResponse?.posts ?: emptyList()).filter { it.id !in deletedIds }
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@ -171,7 +172,6 @@ fun MyProfileScreen(
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 8.dp) .padding(horizontal = 24.dp, vertical = 8.dp)
) { ) {
// NAME, AGE & EDIT BUTTON
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@ -188,12 +188,10 @@ fun MyProfileScreen(
color = textDark color = textDark
) )
} }
} }
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
// LOCATION & STATUS
val locationText = listOfNotNull(profile?.city, profile?.country).joinToString(", ") val locationText = listOfNotNull(profile?.city, profile?.country).joinToString(", ")
if (locationText.isNotBlank()) { if (locationText.isNotBlank()) {
Text( Text(
@ -204,7 +202,6 @@ fun MyProfileScreen(
Spacer(modifier = Modifier.height(6.dp)) Spacer(modifier = Modifier.height(6.dp))
} }
// BIO CONTAINER
if (!profile?.bio.isNullOrBlank()) { if (!profile?.bio.isNullOrBlank()) {
Card( Card(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@ -223,7 +220,6 @@ fun MyProfileScreen(
Spacer(modifier = Modifier.height(10.dp)) Spacer(modifier = Modifier.height(10.dp))
} }
// INTERESTS CHIPS
if (!profile?.interests.isNullOrEmpty()) { if (!profile?.interests.isNullOrEmpty()) {
FlowRow( FlowRow(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@ -348,7 +344,7 @@ fun MyProfileScreen(
Spacer(modifier = Modifier.height(20.dp)) Spacer(modifier = Modifier.height(20.dp))
// ==================================================== // ====================================================
// ALREADY POSTED / DYNAMIC POSTS SECTION // POSTED SECTION
// ==================================================== // ====================================================
Text( Text(
text = "Posted", text = "Posted",
@ -359,14 +355,12 @@ fun MyProfileScreen(
) )
Spacer(modifier = Modifier.height(10.dp)) Spacer(modifier = Modifier.height(10.dp))
ProfilePostsGrid(posts = profileResponse?.posts ?: emptyList()) ProfilePostsGrid(posts = visiblePosts) // CHANGED: deleted posts are filtered out
Spacer(modifier = Modifier.height(30.dp)) Spacer(modifier = Modifier.height(30.dp))
} }
} }
// ============================================================ // ============================================================
// STAT CARD // STAT CARD
// ============================================================ // ============================================================
@ -406,11 +400,9 @@ private fun StatCard(
} }
} }
// ============================================================ // ============================================================
// STAGGERED PROFILE POSTS GRID (Matches your custom Masonry layout) // STAGGERED PROFILE POSTS GRID
// ============================================================ // ============================================================
@Composable @Composable
private fun ProfilePostsGrid(posts: List<Post>) { private fun ProfilePostsGrid(posts: List<Post>) {
if (posts.isEmpty()) { if (posts.isEmpty()) {
@ -425,17 +417,12 @@ private fun ProfilePostsGrid(posts: List<Post>) {
return return
} }
// Distribute posts into two columns
val columns = remember(posts) { val columns = remember(posts) {
val col1 = mutableListOf<Post>() val col1 = mutableListOf<Post>()
val col2 = mutableListOf<Post>() val col2 = mutableListOf<Post>()
posts.forEachIndexed { index, post -> posts.forEachIndexed { index, post ->
if (index % 2 == 0) { if (index % 2 == 0) col1.add(post) else col2.add(post)
col1.add(post)
} else {
col2.add(post)
}
} }
Pair(col1, col2) Pair(col1, col2)
} }
@ -446,7 +433,6 @@ private fun ProfilePostsGrid(posts: List<Post>) {
.padding(horizontal = 16.dp), .padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(6.dp) horizontalArrangement = Arrangement.spacedBy(6.dp)
) { ) {
// Column 1
Column( Column(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(6.dp) verticalArrangement = Arrangement.spacedBy(6.dp)
@ -456,7 +442,6 @@ private fun ProfilePostsGrid(posts: List<Post>) {
MasonryProfilePostItem(post = post, aspectRatio = aspectRatio) MasonryProfilePostItem(post = post, aspectRatio = aspectRatio)
} }
} }
// Column 2
Column( Column(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(6.dp) verticalArrangement = Arrangement.spacedBy(6.dp)
@ -488,10 +473,7 @@ private fun MasonryProfilePostItem(post: Post, aspectRatio: Float) {
context.startActivity(intent) context.startActivity(intent)
} }
) { ) {
Box( Box(modifier = Modifier.fillMaxSize()) {
modifier = Modifier
.fillMaxSize()
) {
if (isImage && !primaryMedia.isNullOrEmpty()) { if (isImage && !primaryMedia.isNullOrEmpty()) {
AsyncImage( AsyncImage(
model = primaryMedia, model = primaryMedia,
@ -500,7 +482,6 @@ private fun MasonryProfilePostItem(post: Post, aspectRatio: Float) {
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) )
// Dark Gradient Scrim for text readability over images
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@ -512,7 +493,6 @@ private fun MasonryProfilePostItem(post: Post, aspectRatio: Float) {
) )
) )
// Image metadata overlay
Column( Column(
modifier = Modifier modifier = Modifier
.align(Alignment.BottomStart) .align(Alignment.BottomStart)
@ -538,7 +518,6 @@ private fun MasonryProfilePostItem(post: Post, aspectRatio: Float) {
} }
} }
} else { } else {
// Text / Status Card Design
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@ -600,7 +579,6 @@ private fun MasonryProfilePostItem(post: Post, aspectRatio: Float) {
) )
} }
// Footer Likes
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(2.dp) horizontalArrangement = Arrangement.spacedBy(2.dp)
@ -624,14 +602,12 @@ private fun MasonryProfilePostItem(post: Post, aspectRatio: Float) {
} }
// ============================================================ // ============================================================
// PREVIEWS // PREVIEW
// ============================================================ // ============================================================
@Preview(showBackground = true, showSystemUi = true) @Preview(showBackground = true, showSystemUi = true)
@Composable @Composable
fun MyProfileScreenSuccessPreview() { fun MyProfileScreenSuccessPreview() {
DattingAPPTheme { DattingAPPTheme {
// Mock UserResponse matching your backend model structure
val mockUserResponse = UserResponse( val mockUserResponse = UserResponse(
id = 1, id = 1,
email_or_phone = "elena@example.com", email_or_phone = "elena@example.com",
@ -639,7 +615,7 @@ fun MyProfileScreenSuccessPreview() {
profile = ProfileData( profile = ProfileData(
full_name = "Elena", full_name = "Elena",
gender = "Female", gender = "Female",
birth_date = "2002-04-12", // 24 years old birth_date = "2002-04-12",
city = "New York", city = "New York",
county = null, county = null,
country = "USA", country = "USA",
@ -663,14 +639,7 @@ fun MyProfileScreenSuccessPreview() {
comment_count = 3, comment_count = 3,
liked_by_me = false, liked_by_me = false,
media = listOf( media = listOf(
Media( Media(1, "image", "image/jpeg", 1024, 1, "https://picsum.photos/400/400?random=1"),
1,
"image",
"image/jpeg",
1024,
1,
"https://picsum.photos/400/400?random=1"
),
Media(2, "image", "image/jpeg", 1024, 2, "https://picsum.photos/400/400?random=2"), Media(2, "image", "image/jpeg", 1024, 2, "https://picsum.photos/400/400?random=2"),
Media(3, "image", "image/jpeg", 1024, 3, "https://picsum.photos/400/400?random=3") Media(3, "image", "image/jpeg", 1024, 3, "https://picsum.photos/400/400?random=3")
), ),

17
app/src/main/java/com/example/dattingapp/homes/networkandapi/HomeApiService.kt

@ -109,4 +109,21 @@ interface HomeApiService {
@Query("offset") offset: Int = 0, @Query("offset") offset: Int = 0,
): Response<List<ShowPostResponse>> ): Response<List<ShowPostResponse>>
@POST("api/v1/explore/posts/{post_id}/like")
suspend fun toggleLikePost(
@Path("post_id") postId: Int
): Response<LikePostResponse>
@GET("api/v1/explore/posts/{post_id}/like")
suspend fun getPostLikers(
@Path("post_id") postId: Int
): Response<List<PostAllLikerResponse>>
@DELETE("api/v1/explore/posts/{post_id}")
suspend fun deletePost(
@Path("post_id") postId: Int
): Response<DeletePostResponse>
} }

142
app/src/main/java/com/example/dattingapp/homes/networkandapi/HomeDataClass.kt

@ -1,6 +1,15 @@
package com.example.dattingapp.homes.networkandapi package com.example.dattingapp.homes.networkandapi
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
private const val MEDIA_BASE_URL = "https://melove.youthome.com.cn" private const val MEDIA_BASE_URL = "https://melove.youthome.com.cn"
@ -54,26 +63,30 @@ data class MeetUserProfile(
} }
/**
* Standard API wrapper format for MyProfile
*/ data class LikedPost(
//data class MyProfileResponse( @SerializedName("id")
// @SerializedName("id") val id: Int?, val id: Int,
// @SerializedName("email_or_phone") val emailOrPhone: String?, @SerializedName("full_name")
// @SerializedName("status") val status: String?, val fullName: String
// @SerializedName("profile") val profile: UserProfileDetails?, // Nested profile details )
// @SerializedName("created_at") val createdAt: String?
//) data class LikePostResponse(
// @SerializedName("message")
//data class UserProfileDetails( val message: String,
// @SerializedName("full_name") val fullName: String?, @SerializedName("like_count")
// @SerializedName("gender") val gender: String?, val likeCount: Int,
// @SerializedName("city") val city: String?, @SerializedName("liked_by_me")
// @SerializedName("country") val country: String?, val likedByMe: Boolean,
// @SerializedName("bio") val bio: String?, @SerializedName("liked_by_users")
// @SerializedName("profile_image_url") val profileImageUrl: String?, // Resolved field val likedByUsers: List<LikedPost>?
// @SerializedName("video_url") val videoUrl: String? )
//)
data class PostAllLikerResponse(
val id: Int,
@SerializedName("full_name") val fullName: String
)
data class MediaItem( data class MediaItem(
@SerializedName("id") @SerializedName("id")
@ -206,7 +219,9 @@ data class CreatePostResponse(
val created_at: String val created_at: String
) )
data class DeletePostResponse(
val message: String
)
data class PostMediaResponse( data class PostMediaResponse(
val id: Int, val id: Int,
@ -216,3 +231,88 @@ data class PostMediaResponse(
val position: Int, val position: Int,
val url: String val url: String
) )
data class PostInteraction(
val likedByMe: Boolean,
val likeCount: Int,
val commentCount: Int
)
object PostInteractionStore {
private val _state = MutableStateFlow<Map<Int, PostInteraction>>(emptyMap())
val state: StateFlow<Map<Int, PostInteraction>> = _state.asStateFlow()
// NEW: ids of posts deleted in this session; every screen hides these
private val _deletedIds = MutableStateFlow<Set<Int>>(emptySet())
val deletedIds: StateFlow<Set<Int>> = _deletedIds.asStateFlow()
private val pendingLikes = java.util.Collections.synchronizedSet(mutableSetOf<Int>())
fun markLikePending(postId: Int) { pendingLikes.add(postId) }
fun clearLikePending(postId: Int) { pendingLikes.remove(postId) }
/** Call whenever data arrives from the server (feed load, fetchPostById). */
fun setFromServer(postId: Int, liked: Boolean, likeCount: Int, commentCount: Int) {
_state.update { map ->
val cur = map[postId]
if (postId in pendingLikes && cur != null) {
map + (postId to cur.copy(commentCount = commentCount))
} else {
map + (postId to PostInteraction(liked, likeCount, commentCount))
}
}
}
fun toggleLike(postId: Int) {
_state.update { map ->
val cur = map[postId] ?: return@update map
val liked = !cur.likedByMe
map + (postId to cur.copy(
likedByMe = liked,
likeCount = (cur.likeCount + if (liked) 1 else -1).coerceAtLeast(0)
))
}
}
fun applyLikeResult(postId: Int, liked: Boolean, likeCount: Int) {
_state.update { map ->
val cur = map[postId] ?: return@update map
map + (postId to cur.copy(likedByMe = liked, likeCount = likeCount))
}
}
fun changeCommentCount(postId: Int, delta: Int) {
_state.update { map ->
val cur = map[postId] ?: return@update map
map + (postId to cur.copy(commentCount = (cur.commentCount + delta).coerceAtLeast(0)))
}
}
fun setCommentCount(postId: Int, count: Int) {
_state.update { map ->
val cur = map[postId] ?: return@update map
map + (postId to cur.copy(commentCount = count))
}
}
/** Call after a post is deleted on the server. */
fun remove(postId: Int) {
_state.update { it - postId }
_deletedIds.update { it + postId } // NEW: tells every screen to hide this post
}
/** Call on logout. */
fun clear() {
_state.value = emptyMap()
_deletedIds.value = emptySet() // NEW
pendingLikes.clear()
}
}
@Composable
fun rememberPostInteraction(postId: Int): PostInteraction? {
val flow = remember(postId) {
PostInteractionStore.state.map { it[postId] }.distinctUntilChanged()
}
return flow.collectAsState(initial = PostInteractionStore.state.value[postId]).value
}

133
app/src/main/java/com/example/dattingapp/homes/networkandapi/HomeViewModelClass.kt

@ -595,7 +595,6 @@ class LikedViewModel : ViewModel() {
} }
} }
// --- UI State ---
sealed interface ShowPostUiState { sealed interface ShowPostUiState {
object Idle : ShowPostUiState object Idle : ShowPostUiState
object Loading : ShowPostUiState object Loading : ShowPostUiState
@ -609,17 +608,20 @@ class ShowPostViewModel : ViewModel() {
private val _showPostUiState = MutableStateFlow<ShowPostUiState>(ShowPostUiState.Idle) private val _showPostUiState = MutableStateFlow<ShowPostUiState>(ShowPostUiState.Idle)
val showPostUiState: StateFlow<ShowPostUiState> = _showPostUiState.asStateFlow() val showPostUiState: StateFlow<ShowPostUiState> = _showPostUiState.asStateFlow()
private val _commentsUiState = MutableStateFlow<List<CommentResponse>>(emptyList())
val commentsUiState: StateFlow<List<CommentResponse>> = _commentsUiState.asStateFlow()
private val _postLikers = MutableStateFlow<List<PostAllLikerResponse>>(emptyList())
val postLikers: StateFlow<List<PostAllLikerResponse>> = _postLikers.asStateFlow()
fun fetchPostById(context: Context, postId: Int) { fun fetchPostById(context: Context, postId: Int) {
if (_showPostUiState.value is ShowPostUiState.Loading) return if (_showPostUiState.value is ShowPostUiState.Loading) return
val appContext = context.applicationContext val appContext = context.applicationContext
viewModelScope.launch { viewModelScope.launch {
_showPostUiState.value = ShowPostUiState.Loading _showPostUiState.value = ShowPostUiState.Loading
// Token check
if (TokenManager.getInstance(appContext).getAccessToken().isNullOrBlank()) { if (TokenManager.getInstance(appContext).getAccessToken().isNullOrBlank()) {
Log.e("PostViewModel", "No access token found in preferences") Log.e("PostViewModel", "No access token found in preferences")
_showPostUiState.value = ShowPostUiState.Error("No access token found. Please log in again.") _showPostUiState.value = ShowPostUiState.Error("No access token found. Please log in again.")
@ -627,13 +629,17 @@ class ShowPostViewModel : ViewModel() {
} }
try { try {
// Call API via Network Module
val response = HomeNetworkModule val response = HomeNetworkModule
.getApiService(appContext) .getApiService(appContext)
.getPostById(postId) .getPostById(postId)
if (response.isSuccessful && response.body() != null) { if (response.isSuccessful && response.body() != null) {
_showPostUiState.value = ShowPostUiState.Success(response.body()!!) val post = response.body()!!
// NEW: put server data in the shared store (ignored for likes while a like is pending)
PostInteractionStore.setFromServer(
post.id, post.liked_by_me, post.like_count, post.comment_count
)
_showPostUiState.value = ShowPostUiState.Success(post)
} else { } else {
val errorBody = response.errorBody()?.string() val errorBody = response.errorBody()?.string()
Log.e("PostError", "Code: ${response.code()}, Error: $errorBody") Log.e("PostError", "Code: ${response.code()}, Error: $errorBody")
@ -654,6 +660,7 @@ class ShowPostViewModel : ViewModel() {
} }
} }
} }
fun addCommentLocallyAndServer(context: Context, postId: Int, content: String, currentUserName: String?) { fun addCommentLocallyAndServer(context: Context, postId: Int, content: String, currentUserName: String?) {
val appContext = context.applicationContext val appContext = context.applicationContext
@ -673,10 +680,9 @@ class ShowPostViewModel : ViewModel() {
) )
val currentList = _commentsUiState.value.toMutableList() val currentList = _commentsUiState.value.toMutableList()
// Add the new comment at index 0 so it's instantly at the top
currentList.add(0, optimisticComment) currentList.add(0, optimisticComment)
_commentsUiState.value = currentList _commentsUiState.value = currentList
PostInteractionStore.changeCommentCount(postId, +1) // NEW
viewModelScope.launch { viewModelScope.launch {
try { try {
@ -689,17 +695,15 @@ class ShowPostViewModel : ViewModel() {
fetchComments(appContext, postId) fetchComments(appContext, postId)
} else { } else {
Log.e("CommentError", "Failed to sync comment with server") Log.e("CommentError", "Failed to sync comment with server")
PostInteractionStore.changeCommentCount(postId, -1) // NEW
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e("CommentViewModel", "Exception while posting comment", e) Log.e("CommentViewModel", "Exception while posting comment", e)
PostInteractionStore.changeCommentCount(postId, -1) // NEW
} }
} }
} }
private val _commentsUiState = MutableStateFlow<List<CommentResponse>>(emptyList())
val commentsUiState: StateFlow<List<CommentResponse>> = _commentsUiState.asStateFlow()
fun fetchComments(context: Context, postId: Int) { fun fetchComments(context: Context, postId: Int) {
val appContext = context.applicationContext val appContext = context.applicationContext
@ -710,7 +714,9 @@ class ShowPostViewModel : ViewModel() {
.getPostComments(postId) .getPostComments(postId)
if (response.isSuccessful && response.body() != null) { if (response.isSuccessful && response.body() != null) {
_commentsUiState.value = response.body()!! val list = response.body()!!
_commentsUiState.value = list
PostInteractionStore.setCommentCount(postId, list.size) // NEW
} else { } else {
Log.e("CommentError", "Failed to fetch comments: ${response.errorBody()?.string()}") Log.e("CommentError", "Failed to fetch comments: ${response.errorBody()?.string()}")
} }
@ -720,11 +726,75 @@ class ShowPostViewModel : ViewModel() {
} }
} }
// NEW: no longer depends on ShowPostUiState.Success, so it works from the feed too
fun toggleLikeLocallyAndServer(context: Context, postId: Int) {
val appContext = context.applicationContext
// 1. Optimistic update: feed AND details screens change instantly
PostInteractionStore.markLikePending(postId)
PostInteractionStore.toggleLike(postId)
// 2. Server request
viewModelScope.launch {
try {
val response = HomeNetworkModule
.getApiService(appContext)
.toggleLikePost(postId)
if (response.isSuccessful && response.body() != null) {
val serverData = response.body()!!
PostInteractionStore.applyLikeResult(
postId, serverData.likedByMe, serverData.likeCount
)
// keep this screen's own post state in sync too (if loaded)
val s = _showPostUiState.value
if (s is ShowPostUiState.Success && s.post.id == postId) {
_showPostUiState.value = ShowPostUiState.Success(
s.post.copy(
liked_by_me = serverData.likedByMe,
like_count = serverData.likeCount
)
)
}
} else {
Log.e("LikeError", "Failed to sync like with server, rolling back")
PostInteractionStore.toggleLike(postId)
}
} catch (e: Exception) {
Log.e("LikeViewModel", "Exception while toggling like, rolling back", e)
PostInteractionStore.toggleLike(postId)
} finally {
PostInteractionStore.clearLikePending(postId)
}
}
}
fun fetchPostLikers(context: Context, postId: Int) {
val appContext = context.applicationContext
viewModelScope.launch {
try {
val response = HomeNetworkModule
.getApiService(appContext)
.getPostLikers(postId)
if (response.isSuccessful && response.body() != null) {
_postLikers.value = response.body()!!
} else {
Log.e("LikersError", "Failed to fetch likers: ${response.errorBody()?.string()}")
}
} catch (e: Exception) {
Log.e("LikersViewModel", "Exception while fetching likers", e)
}
}
}
fun deleteComment(context: Context, postId: Int, commentId: Int) { fun deleteComment(context: Context, postId: Int, commentId: Int) {
val appContext = context.applicationContext val appContext = context.applicationContext
val previousList = _commentsUiState.value val previousList = _commentsUiState.value
_commentsUiState.value = previousList.filter { it.id != commentId } _commentsUiState.value = previousList.filter { it.id != commentId }
PostInteractionStore.changeCommentCount(postId, -1) // NEW
viewModelScope.launch { viewModelScope.launch {
try { try {
@ -736,19 +806,16 @@ class ShowPostViewModel : ViewModel() {
Log.d("CommentError", "Delete success: code=${response.code()} commentId=$commentId") Log.d("CommentError", "Delete success: code=${response.code()} commentId=$commentId")
fetchComments(appContext, postId) fetchComments(appContext, postId)
} else { } else {
// ---- read the RAW error body first, so nothing is lost ----
val rawError = try { val rawError = try {
response.errorBody()?.string() ?: "(empty error body)" response.errorBody()?.string() ?: "(empty error body)"
} catch (e: Exception) { } catch (e: Exception) {
"(failed to read error body: ${e.message})" "(failed to read error body: ${e.message})"
} }
// ---- try to extract "detail" from the JSON ----
val detail = try { val detail = try {
JSONObject(rawError).optString("detail") JSONObject(rawError).optString("detail")
} catch (e: Exception) { "" } } catch (e: Exception) { "" }
// ---- human-readable reason per status code ----
val reason = when (response.code()) { val reason = when (response.code()) {
401 -> "Not authorized (token missing/expired — log in again)" 401 -> "Not authorized (token missing/expired — log in again)"
403 -> "Not allowed: $detail" 403 -> "Not allowed: $detail"
@ -758,18 +825,13 @@ class ShowPostViewModel : ViewModel() {
else -> detail.ifBlank { rawError } else -> detail.ifBlank { rawError }
} }
// ---- FULL diagnosis in Logcat (filter: CommentError) ----
Log.e( Log.e(
"CommentError", "CommentError",
"Delete failed\n" + "Delete failed\n code = ${response.code()}\n message = ${response.message()}\n url = ${response.raw().request.url}\n raw body = $rawError\n parsed = $reason"
" code = ${response.code()}\n" +
" message = ${response.message()}\n" +
" url = ${response.raw().request.url}\n" +
" raw body = $rawError\n" +
" parsed = $reason"
) )
_commentsUiState.value = previousList _commentsUiState.value = previousList
PostInteractionStore.changeCommentCount(postId, +1) // NEW (rollback)
Toast.makeText( Toast.makeText(
appContext, appContext,
"Delete failed (${response.code()}): $reason", "Delete failed (${response.code()}): $reason",
@ -777,13 +839,13 @@ class ShowPostViewModel : ViewModel() {
).show() ).show()
} }
} catch (e: Exception) { } catch (e: Exception) {
// ---- network-level failure: no internet, timeout, DNS, SSL... ----
Log.e( Log.e(
"CommentError", "CommentError",
"Exception while deleting commentId=$commentId: ${e.javaClass.simpleName}: ${e.message}", "Exception while deleting commentId=$commentId: ${e.javaClass.simpleName}: ${e.message}",
e e
) )
_commentsUiState.value = previousList _commentsUiState.value = previousList
PostInteractionStore.changeCommentCount(postId, +1) // NEW (rollback)
Toast.makeText( Toast.makeText(
appContext, appContext,
"Delete failed: ${e.javaClass.simpleName} — ${e.message ?: "no details"}", "Delete failed: ${e.javaClass.simpleName} — ${e.message ?: "no details"}",
@ -793,8 +855,29 @@ class ShowPostViewModel : ViewModel() {
} }
} }
fun deletePost(context: Context, postId: Int, onDeleted: () -> Unit) {
val appContext = context.applicationContext
viewModelScope.launch {
try {
val response = HomeNetworkModule
.getApiService(appContext)
.deletePost(postId)
if (response.isSuccessful) {
PostInteractionStore.remove(postId) // clean up shared store
onDeleted() // e.g. finish() the activity
} else {
val err = response.errorBody()?.string()
Log.e("DeletePost", "Failed ${response.code()}: $err")
Toast.makeText(appContext, "Delete failed (${response.code()})", Toast.LENGTH_LONG).show()
}
} catch (e: Exception) {
Log.e("DeletePost", "Exception while deleting post", e)
Toast.makeText(appContext, "Delete failed: ${e.javaClass.simpleName}", Toast.LENGTH_LONG).show()
}
}
}
} }
sealed interface FeedUiState { sealed interface FeedUiState {
@ -809,6 +892,8 @@ class ExploreFeedViewModel : ViewModel() {
private val _uiState = MutableStateFlow<FeedUiState>(FeedUiState.Loading) private val _uiState = MutableStateFlow<FeedUiState>(FeedUiState.Loading)
val uiState: StateFlow<FeedUiState> = _uiState.asStateFlow() val uiState: StateFlow<FeedUiState> = _uiState.asStateFlow()
private val pageSize = 20 private val pageSize = 20
private var isLoadingMore = false private var isLoadingMore = false

Loading…
Cancel
Save