diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ff18782..afc894b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -41,6 +41,7 @@ dependencies { implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.ktx) implementation(libs.androidx.appcompat) + implementation(libs.androidx.compose.foundation) implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.ui) implementation(libs.androidx.compose.ui.graphics) @@ -90,9 +91,6 @@ dependencies { // For Hilt ViewModel support in Jetpack Compose (if using Compose) implementation("androidx.hilt:hilt-navigation-compose:1.2.0") - // For Hilt support in Fragment / Activity - implementation("androidx.fragment:fragment-ktx:1.8.3") - diff --git a/app/src/main/java/com/example/dattingapp/Login/LoginActivity.kt b/app/src/main/java/com/example/dattingapp/Login/LoginActivity.kt index 0a25635..464da43 100644 --- a/app/src/main/java/com/example/dattingapp/Login/LoginActivity.kt +++ b/app/src/main/java/com/example/dattingapp/Login/LoginActivity.kt @@ -3,6 +3,7 @@ package com.example.dattingapp.Login import android.content.Intent import android.os.Bundle +import android.util.Log import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.setContent @@ -269,12 +270,15 @@ fun LoginScreenContent( // Save retrieved tokens via TokenManager val tokenManager = TokenManager.getInstance(context) + val id=loginData.user?.profile?.full_name + Log.d("useridlogin","$id") tokenManager.saveTokens( accessToken = loginData.accessToken ?: "", refreshToken = loginData.refreshToken ?: "", + userId = loginData.user?.id ?: -1, + currenUserName = loginData.user?.profile?.full_name ) - Toast.makeText(context, "Login successful!", Toast.LENGTH_SHORT).show() onLoginSuccess() } else { diff --git a/app/src/main/java/com/example/dattingapp/ShowPostingActivity.kt b/app/src/main/java/com/example/dattingapp/ShowPostingActivity.kt index 2a35d91..19826ad 100644 --- a/app/src/main/java/com/example/dattingapp/ShowPostingActivity.kt +++ b/app/src/main/java/com/example/dattingapp/ShowPostingActivity.kt @@ -1,5 +1,4 @@ package com.example.dattingapp - import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent @@ -26,6 +25,7 @@ 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 @@ -43,9 +43,14 @@ 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.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) @@ -174,23 +179,27 @@ fun PostCard( } // --- 2. Post Text Content --- - if (!post.content.isNullOrEmpty()) { - Text( - text = post.content, - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 12.dp, vertical = 4.dp) - ) - } + // --- 3. Media Carousel (Images) --- if (post.media.isNotEmpty()) { - Box( + + 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() - .height(350.dp) - .padding(vertical = 4.dp) + .padding(start = 10.dp, end = 10.dp) + .height(360.dp), + ) { AsyncImage( model = post.media.first().url, @@ -199,6 +208,36 @@ fun PostCard( 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 --- @@ -232,21 +271,32 @@ fun PostCard( CommentSection( comments = comments, postId = post.id, - currentUserName = post.author?.full_name // see note below + postUserName = post.author?.full_name // see note below ) } } + + @Composable fun CommentSection( comments: List, postId: Int, - currentUserName: String?, + postUserName: String?, + postUserId: Int? = null, viewModel: ShowPostViewModel = viewModel() ) { val context = LocalContext.current var newCommentText by remember { mutableStateOf("") } + var menuCommentId by remember { mutableStateOf(null) } + var editingCommentId by remember { mutableStateOf(null) } + var editText by remember { mutableStateOf("") } + + val tokenManager = remember { TokenManager.getInstance(context) } + val currentUserName = remember { tokenManager.currentUserName() } + // ✅ get the logged-in user's id once + val currentUserId = remember { tokenManager.getUserId() } Column( modifier = Modifier @@ -259,7 +309,8 @@ fun CommentSection( fontWeight = FontWeight.Bold, modifier = Modifier.padding(bottom = 8.dp) ) -// Bottom Comment Input Field Bar + + // Comment input bar Row( modifier = Modifier .fillMaxWidth() @@ -307,8 +358,25 @@ fun CommentSection( ) } } - // List of fetched comments + + // 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() @@ -331,29 +399,97 @@ fun CommentSection( Spacer(modifier = Modifier.width(8.dp)) - Column( - modifier = Modifier - .weight(1f) - .background(CommentBubbleBg, shape = RoundedCornerShape(12.dp)) - .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)) - Text( - text = comment.content, - style = MaterialTheme.typography.bodyMedium - ) + 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) diff --git a/app/src/main/java/com/example/dattingapp/dataclass/DataClass.kt b/app/src/main/java/com/example/dattingapp/dataclass/DataClass.kt index 8a82cb7..d2a6174 100644 --- a/app/src/main/java/com/example/dattingapp/dataclass/DataClass.kt +++ b/app/src/main/java/com/example/dattingapp/dataclass/DataClass.kt @@ -15,7 +15,7 @@ data class ShowPostResponse( @SerialName("like_count") val like_count: Int, @SerialName("comment_count") val comment_count: Int = 0, @SerialName("liked_by_me") val liked_by_me: Boolean = false, - val media: List = emptyList(), + @SerialName("media")val media: List = emptyList(), @SerialName("created_at") val createdAt: String? ) diff --git a/app/src/main/java/com/example/dattingapp/homes/ExploreScreen.kt b/app/src/main/java/com/example/dattingapp/homes/ExploreScreen.kt index 2cf0b5b..98492e5 100644 --- a/app/src/main/java/com/example/dattingapp/homes/ExploreScreen.kt +++ b/app/src/main/java/com/example/dattingapp/homes/ExploreScreen.kt @@ -1,15 +1,18 @@ package com.example.dattingapp.homes -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.activity.enableEdgeToEdge +import android.content.Intent +import androidx.compose.foundation.BorderStroke +import androidx.media3.common.MediaItem as ExoMediaItem import androidx.compose.foundation.background import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.indication import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons @@ -20,27 +23,38 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush 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.platform.LocalContext +import androidx.compose.ui.text.TextStyle 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.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.media3.common.Player +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.ui.PlayerView +import coil.compose.AsyncImage +import com.example.dattingapp.ShowPostingActivity +import com.example.dattingapp.dataclass.Author +import com.example.dattingapp.dataclass.Location +import com.example.dattingapp.dataclass.MediaItem +import com.example.dattingapp.dataclass.ShowPostResponse +import com.example.dattingapp.homes.networkandapi.PostResponse +import com.example.dattingapp.homes.profile.UserResponse import com.example.dattingapp.ui.theme.DattingAPPTheme +import com.example.dattingapp.ui.theme.OverlayShadow +import com.example.dattingapp.ui.theme.TextPostTextColor import com.example.dattingapp.ui.theme.lightChipBg import com.example.dattingapp.ui.theme.primaryColor -class MeetActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - enableEdgeToEdge() - setContent { - DattingAPPTheme { - VibeCheckHomeScreen() - } - } - } -} // Data Models data class VibeCategory( @@ -68,10 +82,13 @@ data class UserMatch( @Composable fun VibeCheckHomeScreen( - modifier: Modifier = Modifier + feedResponse: List = emptyList(), // Change from MyProfileData? to MyProfileResponse? + isLoading: Boolean, + errorMessage: String?, + onRetry: () -> Unit = {}, ) { Scaffold( - modifier = modifier.fillMaxSize(), + modifier = Modifier.fillMaxSize(), topBar = { VibeCheckTopAppBar(primaryColor) } @@ -87,45 +104,22 @@ fun VibeCheckHomeScreen( SearchBarSection() } - item { - VibeFilterChipsSection( - selectedColor = primaryColor, - unselectedBg = lightChipBg - ) - } +// item { +// VibeFilterChipsSection( +// selectedColor = primaryColor, +// unselectedBg = lightChipBg +// ) +// } - item { + // One card per post from the API (replaces both hardcoded cards) + items(feedResponse, key = { it.id }) { post -> FeaturedEventCard( - data = EventCardData( - id = "1", - title = "Coffee & Pottery", - description = "Get your hands dirty and your caffeine fix. Perfect for a low-pressure first date.", - imageUrl = "", - tagLabel = "Live", - isTrending = false, - tags = listOf("Creative", "Chill") - ), + post = post, badgeColor = primaryColor ) } - item { - MostMatchingSection(primaryColor) - } - item { - FeaturedEventCard( - data = EventCardData( - id = "2", - title = "Sunset Beach Volley", - description = "Join a casual pickup game down at the pier. Bring sunscreen and good vibes.", - imageUrl = "", - tagLabel = "TRENDING", - isTrending = true - ), - badgeColor = Color(0xFF7C4DFF) - ) - } item { Spacer(modifier = Modifier.height(16.dp)) } @@ -266,56 +260,103 @@ private fun VibeFilterChipsSection( @Composable private fun FeaturedEventCard( - data: EventCardData, + post: ShowPostResponse, badgeColor: Color ) { + val context = LocalContext.current + + val hasMedia = post.media.isNotEmpty() + 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 shadowStyle = if (hasMedia) TextStyle(shadow = OverlayShadow) else TextStyle.Default + Card( shape = RoundedCornerShape(16.dp), + border = if (hasMedia) null else BorderStroke(1.dp, Color(0xFFEDEDED)), + colors = CardDefaults.cardColors( + containerColor = if (hasMedia) Color.DarkGray else Color.White + ), modifier = Modifier .fillMaxWidth() .height(360.dp) + .clickable { + val intent = Intent(context, ShowPostingActivity::class.java).apply { + putExtra("Post_ID", post.id) + } + context.startActivity(intent) + } ) { Box(modifier = Modifier.fillMaxSize()) { - Box( - modifier = Modifier - .fillMaxSize() - .background(Color.DarkGray) - ) - Box( - modifier = Modifier - .fillMaxSize() - .background( - Brush.verticalGradient( - colors = listOf( - Color.Transparent, - Color.Black.copy(alpha = 0.85f) - ), - startY = 200f + // 1. Content: swipeable media, or big centered text + if (hasMedia) { + PostMediaPager(media = post.media, modifier = Modifier.fillMaxSize()) + + // 2. Top scrim (keeps the badge readable) + Box( + modifier = Modifier + .fillMaxWidth() + .height(90.dp) + .align(Alignment.TopCenter) + .background( + Brush.verticalGradient( + listOf(Color.Black.copy(alpha = 0.45f), Color.Transparent) + ) ) - ) - ) + ) + // 3. Bottom scrim (keeps author, text and tags readable) + Box( + modifier = Modifier + .fillMaxWidth() + .height(180.dp) + .align(Alignment.BottomCenter) + .background( + Brush.verticalGradient( + listOf(Color.Transparent, Color.Black.copy(alpha = 0.8f)) + ) + ) + ) + } else { + 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 + .align(Alignment.Center) + .padding(horizontal = 32.dp) + ) + } + + // 4. Badge (it has its own colored background, so white text is fine) Surface( color = badgeColor, shape = RoundedCornerShape(12.dp), - modifier = Modifier - .padding(12.dp) - .align(Alignment.TopStart) + modifier = Modifier.padding(12.dp).align(Alignment.TopStart) ) { Row( modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp), verticalAlignment = Alignment.CenterVertically ) { Icon( - imageVector = if (!data.isTrending) Icons.Default.RadioButtonChecked else Icons.Default.Bolt, + imageVector = when (post.postType) { + "video" -> Icons.Default.Videocam + "image" -> Icons.Default.Image + else -> Icons.Default.TextFields + }, contentDescription = null, tint = Color.White, modifier = Modifier.size(12.dp) ) - Spacer(modifier = Modifier.width(4.dp)) + Spacer(Modifier.width(4.dp)) Text( - text = data.tagLabel, + text = post.postType?.uppercase() ?: "POST", color = Color.White, fontSize = 10.sp, fontWeight = FontWeight.Bold @@ -323,69 +364,89 @@ private fun FeaturedEventCard( } } - if (!data.isTrending) { - Column( - modifier = Modifier - .align(Alignment.CenterEnd) - .padding(end = 12.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - IconButton(onClick = { }) { - Icon( - imageVector = Icons.Outlined.ThumbUp, - contentDescription = "Like", - tint = Color.White - ) - } - IconButton(onClick = { }) { - Icon( - imageVector = Icons.Outlined.ChatBubbleOutline, - contentDescription = "Comment", - tint = Color.White - ) - } - IconButton(onClick = { }) { - Icon( - imageVector = Icons.Outlined.BookmarkBorder, - contentDescription = "Save", - tint = Color.White - ) - } - } - } - + // 5. Like / comment, on a dark pill when over a photo Column( modifier = Modifier - .align(Alignment.BottomStart) - .padding(16.dp) + .align(Alignment.BottomEnd) + .padding(end = 12.dp, bottom = 16.dp) + .clip(RoundedCornerShape(24.dp)) + .padding(vertical = 4.dp, horizontal = 2.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) // space BETWEEN like and comment groups ) { - Text( - text = data.title, - color = Color.White, - fontSize = 20.sp, - fontWeight = FontWeight.Bold - ) + // Like + count + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Icon( + imageVector = if (post.liked_by_me) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder, + contentDescription = "Like", + tint = if (post.liked_by_me) Color.Red else contentColor, + modifier = Modifier + .size(28.dp) + .clickable { } + ) + Text( + text = "${post.like_count}", + color = contentColor, + fontSize = 12.sp, + style = shadowStyle + ) + } - Spacer(modifier = Modifier.height(4.dp)) + // Comment + count + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Icon( + imageVector = Icons.Outlined.ChatBubbleOutline, + contentDescription = "Comment", + tint = contentColor, + modifier = Modifier + .size(28.dp) + .clickable { } + ) + Text( + text = "${post.comment_count}", + color = contentColor, + fontSize = 12.sp, + style = shadowStyle + ) + } + } + // 6. Bottom info + Column(modifier = Modifier.align(Alignment.BottomStart) + .padding(start = 16.dp, top = 16.dp, bottom = 16.dp, end = 72.dp)) { Text( - text = data.description, - color = Color.LightGray, - fontSize = 12.sp, - lineHeight = 16.sp + text = post.author?.full_name ?: "Anonymous", + color = contentColor, + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + style = shadowStyle ) - if (data.tags.isNotEmpty()) { - Spacer(modifier = Modifier.height(8.dp)) + if (hasMedia && !post.content.isNullOrBlank()) { + Spacer(Modifier.height(4.dp)) + Text( + text = post.content, + color = Color.White, + fontSize = 13.sp, + lineHeight = 17.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = shadowStyle + ) + } + + if (tags.isNotEmpty()) { + Spacer(Modifier.height(8.dp)) Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { - data.tags.forEach { tag -> + tags.forEach { tag -> Surface( - color = Color.White.copy(alpha = 0.2f), + color = if (hasMedia) Color.Black.copy(alpha = 0.45f) + else Color(0xFFF1F1F1), shape = RoundedCornerShape(6.dp) ) { Text( text = tag, - color = Color.White, + color = contentColor, fontSize = 10.sp, modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp) ) @@ -397,6 +458,100 @@ private fun FeaturedEventCard( } } } +@Composable +private fun PostMediaPager(media: List, modifier: Modifier = Modifier) { + val pagerState = rememberPagerState(pageCount = { media.size }) + + Box(modifier) { + HorizontalPager(state = pagerState, modifier = Modifier.fillMaxSize()) { page -> + val item = media[page] + if (item.mediaType == "video") { + PostVideoPlayer( + url = item.url, + isActive = pagerState.currentPage == page + ) + } else { + AsyncImage( + model = item.url, + contentDescription = "Post media ${page + 1}", + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize() + ) + } + } + + // "1/3" counter, only when there is more than one item + if (media.size > 1) { + Surface( + color = Color.Black.copy(alpha = 0.5f), + shape = RoundedCornerShape(12.dp), + modifier = Modifier.align(Alignment.TopEnd).padding(12.dp) + ) { + Text( + text = "${pagerState.currentPage + 1}/${media.size}", + color = Color.White, + fontSize = 11.sp, + modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp) + ) + } + } + } +} + +@Composable +private fun PostVideoPlayer(url: String, isActive: Boolean) { + val context = LocalContext.current + var isPlaying by remember { mutableStateOf(false) } + + val player = remember(url) { + ExoPlayer.Builder(context).build().apply { + setMediaItem(ExoMediaItem.fromUri(url)) + repeatMode = Player.REPEAT_MODE_ONE + prepare() + } + } + + // Pause when the user swipes to another page + LaunchedEffect(isActive) { + if (!isActive) player.pause() + } + + DisposableEffect(player) { + val listener = object : Player.Listener { + override fun onIsPlayingChanged(playing: Boolean) { isPlaying = playing } + } + player.addListener(listener) + onDispose { + player.removeListener(listener) + player.release() + } + } + + Box( + modifier = Modifier + .fillMaxSize() + .clickable { if (player.isPlaying) player.pause() else player.play() }, + contentAlignment = Alignment.Center + ) { + AndroidView( + factory = { ctx -> + PlayerView(ctx).apply { + this.player = player + useController = false + } + }, + modifier = Modifier.fillMaxSize() + ) + if (!isPlaying) { + Icon( + imageVector = Icons.Default.PlayCircle, + contentDescription = "Play", + tint = Color.White.copy(alpha = 0.9f), + modifier = Modifier.size(64.dp) + ) + } + } +} @Composable private fun MostMatchingSection( @@ -460,19 +615,90 @@ private fun MostMatchingSection( } } -@Preview( - showBackground = true, - showSystemUi = true, - widthDp = 360, - heightDp = 800 +private val mockFeed = listOf( + ShowPostResponse( + id = 1, + content = "Exploring the city and enjoying some coffee! ☕🏙️", + postType = "image", + contentFormat = "markdown", + feeling = "happy 😊", + location = Location(name = "Shanghai", latitude = 31.23, longitude = 121.47), + author = Author( + id = 49, + full_name = "Alex Rivera", + city = "Shanghai", + country = "China", + compatibilityPercentage = 95 + ), + like_count = 12, + comment_count = 3, + liked_by_me = true, + media = listOf( + 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" + ), + ShowPostResponse( + id = 2, + content = "Text only post with no media.", + postType = "text", + contentFormat = "markdown", + feeling = null, + location = null, + author = Author( + id = 54, + full_name = "Sarah", + city = "Beijing", + country = "China", + compatibilityPercentage = 88 + ), + like_count = 0, + comment_count = 0, + liked_by_me = false, + media = emptyList(), + createdAt = "2026-10-13T07:45:15" + ) ) + +@Preview(showBackground = true, showSystemUi = true, widthDp = 360, heightDp = 800) @Composable fun VibeCheckHomeScreenPreview() { - MaterialTheme { - Surface(modifier = Modifier.fillMaxSize()) { - VibeCheckHomeScreen( - modifier = Modifier.fillMaxSize() - ) - } + DattingAPPTheme { + VibeCheckHomeScreen( + feedResponse = mockFeed, + isLoading = false, + errorMessage = null + ) + } +} + +@Preview(showBackground = true, widthDp = 360, heightDp = 800) +@Composable +fun VibeCheckHomeScreenLoadingPreview() { + DattingAPPTheme { + VibeCheckHomeScreen( + feedResponse = emptyList(), + isLoading = true, + errorMessage = null + ) + } +} + +@Preview(showBackground = true, widthDp = 360, heightDp = 800) +@Composable +fun VibeCheckHomeScreenErrorPreview() { + DattingAPPTheme { + VibeCheckHomeScreen( + feedResponse = emptyList(), + isLoading = false, + errorMessage = "Failed to load feed (401)" + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/example/dattingapp/homes/HomeActivity.kt b/app/src/main/java/com/example/dattingapp/homes/HomeActivity.kt index 9499cf6..4dde083 100644 --- a/app/src/main/java/com/example/dattingapp/homes/HomeActivity.kt +++ b/app/src/main/java/com/example/dattingapp/homes/HomeActivity.kt @@ -33,6 +33,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.viewmodel.compose.viewModel +import com.example.dattingapp.homes.networkandapi.ExploreFeedViewModel +import com.example.dattingapp.homes.networkandapi.FeedUiState import com.example.dattingapp.homes.networkandapi.MeetUiState import com.example.dattingapp.homes.networkandapi.MeetViewModel import com.example.dattingapp.homes.networkandapi.ProfileViewModel @@ -304,7 +306,45 @@ fun HomeScreen() { // EXPLORE // ========================================= 1 -> { - VibeCheckHomeScreen() + // 1. Obtain instance of ViewModel and LocalContext + val feedViewModel: ExploreFeedViewModel = viewModel() + val context = LocalContext.current + + // 2. Collect state from ViewModel + val uiState by feedViewModel.uiState.collectAsState() + + // 3. Trigger network call once when screen loads + LaunchedEffect(Unit) { + feedViewModel.loadFeed(context) + } + + // 4. Map UI state to Composable parameters + + when (val state = uiState) { + is FeedUiState.Loading, FeedUiState.Idle -> { + VibeCheckHomeScreen( + feedResponse = emptyList(), + isLoading = true, + errorMessage = null, +// onRetry = { viewModel.meetUserResponse(context) } + ) + } + is FeedUiState.Success -> { + VibeCheckHomeScreen( + feedResponse = state.posts, // or state.response depending on your data class naming + isLoading = false, + errorMessage = null, + ) + } + is FeedUiState.Error -> { + VibeCheckHomeScreen( + feedResponse = emptyList(), + isLoading = false, + errorMessage = state.message, + onRetry = { feedViewModel.loadFeed(context) } + ) + } + } } // ========================================= // CREATE POST diff --git a/app/src/main/java/com/example/dattingapp/homes/MyProfile.kt b/app/src/main/java/com/example/dattingapp/homes/MyProfile.kt index aabd872..abe6893 100644 --- a/app/src/main/java/com/example/dattingapp/homes/MyProfile.kt +++ b/app/src/main/java/com/example/dattingapp/homes/MyProfile.kt @@ -36,6 +36,7 @@ import com.example.dattingapp.ui.theme.TextMuted import com.example.dattingapp.homes.profile.ProfileData import com.example.dattingapp.LikeANdMatchActivity import android.content.Intent +import android.util.Log import androidx.compose.foundation.clickable import androidx.compose.ui.platform.LocalContext import androidx.core.content.ContextCompat.startActivity @@ -208,6 +209,7 @@ fun MyProfileScreen( // BIO // ================================================= + Log.d("my id","${profileResponse?.id}" ) Text( text = profileResponse?.profile?.bio?:"", diff --git a/app/src/main/java/com/example/dattingapp/homes/networkandapi/HomeApiService.kt b/app/src/main/java/com/example/dattingapp/homes/networkandapi/HomeApiService.kt index 78ea62a..15a788e 100644 --- a/app/src/main/java/com/example/dattingapp/homes/networkandapi/HomeApiService.kt +++ b/app/src/main/java/com/example/dattingapp/homes/networkandapi/HomeApiService.kt @@ -15,6 +15,7 @@ import retrofit2.http.Multipart import retrofit2.http.Part import com.example.dattingapp.homes.profile.UserResponse import com.example.dattingapp.homes.profile.LikedUserResponse +import retrofit2.http.DELETE interface HomeApiService { @@ -87,4 +88,14 @@ interface HomeApiService { suspend fun getPostComments( @Path("post_id") postId: Int ): Response> + + // ✅ what it should be (if baseUrl is just the domain) + @DELETE("api/v1/explore/comments/{comment_id}") + suspend fun deleteComment(@Path("comment_id") commentId: Int): Response + @GET("api/v1/explore/feed") + suspend fun getExploreFeed( + @Query("limit") limit: Int = 20, + @Query("offset") offset: Int = 0, + ): Response> + } \ No newline at end of file diff --git a/app/src/main/java/com/example/dattingapp/homes/networkandapi/HomeViewModelClass.kt b/app/src/main/java/com/example/dattingapp/homes/networkandapi/HomeViewModelClass.kt index de4e016..2882052 100644 --- a/app/src/main/java/com/example/dattingapp/homes/networkandapi/HomeViewModelClass.kt +++ b/app/src/main/java/com/example/dattingapp/homes/networkandapi/HomeViewModelClass.kt @@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import retrofit2.HttpException import android.net.Uri +import android.widget.Toast import com.example.dattingapp.dataclass.Author import com.example.dattingapp.dataclass.CommentResponse import com.example.dattingapp.dataclass.CreateCommentRequest @@ -24,6 +25,7 @@ import java.io.File import java.io.FileOutputStream import java.lang.Exception import com.example.dattingapp.homes.profile.LikedUserResponse +import org.json.JSONObject sealed class MeetUiState { object Idle : MeetUiState() @@ -704,5 +706,142 @@ class ShowPostViewModel : ViewModel() { } } + fun deleteComment(context: Context, postId: Int, commentId: Int) { + val appContext = context.applicationContext + + val previousList = _commentsUiState.value + _commentsUiState.value = previousList.filter { it.id != commentId } + + viewModelScope.launch { + try { + val response = HomeNetworkModule + .getApiService(appContext) + .deleteComment(commentId) + + if (response.isSuccessful) { + Log.d("CommentError", "Delete success: code=${response.code()} commentId=$commentId") + fetchComments(appContext, postId) + } else { + // ---- read the RAW error body first, so nothing is lost ---- + val rawError = try { + response.errorBody()?.string() ?: "(empty error body)" + } catch (e: Exception) { + "(failed to read error body: ${e.message})" + } + + // ---- try to extract "detail" from the JSON ---- + val detail = try { + JSONObject(rawError).optString("detail") + } catch (e: Exception) { "" } + + // ---- human-readable reason per status code ---- + val reason = when (response.code()) { + 401 -> "Not authorized (token missing/expired — log in again)" + 403 -> "Not allowed: $detail" + 404 -> "Comment not found (wrong id or path)" + 405 -> "Server doesn't accept DELETE on this path" + 500 -> "Server error: $detail" + else -> detail.ifBlank { rawError } + } + + // ---- FULL diagnosis in Logcat (filter: CommentError) ---- + Log.e( + "CommentError", + "Delete failed\n" + + " code = ${response.code()}\n" + + " message = ${response.message()}\n" + + " url = ${response.raw().request.url}\n" + + " raw body = $rawError\n" + + " parsed = $reason" + ) + + _commentsUiState.value = previousList + Toast.makeText( + appContext, + "Delete failed (${response.code()}): $reason", + Toast.LENGTH_LONG + ).show() + } + } catch (e: Exception) { + // ---- network-level failure: no internet, timeout, DNS, SSL... ---- + Log.e( + "CommentError", + "Exception while deleting commentId=$commentId: ${e.javaClass.simpleName}: ${e.message}", + e + ) + _commentsUiState.value = previousList + Toast.makeText( + appContext, + "Delete failed: ${e.javaClass.simpleName} — ${e.message ?: "no details"}", + Toast.LENGTH_LONG + ).show() + } + } + } + + + +} + +sealed interface FeedUiState { + object Idle:FeedUiState + object Loading : FeedUiState + data class Success(val posts: List, val endReached: Boolean) : FeedUiState + data class Error(val message: String) : FeedUiState } +class ExploreFeedViewModel : ViewModel() { + + private val _uiState = MutableStateFlow(FeedUiState.Loading) + val uiState: StateFlow = _uiState.asStateFlow() + + private val pageSize = 20 + private var isLoadingMore = false + + fun loadFeed(context: Context) { + val appContext = context.applicationContext + viewModelScope.launch { + _uiState.value = FeedUiState.Loading + try { + val response = HomeNetworkModule.getApiService(appContext) + .getExploreFeed(limit = pageSize, offset = 0) + + if (response.isSuccessful && response.body() != null) { + val posts = response.body()!! + _uiState.value = FeedUiState.Success(posts, endReached = posts.size < pageSize) + } else { + _uiState.value = FeedUiState.Error("Failed to load feed (${response.code()})") + } + } catch (e: Exception) { + Log.e("ExploreFeed", "Feed request failed", e) + _uiState.value = FeedUiState.Error("Request failed: ${e.javaClass.simpleName}") + } + } + } + + fun loadMore(context: Context) { + val current = _uiState.value as? FeedUiState.Success ?: return + if (isLoadingMore || current.endReached) return + isLoadingMore = true + + val appContext = context.applicationContext + viewModelScope.launch { + try { + val response = HomeNetworkModule.getApiService(appContext) + .getExploreFeed(limit = pageSize, offset = current.posts.size) + + if (response.isSuccessful && response.body() != null) { + val newPosts = response.body()!! + _uiState.value = FeedUiState.Success( + posts = current.posts + newPosts, + endReached = newPosts.size < pageSize + ) + } + } catch (e: Exception) { + Log.e("ExploreFeed", "Load more failed", e) + } finally { + isLoadingMore = false + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/dattingapp/homes/networkandapi/TokenManager.kt b/app/src/main/java/com/example/dattingapp/homes/networkandapi/TokenManager.kt index e3ee0c8..ee36cc5 100644 --- a/app/src/main/java/com/example/dattingapp/homes/networkandapi/TokenManager.kt +++ b/app/src/main/java/com/example/dattingapp/homes/networkandapi/TokenManager.kt @@ -11,7 +11,7 @@ class TokenManager private constructor(context: Context) { Context.MODE_PRIVATE ) - fun saveTokens(accessToken: String, refreshToken: String?) { + fun saveTokens(accessToken: String, refreshToken: String?,userId: Int,currenUserName:String?) { val cleanToken = accessToken.trim() .replace( Regex("^Bearer\\s+", RegexOption.IGNORE_CASE), @@ -22,12 +22,22 @@ class TokenManager private constructor(context: Context) { val saved = preferences.edit() .putString(KEY_ACCESS_TOKEN, cleanToken) .putString(KEY_REFRESH_TOKEN, refreshToken) + .putInt("user_id", userId) + .putString("currenUserName", currenUserName) .commit() // Do not log the token itself. Log.d("TokenManager", "Tokens saved: $saved") } + + + fun getUserId(): Int { + return preferences.getInt("user_id", -1) + } + + fun currentUserName(): String? = preferences.getString("full_name", null) + fun getAccessToken(): String? { return preferences.getString(KEY_ACCESS_TOKEN, null) ?.trim() diff --git a/app/src/main/java/com/example/dattingapp/registration/RegisterRequest.kt b/app/src/main/java/com/example/dattingapp/registration/RegisterRequest.kt index 703499e..ae6ebb5 100644 --- a/app/src/main/java/com/example/dattingapp/registration/RegisterRequest.kt +++ b/app/src/main/java/com/example/dattingapp/registration/RegisterRequest.kt @@ -59,5 +59,20 @@ data class LoginResponse( val refreshToken: String?, @SerializedName("token_type") - val tokenType: String? -) \ No newline at end of file + val tokenType: String?, + + @SerializedName("user") // ✅ matches the JSON key + val user: LoginUser? = null +) + +data class LoginUser( + @SerializedName("id") + val id: Int, + @SerializedName("profile") + val profile: LoginProfile? = null +) + +data class LoginProfile( + @SerializedName("full_name") + val full_name: String? = null +) diff --git a/app/src/main/java/com/example/dattingapp/registration/RegistrationViewModel.kt b/app/src/main/java/com/example/dattingapp/registration/RegistrationViewModel.kt index 608f794..a775d1d 100644 --- a/app/src/main/java/com/example/dattingapp/registration/RegistrationViewModel.kt +++ b/app/src/main/java/com/example/dattingapp/registration/RegistrationViewModel.kt @@ -37,10 +37,10 @@ class RegistrationViewModel : ViewModel() { if (response.isSuccessful && response.body() != null) { val body = response.body()!! - TokenManager.getInstance(context.applicationContext).saveTokens( - accessToken = body.accessToken, - refreshToken = body.refreshToken - ) +// TokenManager.getInstance(context.applicationContext).saveTokens( +// accessToken = body.accessToken, +// refreshToken = body.refreshToken +// ) Log.d(TAG, "Registration successful. Access token saved.") _uiState.value = RegistrationUiState.Success(body) diff --git a/app/src/main/java/com/example/dattingapp/ui/theme/Color.kt b/app/src/main/java/com/example/dattingapp/ui/theme/Color.kt index f21aae6..785ea8b 100644 --- a/app/src/main/java/com/example/dattingapp/ui/theme/Color.kt +++ b/app/src/main/java/com/example/dattingapp/ui/theme/Color.kt @@ -1,7 +1,10 @@ package com.example.dattingapp.ui.theme +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shadow +import androidx.compose.ui.graphics.luminance val Purple80 = Color(0xFFD0BCFF) val PurpleGrey80 = Color(0xFFCCC2DC) @@ -104,18 +107,18 @@ val RecipientBubbleBg = Color(0xFFC2185B) val AppBackground = Color(0xFFF9F8FD) val SubtextColor = Color(0xFFB0BEC5) val InputFieldBg = Color(0xFFF0EFF4) - -// Color Palette matching the UI -val ScreenBackground = Color(0xFFFAF9FD) -val SectionHeaderColor = Color(0xFF757575) -val CardBackgroundColor = Color.White -val LinkTextColor = Color(0xFF7B1FA2) -val UnreadDotColor = Color(0xFFD32F2F) - val gradientBrush = Brush.horizontalGradient( colors = listOf( Color(0xFFB00040), // Dark Pink/Magenta Color(0xFF6B00B0) // Deep Purple ) ) -val purpleButtonColor = Color(0xFF6B00F5) \ No newline at end of file +val purpleButtonColor = Color(0xFF6B00F5) + +// Same post id always gives the same color +val OverlayShadow = Shadow( + color = Color.Black.copy(alpha = 0.6f), + offset = Offset(0f, 2f), + blurRadius = 6f +) +val TextPostTextColor = Color(0xFF1E1E1E) \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1fc311d..4238356 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,6 +18,7 @@ navigationFragmentKtx = "2.6.0" navigationUiKtx = "2.6.0" material3 = "1.4.0" ui = "1.12.0" +foundation = "1.12.1" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -44,6 +45,7 @@ androidx-material3 = { group = "androidx.compose.material3", name = "material3", media3-ui = { module = "androidx.media3:media3-ui", version.ref = "media3UiVersion" } media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3ExoplayerVersion" } androidx-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "ui" } +androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "foundation" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" }