From ea01f4d88e9a59860ba301b4ef7fd8396dd9e9c9 Mon Sep 17 00:00:00 2001 From: JothishKamal Date: Sat, 19 Jul 2025 11:58:04 +0530 Subject: [PATCH 1/7] feat: refactor and enhance various components in the app - Removed unused parseTimeToTimestamp function and replaced it with a utility function in the widget. - Updated FriendDetailScreenContent to use the new parseTimeToTimestamp utility. - Improved FriendRequestsScreenContent by resetting processedRequests on friendRequests change. - Enhanced ConnectScreenComponents with a new filter chip layout for better UI. - Refactored EmptyClassroomsContent to fetch empty classrooms based on selected time slots and improved error handling. - Updated MainComposeApp to pass navController to ScheduleComposeScreen for navigation. - Modified ScheduleScreenContent to handle card clicks for navigating to course details. - Added URL decoding utility to Constants for better handling of encoded strings. - Cleaned up TodayWidget by optimizing data fetching and updating logic. --- app/build.gradle | 4 +- .../network/api/community/APICommunity.kt | 73 ++- .../api/community/APICommunityRestClient.kt | 121 +++++ .../connect/AddParticipantsScreenContent.kt | 20 +- .../ui/connect/CircleDetailScreenContent.kt | 31 +- .../CircleMemberDetailScreenContent.kt | 155 +++--- .../ui/connect/CircleRequestsScreenContent.kt | 142 +++++- .../vitty/ui/connect/ConnectScreenContent.kt | 68 ++- .../vitty/ui/connect/ConnectViewModel.kt | 87 +++- .../ui/connect/FriendDetailScreenContent.kt | 32 +- .../ui/connect/FriendRequestsScreenContent.kt | 2 +- .../components/ConnectScreenComponents.kt | 36 +- .../emptyclassrooms/EmptyClassroomsContent.kt | 377 ++++++++++----- .../dscvit/vitty/ui/main/MainComposeApp.kt | 16 +- .../ui/schedule/ScheduleScreenContent.kt | 44 +- .../java/com/dscvit/vitty/util/Constants.kt | 10 + .../com/dscvit/vitty/widget/TodayWidget.kt | 455 +++++++++--------- 17 files changed, 1070 insertions(+), 603 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 2dba5d8..3a4fa2b 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -35,8 +35,8 @@ android { namespace "com.dscvit.vitty" minSdkVersion 26 targetSdkVersion 36 - versionCode 40 - versionName "2.0.3" + versionCode 41 + versionName "3.0.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" signingConfig signingConfigs.release } diff --git a/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunity.kt b/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunity.kt index c1dffb3..30ebf5f 100644 --- a/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunity.kt +++ b/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunity.kt @@ -23,143 +23,168 @@ import retrofit2.http.Query interface APICommunity { @Headers("Content-Type: application/json") - @POST("/api/v2/auth/check-username") + @POST("/api/v3/auth/check-username") fun checkUsername( @Body body: UsernameRequestBody, ): Call @Headers("Content-Type: application/json") - @POST("/api/v2/auth/firebase/") + @POST("/api/v3/auth/firebase/") fun signInInfo( @Body body: Any, ): Call - @GET("/api/v2/users/{username}") + @GET("/api/v3/users/{username}") fun getUser( @Header("Authorization") authToken: String, @Path("username") username: String, ): Call - @GET("/api/v2/timetable/{username}/") + @GET("/api/v3/timetable/{username}/") fun getTimeTable( @Header("Authorization") authToken: String, @Path("username") username: String, ): Call - @GET("/api/v2/circles/{circleId}/{username}/") + @GET("/api/v3/circles/{circleId}/{username}/") fun getCircleTimeTable( @Header("Authorization") authToken: String, @Path("circleId") circleId: String, @Path("username") username: String, ): Call - @GET("/api/v2/friends/{username}/") + @GET("/api/v3/friends/{username}/") fun getFriendList( @Header("Authorization") authToken: String, @Path("username") username: String, ): Call - @GET("/api/v2/users/search") + @GET("/api/v3/users/search") fun searchUsers( @Header("Authorization") authToken: String, @Query("query") query: String, ): Call> - @GET("/api/v2/requests/") + @GET("/api/v3/requests/") fun getFriendRequests( @Header("Authorization") authToken: String, ): Call - @GET("/api/v2/users/suggested/") + @GET("/api/v3/users/suggested/") fun getSuggestedFriends( @Header("Authorization") authToken: String, ): Call> - @POST("/api/v2/requests/{username}/send") + @POST("/api/v3/requests/{username}/send") fun sendRequest( @Header("Authorization") authToken: String, @Path("username") username: String, ): Call - @POST("/api/v2/requests/{username}/accept/") + @POST("/api/v3/requests/{username}/accept/") fun acceptRequest( @Header("Authorization") authToken: String, @Path("username") username: String, ): Call - @POST("/api/v2/requests/{username}/decline/") + @POST("/api/v3/requests/{username}/decline/") fun declineRequest( @Header("Authorization") authToken: String, @Path("username") username: String, ): Call - @DELETE("/api/v2/friends/{username}/") + @DELETE("/api/v3/friends/{username}/") fun deleteFriend( @Header("Authorization") authToken: String, @Path("username") username: String, ): Call - @POST("/api/v2/friends/ghost/{username}") + @POST("/api/v3/friends/ghost/{username}") fun enableGhostMode( @Header("Authorization") authToken: String, @Path("username") username: String, ): Call - @POST("/api/v2/friends/alive/{username}") + @POST("/api/v3/friends/alive/{username}") fun disableGhostMode( @Header("Authorization") authToken: String, @Path("username") username: String, ): Call - @GET("/api/v2/circles") + @GET("/api/v3/circles") fun getCircles( @Header("Authorization") authToken: String, ): Call - @POST("/api/v2/circles/create/{circleName}") + @POST("/api/v3/circles/create/{circleName}") fun createCircle( @Header("Authorization") authToken: String, @Path("circleName") circleName: String, ): Call - @POST("/api/v2/circles/join") + @POST("/api/v3/circles/join") fun joinCircleByCode( @Header("Authorization") authToken: String, @Query("code") joinCode: String, ): Call - @GET("/api/v2/circles/{circleId}") + @GET("/api/v3/circles/{circleId}") fun getCircleDetails( @Header("Authorization") authToken: String, @Path("circleId") circleId: String, ): Call - @POST("/api/v2/circles/sendRequest/{circleId}/{username}") + @POST("/api/v3/circles/sendRequest/{circleId}/{username}") fun sendCircleRequest( @Header("Authorization") authToken: String, @Path("circleId") circleId: String, @Path("username") username: String, ): Call - @GET("/api/v2/circles/requests/received") + @GET("/api/v3/circles/requests/received") fun getReceivedCircleRequests( @Header("Authorization") authToken: String, ): Call - @GET("/api/v2/circles/requests/sent") + @GET("/api/v3/circles/requests/sent") fun getSentCircleRequests( @Header("Authorization") authToken: String, ): Call - @DELETE("/api/v2/circles/{circleId}") + @DELETE("/api/v3/circles/{circleId}") fun deleteCircle( @Header("Authorization") authToken: String, @Path("circleId") circleId: String, ): Call - @DELETE("/api/v2/circles/leave/{circleId}") + @DELETE("/api/v3/circles/leave/{circleId}") fun leaveCircle( @Header("Authorization") authToken: String, @Path("circleId") circleId: String, ): Call + + @POST("/api/v3/circles/acceptRequest/{circleId}") + fun acceptCircleRequest( + @Header("Authorization") authToken: String, + @Path("circleId") circleId: String, + ): Call + + @POST("/api/v3/circles/declineRequest/{circleId}") + fun declineCircleRequest( + @Header("Authorization") authToken: String, + @Path("circleId") circleId: String, + ): Call + + @DELETE("/api/v3/circles/unsendRequest/{circleId}/{username}") + fun unsendCircleRequest( + @Header("Authorization") authToken: String, + @Path("circleId") circleId: String, + @Path("username") username: String, + ): Call + + @GET("/api/v3/timetable/emptyClassRooms") + fun getEmptyClassrooms( + @Header("Authorization") authToken: String, + @Query("slot") slot: String, + ): Call>> } diff --git a/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunityRestClient.kt b/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunityRestClient.kt index 7a0569d..f14a94a 100644 --- a/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunityRestClient.kt +++ b/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunityRestClient.kt @@ -793,4 +793,125 @@ class APICommunityRestClient { }, ) } + + fun acceptCircleRequest( + token: String, + circleId: String, + retrofitUserActionListener: RetrofitUserActionListener, + ) { + val bearerToken = "Bearer $token" + + mApiUser = retrofit.create(APICommunity::class.java) + val apiAcceptCircleRequestCall = mApiUser!!.acceptCircleRequest(bearerToken, circleId) + apiAcceptCircleRequestCall.enqueue( + object : Callback { + override fun onResponse( + call: Call, + response: Response, + ) { + Timber.d("AcceptCircleRequest: ${response.body()}") + retrofitUserActionListener.onSuccess(call, response.body()) + } + + override fun onFailure( + call: Call, + t: Throwable, + ) { + Timber.d("AcceptCircleRequestError: ${t.message}") + retrofitUserActionListener.onError(call, t) + } + }, + ) + } + + fun declineCircleRequest( + token: String, + circleId: String, + retrofitUserActionListener: RetrofitUserActionListener, + ) { + val bearerToken = "Bearer $token" + + mApiUser = retrofit.create(APICommunity::class.java) + val apiDeclineCircleRequestCall = mApiUser!!.declineCircleRequest(bearerToken, circleId) + apiDeclineCircleRequestCall.enqueue( + object : Callback { + override fun onResponse( + call: Call, + response: Response, + ) { + Timber.d("DeclineCircleRequest: ${response.body()}") + retrofitUserActionListener.onSuccess(call, response.body()) + } + + override fun onFailure( + call: Call, + t: Throwable, + ) { + Timber.d("DeclineCircleRequestError: ${t.message}") + retrofitUserActionListener.onError(call, t) + } + }, + ) + } + + fun unsendCircleRequest( + token: String, + circleId: String, + username: String, + retrofitUserActionListener: RetrofitUserActionListener, + ) { + val bearerToken = "Bearer $token" + + mApiUser = retrofit.create(APICommunity::class.java) + val apiUnsendCircleRequestCall = mApiUser!!.unsendCircleRequest(bearerToken, circleId, username) + apiUnsendCircleRequestCall.enqueue( + object : Callback { + override fun onResponse( + call: Call, + response: Response, + ) { + Timber.d("UnsendCircleRequest: ${response.body()}") + retrofitUserActionListener.onSuccess(call, response.body()) + } + + override fun onFailure( + call: Call, + t: Throwable, + ) { + Timber.d("UnsendCircleRequestError: ${t.message}") + retrofitUserActionListener.onError(call, t) + } + }, + ) + } + + fun getEmptyClassrooms( + token: String, + slot: String, + callback: (Map>?) -> Unit, + ) { + val bearerToken = "Bearer $token" + + mApiUser = retrofit.create(APICommunity::class.java) + val apiEmptyClassroomsCall = mApiUser!!.getEmptyClassrooms(bearerToken, slot) + apiEmptyClassroomsCall.enqueue( + object : Callback>> { + override fun onResponse( + call: Call>>, + response: Response>>, + ) { + Timber.d("EmptyClassrooms: ${response.body()}") + callback(response.body()) + } + + override fun onFailure( + call: Call>>, + t: Throwable, + ) { + Timber.d("EmptyClassroomsError: ${t.message}") + callback(null) + } + }, + ) + } } diff --git a/app/src/main/java/com/dscvit/vitty/ui/connect/AddParticipantsScreenContent.kt b/app/src/main/java/com/dscvit/vitty/ui/connect/AddParticipantsScreenContent.kt index 73132f6..4827414 100644 --- a/app/src/main/java/com/dscvit/vitty/ui/connect/AddParticipantsScreenContent.kt +++ b/app/src/main/java/com/dscvit/vitty/ui/connect/AddParticipantsScreenContent.kt @@ -85,7 +85,7 @@ fun AddParticipantsScreenContent( var isSearching by remember { mutableStateOf(false) } var selectedUsers by remember { mutableStateOf(setOf()) } var sendingRequests by remember { mutableStateOf(false) } - var requestStates by remember { mutableStateOf(mapOf()) } + var requestStates by remember { mutableStateOf(mapOf()) } var pendingRequestsCount by remember { mutableStateOf(0) } val sharedPreferences = @@ -109,7 +109,6 @@ fun AddParticipantsScreenContent( val searchResults by communityViewModel.searchResult.observeAsState() val actionResponse by communityViewModel.actionResponse.observeAsState() - LaunchedEffect(actionResponse) { if (sendingRequests && actionResponse != null) { val currentUser = requestStates.keys.find { requestStates[it] == null } @@ -121,11 +120,9 @@ fun AddParticipantsScreenContent( } pendingRequestsCount-- - - communityViewModel.actionResponse.postValue(null) - - if (pendingRequestsCount == 0) { + communityViewModel.actionResponse.postValue(null) + val failedUsers = requestStates.filter { it.value == false }.keys.toList() if (failedUsers.isEmpty()) { @@ -205,7 +202,6 @@ fun AddParticipantsScreenContent( .fillMaxSize() .background(Background), ) { - CenterAlignedTopAppBar( title = { Text( @@ -237,7 +233,6 @@ fun AddParticipantsScreenContent( .fillMaxSize() .padding(horizontal = 24.dp, vertical = 16.dp), ) { - Box( modifier = Modifier @@ -300,9 +295,7 @@ fun AddParticipantsScreenContent( Spacer(modifier = Modifier.height(16.dp)) - if (selectedUsers.isNotEmpty()) { - LazyColumn( modifier = Modifier.height(150.dp), contentPadding = PaddingValues(vertical = 4.dp), @@ -335,7 +328,6 @@ fun AddParticipantsScreenContent( Spacer(modifier = Modifier.height(8.dp)) } - val displayUsers = if (searchQuery.isBlank()) filteredSuggestedFriends else filteredSearchResults val filteredUsers = displayUsers?.filter { user -> @@ -395,7 +387,6 @@ fun AddParticipantsScreenContent( } } - if (selectedUsers.isNotEmpty()) { Spacer(modifier = Modifier.height(16.dp)) Button( @@ -453,7 +444,6 @@ private fun UserSelectionCard( .padding(horizontal = 24.dp, vertical = 20.dp), verticalAlignment = Alignment.CenterVertically, ) { - Box( modifier = Modifier @@ -486,7 +476,6 @@ private fun UserSelectionCard( Spacer(modifier = Modifier.width(12.dp)) - Column(modifier = Modifier.weight(1f)) { Text( text = user.name, @@ -505,7 +494,6 @@ private fun UserSelectionCard( ) } - Box( modifier = Modifier @@ -551,7 +539,6 @@ private fun SelectedUserCard( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, ) { - Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.weight(1f), @@ -607,7 +594,6 @@ private fun SelectedUserCard( } } - IconButton( onClick = onRemove, modifier = Modifier.size(24.dp), diff --git a/app/src/main/java/com/dscvit/vitty/ui/connect/CircleDetailScreenContent.kt b/app/src/main/java/com/dscvit/vitty/ui/connect/CircleDetailScreenContent.kt index a0f07c6..fe754f7 100644 --- a/app/src/main/java/com/dscvit/vitty/ui/connect/CircleDetailScreenContent.kt +++ b/app/src/main/java/com/dscvit/vitty/ui/connect/CircleDetailScreenContent.kt @@ -69,6 +69,7 @@ import com.dscvit.vitty.theme.Secondary import com.dscvit.vitty.theme.TextColor import com.dscvit.vitty.util.Constants import com.dscvit.vitty.util.QRCodeGenerator +import com.dscvit.vitty.util.urlDecode import java.util.Locale @OptIn(ExperimentalMaterial3Api::class) @@ -116,6 +117,7 @@ fun CircleDetailScreenContent( val sharedPreferences = context.getSharedPreferences(Constants.USER_INFO, Context.MODE_PRIVATE) val token = sharedPreferences.getString(Constants.COMMUNITY_TOKEN, "") ?: "" connectViewModel.getCircleList(token) + connectViewModel.refreshCircleRequests(token) connectViewModel.clearCircleActionResponse() onBackClick() } @@ -124,6 +126,7 @@ fun CircleDetailScreenContent( val sharedPreferences = context.getSharedPreferences(Constants.USER_INFO, Context.MODE_PRIVATE) val token = sharedPreferences.getString(Constants.COMMUNITY_TOKEN, "") ?: "" connectViewModel.getCircleList(token) + connectViewModel.refreshCircleRequests(token) connectViewModel.clearCircleActionResponse() onBackClick() } @@ -160,7 +163,6 @@ fun CircleDetailScreenContent( ) } }, - actions = { Box { IconButton(onClick = { @@ -188,7 +190,6 @@ fun CircleDetailScreenContent( } if (showDropdownMenu) { - DropdownMenu( expanded = showDropdownMenu, onDismissRequest = { showDropdownMenu = false }, @@ -311,7 +312,7 @@ fun CircleDetailScreenContent( verticalAlignment = Alignment.CenterVertically, ) { Text( - text = circle.circle_name, + text = circle.circle_name.urlDecode(), style = MaterialTheme.typography.titleLarge.copy( letterSpacing = (0.28).sp, @@ -528,7 +529,6 @@ fun CircleDetailScreenContent( ) } - if (showDeleteConfirmDialog) { AlertDialog( onDismissRequest = { showDeleteConfirmDialog = false }, @@ -581,7 +581,6 @@ fun CircleDetailScreenContent( ) } - if (showLeaveConfirmDialog) { AlertDialog( onDismissRequest = { showLeaveConfirmDialog = false }, @@ -633,28 +632,6 @@ fun CircleDetailScreenContent( }, ) } - - val circleActionResponse by connectViewModel.circleActionResponse.observeAsState() - - - LaunchedEffect(circleActionResponse) { - circleActionResponse?.let { response -> - when (response.detail) { - "Circle deleted successfully" -> { - Toast.makeText(context, "Circle deleted successfully", Toast.LENGTH_SHORT).show() - onBackClick() - } - "Left circle successfully" -> { - Toast.makeText(context, "Left circle successfully", Toast.LENGTH_SHORT).show() - onBackClick() - } - else -> { - Toast.makeText(context, response.detail ?: "Action completed", Toast.LENGTH_SHORT).show() - } - } - connectViewModel.clearCircleActionResponse() - } - } } @Composable diff --git a/app/src/main/java/com/dscvit/vitty/ui/connect/CircleMemberDetailScreenContent.kt b/app/src/main/java/com/dscvit/vitty/ui/connect/CircleMemberDetailScreenContent.kt index 06368e7..537736a 100644 --- a/app/src/main/java/com/dscvit/vitty/ui/connect/CircleMemberDetailScreenContent.kt +++ b/app/src/main/java/com/dscvit/vitty/ui/connect/CircleMemberDetailScreenContent.kt @@ -1,7 +1,11 @@ package com.dscvit.vitty.ui.connect import android.content.Context +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -65,6 +69,8 @@ import com.dscvit.vitty.theme.TextColor import com.dscvit.vitty.ui.schedule.ScheduleViewModel import com.dscvit.vitty.util.Constants import com.dscvit.vitty.util.Quote +import com.dscvit.vitty.util.VITMap +import com.dscvit.vitty.widget.parseTimeToTimestamp import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -345,6 +351,8 @@ private fun DayScheduleContent( dayIndex: Int, quote: String = "Every day is a new opportunity to learn and grow.", ) { + val context = LocalContext.current + if (periods.isEmpty()) { Box( modifier = @@ -395,6 +403,9 @@ private fun DayScheduleContent( CircleMemberPeriodCard( period = period, dayIndex = dayIndex, + onLocationClick = { roomNo -> + VITMap.openClassMap(context, roomNo) + }, ) } } @@ -405,6 +416,7 @@ private fun DayScheduleContent( private fun CircleMemberPeriodCard( period: PeriodDetails, dayIndex: Int, + onLocationClick: (String) -> Unit, ) { val timeFormat = remember { SimpleDateFormat("h:mm a", Locale.getDefault()) } val startTimeStr = @@ -455,80 +467,95 @@ private fun CircleMemberPeriodCard( .fillMaxWidth(), colors = CardDefaults.cardColors( - containerColor = if (isActive) Accent.copy(alpha = 0.1f) else Secondary, + containerColor = Secondary, ), + border = + if (isActive) { + BorderStroke( + 1.dp, + Accent, + ) + } else { + null + }, shape = RoundedCornerShape(16.dp), ) { Column( modifier = Modifier .fillMaxWidth() - .padding(20.dp), + .padding(horizontal = 24.dp, vertical = 28.dp), ) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, + verticalAlignment = Alignment.Top, ) { - Column(modifier = Modifier.weight(1f)) { + Column( + modifier = Modifier.weight(1f), + ) { Text( text = period.courseName, - style = MaterialTheme.typography.titleMedium, + style = MaterialTheme.typography.labelLarge, fontWeight = FontWeight.Bold, color = TextColor, maxLines = 2, overflow = TextOverflow.Ellipsis, ) - - Spacer(modifier = Modifier.height(4.dp)) - - Text( - text = period.courseCode, - style = MaterialTheme.typography.bodyMedium, - color = Accent, - fontWeight = FontWeight.Medium, - ) } + } - Column( - horizontalAlignment = Alignment.End, - ) { - Text( - text = "$startTimeStr - $endTimeStr", - style = MaterialTheme.typography.bodyMedium, - color = TextColor, - fontWeight = FontWeight.Medium, - ) + Spacer(modifier = Modifier.height(4.dp)) - if (period.roomNo.isNotEmpty()) { - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = period.roomNo, - style = MaterialTheme.typography.bodySmall, - color = Accent, - ) - } - } - } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = "$startTimeStr - $endTimeStr | ${period.slot}", + style = MaterialTheme.typography.bodyMedium, + color = Accent, + fontWeight = FontWeight.Medium, + ) - if (isActive) { - Spacer(modifier = Modifier.height(12.dp)) - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(6.dp), - ) { + if (period.roomNo.isNotEmpty()) { Box( modifier = Modifier - .size(8.dp) - .background(Accent, CircleShape), - ) - Text( - text = "Currently in this class", - style = MaterialTheme.typography.bodySmall, - color = Accent, - fontWeight = FontWeight.Medium, - ) + .clip(RoundedCornerShape(9999.dp)) + .clickable( + onClick = { onLocationClick(period.roomNo) }, + ).border( + width = 1.dp, + color = Accent, + shape = RoundedCornerShape(9999.dp), + ).padding(horizontal = 8.dp, vertical = 4.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Image( + painter = painterResource(id = R.drawable.ic_compass), + contentDescription = "Compass icon", + modifier = Modifier.size(12.dp), + alignment = Alignment.Center, + ) + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = period.roomNo, + style = + MaterialTheme.typography.bodyMedium.copy( + fontSize = 12.sp, + lineHeight = 12.sp, + letterSpacing = (-0.12).sp, + textAlign = TextAlign.Center, + ), + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.Medium, + ) + } + } } } } @@ -575,35 +602,3 @@ private suspend fun processFriendTimetableData( result } - -private fun parseTimeToTimestamp(timeString: String): com.google.firebase.Timestamp = - try { - val sanitizedTime = - if (timeString.contains("+05:53")) { - timeString.replace("+05:53", "+05:30") - } else { - timeString - } - val time = replaceYearIfZero(sanitizedTime) - - val dateFormat = java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX", java.util.Locale.getDefault()) - val date = dateFormat.parse(time) - if (date != null) { - com.google.firebase.Timestamp(date) - } else { - Timber.d("Date parsing error: Unable to parse sanitized time: $time") - com.google.firebase.Timestamp - .now() - } - } catch (e: Exception) { - Timber.d("Date parsing error: Unparseable date: \"$timeString\"") - com.google.firebase.Timestamp - .now() - } - -private fun replaceYearIfZero(timeString: String): String = - if (timeString.startsWith("0000")) { - "2023" + timeString.substring(4) - } else { - timeString - } diff --git a/app/src/main/java/com/dscvit/vitty/ui/connect/CircleRequestsScreenContent.kt b/app/src/main/java/com/dscvit/vitty/ui/connect/CircleRequestsScreenContent.kt index 167b886..bd8d0b6 100644 --- a/app/src/main/java/com/dscvit/vitty/ui/connect/CircleRequestsScreenContent.kt +++ b/app/src/main/java/com/dscvit/vitty/ui/connect/CircleRequestsScreenContent.kt @@ -1,5 +1,7 @@ package com.dscvit.vitty.ui.connect +import android.content.Context +import android.widget.Toast import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -17,6 +19,9 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Close import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.CenterAlignedTopAppBar @@ -32,13 +37,16 @@ import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -49,8 +57,12 @@ import com.dscvit.vitty.R import com.dscvit.vitty.network.api.community.responses.circle.CircleRequestItem import com.dscvit.vitty.theme.Accent import com.dscvit.vitty.theme.Background +import com.dscvit.vitty.theme.Green +import com.dscvit.vitty.theme.Red import com.dscvit.vitty.theme.Secondary import com.dscvit.vitty.theme.TextColor +import com.dscvit.vitty.util.Constants +import com.dscvit.vitty.util.urlDecode @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -58,12 +70,57 @@ fun CircleRequestsScreenContent( onBackClick: () -> Unit = {}, connectViewModel: ConnectViewModel, ) { + val context = LocalContext.current var selectedTab by remember { mutableIntStateOf(0) } + var processedRequests by remember { mutableStateOf(setOf()) } val tabs = listOf("Received", "Sent") val receivedCircleRequests by connectViewModel.receivedCircleRequests.observeAsState() val sentCircleRequests by connectViewModel.sentCircleRequests.observeAsState() val isLoading by connectViewModel.isCircleRequestsLoading.observeAsState(false) + val circleActionResponse by connectViewModel.circleActionResponse.observeAsState() + + LaunchedEffect(receivedCircleRequests, sentCircleRequests) { + processedRequests = setOf() + } + + LaunchedEffect(circleActionResponse) { + circleActionResponse?.let { response -> + when (response.detail) { + "request accepted successfully" -> { + Toast.makeText(context, "Circle request accepted", Toast.LENGTH_SHORT).show() + connectViewModel.clearCircleActionResponse() + + val sharedPreferences = context.getSharedPreferences(Constants.USER_INFO, Context.MODE_PRIVATE) + val token = sharedPreferences.getString(Constants.COMMUNITY_TOKEN, "") ?: "" + if (token.isNotEmpty()) { + connectViewModel.refreshCircleRequests(token) + connectViewModel.getCircleList(token) + } + } + "request declined successfully" -> { + Toast.makeText(context, "Circle request declined", Toast.LENGTH_SHORT).show() + connectViewModel.clearCircleActionResponse() + + val sharedPreferences = context.getSharedPreferences(Constants.USER_INFO, Context.MODE_PRIVATE) + val token = sharedPreferences.getString(Constants.COMMUNITY_TOKEN, "") ?: "" + if (token.isNotEmpty()) { + connectViewModel.refreshCircleRequests(token) + } + } + "request unsent successfully" -> { + Toast.makeText(context, "Circle request unsent", Toast.LENGTH_SHORT).show() + connectViewModel.clearCircleActionResponse() + + val sharedPreferences = context.getSharedPreferences(Constants.USER_INFO, Context.MODE_PRIVATE) + val token = sharedPreferences.getString(Constants.COMMUNITY_TOKEN, "") ?: "" + if (token.isNotEmpty()) { + connectViewModel.refreshCircleRequests(token) + } + } + } + } + } Column( modifier = @@ -152,20 +209,32 @@ fun CircleRequestsScreenContent( when (selectedTab) { 0 -> { val requests = receivedCircleRequests?.data ?: emptyList() + val pendingRequests = requests.filter { !processedRequests.contains(it.circle_id) } CircleRequestsList( - requests = requests, + requests = pendingRequests, isReceived = true, emptyTitle = "No circle requests", emptySubtitle = "You have no pending circle invitations", + connectViewModel = connectViewModel, + processedRequests = processedRequests, + onRequestProcessed = { circleId -> + processedRequests = processedRequests + circleId + }, ) } 1 -> { val requests = sentCircleRequests?.data ?: emptyList() + val pendingRequests = requests.filter { !processedRequests.contains(it.circle_id) } CircleRequestsList( - requests = requests, + requests = pendingRequests, isReceived = false, emptyTitle = "No sent requests", emptySubtitle = "You haven't sent any circle requests", + connectViewModel = connectViewModel, + processedRequests = processedRequests, + onRequestProcessed = { circleId -> + processedRequests = processedRequests + circleId + }, ) } } @@ -179,6 +248,9 @@ private fun CircleRequestsList( isReceived: Boolean, emptyTitle: String, emptySubtitle: String, + connectViewModel: ConnectViewModel, + processedRequests: Set, + onRequestProcessed: (String) -> Unit, ) { if (requests.isEmpty()) { Box( @@ -219,6 +291,9 @@ private fun CircleRequestsList( CircleRequestCard( request = request, isReceived = isReceived, + connectViewModel = connectViewModel, + processedRequests = processedRequests, + onRequestProcessed = onRequestProcessed, ) } } @@ -229,7 +304,12 @@ private fun CircleRequestsList( private fun CircleRequestCard( request: CircleRequestItem, isReceived: Boolean, + connectViewModel: ConnectViewModel, + processedRequests: Set, + onRequestProcessed: (String) -> Unit, ) { + val context = LocalContext.current + Card( modifier = Modifier.fillMaxWidth(), colors = CardDefaults.cardColors(containerColor = Secondary), @@ -240,7 +320,7 @@ private fun CircleRequestCard( modifier = Modifier .fillMaxWidth() - .padding(24.dp), + .padding(start = 24.dp, top = 20.dp, end = 16.dp, bottom = 20.dp), ) { Row( modifier = Modifier.fillMaxWidth(), @@ -269,7 +349,7 @@ private fun CircleRequestCard( Column(modifier = Modifier.weight(1f)) { Text( - text = request.circle_name, + text = request.circle_name.urlDecode(), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Medium, color = TextColor, @@ -295,8 +375,58 @@ private fun CircleRequestCard( ) } - // TODO: Add accept/reject buttons for received requests - // For now, we'll just show the request information + if (isReceived) { + IconButton( + onClick = { + val sharedPreferences = context.getSharedPreferences(Constants.USER_INFO, Context.MODE_PRIVATE) + val token = sharedPreferences.getString(Constants.COMMUNITY_TOKEN, "") ?: "" + if (token.isNotEmpty()) { + onRequestProcessed(request.circle_id) + connectViewModel.declineCircleRequest(token, request.circle_id) + } + }, + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "Decline", + tint = Red, + ) + } + + IconButton( + onClick = { + val sharedPreferences = context.getSharedPreferences(Constants.USER_INFO, Context.MODE_PRIVATE) + val token = sharedPreferences.getString(Constants.COMMUNITY_TOKEN, "") ?: "" + if (token.isNotEmpty()) { + onRequestProcessed(request.circle_id) + connectViewModel.acceptCircleRequest(token, request.circle_id) + } + }, + ) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = "Accept", + tint = Green, + ) + } + } else { + IconButton( + onClick = { + val sharedPreferences = context.getSharedPreferences(Constants.USER_INFO, Context.MODE_PRIVATE) + val token = sharedPreferences.getString(Constants.COMMUNITY_TOKEN, "") ?: "" + if (token.isNotEmpty()) { + onRequestProcessed(request.circle_id) + connectViewModel.unsendCircleRequest(token, request.circle_id, request.to_username) + } + }, + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "Unsend", + tint = Red, + ) + } + } } } } diff --git a/app/src/main/java/com/dscvit/vitty/ui/connect/ConnectScreenContent.kt b/app/src/main/java/com/dscvit/vitty/ui/connect/ConnectScreenContent.kt index 1630161..6697bdc 100644 --- a/app/src/main/java/com/dscvit/vitty/ui/connect/ConnectScreenContent.kt +++ b/app/src/main/java/com/dscvit/vitty/ui/connect/ConnectScreenContent.kt @@ -66,14 +66,14 @@ fun ConnectScreenContent( val isCircleRefreshing by connectViewModel.isCircleRefreshing.observeAsState(false) val friendRequests by connectViewModel.friendRequest.observeAsState() val receivedCircleRequests by connectViewModel.receivedCircleRequests.observeAsState() - val sentCircleRequests by connectViewModel.sentCircleRequests.observeAsState() + val requestActionResponse by connectViewModel.requestActionResponse.observeAsState() + val circleActionResponse by connectViewModel.circleActionResponse.observeAsState() val isNetworkAvailable = remember { mutableStateOf(isNetworkAvailable(context)) } var isCircleActionSheetVisible by remember { mutableStateOf(false) } - - val totalCircleRequests = (receivedCircleRequests?.data?.size ?: 0) + (sentCircleRequests?.data?.size ?: 0) + val totalCircleRequests = (receivedCircleRequests?.data?.size ?: 0) LaunchedEffect(Unit) { while (true) { @@ -82,6 +82,60 @@ fun ConnectScreenContent( } } + LaunchedEffect(requestActionResponse) { + requestActionResponse?.let { response -> + if (response.detail == "Friend request accepted successfully!" || + response.detail == "Friend request rejected successfully" + ) { + connectViewModel.clearRequestActionResponse() + + val sharedPreferences = context.getSharedPreferences(Constants.USER_INFO, Context.MODE_PRIVATE) + val token = sharedPreferences.getString(Constants.COMMUNITY_TOKEN, "") ?: "" + val username = sharedPreferences.getString(Constants.COMMUNITY_USERNAME, "") ?: "" + + if (token.isNotEmpty()) { + connectViewModel.getFriendRequest(token) + connectViewModel.getFriendList(token, username) + } + } + } + } + + LaunchedEffect(circleActionResponse) { + circleActionResponse?.let { response -> + when (response.detail) { + "request accepted successfully" -> { + connectViewModel.clearCircleActionResponse() + + val sharedPreferences = context.getSharedPreferences(Constants.USER_INFO, Context.MODE_PRIVATE) + val token = sharedPreferences.getString(Constants.COMMUNITY_TOKEN, "") ?: "" + if (token.isNotEmpty()) { + connectViewModel.refreshCircleRequests(token) + connectViewModel.getCircleList(token) + } + } + "request declined successfully" -> { + connectViewModel.clearCircleActionResponse() + + val sharedPreferences = context.getSharedPreferences(Constants.USER_INFO, Context.MODE_PRIVATE) + val token = sharedPreferences.getString(Constants.COMMUNITY_TOKEN, "") ?: "" + if (token.isNotEmpty()) { + connectViewModel.refreshCircleRequests(token) + } + } + "request unsent successfully" -> { + connectViewModel.clearCircleActionResponse() + + val sharedPreferences = context.getSharedPreferences(Constants.USER_INFO, Context.MODE_PRIVATE) + val token = sharedPreferences.getString(Constants.COMMUNITY_TOKEN, "") ?: "" + if (token.isNotEmpty()) { + connectViewModel.refreshCircleRequests(token) + } + } + } + } + } + val tabs = listOf("Friends", "Circles") var selectedTab by rememberSaveable { mutableIntStateOf(0) } var searchQuery by remember { mutableStateOf("") } @@ -166,7 +220,6 @@ fun ConnectScreenContent( selectedTab = pagerState.currentPage } - LaunchedEffect(createCircleResponse) { createCircleResponse?.let { response -> Toast @@ -176,19 +229,16 @@ fun ConnectScreenContent( Toast.LENGTH_LONG, ).show() - val sharedPreferences = context.getSharedPreferences(Constants.USER_INFO, Context.MODE_PRIVATE) val token = sharedPreferences.getString(Constants.COMMUNITY_TOKEN, "") ?: "" if (token.isNotEmpty()) { connectViewModel.refreshCircleList(token) } - connectViewModel.clearCreateCircleResponse() } } - LaunchedEffect(joinCircleResponse) { joinCircleResponse?.let { response -> Toast @@ -198,19 +248,16 @@ fun ConnectScreenContent( Toast.LENGTH_LONG, ).show() - val sharedPreferences = context.getSharedPreferences(Constants.USER_INFO, Context.MODE_PRIVATE) val token = sharedPreferences.getString(Constants.COMMUNITY_TOKEN, "") ?: "" if (token.isNotEmpty()) { connectViewModel.refreshCircleList(token) } - connectViewModel.clearJoinCircleResponse() } } - LaunchedEffect(joinCircleError) { joinCircleError?.let { error -> Toast @@ -220,7 +267,6 @@ fun ConnectScreenContent( Toast.LENGTH_LONG, ).show() - connectViewModel.clearJoinCircleError() } } diff --git a/app/src/main/java/com/dscvit/vitty/ui/connect/ConnectViewModel.kt b/app/src/main/java/com/dscvit/vitty/ui/connect/ConnectViewModel.kt index 8540c6c..502d1f4 100644 --- a/app/src/main/java/com/dscvit/vitty/ui/connect/ConnectViewModel.kt +++ b/app/src/main/java/com/dscvit/vitty/ui/connect/ConnectViewModel.kt @@ -227,6 +227,7 @@ class ConnectViewModel : ViewModel() { t: Throwable?, ) { Timber.d("AcceptRequest: ${t?.message}") + _requestActionResponse.postValue(null) } }, ) @@ -253,6 +254,7 @@ class ConnectViewModel : ViewModel() { t: Throwable?, ) { Timber.d("RejectRequest: ${t?.message}") + _requestActionResponse.postValue(null) } }, ) @@ -299,7 +301,6 @@ class ConnectViewModel : ViewModel() { _circleList.postValue(response) _isCircleRefreshing.postValue(false) - response?.data?.forEach { circle -> getCircleDetails(token, circle.circle_id) } @@ -404,7 +405,6 @@ class ConnectViewModel : ViewModel() { "ConnectViewModel.joinCircleByCode called with token: ${if (token.isNotEmpty()) "exists" else "empty"}, joinCode: $joinCode", ) - _joinCircleResponse.postValue(null) _joinCircleError.postValue(null) @@ -558,4 +558,87 @@ class ConnectViewModel : ViewModel() { fun clearCircleActionResponse() { _circleActionResponse.postValue(null) } + + fun acceptCircleRequest( + token: String, + circleId: String, + ) { + APICommunityRestClient.instance.acceptCircleRequest( + token, + circleId, + object : RetrofitUserActionListener { + override fun onSuccess( + call: Call?, + response: PostResponse?, + ) { + Timber.d("AcceptCircleRequest: $response") + _circleActionResponse.postValue(response) + } + + override fun onError( + call: Call?, + t: Throwable?, + ) { + Timber.d("AcceptCircleRequestError: ${t?.message}") + _circleActionResponse.postValue(null) + } + }, + ) + } + + fun declineCircleRequest( + token: String, + circleId: String, + ) { + APICommunityRestClient.instance.declineCircleRequest( + token, + circleId, + object : RetrofitUserActionListener { + override fun onSuccess( + call: Call?, + response: PostResponse?, + ) { + Timber.d("DeclineCircleRequest: $response") + _circleActionResponse.postValue(response) + } + + override fun onError( + call: Call?, + t: Throwable?, + ) { + Timber.d("DeclineCircleRequestError: ${t?.message}") + _circleActionResponse.postValue(null) + } + }, + ) + } + + fun unsendCircleRequest( + token: String, + circleId: String, + username: String, + ) { + APICommunityRestClient.instance.unsendCircleRequest( + token, + circleId, + username, + object : RetrofitUserActionListener { + override fun onSuccess( + call: Call?, + response: PostResponse?, + ) { + Timber.d("UnsendCircleRequest: $response") + _circleActionResponse.postValue(response) + } + + override fun onError( + call: Call?, + t: Throwable?, + ) { + Timber.d("UnsendCircleRequestError: ${t?.message}") + _circleActionResponse.postValue(null) + } + }, + ) + } } diff --git a/app/src/main/java/com/dscvit/vitty/ui/connect/FriendDetailScreenContent.kt b/app/src/main/java/com/dscvit/vitty/ui/connect/FriendDetailScreenContent.kt index b95764a..e770ab6 100644 --- a/app/src/main/java/com/dscvit/vitty/ui/connect/FriendDetailScreenContent.kt +++ b/app/src/main/java/com/dscvit/vitty/ui/connect/FriendDetailScreenContent.kt @@ -74,7 +74,7 @@ import com.dscvit.vitty.theme.TextColor import com.dscvit.vitty.ui.schedule.ScheduleViewModel import com.dscvit.vitty.util.Constants import com.dscvit.vitty.util.Quote -import com.google.firebase.Timestamp +import com.dscvit.vitty.widget.parseTimeToTimestamp import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -826,33 +826,3 @@ private suspend fun processFriendTimetableData(friend: TimetableResponse): Map + FilterChip( + label = label, + isSelected = friendsFilter == index, + onClick = { onFriendsFilterChange(index) }, + ) + if (index < options.lastIndex) { + Spacer(Modifier.width(12.dp)) + } + } + } + val requestCount = friendRequests?.size ?: 0 if (requestCount > 0) { Box( @@ -232,32 +248,19 @@ fun ConnectHeader( style = MaterialTheme.typography.bodyMedium, ) } - Spacer(Modifier.width(12.dp)) - } - - val options = listOf("Available", "View All") - options.forEachIndexed { index, label -> - FilterChip( - label = label, - isSelected = friendsFilter == index, - onClick = { onFriendsFilterChange(index) }, - ) - if (index < options.lastIndex) { - Spacer(Modifier.width(12.dp)) - } } } } if (selectedTab == 1) { Spacer(modifier = Modifier.height(16.dp)) - Row( modifier = Modifier .fillMaxWidth() .padding(horizontal = 20.dp), verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.End, ) { Box( modifier = @@ -268,7 +271,7 @@ fun ConnectHeader( .padding(horizontal = 12.dp, vertical = 8.dp), ) { Text( - text = "$circleRequests circle request${if (circleRequests > 1) "s" else ""}", + text = "$circleRequests ${if (circleRequests == 0) "new" else "circle"} request${if (circleRequests > 1 || circleRequests == 0) "s" else ""}", color = Accent, fontWeight = FontWeight.Medium, style = MaterialTheme.typography.bodyMedium, @@ -632,7 +635,6 @@ fun CircleCard( val circleData = circleMembers?.get(circle.circle_id) val isLoadingMembers = circleMembersLoading?.contains(circle.circle_id) == true - LaunchedEffect(circle.circle_id, circleData, isLoadingMembers) { if (circleData == null && !isLoadingMembers) { val sharedPreferences = context.getSharedPreferences(Constants.USER_INFO, Context.MODE_PRIVATE) @@ -690,7 +692,7 @@ fun CircleCard( Column(modifier = Modifier.weight(1f)) { Text( - text = circle.circle_name, + text = circle.circle_name.urlDecode(), color = TextColor, fontWeight = FontWeight.Medium, fontSize = 16.sp, diff --git a/app/src/main/java/com/dscvit/vitty/ui/emptyclassrooms/EmptyClassroomsContent.kt b/app/src/main/java/com/dscvit/vitty/ui/emptyclassrooms/EmptyClassroomsContent.kt index f3be8d6..2c97b80 100644 --- a/app/src/main/java/com/dscvit/vitty/ui/emptyclassrooms/EmptyClassroomsContent.kt +++ b/app/src/main/java/com/dscvit/vitty/ui/emptyclassrooms/EmptyClassroomsContent.kt @@ -1,5 +1,8 @@ package com.dscvit.vitty.ui.emptyclassrooms +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -10,17 +13,26 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.LocationOn +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material.icons.filled.Warning import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults @@ -32,6 +44,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight @@ -39,19 +52,12 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.dscvit.vitty.R +import com.dscvit.vitty.network.api.community.APICommunityRestClient import com.dscvit.vitty.theme.Accent import com.dscvit.vitty.theme.Background import com.dscvit.vitty.theme.Secondary import com.dscvit.vitty.theme.TextColor import com.dscvit.vitty.util.Constants -import java.util.Calendar - -data class EmptyClassroom( - val roomNumber: String, - val building: String, - val capacity: Int, - val availableUntil: String, -) @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -61,15 +67,69 @@ fun EmptyClassroomsContent( ) { val context = LocalContext.current val prefs = remember { context.getSharedPreferences(Constants.USER_INFO, 0) } + val token = remember { prefs.getString(Constants.COMMUNITY_TOKEN, "") ?: "" } - var emptyClassrooms by remember { mutableStateOf>(emptyList()) } + var emptyClassrooms by remember { mutableStateOf>(emptyList()) } var isLoading by remember { mutableStateOf(true) } - var currentTimeSlot by remember { mutableStateOf("") } + var selectedSlot by remember { mutableStateOf("A1") } + var errorMessage by remember { mutableStateOf(null) } + + val regularSlots = + listOf( + "A1", + "A2", + "B1", + "B2", + "C1", + "C2", + "D1", + "D2", + "E1", + "E2", + "F1", + "F2", + "G1", + "G2", + "V1", + "V2", + "TA1", + "TA2", + "TB1", + "TB2", + "TC1", + "TC2", + "TD1", + "TD2", + "TE1", + "TE2", + "TF1", + "TF2", + "TG1", + "TG2", + ) + + fun fetchEmptyClassrooms(slot: String) { + if (token.isEmpty()) { + errorMessage = "Authentication required" + isLoading = false + return + } - LaunchedEffect(Unit) { - currentTimeSlot = getCurrentTimeSlot() - emptyClassrooms = generateMockEmptyClassrooms() - isLoading = false + isLoading = true + errorMessage = null + APICommunityRestClient.instance.getEmptyClassrooms(token, slot) { response -> + isLoading = false + if (response != null) { + emptyClassrooms = response[slot] ?: emptyList() + } else { + errorMessage = "Failed to fetch empty classrooms" + emptyClassrooms = emptyList() + } + } + } + + LaunchedEffect(selectedSlot) { + fetchEmptyClassrooms(selectedSlot) } Scaffold( @@ -92,6 +152,15 @@ fun EmptyClassroomsContent( ) } }, + actions = { + IconButton(onClick = { fetchEmptyClassrooms(selectedSlot) }) { + Icon( + imageVector = Icons.Default.Refresh, + contentDescription = "Refresh", + tint = TextColor, + ) + } + }, colors = TopAppBarDefaults.centerAlignedTopAppBarColors( containerColor = Background, @@ -101,71 +170,128 @@ fun EmptyClassroomsContent( containerColor = Background, modifier = modifier, ) { paddingValues -> - LazyVerticalGrid( - columns = GridCells.Fixed(2), + Column( modifier = Modifier .fillMaxSize() .padding(paddingValues), - contentPadding = PaddingValues(16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp), ) { - if (isLoading) { - item(span = { GridItemSpan(2) }) { - Box( - modifier = - Modifier - .fillMaxWidth() - .height(200.dp), - contentAlignment = Alignment.Center, - ) { - Text( - text = "Loading empty classrooms...", - color = Accent, - fontSize = 16.sp, + Column( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), + ) { + LazyRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = PaddingValues(horizontal = 4.dp), + ) { + items(regularSlots) { slot -> + SlotChip( + slot = slot, + isSelected = selectedSlot == slot, + onClick = { selectedSlot = slot }, ) } } - } else if (emptyClassrooms.isEmpty()) { - item(span = { GridItemSpan(2) }) { - Box( - modifier = - Modifier - .fillMaxWidth() - .height(200.dp), - contentAlignment = Alignment.Center, - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, + } + + LazyVerticalGrid( + columns = GridCells.Fixed(2), + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + if (isLoading) { + item(span = { GridItemSpan(2) }) { + Box( + modifier = + Modifier + .fillMaxWidth() + .height(200.dp), + contentAlignment = Alignment.Center, ) { - Icon( - painter = painterResource(R.drawable.ic_empty_classroom), - contentDescription = null, - tint = Accent, - modifier = Modifier.size(48.dp), - ) - Spacer(modifier = Modifier.height(16.dp)) - Text( - text = "No empty classrooms found", - color = TextColor, - fontSize = 18.sp, - fontWeight = FontWeight.Medium, - textAlign = TextAlign.Center, - ) - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = "All classrooms are currently occupied", - color = Accent, - fontSize = 14.sp, - textAlign = TextAlign.Center, - ) + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + CircularProgressIndicator(color = Accent) + } } } - } - } else { - items(emptyClassrooms) { classroom -> - EmptyClassroomItem(classroom = classroom) + } else if (errorMessage != null) { + item(span = { GridItemSpan(2) }) { + Box( + modifier = + Modifier + .fillMaxSize() + .padding(32.dp), + contentAlignment = Alignment.Center, + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + imageVector = Icons.Default.Warning, + contentDescription = null, + tint = Accent, + modifier = Modifier.size(48.dp), + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = errorMessage!!, + color = TextColor, + fontSize = 16.sp, + textAlign = TextAlign.Center, + ) + } + } + } + } else if (emptyClassrooms.isEmpty()) { + item(span = { GridItemSpan(2) }) { + Box( + modifier = + Modifier + .fillMaxSize() + .padding(32.dp), + contentAlignment = Alignment.Center, + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + painter = painterResource(R.drawable.ic_empty_classroom), + contentDescription = null, + tint = Accent, + modifier = Modifier.size(48.dp), + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = "No empty classrooms found", + color = TextColor, + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = "All classrooms are occupied during $selectedSlot slot", + color = Accent, + fontSize = 14.sp, + textAlign = TextAlign.Center, + ) + } + } + } + } else { + item(span = { GridItemSpan(2) }) { + Text( + text = "${emptyClassrooms.size} empty classrooms found for $selectedSlot", + style = MaterialTheme.typography.titleMedium, + color = TextColor, + modifier = Modifier.padding(bottom = 8.dp), + ) + } + + items(emptyClassrooms) { classroom -> + ClassroomCard(classroom = classroom) + } } } } @@ -173,74 +299,81 @@ fun EmptyClassroomsContent( } @Composable -private fun EmptyClassroomItem( - classroom: EmptyClassroom, - modifier: Modifier = Modifier, +private fun SlotChip( + slot: String, + isSelected: Boolean, + onClick: () -> Unit, ) { + Box( + modifier = + Modifier + .clip(RoundedCornerShape(20.dp)) + .background( + if (isSelected) Accent else Secondary, + ).border( + 1.dp, + if (isSelected) Accent else Secondary, + RoundedCornerShape(20.dp), + ).clickable { onClick() } + .padding(horizontal = 16.dp, vertical = 8.dp), + ) { + Text( + text = slot, + color = if (isSelected) Background else TextColor, + fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal, + fontSize = 14.sp, + ) + } +} + +@Composable +private fun ClassroomCard(classroom: String) { Card( - modifier = modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = Secondary), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp), shape = RoundedCornerShape(12.dp), - colors = - CardDefaults.cardColors( - containerColor = Secondary, - ), ) { Column( - modifier = Modifier.padding(16.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, ) { + Box( + modifier = + Modifier + .size(40.dp) + .background(Accent.copy(alpha = 0.1f), CircleShape), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = Icons.Default.LocationOn, + contentDescription = null, + tint = Accent, + modifier = Modifier.size(24.dp), + ) + } + + Spacer(modifier = Modifier.height(12.dp)) + Text( - text = classroom.roomNumber, + text = classroom, + style = MaterialTheme.typography.titleMedium, color = TextColor, - fontSize = 16.sp, fontWeight = FontWeight.SemiBold, + textAlign = TextAlign.Center, ) + Spacer(modifier = Modifier.height(4.dp)) + Text( - text = classroom.building, + text = "Available", + style = MaterialTheme.typography.bodySmall, color = Accent, - fontSize = 12.sp, + textAlign = TextAlign.Center, ) } } } - -private fun getCurrentTimeSlot(): String { - val calendar = Calendar.getInstance() - val hour = calendar.get(Calendar.HOUR_OF_DAY) - val minute = calendar.get(Calendar.MINUTE) - - return when { - hour < 8 -> "Pre-class hours" - hour == 8 && minute < 50 -> "A1 (8:00 - 8:50)" - hour == 8 && minute >= 50 || hour == 9 && minute < 40 -> "A2 (8:50 - 9:40)" - hour == 9 && minute >= 40 || hour == 10 && minute < 30 -> "B1 (9:40 - 10:30)" - hour == 10 && minute >= 30 || hour == 11 && minute < 20 -> "B2 (10:30 - 11:20)" - hour == 11 && minute >= 20 || hour == 12 && minute < 10 -> "C1 (11:20 - 12:10)" - hour == 12 && minute >= 10 || hour == 13 && minute < 0 -> "C2 (12:10 - 13:00)" - hour == 13 || hour == 14 && minute < 0 -> "Lunch Break" - hour == 14 && minute < 50 -> "D1 (14:00 - 14:50)" - hour == 14 && minute >= 50 || hour == 15 && minute < 40 -> "D2 (14:50 - 15:40)" - hour == 15 && minute >= 40 || hour == 16 && minute < 30 -> "E1 (15:40 - 16:30)" - hour == 16 && minute >= 30 || hour == 17 && minute < 20 -> "E2 (16:30 - 17:20)" - hour == 17 && minute >= 20 || hour == 18 && minute < 10 -> "F1 (17:20 - 18:10)" - hour == 18 && minute >= 10 || hour == 19 && minute < 0 -> "F2 (18:10 - 19:00)" - else -> "After class hours" - } -} - -private fun generateMockEmptyClassrooms(): List { - val buildings = listOf("SJT", "TT", "MB", "PRP", "SMV", "CDMM", "GDN") - val roomNumbers = (101..520).random() to (101..520).random() - - return listOf( - EmptyClassroom("SJT 101", "SJT Block", 60, "11:20 AM"), - EmptyClassroom("TT 205", "TT Block", 45, "12:10 PM"), - EmptyClassroom("MB 303", "Main Building", 80, "1:00 PM"), - EmptyClassroom("PRP 150", "PRP Block", 40, "2:00 PM"), - EmptyClassroom("SMV 201", "SMV Block", 55, "3:00 PM"), - EmptyClassroom("CDMM 102", "CDMM Block", 35, "4:00 PM"), - EmptyClassroom("GDN 301", "GDN Block", 70, "5:00 PM"), - EmptyClassroom("SJT 205", "SJT Block", 50, "6:00 PM"), - EmptyClassroom("TT 101", "TT Block", 65, "7:00 PM"), - ).shuffled().take(6) -} diff --git a/app/src/main/java/com/dscvit/vitty/ui/main/MainComposeApp.kt b/app/src/main/java/com/dscvit/vitty/ui/main/MainComposeApp.kt index 402d402..5c978ea 100644 --- a/app/src/main/java/com/dscvit/vitty/ui/main/MainComposeApp.kt +++ b/app/src/main/java/com/dscvit/vitty/ui/main/MainComposeApp.kt @@ -257,6 +257,7 @@ fun MainComposeApp() { }, ) { ScheduleComposeScreen( + navController = navController, onOpenDrawer = { scope.launch { drawerState.open() @@ -856,8 +857,19 @@ fun AcademicsComposeScreen( } @Composable -fun ScheduleComposeScreen(onOpenDrawer: () -> Unit) { - ScheduleScreenContent(onOpenDrawer = onOpenDrawer) +fun ScheduleComposeScreen( + onOpenDrawer: () -> Unit, + navController: NavHostController, +) { + ScheduleScreenContent( + onOpenDrawer = onOpenDrawer, + onCardClick = + { title: String, code: String -> + val encodedTitle = URLEncoder.encode(title, StandardCharsets.UTF_8.toString()) + val encodedCode = URLEncoder.encode(code, StandardCharsets.UTF_8.toString()) + navController.navigate("course_page/$encodedTitle/$encodedCode") + }, + ) } @Composable diff --git a/app/src/main/java/com/dscvit/vitty/ui/schedule/ScheduleScreenContent.kt b/app/src/main/java/com/dscvit/vitty/ui/schedule/ScheduleScreenContent.kt index 43bcde1..602d9f4 100644 --- a/app/src/main/java/com/dscvit/vitty/ui/schedule/ScheduleScreenContent.kt +++ b/app/src/main/java/com/dscvit/vitty/ui/schedule/ScheduleScreenContent.kt @@ -78,7 +78,7 @@ import com.dscvit.vitty.util.Constants import com.dscvit.vitty.util.Quote import com.dscvit.vitty.util.UtilFunctions import com.dscvit.vitty.util.VITMap -import com.google.firebase.Timestamp +import com.dscvit.vitty.widget.parseTimeToTimestamp import com.google.gson.Gson import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -90,7 +90,10 @@ import java.util.Locale @OptIn(ExperimentalMaterial3Api::class) @Composable -fun ScheduleScreenContent(onOpenDrawer: () -> Unit = {}) { +fun ScheduleScreenContent( + onOpenDrawer: () -> Unit = {}, + onCardClick: (String, String) -> Unit = { _, _ -> }, +) { val context = LocalContext.current val prefs = context.getSharedPreferences(Constants.USER_INFO, Context.MODE_PRIVATE) val scheduleViewModel: ScheduleViewModel = viewModel() @@ -307,6 +310,7 @@ fun ScheduleScreenContent(onOpenDrawer: () -> Unit = {}) { periods = allDaysData[page] ?: emptyList(), quote = quote, dayIndex = page, + onCardClick = onCardClick, ) } } @@ -357,6 +361,7 @@ private fun DayContent( periods: List, quote: String, dayIndex: Int, + onCardClick: (String, String) -> Unit = { _, _ -> }, ) { val context = LocalContext.current @@ -413,6 +418,7 @@ private fun DayContent( onLocationClick = { roomNo -> VITMap.openClassMap(context, roomNo) }, + onCardClick = onCardClick, ) } } @@ -424,6 +430,7 @@ private fun PeriodCard( period: PeriodDetails, dayIndex: Int, onLocationClick: (String) -> Unit, + onCardClick: (String, String) -> Unit = { _, _ -> }, ) { val timeFormat = remember { SimpleDateFormat("h:mm a", Locale.getDefault()) } val startTimeStr = @@ -486,6 +493,9 @@ private fun PeriodCard( null }, shape = RoundedCornerShape(16.dp), + onClick = { + onCardClick(period.courseName, period.courseCode) + }, ) { Column( modifier = @@ -616,33 +626,3 @@ private fun processAllDaysData( return result } - -private fun parseTimeToTimestamp(timeString: String): Timestamp = - try { - val sanitizedTime = - if (timeString.contains("+05:53")) { - timeString.replace("+05:53", "+05:30") - } else { - timeString - } - val time = replaceYearIfZero(sanitizedTime) - - val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX", Locale.getDefault()) - val date = dateFormat.parse(time) - if (date != null) { - Timestamp(date) - } else { - Timber.d("Date parsing error: Unable to parse sanitized time: $time") - Timestamp.now() - } - } catch (e: Exception) { - Timber.d("Date parsing error: Unparseable date: \"$timeString\"") - Timestamp.now() - } - -private fun replaceYearIfZero(dateStr: String): String = - if (dateStr.startsWith("0")) { - "2023" + dateStr.substring(4) - } else { - dateStr - } diff --git a/app/src/main/java/com/dscvit/vitty/util/Constants.kt b/app/src/main/java/com/dscvit/vitty/util/Constants.kt index 7ecb6e3..6246bb2 100755 --- a/app/src/main/java/com/dscvit/vitty/util/Constants.kt +++ b/app/src/main/java/com/dscvit/vitty/util/Constants.kt @@ -1,5 +1,8 @@ package com.dscvit.vitty.util +import java.net.URLDecoder +import java.nio.charset.StandardCharsets + object Constants { const val GHOST_MODE = "false" const val PERIODS = "periods" @@ -56,3 +59,10 @@ object Constants { const val REMINDER_CHANNEL_NAME = "Reminders" const val REMINDER_CHANNEL_DESC = "Notifications for course reminders" } + +fun String.urlDecode(): String = + try { + URLDecoder.decode(this, StandardCharsets.UTF_8.toString()) + } catch (e: Exception) { + this + } diff --git a/app/src/main/java/com/dscvit/vitty/widget/TodayWidget.kt b/app/src/main/java/com/dscvit/vitty/widget/TodayWidget.kt index 0d90b84..218a58b 100755 --- a/app/src/main/java/com/dscvit/vitty/widget/TodayWidget.kt +++ b/app/src/main/java/com/dscvit/vitty/widget/TodayWidget.kt @@ -30,18 +30,15 @@ import java.text.SimpleDateFormat import java.util.Calendar import java.util.Date import java.util.Locale -import java.util.TimeZone /** * Implementation of App Widget functionality. */ class TodayWidget : AppWidgetProvider() { - - override fun onUpdate( context: Context, appWidgetManager: AppWidgetManager, - appWidgetIds: IntArray + appWidgetIds: IntArray, ) { // There may be multiple widgets active, so update all of them for (appWidgetId in appWidgetIds) { @@ -64,22 +61,21 @@ internal fun updateTodayWidget( appWidgetId: Int, courseList: ArrayList?, timeList: ArrayList?, - roomList: ArrayList? + roomList: ArrayList?, ) { // Construct the RemoteViews object val views = RemoteViews(context.packageName, R.layout.today_widget) - val intent = Intent(context, AuthActivity::class.java) - val pendingIntent = PendingIntent.getActivity( - context, - TODAY_INTENT, - intent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) + val pendingIntent = + PendingIntent.getActivity( + context, + TODAY_INTENT, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) views.setOnClickPendingIntent(R.id.today_widget, pendingIntent) - // Add the following lines to update the text with the current date and time val currentDate = SimpleDateFormat("MMM dd, yyyy", Locale.getDefault()).format(Date()) val currentTime = SimpleDateFormat("hh:mm a", Locale.getDefault()).format(Date()) @@ -88,19 +84,19 @@ internal fun updateTodayWidget( // Assuming "refesr_text" is the ID of the TextView you want to update // views.setTextViewText(R.id.refresh_widget, currentDateTime) - val days = listOf("monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday") val calendar: Calendar = Calendar.getInstance() - val d = when (calendar.get(Calendar.DAY_OF_WEEK)) { - Calendar.MONDAY -> 0 - Calendar.TUESDAY -> 1 - Calendar.WEDNESDAY -> 2 - Calendar.THURSDAY -> 3 - Calendar.FRIDAY -> 4 - Calendar.SATURDAY -> 5 - Calendar.SUNDAY -> 6 - else -> 0 - } + val d = + when (calendar.get(Calendar.DAY_OF_WEEK)) { + Calendar.MONDAY -> 0 + Calendar.TUESDAY -> 1 + Calendar.WEDNESDAY -> 2 + Calendar.THURSDAY -> 3 + Calendar.FRIDAY -> 4 + Calendar.SATURDAY -> 5 + Calendar.SUNDAY -> 6 + else -> 0 + } if (courseList == null) { fetchTodayFirestore(context, days[d], appWidgetManager, appWidgetId) @@ -113,12 +109,12 @@ internal fun updateTodayWidget( val bundle1 = Bundle() bundle1.putStringArrayList( PERIODS, - courseList + courseList, ) val bundle2 = Bundle() bundle2.putStringArrayList( TIME_SLOTS, - timeList + timeList, ) views.setRemoteAdapter(R.id.periods, serviceIntent) appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.periods) @@ -133,247 +129,248 @@ fun fetchTodayFirestore( day: String, appWidgetManager: AppWidgetManager, appWidgetId: Int, -) = - runBlocking { - fetchTodayData(context, day, appWidgetManager, appWidgetId) - } +) = runBlocking { + fetchTodayData(context, day, appWidgetManager, appWidgetId) +} suspend fun fetchTodayData( context: Context, oldDay: String, appWidgetManager: AppWidgetManager, appWidgetId: Int, -) = - coroutineScope { - val db = FirebaseFirestore.getInstance() - val sharedPref = context.getSharedPreferences("login_info", Context.MODE_PRIVATE)!! - val day = if (oldDay == "saturday") sharedPref.getString( - UtilFunctions.getSatModeCode(), - "saturday" - ).toString() else oldDay - val uid = sharedPref.getString("uid", "") - val courseList: ArrayList = ArrayList() - val timeList: ArrayList = ArrayList() - val roomList: ArrayList = ArrayList() - val sharedPreferences = - context.getSharedPreferences(Constants.USER_INFO, Context.MODE_PRIVATE) - val token = sharedPreferences?.getString(Constants.COMMUNITY_TOKEN, "") ?: "" - val username = sharedPreferences?.getString(Constants.COMMUNITY_USERNAME, null) ?: "" - val cachedData = sharedPref.getString(Constants.CACHE_COMMUNITY_TIMETABLE, null) - if (cachedData != null) { - // If cached data is available, load from cache - Timber.d("Loading from cache") - Timber.d("$cachedData") - val response = Gson().fromJson(cachedData, UserResponse::class.java) - - val user = response - if (user?.timetable?.data == null) { - updateTodayWidget( - context, - appWidgetManager, - appWidgetId, - courseList, - timeList, - roomList - ) - return@coroutineScope - } - var today = user.timetable?.data?.Monday +) = coroutineScope { + val db = FirebaseFirestore.getInstance() + val sharedPref = context.getSharedPreferences("login_info", Context.MODE_PRIVATE)!! + val day = + if (oldDay == "saturday") { + sharedPref + .getString( + UtilFunctions.getSatModeCode(), + "saturday", + ).toString() + } else { + oldDay + } + val uid = sharedPref.getString("uid", "") + val courseList: ArrayList = ArrayList() + val timeList: ArrayList = ArrayList() + val roomList: ArrayList = ArrayList() + val sharedPreferences = + context.getSharedPreferences(Constants.USER_INFO, Context.MODE_PRIVATE) + val token = sharedPreferences?.getString(Constants.COMMUNITY_TOKEN, "") ?: "" + val username = sharedPreferences?.getString(Constants.COMMUNITY_USERNAME, null) ?: "" + val cachedData = sharedPref.getString(Constants.CACHE_COMMUNITY_TIMETABLE, null) + if (cachedData != null) { + // If cached data is available, load from cache + Timber.d("Loading from cache") + Timber.d("$cachedData") + val response = Gson().fromJson(cachedData, UserResponse::class.java) + + val user = response + if (user?.timetable?.data == null) { + updateTodayWidget( + context, + appWidgetManager, + appWidgetId, + courseList, + timeList, + roomList, + ) + return@coroutineScope + } + var today = user.timetable?.data?.Monday - when (day) { - "monday" -> { - today = user.timetable?.data?.Monday - } + when (day) { + "monday" -> { + today = user.timetable?.data?.Monday + } - "tuesday" -> { - today = user.timetable?.data?.Tuesday - } + "tuesday" -> { + today = user.timetable?.data?.Tuesday + } - "wednesday" -> { - today = user.timetable?.data?.Wednesday - } + "wednesday" -> { + today = user.timetable?.data?.Wednesday + } - "thursday" -> { - today = user.timetable?.data?.Thursday - } + "thursday" -> { + today = user.timetable?.data?.Thursday + } - "friday" -> { - today = user.timetable?.data?.Friday - } + "friday" -> { + today = user.timetable?.data?.Friday + } - "saturday" -> { - today = user.timetable?.data?.Saturday - } + "saturday" -> { + today = user.timetable?.data?.Saturday + } - "sunday" -> { - today = user.timetable?.data?.Sunday - } + "sunday" -> { + today = user.timetable?.data?.Sunday } - Timber.d("Today: ${user.timetable}") - today = today?.sortedBy { it.start_time } - if (today != null) { - for (period in today) { - var startTime = parseTimeToTimestamp(period.start_time).toDate() - var endTime = parseTimeToTimestamp(period.end_time).toDate() - - val simpleDateFormat = SimpleDateFormat("h:mm a", Locale.getDefault()) - val sTime: String = simpleDateFormat.format(startTime).uppercase(Locale.ROOT) - val eTime: String = simpleDateFormat.format(endTime).uppercase(Locale.ROOT) - courseList.add(period.name) - timeList.add("$sTime - $eTime") - roomList.add(period.venue) - } + } + Timber.d("Today: ${user.timetable}") + today = today?.sortedBy { it.start_time } + if (today != null) { + for (period in today) { + var startTime = parseTimeToTimestamp(period.start_time).toDate() + var endTime = parseTimeToTimestamp(period.end_time).toDate() + + val simpleDateFormat = SimpleDateFormat("h:mm a", Locale.getDefault()) + val sTime: String = simpleDateFormat.format(startTime).uppercase(Locale.ROOT) + val eTime: String = simpleDateFormat.format(endTime).uppercase(Locale.ROOT) + courseList.add(period.name) + timeList.add("$sTime - $eTime") + roomList.add(period.venue) } - - updateTodayWidget( - context, - appWidgetManager, - appWidgetId, - courseList, - timeList, - roomList - ) } + updateTodayWidget( + context, + appWidgetManager, + appWidgetId, + courseList, + timeList, + roomList, + ) + } + APICommunityRestClient.instance.getUserWithTimeTable( + token, + username, + object : RetrofitSelfUserListener { + override fun onSuccess( + call: Call?, + response: UserResponse?, + ) { + // clear the lists + courseList.clear() + timeList.clear() + roomList.clear() + + val user = response + + // cache response for widget + val jsonResponse = Gson().toJson(response) + val editor = sharedPref.edit() + editor.putString(Constants.CACHE_COMMUNITY_TIMETABLE, jsonResponse) + editor.apply() + + if (user?.timetable?.data == null) { + updateTodayWidget( + context, + appWidgetManager, + appWidgetId, + courseList, + timeList, + roomList, + ) + return + } + var today = user.timetable?.data?.Monday - APICommunityRestClient.instance.getUserWithTimeTable(token, username, - - object : RetrofitSelfUserListener { - override fun onSuccess(call: Call?, response: UserResponse?) { - //clear the lists - courseList.clear() - timeList.clear() - roomList.clear() - - val user = response - - //cache response for widget - val jsonResponse = Gson().toJson(response) - val editor = sharedPref.edit() - editor.putString(Constants.CACHE_COMMUNITY_TIMETABLE, jsonResponse) - editor.apply() - - if (user?.timetable?.data == null) { - updateTodayWidget( - context, - appWidgetManager, - appWidgetId, - courseList, - timeList, - roomList - ) - return - } - var today = user.timetable?.data?.Monday - - when (day) { - "monday" -> { - today = user.timetable?.data?.Monday - } - - "tuesday" -> { - today = user.timetable?.data?.Tuesday - } - - "wednesday" -> { - today = user.timetable?.data?.Wednesday - } - - "thursday" -> { - today = user.timetable?.data?.Thursday - } - - "friday" -> { - today = user.timetable?.data?.Friday - } - - "saturday" -> { - today = user.timetable?.data?.Saturday - } - - "sunday" -> { - today = user.timetable?.data?.Sunday - } - } - today = today?.sortedBy { it.start_time } - val todayTimeTable = today - if(todayTimeTable != null) { - - for (period in todayTimeTable) { - var startTime = parseTimeToTimestamp(period.start_time).toDate() - var endTime = parseTimeToTimestamp(period.end_time).toDate() - - val simpleDateFormat = - SimpleDateFormat("h:mm a", Locale.getDefault()) - val sTime: String = - simpleDateFormat.format(startTime).uppercase(Locale.ROOT) - val eTime: String = - simpleDateFormat.format(endTime).uppercase(Locale.ROOT) - courseList.add(period.name) - timeList.add("$sTime - $eTime") - roomList.add(period.venue) - } - } - - updateTodayWidget( - context, - appWidgetManager, - appWidgetId, - courseList, - timeList, - roomList - ) + when (day) { + "monday" -> { + today = user.timetable?.data?.Monday + } + "tuesday" -> { + today = user.timetable?.data?.Tuesday + } + "wednesday" -> { + today = user.timetable?.data?.Wednesday } - override fun onError(call: Call?, t: Throwable?) { - Timber.d("Error YO: $t") - updateTodayWidget( - context, - appWidgetManager, - appWidgetId, - courseList, - timeList, - roomList - ) + "thursday" -> { + today = user.timetable?.data?.Thursday + } + "friday" -> { + today = user.timetable?.data?.Friday } - }) + "saturday" -> { + today = user.timetable?.data?.Saturday + } + "sunday" -> { + today = user.timetable?.data?.Sunday + } + } + today = today?.sortedBy { it.start_time } + val todayTimeTable = today + if (todayTimeTable != null) { + for (period in todayTimeTable) { + var startTime = parseTimeToTimestamp(period.start_time).toDate() + var endTime = parseTimeToTimestamp(period.end_time).toDate() + + val simpleDateFormat = + SimpleDateFormat("h:mm a", Locale.getDefault()) + val sTime: String = + simpleDateFormat.format(startTime).uppercase(Locale.ROOT) + val eTime: String = + simpleDateFormat.format(endTime).uppercase(Locale.ROOT) + courseList.add(period.name) + timeList.add("$sTime - $eTime") + roomList.add(period.venue) + } + } - } + updateTodayWidget( + context, + appWidgetManager, + appWidgetId, + courseList, + timeList, + roomList, + ) + } + override fun onError( + call: Call?, + t: Throwable?, + ) { + Timber.d("Error YO: $t") + updateTodayWidget( + context, + appWidgetManager, + appWidgetId, + courseList, + timeList, + roomList, + ) + } + }, + ) +} -fun parseTimeToTimestamp(timeString: String): Timestamp { +fun parseTimeToTimestamp(timeString: String): Timestamp = try { - val time = replaceYearIfZero(timeString) - val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'") - // Set the time zone of the date format to UTC - val date = dateFormat.parse(time) - Timber.d("Date----: $date") + val sanitizedTime = + if (timeString.contains("+05:53")) { + timeString.replace("+05:53", "+05:30") + } else { + timeString + } + val time = replaceYearIfZero(sanitizedTime) + val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX", Locale.getDefault()) + val date = dateFormat.parse(time) if (date != null) { - val localTimeZone = TimeZone.getDefault() - val localDate = Date(date.time) - return Timestamp(localDate) + Timestamp(date) } else { - return Timestamp.now() + Timber.d("Date parsing error: Unable to parse sanitized time: $time") + Timestamp.now() } } catch (e: Exception) { - Timber.d("Date----: ${e.message}") - return Timestamp.now() + Timber.d("Date parsing error: Unparseable date: \"$timeString\"") + Timestamp.now() } -} -private fun replaceYearIfZero(dateStr: String): String { +fun replaceYearIfZero(dateStr: String): String = if (dateStr.startsWith("0")) { - // Replace the first 4 characters with "2023" - return "2023" + dateStr.substring(4) + "2023" + dateStr.substring(4) } else { - // No change needed - return dateStr + dateStr } -} From 8d01656be5146990b158f9ea747b5db3ea0e8e93 Mon Sep 17 00:00:00 2001 From: JothishKamal Date: Sat, 19 Jul 2025 23:21:30 +0530 Subject: [PATCH 2/7] feat: add batch circle request functionality with UI integration for managing participants --- .../network/api/community/APICommunity.kt | 10 ++ .../api/community/APICommunityRestClient.kt | 34 +++++ .../requests/CircleBatchRequestBody.kt | 5 + .../circle/CircleBatchRequestResponse.kt | 10 ++ .../vitty/ui/community/CommunityViewModel.kt | 21 +++ .../connect/AddParticipantsScreenContent.kt | 128 +++++++++--------- 6 files changed, 142 insertions(+), 66 deletions(-) create mode 100644 app/src/main/java/com/dscvit/vitty/network/api/community/requests/CircleBatchRequestBody.kt create mode 100644 app/src/main/java/com/dscvit/vitty/network/api/community/responses/circle/CircleBatchRequestResponse.kt diff --git a/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunity.kt b/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunity.kt index 30ebf5f..37febc5 100644 --- a/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunity.kt +++ b/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunity.kt @@ -1,6 +1,8 @@ package com.dscvit.vitty.network.api.community +import com.dscvit.vitty.network.api.community.requests.CircleBatchRequestBody import com.dscvit.vitty.network.api.community.requests.UsernameRequestBody +import com.dscvit.vitty.network.api.community.responses.circle.CircleBatchRequestResponse import com.dscvit.vitty.network.api.community.responses.circle.CircleRequestsResponse import com.dscvit.vitty.network.api.community.responses.circle.CreateCircleResponse import com.dscvit.vitty.network.api.community.responses.circle.JoinCircleResponse @@ -141,6 +143,14 @@ interface APICommunity { @Path("username") username: String, ): Call + @Headers("Content-Type: application/json") + @POST("/api/v3/circles/sendRequest/{circleId}") + fun sendBatchCircleRequest( + @Header("Authorization") authToken: String, + @Path("circleId") circleId: String, + @Body body: CircleBatchRequestBody, + ): Call + @GET("/api/v3/circles/requests/received") fun getReceivedCircleRequests( @Header("Authorization") authToken: String, diff --git a/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunityRestClient.kt b/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunityRestClient.kt index f14a94a..9799786 100644 --- a/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunityRestClient.kt +++ b/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunityRestClient.kt @@ -2,7 +2,9 @@ package com.dscvit.vitty.network.api.community import com.dscvit.vitty.network.api.community.requests.AuthRequestBodyWithCampus import com.dscvit.vitty.network.api.community.requests.AuthRequestBodyWithoutCampus +import com.dscvit.vitty.network.api.community.requests.CircleBatchRequestBody import com.dscvit.vitty.network.api.community.requests.UsernameRequestBody +import com.dscvit.vitty.network.api.community.responses.circle.CircleBatchRequestResponse import com.dscvit.vitty.network.api.community.responses.circle.CircleRequestsResponse import com.dscvit.vitty.network.api.community.responses.circle.CreateCircleResponse import com.dscvit.vitty.network.api.community.responses.circle.JoinCircleResponse @@ -579,6 +581,38 @@ class APICommunityRestClient { ) } + fun sendBatchCircleRequest( + token: String, + circleId: String, + usernames: List, + callback: (CircleBatchRequestResponse?) -> Unit, + ) { + val bearerToken = "Bearer $token" + + mApiUser = retrofit.create(APICommunity::class.java) + val requestBody = CircleBatchRequestBody(usernames) + val apiSendBatchCircleRequestCall = mApiUser!!.sendBatchCircleRequest(bearerToken, circleId, requestBody) + apiSendBatchCircleRequestCall.enqueue( + object : Callback { + override fun onResponse( + call: Call, + response: Response, + ) { + Timber.d("SendBatchCircleResponse: $response") + callback(response.body()) + } + + override fun onFailure( + call: Call, + t: Throwable, + ) { + Timber.d("SendBatchCircleRequestError: ${t.message}") + callback(null) + } + }, + ) + } + fun checkUsername( username: String, retrofitUserActionListener: RetrofitUserActionListener, diff --git a/app/src/main/java/com/dscvit/vitty/network/api/community/requests/CircleBatchRequestBody.kt b/app/src/main/java/com/dscvit/vitty/network/api/community/requests/CircleBatchRequestBody.kt new file mode 100644 index 0000000..6ac5677 --- /dev/null +++ b/app/src/main/java/com/dscvit/vitty/network/api/community/requests/CircleBatchRequestBody.kt @@ -0,0 +1,5 @@ +package com.dscvit.vitty.network.api.community.requests + +data class CircleBatchRequestBody( + val usernames: List, +) diff --git a/app/src/main/java/com/dscvit/vitty/network/api/community/responses/circle/CircleBatchRequestResponse.kt b/app/src/main/java/com/dscvit/vitty/network/api/community/responses/circle/CircleBatchRequestResponse.kt new file mode 100644 index 0000000..0573154 --- /dev/null +++ b/app/src/main/java/com/dscvit/vitty/network/api/community/responses/circle/CircleBatchRequestResponse.kt @@ -0,0 +1,10 @@ +package com.dscvit.vitty.network.api.community.responses.circle + +data class CircleBatchResponseItem( + val request_status: String, + val username: String, +) + +data class CircleBatchRequestResponse( + val data: List, +) diff --git a/app/src/main/java/com/dscvit/vitty/ui/community/CommunityViewModel.kt b/app/src/main/java/com/dscvit/vitty/ui/community/CommunityViewModel.kt index 4d0826b..d1a9843 100644 --- a/app/src/main/java/com/dscvit/vitty/ui/community/CommunityViewModel.kt +++ b/app/src/main/java/com/dscvit/vitty/ui/community/CommunityViewModel.kt @@ -7,6 +7,7 @@ import com.dscvit.vitty.network.api.community.RetrofitFriendListListener import com.dscvit.vitty.network.api.community.RetrofitFriendRequestListener import com.dscvit.vitty.network.api.community.RetrofitSearchResultListener import com.dscvit.vitty.network.api.community.RetrofitUserActionListener +import com.dscvit.vitty.network.api.community.responses.circle.CircleBatchRequestResponse import com.dscvit.vitty.network.api.community.responses.requests.RequestsResponse import com.dscvit.vitty.network.api.community.responses.user.FriendResponse import com.dscvit.vitty.network.api.community.responses.user.PostResponse @@ -20,12 +21,14 @@ class CommunityViewModel : ViewModel() { private val _suggestedFriends = MutableLiveData?>() private val _searchResult = MutableLiveData?>() private val _actionResponse = MutableLiveData() + private val _batchCircleRequestResponse = MutableLiveData() val friendList: MutableLiveData = _friendList val friendRequest: MutableLiveData = _friendRequest val suggestedFriends: MutableLiveData?> = _suggestedFriends val searchResult: MutableLiveData?> = _searchResult val actionResponse: MutableLiveData = _actionResponse + val batchCircleRequestResponse: MutableLiveData = _batchCircleRequestResponse fun getFriendList( token: String, @@ -232,6 +235,24 @@ class CommunityViewModel : ViewModel() { ) } + fun sendBatchCircleRequest( + token: String, + circleId: String, + usernames: List, + ) { + APICommunityRestClient.instance.sendBatchCircleRequest( + token, + circleId, + usernames, + ) { response -> + _batchCircleRequestResponse.postValue(response) + } + } + + fun clearBatchCircleRequestResponse() { + _batchCircleRequestResponse.postValue(null) + } + fun unfriend( token: String, username: String, diff --git a/app/src/main/java/com/dscvit/vitty/ui/connect/AddParticipantsScreenContent.kt b/app/src/main/java/com/dscvit/vitty/ui/connect/AddParticipantsScreenContent.kt index 4827414..d61f087 100644 --- a/app/src/main/java/com/dscvit/vitty/ui/connect/AddParticipantsScreenContent.kt +++ b/app/src/main/java/com/dscvit/vitty/ui/connect/AddParticipantsScreenContent.kt @@ -83,10 +83,8 @@ fun AddParticipantsScreenContent( var searchQuery by remember { mutableStateOf("") } var isSearching by remember { mutableStateOf(false) } - var selectedUsers by remember { mutableStateOf(setOf()) } + var selectedUsers by remember { mutableStateOf(listOf()) } var sendingRequests by remember { mutableStateOf(false) } - var requestStates by remember { mutableStateOf(mapOf()) } - var pendingRequestsCount by remember { mutableStateOf(0) } val sharedPreferences = remember { @@ -107,50 +105,48 @@ fun AddParticipantsScreenContent( val suggestedFriends by communityViewModel.suggestedFriends.observeAsState() val searchResults by communityViewModel.searchResult.observeAsState() - val actionResponse by communityViewModel.actionResponse.observeAsState() - - LaunchedEffect(actionResponse) { - if (sendingRequests && actionResponse != null) { - val currentUser = requestStates.keys.find { requestStates[it] == null } - if (currentUser != null) { - val isSuccess = actionResponse?.detail == "request sent successfully" - requestStates = - requestStates.toMutableMap().apply { - this[currentUser] = isSuccess - } - pendingRequestsCount-- - - if (pendingRequestsCount == 0) { - communityViewModel.actionResponse.postValue(null) - - val failedUsers = requestStates.filter { it.value == false }.keys.toList() - - if (failedUsers.isEmpty()) { - Toast - .makeText( - context, - "All requests sent successfully!", - Toast.LENGTH_SHORT, - ).show() - } else { - val failedUsersText = failedUsers.joinToString(", ") - Toast - .makeText( - context, - "Failed to send requests to: $failedUsersText", - Toast.LENGTH_LONG, - ).show() - } + val batchResponse by communityViewModel.batchCircleRequestResponse.observeAsState() + + LaunchedEffect(batchResponse) { + batchResponse?.let { response -> + val successCount = response.data.count { it.request_status == "pending" || it.request_status == "added" } + val failedCount = response.data.size - successCount + + if (failedCount == 0) { + Toast.makeText( + context, + "All requests sent successfully!", + Toast.LENGTH_SHORT, + ).show() + } else { + val failedUsers = + response.data + .filter { + it.request_status != "pending" && it.request_status != "added" + }.map { it.username } + + if (successCount > 0) { + Toast.makeText( + context, + "Sent $successCount requests successfully. Failed: ${failedUsers.joinToString(", ")}", + Toast.LENGTH_LONG, + ).show() + } else { + Toast.makeText( + context, + "Failed to send requests to: ${failedUsers.joinToString(", ")}", + Toast.LENGTH_LONG, + ).show() + } + } - sendingRequests = false - selectedUsers = setOf() - requestStates = mapOf() + sendingRequests = false + selectedUsers = listOf() + communityViewModel.clearBatchCircleRequestResponse() - if (failedUsers.isEmpty()) { - scope.launch { - navController.popBackStack() - } - } + if (failedCount == 0) { + scope.launch { + navController.popBackStack() } } } @@ -187,15 +183,21 @@ fun AddParticipantsScreenContent( fun sendAllRequests() { if (selectedUsers.isNotEmpty() && !sendingRequests && token.isNotEmpty()) { sendingRequests = true - pendingRequestsCount = selectedUsers.size - requestStates = selectedUsers.associateWith { null } + val usernames = selectedUsers.map { it.username } + communityViewModel.sendBatchCircleRequest(token, circleId, usernames) + } + } - selectedUsers.forEach { username -> - communityViewModel.sendCircleRequest(token, circleId, username) - } + fun addUserToSelection(user: UserResponse) { + if (!selectedUsers.any { it.username == user.username }) { + selectedUsers = selectedUsers + user } } + fun removeUserFromSelection(user: UserResponse) { + selectedUsers = selectedUsers.filter { it.username != user.username } + } + Column( modifier = Modifier @@ -301,17 +303,11 @@ fun AddParticipantsScreenContent( contentPadding = PaddingValues(vertical = 4.dp), verticalArrangement = Arrangement.spacedBy(8.dp), ) { - val allUsers = (filteredSuggestedFriends ?: emptyList()) + (filteredSearchResults ?: emptyList()) - val selectedUsersList = - allUsers.distinctBy { it.username }.filter { user -> - selectedUsers.contains(user.username) - } - - items(selectedUsersList) { user -> + items(selectedUsers) { user -> SelectedUserCard( user = user, onRemove = { - selectedUsers = selectedUsers - user.username + removeUserFromSelection(user) }, ) } @@ -329,9 +325,10 @@ fun AddParticipantsScreenContent( } val displayUsers = if (searchQuery.isBlank()) filteredSuggestedFriends else filteredSearchResults + val selectedUsernames = selectedUsers.map { it.username }.toSet() val filteredUsers = displayUsers?.filter { user -> - !selectedUsers.contains(user.username) && + !selectedUsernames.contains(user.username) && ( searchQuery.isBlank() || user.name.contains(searchQuery, ignoreCase = true) || @@ -373,14 +370,13 @@ fun AddParticipantsScreenContent( items(filteredUsers) { user -> UserSelectionCard( user = user, - isSelected = selectedUsers.contains(user.username), + isSelected = selectedUsernames.contains(user.username), onSelectionChanged = { isSelected -> - selectedUsers = - if (isSelected) { - selectedUsers + user.username - } else { - selectedUsers - user.username - } + if (isSelected) { + addUserToSelection(user) + } else { + removeUserFromSelection(user) + } }, ) } From e9eb0971bda23c51358c9e1d35eaa40d146fb4a4 Mon Sep 17 00:00:00 2001 From: JothishKamal Date: Mon, 21 Jul 2025 23:41:06 +0530 Subject: [PATCH 3/7] feat: Add support and feedback section in settings - Introduced a new preference category for Support & Feedback in settings. - Added functionality to send support emails directly from the app. - Implemented a dialog for campus selection with API integration for updates. - Enhanced analytics with app navigation tracking. - Updated constants for support email and GitHub issues link. - Improved logout functionality with better preference handling. - Added a new drawable for support icon. - Added a new bottom sheet for settings reminders in Reminders tab --- app/build.gradle | 8 + .../vitty/activity/HomeComposeActivity.kt | 138 +- .../network/api/community/APICommunity.kt | 16 + .../api/community/APICommunityRestClient.kt | 63 + .../requests/CampusUpdateRequestBody.kt | 5 + .../vitty/ui/academics/AcademicsFragment.kt | 3 + .../ui/academics/AcademicsScreenContent.kt | 74 +- .../components/AcademicsScreenComponents.kt | 182 +- .../vitty/ui/components/CampusUpdateDialog.kt | 227 ++ .../ui/connect/CircleDetailScreenContent.kt | 173 +- .../vitty/ui/connect/ConnectViewModel.kt | 31 + .../components/AddParticipantsBottomSheet.kt | 651 ----- .../dscvit/vitty/ui/main/MainComposeApp.kt | 2088 +++++++++-------- .../vitty/ui/settings/SettingsFragment.kt | 100 +- .../java/com/dscvit/vitty/util/Analytics.kt | 9 + .../java/com/dscvit/vitty/util/Constants.kt | 8 + .../com/dscvit/vitty/util/LogoutHelper.kt | 5 +- app/src/main/res/drawable/ic_support.xml | 2 +- app/src/main/res/xml/root_preferences.xml | 18 + 19 files changed, 2057 insertions(+), 1744 deletions(-) create mode 100644 app/src/main/java/com/dscvit/vitty/network/api/community/requests/CampusUpdateRequestBody.kt create mode 100644 app/src/main/java/com/dscvit/vitty/ui/components/CampusUpdateDialog.kt delete mode 100644 app/src/main/java/com/dscvit/vitty/ui/connect/components/AddParticipantsBottomSheet.kt diff --git a/app/build.gradle b/app/build.gradle index 3a4fa2b..04b6cb1 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -143,4 +143,12 @@ dependencies { // Camera permissions for QR scanning implementation 'com.google.accompanist:accompanist-permissions:0.37.3' + + // Google Play In-App Updates + implementation 'com.google.android.play:app-update:2.1.0' + implementation 'com.google.android.play:app-update-ktx:2.1.0' + + // Google Play In-App Review + implementation "com.google.android.play:review:2.0.2" + implementation "com.google.android.play:review-ktx:2.0.2" } \ No newline at end of file diff --git a/app/src/main/java/com/dscvit/vitty/activity/HomeComposeActivity.kt b/app/src/main/java/com/dscvit/vitty/activity/HomeComposeActivity.kt index a78cfbb..7d8e489 100644 --- a/app/src/main/java/com/dscvit/vitty/activity/HomeComposeActivity.kt +++ b/app/src/main/java/com/dscvit/vitty/activity/HomeComposeActivity.kt @@ -1,27 +1,159 @@ package com.dscvit.vitty.activity +import android.content.SharedPreferences import android.os.Bundle +import androidx.activity.result.ActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.core.content.edit import androidx.databinding.DataBindingUtil import androidx.fragment.app.FragmentActivity import com.dscvit.vitty.R import com.dscvit.vitty.databinding.ActivityHomeComposeBinding import com.dscvit.vitty.ui.main.MainComposeApp +import com.dscvit.vitty.util.Constants +import com.dscvit.vitty.util.Constants.PREF_LAST_REVIEW_REQUEST +import com.google.android.material.snackbar.Snackbar +import com.google.android.play.core.appupdate.AppUpdateInfo +import com.google.android.play.core.appupdate.AppUpdateManager +import com.google.android.play.core.appupdate.AppUpdateManagerFactory +import com.google.android.play.core.appupdate.AppUpdateOptions +import com.google.android.play.core.install.model.AppUpdateType +import com.google.android.play.core.install.model.InstallStatus +import com.google.android.play.core.install.model.UpdateAvailability +import com.google.android.play.core.review.ReviewInfo +import com.google.android.play.core.review.ReviewManager +import com.google.android.play.core.review.ReviewManagerFactory +import java.util.concurrent.TimeUnit class HomeComposeActivity : FragmentActivity() { private lateinit var binding: ActivityHomeComposeBinding + private lateinit var prefs: SharedPreferences + private lateinit var appUpdateManager: AppUpdateManager + private lateinit var reviewManager: ReviewManager + + private var reviewInfo: ReviewInfo? = null + + companion object { + private val REVIEW_INTERVAL_MILLIS = TimeUnit.DAYS.toMillis(30) + } + + private val updateResultLauncher = + registerForActivityResult( + ActivityResultContracts.StartIntentSenderForResult(), + ) { result: ActivityResult -> if (result.resultCode != RESULT_OK) {} } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_home_compose) + prefs = getSharedPreferences(Constants.USER_INFO, 0) + appUpdateManager = AppUpdateManagerFactory.create(this) + reviewManager = ReviewManagerFactory.create(this) binding.composeView.apply { setViewCompositionStrategy( - ViewCompositionStrategy.DisposeOnLifecycleDestroyed(this@HomeComposeActivity), + ViewCompositionStrategy.DisposeOnLifecycleDestroyed(this@HomeComposeActivity), ) - setContent { - MainComposeApp() + setContent { MainComposeApp() } + } + + checkForAppUpdate() + checkForReviewRequest() + } + + override fun onResume() { + super.onResume() + + appUpdateManager.appUpdateInfo.addOnSuccessListener { appUpdateInfo -> + if (appUpdateInfo.updateAvailability() == + UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS + ) { + + appUpdateManager.startUpdateFlowForResult( + appUpdateInfo, + updateResultLauncher, + AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE).build(), + ) + } + + if (appUpdateInfo.installStatus() == InstallStatus.DOWNLOADED) { + showUpdateDownloadedSnackbar() + } + } + } + + private fun checkForAppUpdate() { + appUpdateManager.appUpdateInfo.addOnSuccessListener { appUpdateInfo -> + when { + appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE && + appUpdateInfo.updatePriority() >= 4 && + appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE) -> { + startImmediateUpdate(appUpdateInfo) + } + appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE && + (appUpdateInfo.clientVersionStalenessDays() ?: -1) >= 3 && + appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE) -> { + startFlexibleUpdate(appUpdateInfo) + } + } + } + } + + private fun startImmediateUpdate(appUpdateInfo: AppUpdateInfo) { + appUpdateManager.startUpdateFlowForResult( + appUpdateInfo, + updateResultLauncher, + AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE).build(), + ) + } + + private fun startFlexibleUpdate(appUpdateInfo: AppUpdateInfo) { + appUpdateManager.startUpdateFlowForResult( + appUpdateInfo, + updateResultLauncher, + AppUpdateOptions.newBuilder(AppUpdateType.FLEXIBLE).build(), + ) + } + + private fun showUpdateDownloadedSnackbar() { + Snackbar.make( + binding.root, + "An update has been downloaded.", + Snackbar.LENGTH_INDEFINITE, + ) + .apply { + setAction("RESTART") { appUpdateManager.completeUpdate() } + show() + } + } + + private fun checkForReviewRequest() { + val lastReviewRequest = prefs.getLong(PREF_LAST_REVIEW_REQUEST, 0) + val currentTime = System.currentTimeMillis() + + if (currentTime - lastReviewRequest >= REVIEW_INTERVAL_MILLIS) { + requestReviewInfo() + } + } + + private fun requestReviewInfo() { + val request = reviewManager.requestReviewFlow() + request.addOnCompleteListener { task -> + if (task.isSuccessful) { + reviewInfo = task.result + + launchReviewFlow() + } else {} + } + } + + private fun launchReviewFlow() { + reviewInfo?.let { info -> + val flow = reviewManager.launchReviewFlow(this, info) + flow.addOnCompleteListener { + prefs.edit { putLong(PREF_LAST_REVIEW_REQUEST, System.currentTimeMillis()) } + reviewInfo = null } } } diff --git a/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunity.kt b/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunity.kt index 37febc5..94651b7 100644 --- a/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunity.kt +++ b/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunity.kt @@ -1,5 +1,6 @@ package com.dscvit.vitty.network.api.community +import com.dscvit.vitty.network.api.community.requests.CampusUpdateRequestBody import com.dscvit.vitty.network.api.community.requests.CircleBatchRequestBody import com.dscvit.vitty.network.api.community.requests.UsernameRequestBody import com.dscvit.vitty.network.api.community.responses.circle.CircleBatchRequestResponse @@ -19,6 +20,7 @@ import retrofit2.http.DELETE import retrofit2.http.GET import retrofit2.http.Header import retrofit2.http.Headers +import retrofit2.http.PATCH import retrofit2.http.POST import retrofit2.http.Path import retrofit2.http.Query @@ -101,6 +103,13 @@ interface APICommunity { @Path("username") username: String, ): Call + @Headers("Content-Type: application/json") + @PATCH("/api/v3/users/campus") + fun updateCampus( + @Header("Authorization") authToken: String, + @Body campusRequestBody: CampusUpdateRequestBody, + ): Call + @POST("/api/v3/friends/ghost/{username}") fun enableGhostMode( @Header("Authorization") authToken: String, @@ -192,6 +201,13 @@ interface APICommunity { @Path("username") username: String, ): Call + @DELETE("/api/v3/circles/remove/{circleId}/{username}") + fun removeUserFromCircle( + @Header("Authorization") authToken: String, + @Path("circleId") circleId: String, + @Path("username") username: String, + ): Call + @GET("/api/v3/timetable/emptyClassRooms") fun getEmptyClassrooms( @Header("Authorization") authToken: String, diff --git a/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunityRestClient.kt b/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunityRestClient.kt index 9799786..4527681 100644 --- a/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunityRestClient.kt +++ b/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunityRestClient.kt @@ -2,6 +2,7 @@ package com.dscvit.vitty.network.api.community import com.dscvit.vitty.network.api.community.requests.AuthRequestBodyWithCampus import com.dscvit.vitty.network.api.community.requests.AuthRequestBodyWithoutCampus +import com.dscvit.vitty.network.api.community.requests.CampusUpdateRequestBody import com.dscvit.vitty.network.api.community.requests.CircleBatchRequestBody import com.dscvit.vitty.network.api.community.requests.UsernameRequestBody import com.dscvit.vitty.network.api.community.responses.circle.CircleBatchRequestResponse @@ -948,4 +949,66 @@ class APICommunityRestClient { }, ) } + + fun removeUserFromCircle( + token: String, + circleId: String, + username: String, + retrofitUserActionListener: RetrofitUserActionListener, + ) { + val bearerToken = "Bearer $token" + + mApiUser = retrofit.create(APICommunity::class.java) + val apiRemoveUserCall = mApiUser!!.removeUserFromCircle(bearerToken, circleId, username) + apiRemoveUserCall.enqueue( + object : Callback { + override fun onResponse( + call: Call, + response: Response, + ) { + Timber.d("RemoveUserFromCircle: ${response.body()}") + retrofitUserActionListener.onSuccess(call, response.body()) + } + + override fun onFailure( + call: Call, + t: Throwable, + ) { + Timber.d("RemoveUserFromCircleError: ${t.message}") + retrofitUserActionListener.onError(call, t) + } + }, + ) + } + + fun updateCampus( + token: String, + campus: String, + retrofitUserActionListener: RetrofitUserActionListener, + ) { + val bearerToken = "Bearer $token" + + mApiUser = retrofit.create(APICommunity::class.java) + val requestBody = CampusUpdateRequestBody(campus) + val apiUpdateCampusCall = mApiUser!!.updateCampus(bearerToken, requestBody) + apiUpdateCampusCall.enqueue( + object : Callback { + override fun onResponse( + call: Call, + response: Response, + ) { + Timber.d("UpdateCampus: ${response.body()}") + retrofitUserActionListener.onSuccess(call, response.body()) + } + + override fun onFailure( + call: Call, + t: Throwable, + ) { + Timber.d("UpdateCampusError: ${t.message}") + retrofitUserActionListener.onError(call, t) + } + }, + ) + } } diff --git a/app/src/main/java/com/dscvit/vitty/network/api/community/requests/CampusUpdateRequestBody.kt b/app/src/main/java/com/dscvit/vitty/network/api/community/requests/CampusUpdateRequestBody.kt new file mode 100644 index 0000000..9b5a24b --- /dev/null +++ b/app/src/main/java/com/dscvit/vitty/network/api/community/requests/CampusUpdateRequestBody.kt @@ -0,0 +1,5 @@ +package com.dscvit.vitty.network.api.community.requests + +data class CampusUpdateRequestBody( + val campus: String, +) diff --git a/app/src/main/java/com/dscvit/vitty/ui/academics/AcademicsFragment.kt b/app/src/main/java/com/dscvit/vitty/ui/academics/AcademicsFragment.kt index 1b61966..bfc16d8 100644 --- a/app/src/main/java/com/dscvit/vitty/ui/academics/AcademicsFragment.kt +++ b/app/src/main/java/com/dscvit/vitty/ui/academics/AcademicsFragment.kt @@ -13,6 +13,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.fragment.findNavController import com.dscvit.vitty.databinding.FragmentAcademicsBinding import com.dscvit.vitty.network.api.community.responses.user.UserResponse @@ -91,6 +92,8 @@ class AcademicsFragment : Fragment() { ) findNavController().navigate(action) }, + academicsViewModel = viewModel(), + coursePageViewModel = viewModel(), ) } } diff --git a/app/src/main/java/com/dscvit/vitty/ui/academics/AcademicsScreenContent.kt b/app/src/main/java/com/dscvit/vitty/ui/academics/AcademicsScreenContent.kt index f8abb52..ca33147 100644 --- a/app/src/main/java/com/dscvit/vitty/ui/academics/AcademicsScreenContent.kt +++ b/app/src/main/java/com/dscvit/vitty/ui/academics/AcademicsScreenContent.kt @@ -1,5 +1,6 @@ package com.dscvit.vitty.ui.academics +import android.widget.Toast import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box @@ -10,9 +11,11 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf @@ -21,17 +24,20 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import coil.compose.AsyncImage import com.dscvit.vitty.R import com.dscvit.vitty.theme.Background +import com.dscvit.vitty.theme.TextColor import com.dscvit.vitty.ui.academics.components.AcademicsContent import com.dscvit.vitty.ui.academics.components.AcademicsHeader import com.dscvit.vitty.ui.academics.models.Course +import com.dscvit.vitty.ui.coursepage.CoursePageViewModel +import com.dscvit.vitty.ui.coursepage.components.SetReminderBottomSheet import com.dscvit.vitty.ui.coursepage.models.Reminder import com.dscvit.vitty.util.SemesterUtils @@ -43,16 +49,21 @@ fun AcademicsScreenContent( allCourses: List, onCourseClick: (Course) -> Unit = {}, onOpenDrawer: () -> Unit = {}, - viewModel: AcademicsViewModel = viewModel(), + academicsViewModel: AcademicsViewModel, + coursePageViewModel: CoursePageViewModel, ) { + val context = LocalContext.current val tabs = listOf("Courses", "Reminders") var selectedTab by remember { mutableIntStateOf(0) } var searchQuery by remember { mutableStateOf("") } var reminderSearchQuery by remember { mutableStateOf("") } var isCurrentSemester by remember { mutableStateOf(true) } var reminderStatus by remember { mutableIntStateOf(0) } + var showSetReminderBottomSheet by remember { mutableStateOf(false) } + var selectedCourseForReminder by remember { mutableStateOf(null) } + val setReminderSheetState = rememberModalBottomSheetState() - val allReminders by viewModel.allReminders.collectAsStateWithLifecycle() + val allReminders by academicsViewModel.allReminders.collectAsStateWithLifecycle() val filteredCourses = remember(allCourses, searchQuery, isCurrentSemester) { @@ -117,6 +128,11 @@ fun AcademicsScreenContent( onReminderStatusChange = { reminderStatus = it }, reminderSearchQuery = reminderSearchQuery, onReminderSearchQueryChange = { reminderSearchQuery = it }, + courses = allCourses, + onCourseSelected = { course -> + selectedCourseForReminder = course + showSetReminderBottomSheet = true + }, ) AcademicsContent( @@ -127,11 +143,59 @@ fun AcademicsScreenContent( reminderSearchQuery = reminderSearchQuery, onCourseClick = onCourseClick, onToggleReminderComplete = { reminderId: Long, isCompleted: Boolean -> - viewModel.updateReminderStatus(reminderId, isCompleted) + academicsViewModel.updateReminderStatus(reminderId, isCompleted) }, onDeleteReminder = { reminder: Reminder -> - viewModel.deleteReminder(reminder) + academicsViewModel.deleteReminder(reminder) }, ) } + + if (showSetReminderBottomSheet && selectedCourseForReminder != null) { + ModalBottomSheet( + onDismissRequest = { showSetReminderBottomSheet = false }, + sheetState = setReminderSheetState, + containerColor = Background, + contentColor = TextColor, + dragHandle = { }, + ) { + SetReminderBottomSheet( + onDismiss = { showSetReminderBottomSheet = false }, + courseTitle = selectedCourseForReminder!!.title, + onSaveReminder = { + title, + description, + dateMillis, + fromTime, + toTime, + isAllDay, + alertDaysBefore, + attachmentUrl, + -> + coursePageViewModel.addReminder( + title = title, + description = description, + dateMillis = dateMillis, + fromTime = fromTime, + toTime = toTime, + isAllDay = isAllDay, + alertDaysBefore = alertDaysBefore, + attachmentUrl = attachmentUrl, + onSuccess = { + showSetReminderBottomSheet = false + Toast + .makeText( + context, + "Reminder created successfully", + Toast.LENGTH_SHORT, + ).show() + }, + onError = { errorMessage -> + Toast.makeText(context, errorMessage, Toast.LENGTH_LONG).show() + }, + ) + }, + ) + } + } } diff --git a/app/src/main/java/com/dscvit/vitty/ui/academics/components/AcademicsScreenComponents.kt b/app/src/main/java/com/dscvit/vitty/ui/academics/components/AcademicsScreenComponents.kt index 1cb4659..91f9bdb 100644 --- a/app/src/main/java/com/dscvit/vitty/ui/academics/components/AcademicsScreenComponents.kt +++ b/app/src/main/java/com/dscvit/vitty/ui/academics/components/AcademicsScreenComponents.kt @@ -19,19 +19,24 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Star +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.SwipeToDismissBox import androidx.compose.material3.SwipeToDismissBoxValue import androidx.compose.material3.Tab @@ -62,6 +67,7 @@ import com.dscvit.vitty.theme.Red import com.dscvit.vitty.theme.Secondary import com.dscvit.vitty.theme.TextColor import com.dscvit.vitty.ui.academics.models.Course +import com.dscvit.vitty.ui.connect.components.FilterChip import com.dscvit.vitty.ui.coursepage.components.RemindersChip import com.dscvit.vitty.ui.coursepage.models.Reminder import com.dscvit.vitty.ui.coursepage.models.ReminderStatus @@ -83,6 +89,8 @@ fun AcademicsHeader( onReminderStatusChange: (Int) -> Unit, reminderSearchQuery: String = "", onReminderSearchQueryChange: (String) -> Unit = {}, + courses: List = emptyList(), + onCourseSelected: (Course) -> Unit = {}, ) { Column( modifier = @@ -113,6 +121,8 @@ fun AcademicsHeader( onReminderStatusChange = onReminderStatusChange, searchQuery = reminderSearchQuery, onSearchQueryChange = onReminderSearchQueryChange, + courses = courses, + onCourseSelected = onCourseSelected, ) } } @@ -181,14 +191,6 @@ fun CoursesTabFilters( searchQuery = searchQuery, onSearchQueryChange = onSearchQueryChange, ) - - Spacer(Modifier.height(16.dp)) - - FilterChipRow( - options = listOf("Current Semester", "All Semesters"), - selectedIndex = if (isCurrentSemester) 0 else 1, - onSelectionChange = { index -> onSemesterFilterChange(index == 0) }, - ) } } @@ -198,7 +200,11 @@ fun RemindersTabFilters( onReminderStatusChange: (Int) -> Unit, searchQuery: String = "", onSearchQueryChange: (String) -> Unit = {}, + courses: List = emptyList(), + onCourseSelected: (Course) -> Unit = {}, ) { + var showCourseSelectionBottomSheet by remember { mutableStateOf(false) } + Column { SearchBar( searchQuery = searchQuery, @@ -208,10 +214,22 @@ fun RemindersTabFilters( Spacer(Modifier.height(16.dp)) - FilterChipRow( + FilterChipRowWithAddButton( options = listOf("Pending", "Completed"), selectedIndex = reminderStatus, onSelectionChange = onReminderStatusChange, + onAddClick = { showCourseSelectionBottomSheet = true }, + ) + } + + if (showCourseSelectionBottomSheet) { + CourseSelectionBottomSheet( + courses = courses, + onDismiss = { showCourseSelectionBottomSheet = false }, + onCourseSelected = { course -> + showCourseSelectionBottomSheet = false + onCourseSelected(course) + }, ) } } @@ -284,63 +302,57 @@ fun SearchBar( } @Composable -fun FilterChipRow( +fun FilterChipRowWithAddButton( options: List, selectedIndex: Int, onSelectionChange: (Int) -> Unit, + onAddClick: () -> Unit, ) { Row( modifier = Modifier .fillMaxWidth() .padding(horizontal = 20.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, ) { - options.forEachIndexed { index, label -> - FilterChip( - label = label, - isSelected = selectedIndex == index, - onClick = { onSelectionChange(index) }, - ) - if (index < options.lastIndex) { - Spacer(Modifier.width(12.dp)) + Row { + options.forEachIndexed { index, label -> + FilterChip( + label = label, + isSelected = selectedIndex == index, + onClick = { onSelectionChange(index) }, + ) + if (index < options.lastIndex) { + Spacer(Modifier.width(12.dp)) + } } } + + AddButton(onClick = onAddClick) } } @Composable -fun FilterChip( - label: String, - isSelected: Boolean, - onClick: () -> Unit, -) { +fun AddButton(onClick: () -> Unit) { Box( modifier = Modifier - .clip(RoundedCornerShape(24.dp)) + .clip(CircleShape) .background(Secondary) - .border( - 1.dp, - if (isSelected) Accent else Color.Transparent, - RoundedCornerShape(24.dp), - ).clickable { onClick() } - .padding(horizontal = 12.dp, vertical = 8.dp), + .border(1.dp, Accent, CircleShape) + .clickable { onClick() } + .padding(8.dp), ) { - Row(verticalAlignment = Alignment.CenterVertically) { - if (isSelected) { - Icon( - imageVector = Icons.Default.Check, - contentDescription = null, - tint = Accent, - modifier = Modifier.size(18.dp), - ) - Spacer(Modifier.width(4.dp)) - } - Text( - text = label, - color = if (isSelected) Accent else TextColor.copy(alpha = 0.5f), - fontWeight = FontWeight.Normal, - style = MaterialTheme.typography.bodyMedium, + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + ) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = "Add Reminder", + tint = Accent, + modifier = Modifier.size(18.dp), ) } } @@ -546,7 +558,6 @@ fun RemindersContent( } } -@RequiresApi(Build.VERSION_CODES.O) fun groupRemindersByDate(reminders: List): LinkedHashMap> = reminders .sortedBy { it.dateMillis } @@ -688,7 +699,6 @@ fun SwipeableReminderCard( } } else -> { - } } }, @@ -793,3 +803,81 @@ fun DateHeaderWithChip( } } } + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CourseSelectionBottomSheet( + courses: List, + onDismiss: () -> Unit, + onCourseSelected: (Course) -> Unit, +) { + ModalBottomSheet( + onDismissRequest = onDismiss, + containerColor = Background, + contentColor = TextColor, + dragHandle = { + Box( + modifier = + Modifier + .padding(top = 16.dp) + .width(120.dp) + .height(7.dp) + .background(Accent.copy(alpha = .4f), shape = RoundedCornerShape(44.dp)), + ) { + } + }, + ) { + Column( + modifier = + Modifier + .fillMaxWidth() + .padding(16.dp), + ) { + Text( + text = "Select a Course", + style = MaterialTheme.typography.titleLarge, + color = TextColor, + modifier = Modifier.padding(bottom = 16.dp), + ) + + LazyColumn( + modifier = Modifier.wrapContentHeight(), + ) { + items(courses) { course -> + CourseItem( + course = course, + onClick = { + onCourseSelected(course) + }, + ) + Spacer(modifier = Modifier.height(8.dp)) + } + item { + Spacer(modifier = Modifier.height(16.dp)) + } + } + } + } +} + +@Composable +fun CourseItem( + course: Course, + onClick: () -> Unit, +) { + Box( + modifier = + Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .background(Secondary) + .clickable { onClick() } + .padding(16.dp), + ) { + Text( + text = course.title, + color = TextColor, + style = MaterialTheme.typography.bodyLarge, + ) + } +} diff --git a/app/src/main/java/com/dscvit/vitty/ui/components/CampusUpdateDialog.kt b/app/src/main/java/com/dscvit/vitty/ui/components/CampusUpdateDialog.kt new file mode 100644 index 0000000..17d6868 --- /dev/null +++ b/app/src/main/java/com/dscvit/vitty/ui/components/CampusUpdateDialog.kt @@ -0,0 +1,227 @@ +package com.dscvit.vitty.ui.components + +import android.widget.Toast +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.dscvit.vitty.network.api.community.APICommunityRestClient +import com.dscvit.vitty.network.api.community.RetrofitUserActionListener +import com.dscvit.vitty.network.api.community.responses.user.PostResponse +import com.dscvit.vitty.util.Constants +import retrofit2.Call + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CampusUpdateDialog(onDismiss: () -> Unit) { + val context = LocalContext.current + val prefs = remember { context.getSharedPreferences(Constants.USER_INFO, 0) } + val authToken = remember { prefs.getString(Constants.COMMUNITY_TOKEN, "") ?: "" } + + var selectedCampus by remember { mutableStateOf("") } + var isLoading by remember { mutableStateOf(false) } + var expanded by remember { mutableStateOf(false) } + + val campusOptions = listOf("Vellore", "Chennai", "Bhopal") + + Dialog( + onDismissRequest = { /* Prevent dismissal */ }, + properties = + DialogProperties( + dismissOnBackPress = false, + dismissOnClickOutside = false, + ), + ) { + Card( + modifier = + Modifier + .fillMaxWidth() + .padding(16.dp), + shape = RoundedCornerShape(16.dp), + colors = + CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surface, + ), + ) { + Column( + modifier = + Modifier + .fillMaxWidth() + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = "Select Your Campus", + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "Please select your campus to continue using the app", + fontSize = 14.sp, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), + textAlign = TextAlign.Center, + ) + + Spacer(modifier = Modifier.height(24.dp)) + + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { if (!isLoading) expanded = !expanded }, + ) { + OutlinedTextField( + value = selectedCampus, + onValueChange = { }, + readOnly = true, + label = { Text("Campus") }, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + colors = + OutlinedTextFieldDefaults.colors( + focusedBorderColor = MaterialTheme.colorScheme.primary, + unfocusedBorderColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f), + focusedLabelColor = MaterialTheme.colorScheme.primary, + unfocusedLabelColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), + ), + modifier = + Modifier + .menuAnchor(MenuAnchorType.PrimaryNotEditable) + .fillMaxWidth(), + enabled = !isLoading, + ) + + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + modifier = Modifier.exposedDropdownSize(), + containerColor = MaterialTheme.colorScheme.surface, + tonalElevation = 8.dp, + shadowElevation = 8.dp, + ) { + campusOptions.forEach { campus -> + DropdownMenuItem( + text = { + Text( + text = campus, + color = MaterialTheme.colorScheme.onSurface, + ) + }, + onClick = { + selectedCampus = campus + expanded = false + }, + colors = + MenuDefaults.itemColors( + textColor = MaterialTheme.colorScheme.onSurface, + leadingIconColor = MaterialTheme.colorScheme.onSurface, + trailingIconColor = MaterialTheme.colorScheme.onSurface, + disabledTextColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f), + disabledLeadingIconColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f), + disabledTrailingIconColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f), + ), + ) + } + } + } + + Spacer(modifier = Modifier.height(32.dp)) + + Button( + onClick = { + if (selectedCampus.isNotEmpty() && !isLoading) { + updateCampus( + authToken = authToken, + campus = selectedCampus, + onLoading = { isLoading = it }, + onSuccess = { + prefs.edit().putString(Constants.COMMUNITY_CAMPUS, selectedCampus).apply() + Toast.makeText(context, "Campus updated successfully", Toast.LENGTH_SHORT).show() + onDismiss() + }, + onError = { /* Keep dialog open on error */ }, + ) + } + }, + enabled = selectedCampus.isNotEmpty() && !isLoading, + modifier = + Modifier + .fillMaxWidth() + .height(48.dp), + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + disabledContainerColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f), + ), + shape = RoundedCornerShape(12.dp), + ) { + if (isLoading) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + ) { + CircularProgressIndicator( + modifier = Modifier.size(16.dp), + color = MaterialTheme.colorScheme.onPrimary, + strokeWidth = 2.dp, + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("Updating...") + } + } else { + Text( + text = "Update Campus", + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + ) + } + } + } + } + } +} + +private fun updateCampus( + authToken: String, + campus: String, + onLoading: (Boolean) -> Unit, + onSuccess: () -> Unit, + onError: (String) -> Unit, +) { + onLoading(true) + + APICommunityRestClient.instance.updateCampus( + token = authToken, + campus = campus.lowercase(), + retrofitUserActionListener = + object : RetrofitUserActionListener { + override fun onSuccess( + call: Call?, + response: PostResponse?, + ) { + onLoading(false) + onSuccess() + } + + override fun onError( + call: Call?, + t: Throwable?, + ) { + onLoading(false) + onError("Failed to update campus: ${t?.message}") + } + }, + ) +} diff --git a/app/src/main/java/com/dscvit/vitty/ui/connect/CircleDetailScreenContent.kt b/app/src/main/java/com/dscvit/vitty/ui/connect/CircleDetailScreenContent.kt index fe754f7..2a1e8f1 100644 --- a/app/src/main/java/com/dscvit/vitty/ui/connect/CircleDetailScreenContent.kt +++ b/app/src/main/java/com/dscvit/vitty/ui/connect/CircleDetailScreenContent.kt @@ -2,6 +2,10 @@ package com.dscvit.vitty.ui.connect import android.content.Context import android.widget.Toast +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -26,6 +30,7 @@ import androidx.compose.foundation.text.BasicTextField import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Menu import androidx.compose.material3.AlertDialog import androidx.compose.material3.CenterAlignedTopAppBar @@ -35,9 +40,12 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SwipeToDismissBox +import androidx.compose.material3.SwipeToDismissBoxValue import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberSwipeToDismissBoxState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -88,6 +96,9 @@ fun CircleDetailScreenContent( var showDropdownMenu by remember { mutableStateOf(false) } var showDeleteConfirmDialog by remember { mutableStateOf(false) } var showLeaveConfirmDialog by remember { mutableStateOf(false) } + var showRemoveUserDialog by remember { mutableStateOf(false) } + var userToRemove by remember { mutableStateOf(null) } + var resetDismissCounter by remember { mutableStateOf(0) } val circleFriends = circleMembers?.data val actionResponse by connectViewModel.circleActionResponse.observeAsState() @@ -130,6 +141,10 @@ fun CircleDetailScreenContent( connectViewModel.clearCircleActionResponse() onBackClick() } + "user removed from circle" -> { + Toast.makeText(context, "User removed from circle", Toast.LENGTH_SHORT).show() + connectViewModel.clearCircleActionResponse() + } else -> { Toast.makeText(context, response.detail, Toast.LENGTH_SHORT).show() connectViewModel.clearCircleActionResponse() @@ -195,8 +210,8 @@ fun CircleDetailScreenContent( onDismissRequest = { showDropdownMenu = false }, modifier = Modifier - .background(Secondary) - .clip(RoundedCornerShape(8.dp)), + .background(Secondary), + shape = RoundedCornerShape(8.dp), ) { if (circle.circle_role == "admin") { DropdownMenuItem( @@ -447,11 +462,25 @@ fun CircleDetailScreenContent( contentPadding = PaddingValues(bottom = 32.dp), verticalArrangement = Arrangement.spacedBy(12.dp), ) { - items(filteredFriends ?: emptyList()) { friend -> - CircleFriendCard( - friend = friend, - onClick = { onMemberClick(friend, circle.circle_id) }, - ) + items(filteredFriends ?: emptyList(), key = { friend -> "${friend.username}-$resetDismissCounter" }) { friend -> + val sharedPreferences = context.getSharedPreferences(Constants.USER_INFO, Context.MODE_PRIVATE) + val currentUsername = sharedPreferences.getString(Constants.COMMUNITY_USERNAME, "") ?: "" + + if (circle.circle_role == "admin" && friend.username != currentUsername) { + SwipeToDismissCircleFriendCard( + friend = friend, + onClick = { onMemberClick(friend, circle.circle_id) }, + onRemove = { + userToRemove = friend + showRemoveUserDialog = true + }, + ) + } else { + CircleFriendCard( + friend = friend, + onClick = { onMemberClick(friend, circle.circle_id) }, + ) + } } } } @@ -632,6 +661,70 @@ fun CircleDetailScreenContent( }, ) } + + if (showRemoveUserDialog) { + userToRemove?.let { user -> + AlertDialog( + onDismissRequest = { + showRemoveUserDialog = false + resetDismissCounter += 1 + }, + containerColor = Background, + titleContentColor = TextColor, + textContentColor = TextColor, + title = { + Text( + "Remove User", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Medium, + color = TextColor, + ) + }, + text = { + Text( + text = "Are you sure you want to remove ${user.name} from the circle?", + style = MaterialTheme.typography.bodyMedium, + color = TextColor, + ) + }, + confirmButton = { + TextButton( + onClick = { + val sharedPreferences = context.getSharedPreferences(Constants.USER_INFO, Context.MODE_PRIVATE) + val token = sharedPreferences.getString(Constants.COMMUNITY_TOKEN, "") ?: "" + + connectViewModel.removeUserFromCircle( + token = token, + circleId = circle.circle_id, + username = user.username, + ) + showRemoveUserDialog = false + }, + ) { + Text( + "Remove", + color = Accent, + fontWeight = FontWeight.Medium, + ) + } + }, + dismissButton = { + TextButton( + onClick = { + showRemoveUserDialog = false + resetDismissCounter += 1 + }, + ) { + Text( + "Cancel", + color = Accent.copy(alpha = 0.7f), + fontWeight = FontWeight.Medium, + ) + } + }, + ) + } + } } @Composable @@ -727,3 +820,69 @@ fun CircleFriendCard( } } } + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SwipeToDismissCircleFriendCard( + friend: UserResponse, + onClick: () -> Unit = {}, + onRemove: () -> Unit = {}, +) { + var isDismissed by remember { mutableStateOf(false) } + + val dismissState = + rememberSwipeToDismissBoxState( + confirmValueChange = { dismissValue -> + when (dismissValue) { + SwipeToDismissBoxValue.EndToStart -> { + isDismissed = true + true + } + else -> false + } + }, + ) + + LaunchedEffect(isDismissed) { + if (isDismissed) { + kotlinx.coroutines.delay(200) + onRemove() + } + } + + AnimatedVisibility( + visible = !isDismissed, + exit = fadeOut(animationSpec = tween(200)), + enter = fadeIn(animationSpec = tween(200)), + ) { + SwipeToDismissBox( + state = dismissState, + backgroundContent = { + Box( + modifier = + Modifier + .fillMaxSize() + .background( + color = Color(0xffd9534f), + shape = RoundedCornerShape(16.dp), + ).padding(horizontal = 16.dp), + contentAlignment = Alignment.CenterEnd, + ) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = "Remove User", + tint = Color.White, + modifier = Modifier.size(24.dp), + ) + } + }, + enableDismissFromEndToStart = true, + enableDismissFromStartToEnd = false, + ) { + CircleFriendCard( + friend = friend, + onClick = onClick, + ) + } + } +} diff --git a/app/src/main/java/com/dscvit/vitty/ui/connect/ConnectViewModel.kt b/app/src/main/java/com/dscvit/vitty/ui/connect/ConnectViewModel.kt index 502d1f4..022edb3 100644 --- a/app/src/main/java/com/dscvit/vitty/ui/connect/ConnectViewModel.kt +++ b/app/src/main/java/com/dscvit/vitty/ui/connect/ConnectViewModel.kt @@ -641,4 +641,35 @@ class ConnectViewModel : ViewModel() { }, ) } + + fun removeUserFromCircle( + token: String, + circleId: String, + username: String, + ) { + APICommunityRestClient.instance.removeUserFromCircle( + token, + circleId, + username, + object : RetrofitUserActionListener { + override fun onSuccess( + call: Call?, + response: PostResponse?, + ) { + Timber.d("RemoveUserFromCircle: $response") + _circleActionResponse.postValue(response) + + getCircleDetails(token, circleId) + } + + override fun onError( + call: Call?, + t: Throwable?, + ) { + Timber.d("RemoveUserFromCircleError: ${t?.message}") + _circleActionResponse.postValue(null) + } + }, + ) + } } diff --git a/app/src/main/java/com/dscvit/vitty/ui/connect/components/AddParticipantsBottomSheet.kt b/app/src/main/java/com/dscvit/vitty/ui/connect/components/AddParticipantsBottomSheet.kt deleted file mode 100644 index 38dc7fe..0000000 --- a/app/src/main/java/com/dscvit/vitty/ui/connect/components/AddParticipantsBottomSheet.kt +++ /dev/null @@ -1,651 +0,0 @@ -package com.dscvit.vitty.ui.connect.components - -import android.content.Context -import android.widget.Toast -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Check -import androidx.compose.material.icons.filled.Close -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.SheetState -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -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.unit.dp -import androidx.compose.ui.unit.sp -import androidx.lifecycle.viewmodel.compose.viewModel -import coil.compose.AsyncImage -import com.dscvit.vitty.R -import com.dscvit.vitty.network.api.community.responses.user.UserResponse -import com.dscvit.vitty.theme.Accent -import com.dscvit.vitty.theme.Background -import com.dscvit.vitty.theme.Poppins -import com.dscvit.vitty.theme.Secondary -import com.dscvit.vitty.theme.TextColor -import com.dscvit.vitty.ui.community.CommunityViewModel -import com.dscvit.vitty.util.Constants -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun AddParticipantsBottomSheet( - sheetState: SheetState, - circleId: String, - onDismiss: () -> Unit, - onSendCircleRequest: (String, String) -> Unit, -) { - val context = LocalContext.current - val communityViewModel: CommunityViewModel = viewModel() - val scope = rememberCoroutineScope() - - var searchQuery by remember { mutableStateOf("") } - var isSearching by remember { mutableStateOf(false) } - var selectedUsers by remember { mutableStateOf(setOf()) } - var sendingRequests by remember { mutableStateOf(false) } - var requestStates by remember { mutableStateOf(mapOf()) } - var pendingRequestsCount by remember { mutableStateOf(0) } - - val sharedPreferences = - remember { - context.getSharedPreferences(Constants.USER_INFO, Context.MODE_PRIVATE) - } - val currentUsername = - remember { - sharedPreferences.getString(Constants.COMMUNITY_USERNAME, "") ?: "" - } - val currentName = - remember { - sharedPreferences.getString(Constants.COMMUNITY_NAME, "") ?: "" - } - val token = - remember { - sharedPreferences.getString(Constants.COMMUNITY_TOKEN, "") ?: "" - } - - val suggestedFriends by communityViewModel.suggestedFriends.observeAsState() - val searchResults by communityViewModel.searchResult.observeAsState() - val actionResponse by communityViewModel.actionResponse.observeAsState() - - - LaunchedEffect(actionResponse) { - if (sendingRequests && actionResponse != null) { - val currentUser = requestStates.keys.find { requestStates[it] == null } - if (currentUser != null) { - val isSuccess = actionResponse?.detail == "request sent successfully" - requestStates = - requestStates.toMutableMap().apply { - this[currentUser] = isSuccess - } - pendingRequestsCount-- - - - communityViewModel.actionResponse.postValue(null) - - - if (pendingRequestsCount == 0) { - val failedUsers = requestStates.filter { it.value == false }.keys.toList() - - if (failedUsers.isEmpty()) { - Toast - .makeText( - context, - "All requests sent successfully!", - Toast.LENGTH_SHORT, - ).show() - } else { - val failedUsersText = failedUsers.joinToString(", ") - Toast - .makeText( - context, - "Failed to send requests to: $failedUsersText", - Toast.LENGTH_LONG, - ).show() - } - - sendingRequests = false - selectedUsers = setOf() - requestStates = mapOf() - - if (failedUsers.isEmpty()) { - scope.launch { - onDismiss() - } - } - } - } - } - } - - LaunchedEffect(Unit) { - if (token.isNotEmpty()) { - communityViewModel.getSuggestedFriends(token) - } - } - - LaunchedEffect(searchQuery) { - if (searchQuery.isNotBlank()) { - isSearching = true - delay(300) - if (token.isNotEmpty() && searchQuery.isNotBlank()) { - communityViewModel.getSearchResult(token, searchQuery) - } - isSearching = false - } else { - - isSearching = false - } - } - - val filteredSuggestedFriends = - suggestedFriends?.filter { user -> - user.username != currentUsername && user.name != currentName - } - val filteredSearchResults = - searchResults?.filter { user -> - user.username != currentUsername && user.name != currentName - } - - fun sendAllRequests() { - if (selectedUsers.isNotEmpty() && !sendingRequests && token.isNotEmpty()) { - sendingRequests = true - pendingRequestsCount = selectedUsers.size - requestStates = selectedUsers.associateWith { null } - - selectedUsers.forEach { username -> - communityViewModel.sendCircleRequest(token, circleId, username) - } - } - } - - ModalBottomSheet( - onDismissRequest = onDismiss, - sheetState = sheetState, - containerColor = Secondary, - contentColor = TextColor, - dragHandle = { - Box( - modifier = - Modifier - .padding(top = 16.dp) - .width(120.dp) - .height(7.dp) - .background(Accent.copy(alpha = .4f), shape = RoundedCornerShape(44.dp)), - ) - }, - ) { - Column( - modifier = - Modifier - .fillMaxSize() - .padding(horizontal = 24.dp, vertical = 16.dp), - ) { - - Box( - modifier = - Modifier - .fillMaxWidth() - .padding(bottom = 16.dp), - ) { - IconButton( - onClick = onDismiss, - modifier = Modifier.align(Alignment.CenterStart).size(40.dp), - ) { - Icon( - painter = painterResource(id = R.drawable.ic_round_chevron_left), - contentDescription = "Back", - tint = TextColor, - ) - } - - Text( - text = "Add Participants", - color = TextColor, - fontFamily = Poppins, - fontSize = 20.sp, - fontWeight = FontWeight.SemiBold, - letterSpacing = 0.2.sp, - textAlign = TextAlign.Center, - modifier = Modifier.align(Alignment.Center), - ) - } - - - Box( - modifier = - Modifier - .fillMaxWidth() - .border(.8.dp, Accent, RoundedCornerShape(9999.dp)) - .background(Background, RoundedCornerShape(9999.dp)), - ) { - Row( - modifier = - Modifier - .fillMaxWidth() - .height(44.dp) - .padding(horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - BasicTextField( - value = searchQuery, - onValueChange = { searchQuery = it }, - singleLine = true, - cursorBrush = SolidColor(Accent), - textStyle = - MaterialTheme.typography.bodyMedium.copy( - color = TextColor, - fontSize = 16.sp, - lineHeight = 16.sp, - ), - modifier = Modifier.weight(1f), - decorationBox = { innerTextField -> - Box( - modifier = Modifier.fillMaxWidth(), - contentAlignment = Alignment.CenterStart, - ) { - if (searchQuery.isEmpty()) { - Text( - text = "Search users...", - color = Accent.copy(alpha = 0.3f), - style = - MaterialTheme.typography.bodyMedium.copy( - fontSize = 16.sp, - lineHeight = 16.sp, - ), - ) - } - innerTextField() - } - }, - ) - if (searchQuery.isNotEmpty()) { - IconButton(onClick = { searchQuery = "" }) { - Icon( - imageVector = Icons.Default.Close, - contentDescription = "Clear", - tint = Accent, - ) - } - } - } - } - - Spacer(modifier = Modifier.height(16.dp)) - - - if (selectedUsers.isNotEmpty()) { - Text( - text = "Selected (${selectedUsers.size})", - style = MaterialTheme.typography.bodyLarge, - color = TextColor, - fontWeight = FontWeight.Medium, - ) - Spacer(modifier = Modifier.height(8.dp)) - - - LazyColumn( - modifier = Modifier.height(120.dp), - contentPadding = PaddingValues(vertical = 4.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - val allUsers = (filteredSuggestedFriends ?: emptyList()) + (filteredSearchResults ?: emptyList()) - val selectedUsersList = - allUsers.distinctBy { it.username }.filter { user -> - selectedUsers.contains(user.username) - } - - items(selectedUsersList) { user -> - SelectedUserCard( - user = user, - onRemove = { - selectedUsers = selectedUsers - user.username - }, - ) - } - } - - Spacer(modifier = Modifier.height(16.dp)) - - Text( - text = "Add More Users", - style = MaterialTheme.typography.bodyLarge, - color = TextColor, - fontWeight = FontWeight.Medium, - ) - Spacer(modifier = Modifier.height(8.dp)) - } - - - val displayUsers = if (searchQuery.isBlank()) filteredSuggestedFriends else filteredSearchResults - val filteredUsers = - displayUsers?.filter { user -> - !selectedUsers.contains(user.username) && - - ( - searchQuery.isBlank() || - user.name.contains(searchQuery, ignoreCase = true) || - user.username.contains(searchQuery, ignoreCase = true) - ) - } - - LazyColumn( - modifier = - Modifier - .weight(1f) - .heightIn(max = 300.dp), - - contentPadding = PaddingValues(vertical = 8.dp), - verticalArrangement = Arrangement.spacedBy(12.dp), - ) { - if (isSearching) { - item { - Box( - modifier = Modifier.fillMaxWidth(), - contentAlignment = Alignment.Center, - ) { - CircularProgressIndicator(color = Accent) - } - } - } else if (filteredUsers.isNullOrEmpty()) { - item { - Box( - modifier = Modifier.fillMaxWidth().padding(32.dp), - contentAlignment = Alignment.Center, - ) { - Text( - text = if (searchQuery.isNotEmpty()) "No users found" else "No users available", - color = Accent.copy(alpha = 0.7f), - style = MaterialTheme.typography.bodyMedium, - ) - } - } - } else { - items(filteredUsers) { user -> - UserSelectionCard( - user = user, - isSelected = selectedUsers.contains(user.username), - onSelectionChanged = { isSelected -> - selectedUsers = - if (isSelected) { - selectedUsers + user.username - } else { - selectedUsers - user.username - } - }, - ) - } - } - } - - - if (selectedUsers.isNotEmpty()) { - Spacer(modifier = Modifier.height(16.dp)) - Button( - onClick = { sendAllRequests() }, - enabled = !sendingRequests, - modifier = Modifier.fillMaxWidth(), - colors = - ButtonDefaults.buttonColors( - containerColor = Accent, - contentColor = Secondary, - ), - shape = RoundedCornerShape(12.dp), - ) { - if (sendingRequests) { - CircularProgressIndicator( - modifier = Modifier.size(16.dp), - color = Secondary, - strokeWidth = 2.dp, - ) - Spacer(modifier = Modifier.width(8.dp)) - } - Text( - text = if (sendingRequests) "Sending..." else "Add ${selectedUsers.size} Participant${if (selectedUsers.size > 1) "s" else ""}", - fontWeight = FontWeight.SemiBold, - ) - } - } - } - } -} - -@Composable -private fun UserSelectionCard( - user: UserResponse, - isSelected: Boolean, - onSelectionChanged: (Boolean) -> Unit, -) { - Card( - modifier = - Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(16.dp)) - .clickable { onSelectionChanged(!isSelected) }, - colors = - CardDefaults.cardColors( - containerColor = Background, - ), - elevation = CardDefaults.cardElevation(defaultElevation = 2.dp), - shape = RoundedCornerShape(16.dp), - ) { - Row( - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp, vertical = 20.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - - Box( - modifier = - Modifier - .size(48.dp) - .background(Accent.copy(alpha = 0.2f), CircleShape), - contentAlignment = Alignment.Center, - ) { - if (user.picture.isNotEmpty()) { - AsyncImage( - model = user.picture, - contentDescription = null, - modifier = - Modifier - .size(48.dp) - .clip(CircleShape), - ) - } else { - Text( - text = - user.name - .take(2) - .map { it.uppercaseChar() } - .joinToString(""), - color = TextColor, - fontWeight = FontWeight.Bold, - fontSize = 18.sp, - ) - } - } - - Spacer(modifier = Modifier.width(12.dp)) - - - Column(modifier = Modifier.weight(1f)) { - Text( - text = user.name, - color = TextColor, - fontWeight = FontWeight.Medium, - fontSize = 16.sp, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - Text( - text = "@${user.username}", - color = TextColor.copy(alpha = 0.7f), - fontSize = 14.sp, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - - - Box( - modifier = - Modifier - .size(24.dp) - .background( - if (isSelected) Accent else Background, - CircleShape, - ).border( - 2.dp, - if (isSelected) Accent else TextColor.copy(alpha = 0.3f), - CircleShape, - ), - contentAlignment = Alignment.Center, - ) { - if (isSelected) { - Icon( - imageVector = Icons.Default.Check, - contentDescription = "Selected", - tint = Background, - modifier = Modifier.size(16.dp), - ) - } - } - } - } -} - -@Composable -private fun SelectedUserCard( - user: UserResponse, - onRemove: () -> Unit, -) { - Box( - modifier = - Modifier - .fillMaxWidth() - .background(Background, RoundedCornerShape(12.dp)) - .border(1.dp, Accent.copy(alpha = 0.3f), RoundedCornerShape(12.dp)) - .padding(horizontal = 16.dp, vertical = 12.dp), - ) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.weight(1f), - ) { - Box( - modifier = - Modifier - .size(32.dp) - .background(Accent.copy(alpha = 0.2f), CircleShape), - contentAlignment = Alignment.Center, - ) { - if (user.picture.isNotEmpty()) { - AsyncImage( - model = user.picture, - contentDescription = null, - modifier = - Modifier - .size(32.dp) - .clip(CircleShape), - ) - } else { - Text( - text = - user.name - .take(2) - .map { it.uppercaseChar() } - .joinToString(""), - color = TextColor, - fontWeight = FontWeight.Bold, - fontSize = 14.sp, - ) - } - } - - Spacer(modifier = Modifier.width(12.dp)) - - Column { - Text( - text = user.name, - color = TextColor, - fontWeight = FontWeight.Medium, - fontSize = 14.sp, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - Text( - text = "@${user.username}", - color = TextColor.copy(alpha = 0.6f), - fontSize = 12.sp, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - } - - - IconButton( - onClick = onRemove, - modifier = Modifier.size(24.dp), - ) { - Icon( - imageVector = Icons.Default.Close, - contentDescription = "Remove", - tint = Accent, - modifier = Modifier.size(16.dp), - ) - } - } - } -} diff --git a/app/src/main/java/com/dscvit/vitty/ui/main/MainComposeApp.kt b/app/src/main/java/com/dscvit/vitty/ui/main/MainComposeApp.kt index 5c978ea..c0be67b 100644 --- a/app/src/main/java/com/dscvit/vitty/ui/main/MainComposeApp.kt +++ b/app/src/main/java/com/dscvit/vitty/ui/main/MainComposeApp.kt @@ -90,7 +90,9 @@ import com.dscvit.vitty.theme.Secondary import com.dscvit.vitty.theme.TextColor import com.dscvit.vitty.theme.VittyTheme import com.dscvit.vitty.ui.academics.AcademicsScreenContent +import com.dscvit.vitty.ui.academics.AcademicsViewModel import com.dscvit.vitty.ui.academics.models.Course +import com.dscvit.vitty.ui.components.CampusUpdateDialog import com.dscvit.vitty.ui.connect.AddFriendScreenContent import com.dscvit.vitty.ui.connect.AddParticipantsScreenContent import com.dscvit.vitty.ui.connect.CircleDetailScreenContent @@ -107,18 +109,19 @@ import com.dscvit.vitty.ui.emptyclassrooms.EmptyClassroomsContent import com.dscvit.vitty.ui.notes.NoteScreenContent import com.dscvit.vitty.ui.schedule.ScheduleScreenContent import com.dscvit.vitty.ui.schedule.ScheduleViewModel +import com.dscvit.vitty.util.Analytics import com.dscvit.vitty.util.Constants import com.dscvit.vitty.util.LogoutHelper import com.dscvit.vitty.util.SemesterUtils import com.dscvit.vitty.util.UtilFunctions import com.google.gson.Gson +import java.net.URLDecoder +import java.net.URLEncoder +import java.nio.charset.StandardCharsets import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import retrofit2.Call -import java.net.URLDecoder -import java.net.URLEncoder -import java.nio.charset.StandardCharsets @Composable fun MainComposeApp() { @@ -127,429 +130,462 @@ fun MainComposeApp() { val currentRoute = navBackStackEntry?.destination?.route val scope = rememberCoroutineScope() val connectViewModel: ConnectViewModel = viewModel() + val academicsViewModel: AcademicsViewModel = viewModel() + val coursePageViewModel: CoursePageViewModel = viewModel() + + val context = LocalContext.current + val prefs = remember { context.getSharedPreferences(Constants.USER_INFO, 0) } + + var campus by remember { mutableStateOf(prefs.getString(Constants.COMMUNITY_CAMPUS, "") ?: "") } + var showCampusDialog by remember { mutableStateOf(campus.isEmpty()) } + + DisposableEffect(Unit) { + val listener = + SharedPreferences.OnSharedPreferenceChangeListener { _, key -> + if (key == Constants.COMMUNITY_CAMPUS) { + val newCampus = prefs.getString(Constants.COMMUNITY_CAMPUS, "") ?: "" + campus = newCampus + showCampusDialog = newCampus.isEmpty() + } + } + prefs.registerOnSharedPreferenceChangeListener(listener) + + onDispose { prefs.unregisterOnSharedPreferenceChangeListener(listener) } + } var bottomNavVisible by remember { mutableStateOf(true) } val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) LaunchedEffect(currentRoute) { - bottomNavVisible = currentRoute?.let { route -> - !route.startsWith("course_page") && - !route.startsWith("note_screen") && - route != "empty_classrooms" && - !route.startsWith("friend_detail") && - !route.startsWith("circle_detail") && - route != "add_friend" && - route != "friend_requests" && - route != "circle_requests" && - !route.startsWith("circle_member_detail") && - !route.startsWith("add_participants") - } ?: true + bottomNavVisible = + currentRoute?.let { route -> + !route.startsWith("course_page") && + !route.startsWith("note_screen") && + route != "empty_classrooms" && + !route.startsWith("friend_detail") && + !route.startsWith("circle_detail") && + route != "add_friend" && + route != "friend_requests" && + route != "circle_requests" && + !route.startsWith("circle_member_detail") && + !route.startsWith("add_participants") + } + ?: true + + currentRoute?.let { route -> + val screenName = + when { + route == "academics" -> "Academics Screen" + route == "schedule" -> "Schedule Screen" + route == "connect" -> "Connect Screen" + route.startsWith("course_page") -> "Course Detail Screen" + route.startsWith("note_screen") -> "Note Editor Screen" + route == "empty_classrooms" -> "Empty Classrooms Screen" + route.startsWith("friend_detail") -> "Friend Detail Screen" + route.startsWith("circle_detail") -> "Circle Detail Screen" + route == "add_friend" -> "Add Friend Screen" + route == "friend_requests" -> "Friend Requests Screen" + route == "circle_requests" -> "Circle Requests Screen" + route.startsWith("circle_member_detail") -> "Circle Member Detail Screen" + route.startsWith("add_participants") -> "Add Participants Screen" + else -> route + } + Analytics.appNavigation(screenName) + } } VittyTheme { ModalNavigationDrawer( - drawerState = drawerState, - drawerContent = { - DrawerContent( - navController = navController, - onCloseDrawer = { - scope.launch { - drawerState.close() - } - }, - ) - }, + drawerState = drawerState, + drawerContent = { + DrawerContent( + navController = navController, + onCloseDrawer = { scope.launch { drawerState.close() } }, + ) + }, ) { Box( - modifier = - Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background), + modifier = + Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background), ) { NavHost( - navController = navController, - startDestination = "schedule", - modifier = Modifier.fillMaxSize(), + navController = navController, + startDestination = "schedule", + modifier = Modifier.fillMaxSize(), ) { composable( - "academics", - enterTransition = { - slideInHorizontally( - initialOffsetX = { -it }, - animationSpec = - spring( - dampingRatio = 0.8f, - stiffness = 300f, - ), - ) + fadeIn(animationSpec = tween(300)) - }, - exitTransition = { - slideOutHorizontally( - targetOffsetX = { -it }, - animationSpec = - spring( - dampingRatio = 0.8f, - stiffness = 300f, - ), - ) + fadeOut(animationSpec = tween(200)) - }, + "academics", + enterTransition = { + slideInHorizontally( + initialOffsetX = { -it }, + animationSpec = + spring( + dampingRatio = 0.8f, + stiffness = 300f, + ), + ) + fadeIn(animationSpec = tween(300)) + }, + exitTransition = { + slideOutHorizontally( + targetOffsetX = { -it }, + animationSpec = + spring( + dampingRatio = 0.8f, + stiffness = 300f, + ), + ) + fadeOut(animationSpec = tween(200)) + }, ) { AcademicsComposeScreen( - navController = navController, - onOpenDrawer = { - scope.launch { - drawerState.open() + navController = navController, + onOpenDrawer = { scope.launch { drawerState.open() } }, + academicsViewModel, + coursePageViewModel, + ) + } + composable( + "schedule", + enterTransition = { + when (initialState.destination.route) { + "academics" -> + slideInHorizontally( + initialOffsetX = { it }, + animationSpec = + spring( + dampingRatio = 0.8f, + stiffness = 300f, + ), + ) + fadeIn(animationSpec = tween(300)) + "connect" -> + slideInHorizontally( + initialOffsetX = { -it }, + animationSpec = + spring( + dampingRatio = 0.8f, + stiffness = 300f, + ), + ) + fadeIn(animationSpec = tween(300)) + else -> fadeIn(animationSpec = tween(300)) + } + }, + exitTransition = { + when (targetState.destination.route) { + "academics" -> + slideOutHorizontally( + targetOffsetX = { it }, + animationSpec = + spring( + dampingRatio = 0.8f, + stiffness = 300f, + ), + ) + fadeOut(animationSpec = tween(200)) + "connect" -> + slideOutHorizontally( + targetOffsetX = { -it }, + animationSpec = + spring( + dampingRatio = 0.8f, + stiffness = 300f, + ), + ) + fadeOut(animationSpec = tween(200)) + else -> fadeOut(animationSpec = tween(200)) } }, + ) { + ScheduleComposeScreen( + navController = navController, + onOpenDrawer = { scope.launch { drawerState.open() } }, ) } composable( - "schedule", - enterTransition = { - when (initialState.destination.route) { - "academics" -> - slideInHorizontally( + "connect", + enterTransition = { + slideInHorizontally( initialOffsetX = { it }, animationSpec = - spring( - dampingRatio = 0.8f, - stiffness = 300f, - ), - ) + fadeIn(animationSpec = tween(300)) - - "connect" -> - slideInHorizontally( - initialOffsetX = { -it }, - animationSpec = - spring( - dampingRatio = 0.8f, - stiffness = 300f, - ), - ) + fadeIn(animationSpec = tween(300)) - - else -> fadeIn(animationSpec = tween(300)) - } - }, - exitTransition = { - when (targetState.destination.route) { - "academics" -> - slideOutHorizontally( + spring( + dampingRatio = 0.8f, + stiffness = 300f, + ), + ) + fadeIn(animationSpec = tween(300)) + }, + exitTransition = { + slideOutHorizontally( targetOffsetX = { it }, animationSpec = - spring( - dampingRatio = 0.8f, - stiffness = 300f, - ), - ) + fadeOut(animationSpec = tween(200)) - - "connect" -> - slideOutHorizontally( - targetOffsetX = { -it }, - animationSpec = - spring( - dampingRatio = 0.8f, - stiffness = 300f, - ), - ) + fadeOut(animationSpec = tween(200)) - - else -> fadeOut(animationSpec = tween(200)) - } - }, - ) { - ScheduleComposeScreen( - navController = navController, - onOpenDrawer = { - scope.launch { - drawerState.open() - } + spring( + dampingRatio = 0.8f, + stiffness = 300f, + ), + ) + fadeOut(animationSpec = tween(200)) }, - ) - } - composable( - "connect", - enterTransition = { - slideInHorizontally( - initialOffsetX = { it }, - animationSpec = - spring( - dampingRatio = 0.8f, - stiffness = 300f, - ), - ) + fadeIn(animationSpec = tween(300)) - }, - exitTransition = { - slideOutHorizontally( - targetOffsetX = { it }, - animationSpec = - spring( - dampingRatio = 0.8f, - stiffness = 300f, - ), - ) + fadeOut(animationSpec = tween(200)) - }, ) { ConnectComposeScreen( - navController = navController, - connectViewModel = connectViewModel, + navController = navController, + connectViewModel = connectViewModel, ) } composable( - "friend_detail/{friendData}", - enterTransition = { - slideInHorizontally( - initialOffsetX = { it }, - animationSpec = - spring( - dampingRatio = 0.9f, - stiffness = 400f, - ), - ) + fadeIn(animationSpec = tween(250)) - }, - exitTransition = { - slideOutHorizontally( - targetOffsetX = { it }, - animationSpec = - spring( - dampingRatio = 0.9f, - stiffness = 400f, - ), - ) + fadeOut(animationSpec = tween(150)) - }, + "friend_detail/{friendData}", + enterTransition = { + slideInHorizontally( + initialOffsetX = { it }, + animationSpec = + spring( + dampingRatio = 0.9f, + stiffness = 400f, + ), + ) + fadeIn(animationSpec = tween(250)) + }, + exitTransition = { + slideOutHorizontally( + targetOffsetX = { it }, + animationSpec = + spring( + dampingRatio = 0.9f, + stiffness = 400f, + ), + ) + fadeOut(animationSpec = tween(150)) + }, ) { backStackEntry -> val encodedFriendData = - backStackEntry.arguments?.getString("friendData") ?: "" - val friendData = URLDecoder.decode(encodedFriendData, StandardCharsets.UTF_8.toString()) + backStackEntry.arguments?.getString("friendData") ?: "" + val friendData = + URLDecoder.decode( + encodedFriendData, + StandardCharsets.UTF_8.toString() + ) val friend = - try { - Gson().fromJson(friendData, UserResponse::class.java) - } catch (e: Exception) { - null - } + try { + Gson().fromJson(friendData, UserResponse::class.java) + } catch (e: Exception) { + null + } if (friend != null) { FriendDetailScreenContent( - friend = friend, - onBackClick = { - navController.popBackStack() - }, - connectViewModel = connectViewModel, + friend = friend, + onBackClick = { navController.popBackStack() }, + connectViewModel = connectViewModel, ) } else { - LaunchedEffect(Unit) { - navController.popBackStack() - } + LaunchedEffect(Unit) { navController.popBackStack() } } } composable( - "add_friend", - enterTransition = { - slideInHorizontally( - initialOffsetX = { it }, - animationSpec = - spring( - dampingRatio = 0.9f, - stiffness = 400f, - ), - ) + fadeIn(animationSpec = tween(250)) - }, - exitTransition = { - slideOutHorizontally( - targetOffsetX = { it }, - animationSpec = - spring( - dampingRatio = 0.9f, - stiffness = 400f, - ), - ) + fadeOut(animationSpec = tween(150)) - }, + "add_friend", + enterTransition = { + slideInHorizontally( + initialOffsetX = { it }, + animationSpec = + spring( + dampingRatio = 0.9f, + stiffness = 400f, + ), + ) + fadeIn(animationSpec = tween(250)) + }, + exitTransition = { + slideOutHorizontally( + targetOffsetX = { it }, + animationSpec = + spring( + dampingRatio = 0.9f, + stiffness = 400f, + ), + ) + fadeOut(animationSpec = tween(150)) + }, ) { AddFriendScreenContent( - onBackClick = { - navController.popBackStack() - }, + onBackClick = { navController.popBackStack() }, ) } composable( - "friend_requests", - enterTransition = { - slideInHorizontally( - initialOffsetX = { it }, - animationSpec = - spring( - dampingRatio = 0.9f, - stiffness = 400f, - ), - ) + fadeIn(animationSpec = tween(250)) - }, - exitTransition = { - slideOutHorizontally( - targetOffsetX = { it }, - animationSpec = - spring( - dampingRatio = 0.9f, - stiffness = 400f, - ), - ) + fadeOut(animationSpec = tween(150)) - }, + "friend_requests", + enterTransition = { + slideInHorizontally( + initialOffsetX = { it }, + animationSpec = + spring( + dampingRatio = 0.9f, + stiffness = 400f, + ), + ) + fadeIn(animationSpec = tween(250)) + }, + exitTransition = { + slideOutHorizontally( + targetOffsetX = { it }, + animationSpec = + spring( + dampingRatio = 0.9f, + stiffness = 400f, + ), + ) + fadeOut(animationSpec = tween(150)) + }, ) { FriendRequestsScreenContent( - onBackClick = { - navController.popBackStack() - }, - connectViewModel = connectViewModel, + onBackClick = { navController.popBackStack() }, + connectViewModel = connectViewModel, ) } composable( - "circle_requests", - enterTransition = { - slideInHorizontally( - initialOffsetX = { it }, - animationSpec = - spring( - dampingRatio = 0.9f, - stiffness = 400f, - ), - ) + fadeIn(animationSpec = tween(250)) - }, - exitTransition = { - slideOutHorizontally( - targetOffsetX = { it }, - animationSpec = - spring( - dampingRatio = 0.9f, - stiffness = 400f, - ), - ) + fadeOut(animationSpec = tween(150)) - }, + "circle_requests", + enterTransition = { + slideInHorizontally( + initialOffsetX = { it }, + animationSpec = + spring( + dampingRatio = 0.9f, + stiffness = 400f, + ), + ) + fadeIn(animationSpec = tween(250)) + }, + exitTransition = { + slideOutHorizontally( + targetOffsetX = { it }, + animationSpec = + spring( + dampingRatio = 0.9f, + stiffness = 400f, + ), + ) + fadeOut(animationSpec = tween(150)) + }, ) { CircleRequestsScreenContent( - onBackClick = { - navController.popBackStack() - }, - connectViewModel = connectViewModel, + onBackClick = { navController.popBackStack() }, + connectViewModel = connectViewModel, ) } composable( - "add_participants/{circleId}", - enterTransition = { - slideInHorizontally( - initialOffsetX = { it }, - animationSpec = - spring( - dampingRatio = 0.9f, - stiffness = 400f, - ), - ) + fadeIn(animationSpec = tween(250)) - }, - exitTransition = { - slideOutHorizontally( - targetOffsetX = { it }, - animationSpec = - spring( - dampingRatio = 0.9f, - stiffness = 400f, - ), - ) + fadeOut(animationSpec = tween(150)) - }, + "add_participants/{circleId}", + enterTransition = { + slideInHorizontally( + initialOffsetX = { it }, + animationSpec = + spring( + dampingRatio = 0.9f, + stiffness = 400f, + ), + ) + fadeIn(animationSpec = tween(250)) + }, + exitTransition = { + slideOutHorizontally( + targetOffsetX = { it }, + animationSpec = + spring( + dampingRatio = 0.9f, + stiffness = 400f, + ), + ) + fadeOut(animationSpec = tween(150)) + }, ) { backStackEntry -> val circleId = backStackEntry.arguments?.getString("circleId") ?: "" AddParticipantsScreenContent( - navController = navController, - circleId = circleId, + navController = navController, + circleId = circleId, ) } composable( - "course_page/{courseTitle}/{courseCode}", - enterTransition = { - slideInHorizontally( - initialOffsetX = { it }, - animationSpec = - spring( - dampingRatio = 0.9f, - stiffness = 400f, - ), - ) + fadeIn(animationSpec = tween(250)) - }, - exitTransition = { - slideOutHorizontally( - targetOffsetX = { it }, - animationSpec = - spring( - dampingRatio = 0.9f, - stiffness = 400f, - ), - ) + fadeOut(animationSpec = tween(150)) - }, + "course_page/{courseTitle}/{courseCode}", + enterTransition = { + slideInHorizontally( + initialOffsetX = { it }, + animationSpec = + spring( + dampingRatio = 0.9f, + stiffness = 400f, + ), + ) + fadeIn(animationSpec = tween(250)) + }, + exitTransition = { + slideOutHorizontally( + targetOffsetX = { it }, + animationSpec = + spring( + dampingRatio = 0.9f, + stiffness = 400f, + ), + ) + fadeOut(animationSpec = tween(150)) + }, ) { backStackEntry -> val encodedCourseTitle = - backStackEntry.arguments?.getString("courseTitle") ?: "" + backStackEntry.arguments?.getString("courseTitle") ?: "" val encodedCourseCode = - backStackEntry.arguments?.getString("courseCode") ?: "" + backStackEntry.arguments?.getString("courseCode") ?: "" val courseTitle = - URLDecoder.decode(encodedCourseTitle, StandardCharsets.UTF_8.toString()) + URLDecoder.decode( + encodedCourseTitle, + StandardCharsets.UTF_8.toString() + ) val courseCode = - URLDecoder.decode(encodedCourseCode, StandardCharsets.UTF_8.toString()) + URLDecoder.decode( + encodedCourseCode, + StandardCharsets.UTF_8.toString() + ) CoursePageContent( - courseTitle = courseTitle, - courseCode = courseCode, - onBackClick = { - navController.popBackStack() - }, - onNavigateToNote = { courseCodeParam, noteId, _ -> - val encodedCourseCodeParam = - URLEncoder.encode( - courseCodeParam, - StandardCharsets.UTF_8.toString(), + courseTitle = courseTitle, + courseCode = courseCode, + onBackClick = { navController.popBackStack() }, + onNavigateToNote = { courseCodeParam, noteId, _ -> + val encodedCourseCodeParam = + URLEncoder.encode( + courseCodeParam, + StandardCharsets.UTF_8.toString(), + ) + val encodedNoteId = + noteId?.let { + URLEncoder.encode( + it, + StandardCharsets.UTF_8.toString(), + ) + } + ?: "new" + navController.navigate( + "note_screen/$encodedCourseCodeParam/$encodedNoteId" ) - val encodedNoteId = - noteId?.let { - URLEncoder.encode( - it, - StandardCharsets.UTF_8.toString(), - ) - } ?: "new" - navController.navigate("note_screen/$encodedCourseCodeParam/$encodedNoteId") - }, + }, ) } composable( - "note_screen/{courseCode}/{noteId}", - enterTransition = { - slideInHorizontally( - initialOffsetX = { it }, - animationSpec = - spring( - dampingRatio = 0.9f, - stiffness = 400f, - ), - ) + fadeIn(animationSpec = tween(250)) - }, - exitTransition = { - slideOutHorizontally( - targetOffsetX = { it }, - animationSpec = - spring( - dampingRatio = 0.9f, - stiffness = 400f, - ), - ) + fadeOut(animationSpec = tween(150)) - }, + "note_screen/{courseCode}/{noteId}", + enterTransition = { + slideInHorizontally( + initialOffsetX = { it }, + animationSpec = + spring( + dampingRatio = 0.9f, + stiffness = 400f, + ), + ) + fadeIn(animationSpec = tween(250)) + }, + exitTransition = { + slideOutHorizontally( + targetOffsetX = { it }, + animationSpec = + spring( + dampingRatio = 0.9f, + stiffness = 400f, + ), + ) + fadeOut(animationSpec = tween(150)) + }, ) { backStackEntry -> val encodedCourseCode = - backStackEntry.arguments?.getString("courseCode") ?: "" + backStackEntry.arguments?.getString("courseCode") ?: "" val encodedNoteId = backStackEntry.arguments?.getString("noteId") ?: "new" val courseCode = - URLDecoder.decode(encodedCourseCode, StandardCharsets.UTF_8.toString()) - val noteId = - if (encodedNoteId == "new") { - null - } else { URLDecoder.decode( - encodedNoteId, - StandardCharsets.UTF_8.toString(), + encodedCourseCode, + StandardCharsets.UTF_8.toString() ) - } + val noteId = + if (encodedNoteId == "new") { + null + } else { + URLDecoder.decode( + encodedNoteId, + StandardCharsets.UTF_8.toString(), + ) + } val viewModel: CoursePageViewModel = viewModel() var noteToEdit by remember { mutableStateOf(null) } @@ -561,239 +597,259 @@ fun MainComposeApp() { } NoteScreenContent( - onBackClick = { - navController.popBackStack() - }, - noteToEdit = noteToEdit, - onSaveNote = { title, content -> - viewModel.setCourseId(courseCode) - if (noteToEdit != null) { - viewModel.updateExistingNote( - noteId = noteToEdit!!.id.toString(), - title = title, - content = content, - isStarred = noteToEdit!!.isStarred, - ) - } else { - viewModel.addTextNote(title, content) - } - navController.popBackStack() - }, + onBackClick = { navController.popBackStack() }, + noteToEdit = noteToEdit, + onSaveNote = { title, content -> + viewModel.setCourseId(courseCode) + if (noteToEdit != null) { + viewModel.updateExistingNote( + noteId = noteToEdit!!.id.toString(), + title = title, + content = content, + isStarred = noteToEdit!!.isStarred, + ) + } else { + viewModel.addTextNote(title, content) + } + navController.popBackStack() + }, ) } composable( - "empty_classrooms", - enterTransition = { - slideInHorizontally( - initialOffsetX = { it }, - animationSpec = - spring( - dampingRatio = 0.9f, - stiffness = 400f, - ), - ) + fadeIn(animationSpec = tween(250)) - }, - exitTransition = { - slideOutHorizontally( - targetOffsetX = { it }, - animationSpec = - spring( - dampingRatio = 0.9f, - stiffness = 400f, - ), - ) + fadeOut(animationSpec = tween(150)) - }, + "empty_classrooms", + enterTransition = { + slideInHorizontally( + initialOffsetX = { it }, + animationSpec = + spring( + dampingRatio = 0.9f, + stiffness = 400f, + ), + ) + fadeIn(animationSpec = tween(250)) + }, + exitTransition = { + slideOutHorizontally( + targetOffsetX = { it }, + animationSpec = + spring( + dampingRatio = 0.9f, + stiffness = 400f, + ), + ) + fadeOut(animationSpec = tween(150)) + }, ) { EmptyClassroomsContent( - onBackClick = { - navController.popBackStack() - }, + onBackClick = { navController.popBackStack() }, ) } composable( - "circle_detail/{circleData}/{circleMembersData}", - enterTransition = { - slideInHorizontally( - initialOffsetX = { it }, - animationSpec = - spring( - dampingRatio = 0.9f, - stiffness = 400f, - ), - ) + fadeIn(animationSpec = tween(250)) - }, - exitTransition = { - slideOutHorizontally( - targetOffsetX = { it }, - animationSpec = - spring( - dampingRatio = 0.9f, - stiffness = 400f, - ), - ) + fadeOut(animationSpec = tween(150)) - }, + "circle_detail/{circleData}/{circleMembersData}", + enterTransition = { + slideInHorizontally( + initialOffsetX = { it }, + animationSpec = + spring( + dampingRatio = 0.9f, + stiffness = 400f, + ), + ) + fadeIn(animationSpec = tween(250)) + }, + exitTransition = { + slideOutHorizontally( + targetOffsetX = { it }, + animationSpec = + spring( + dampingRatio = 0.9f, + stiffness = 400f, + ), + ) + fadeOut(animationSpec = tween(150)) + }, ) { backStackEntry -> val encodedCircleData = - backStackEntry.arguments?.getString("circleData") ?: "" - val encodedCircleMembersData = backStackEntry.arguments?.getString("circleMembersData") - val circleData = URLDecoder.decode(encodedCircleData, StandardCharsets.UTF_8.toString()) - val circleMembersData = URLDecoder.decode(encodedCircleMembersData, StandardCharsets.UTF_8.toString()) + backStackEntry.arguments?.getString("circleData") ?: "" + val encodedCircleMembersData = + backStackEntry.arguments?.getString("circleMembersData") + val circleData = + URLDecoder.decode( + encodedCircleData, + StandardCharsets.UTF_8.toString() + ) + val circleMembersData = + URLDecoder.decode( + encodedCircleMembersData, + StandardCharsets.UTF_8.toString() + ) val circle = - try { - Gson().fromJson(circleData, com.dscvit.vitty.network.api.community.responses.user.CircleItem::class.java) - } catch (e: Exception) { - null - } + try { + Gson().fromJson( + circleData, + com.dscvit.vitty.network.api.community.responses + .user.CircleItem::class + .java + ) + } catch (e: Exception) { + null + } val circleMembers = - try { - Gson().fromJson( - circleMembersData, - com.dscvit.vitty.network.api.community.responses.user.FriendResponse::class.java, - ) - } catch (e: Exception) { - null - } + try { + Gson().fromJson( + circleMembersData, + com.dscvit.vitty.network.api.community.responses + .user.FriendResponse::class + .java, + ) + } catch (e: Exception) { + null + } if (circle != null) { CircleDetailScreenContent( - circle = circle, - circleMembers = circleMembers, - onBackClick = { - navController.popBackStack() - }, - onMemberClick = { member, circleId -> - val memberJson = Gson().toJson(member) - val encodedMemberData = URLEncoder.encode(memberJson, StandardCharsets.UTF_8.toString()) - navController.navigate("circle_member_detail/$encodedMemberData/$circleId") - }, - onAddParticipantsClick = { circleId: String -> - navController.navigate("add_participants/$circleId") - }, - connectViewModel = connectViewModel, + circle = circle, + circleMembers = circleMembers, + onBackClick = { navController.popBackStack() }, + onMemberClick = { member, circleId -> + val memberJson = Gson().toJson(member) + val encodedMemberData = + URLEncoder.encode( + memberJson, + StandardCharsets.UTF_8.toString() + ) + navController.navigate( + "circle_member_detail/$encodedMemberData/$circleId" + ) + }, + onAddParticipantsClick = { circleId: String -> + navController.navigate("add_participants/$circleId") + }, + connectViewModel = connectViewModel, ) } else { - LaunchedEffect(Unit) { - navController.popBackStack() - } + LaunchedEffect(Unit) { navController.popBackStack() } } } composable( - "circle_member_detail/{memberData}/{circleId}", - enterTransition = { - slideInHorizontally( - initialOffsetX = { it }, - animationSpec = - spring( - dampingRatio = 0.9f, - stiffness = 400f, - ), - ) + fadeIn(animationSpec = tween(250)) - }, - exitTransition = { - slideOutHorizontally( - targetOffsetX = { it }, - animationSpec = - spring( - dampingRatio = 0.9f, - stiffness = 400f, - ), - ) + fadeOut(animationSpec = tween(150)) - }, + "circle_member_detail/{memberData}/{circleId}", + enterTransition = { + slideInHorizontally( + initialOffsetX = { it }, + animationSpec = + spring( + dampingRatio = 0.9f, + stiffness = 400f, + ), + ) + fadeIn(animationSpec = tween(250)) + }, + exitTransition = { + slideOutHorizontally( + targetOffsetX = { it }, + animationSpec = + spring( + dampingRatio = 0.9f, + stiffness = 400f, + ), + ) + fadeOut(animationSpec = tween(150)) + }, ) { backStackEntry -> val encodedMemberData = - backStackEntry.arguments?.getString("memberData") ?: "" + backStackEntry.arguments?.getString("memberData") ?: "" val circleId = backStackEntry.arguments?.getString("circleId") ?: "" - val memberData = URLDecoder.decode(encodedMemberData, StandardCharsets.UTF_8.toString()) + val memberData = + URLDecoder.decode( + encodedMemberData, + StandardCharsets.UTF_8.toString() + ) val member = - try { - Gson().fromJson(memberData, UserResponse::class.java) - } catch (e: Exception) { - null - } + try { + Gson().fromJson(memberData, UserResponse::class.java) + } catch (e: Exception) { + null + } if (member != null && circleId.isNotEmpty()) { CircleMemberDetailScreenContent( - member = member, - circleId = circleId, - onBackClick = { - navController.popBackStack() - }, + member = member, + circleId = circleId, + onBackClick = { navController.popBackStack() }, ) } else { - LaunchedEffect(Unit) { - navController.popBackStack() - } + LaunchedEffect(Unit) { navController.popBackStack() } } } } AnimatedVisibility( - visible = bottomNavVisible, - enter = - slideInVertically( - initialOffsetY = { it }, - animationSpec = - spring( - dampingRatio = 0.8f, - stiffness = 300f, - ), - ) + - fadeIn( - animationSpec = - spring( - dampingRatio = 0.8f, - stiffness = 400f, - ), - ) + - scaleIn( - initialScale = 0.6f, - animationSpec = - spring( - dampingRatio = 0.7f, - stiffness = 350f, - ), - ), - exit = - slideOutVertically( - targetOffsetY = { it }, - animationSpec = - spring( - dampingRatio = 0.9f, - stiffness = 500f, - ), - ) + - fadeOut( - animationSpec = - spring( - dampingRatio = 1.0f, - stiffness = 600f, - ), - ) + - scaleOut( - targetScale = 0.6f, - animationSpec = - spring( - dampingRatio = 0.8f, - stiffness = 500f, - ), - ), - modifier = Modifier.align(Alignment.BottomCenter), + visible = bottomNavVisible, + enter = + slideInVertically( + initialOffsetY = { it }, + animationSpec = + spring( + dampingRatio = 0.8f, + stiffness = 300f, + ), + ) + + fadeIn( + animationSpec = + spring( + dampingRatio = 0.8f, + stiffness = 400f, + ), + ) + + scaleIn( + initialScale = 0.6f, + animationSpec = + spring( + dampingRatio = 0.7f, + stiffness = 350f, + ), + ), + exit = + slideOutVertically( + targetOffsetY = { it }, + animationSpec = + spring( + dampingRatio = 0.9f, + stiffness = 500f, + ), + ) + + fadeOut( + animationSpec = + spring( + dampingRatio = 1.0f, + stiffness = 600f, + ), + ) + + scaleOut( + targetScale = 0.6f, + animationSpec = + spring( + dampingRatio = 0.8f, + stiffness = 500f, + ), + ), + modifier = Modifier.align(Alignment.BottomCenter), ) { BottomNavigationBar( - currentRoute = currentRoute, - onDestinationClick = { route -> - navController.navigate(route) { - popUpTo(navController.graph.startDestinationId) { - saveState = true + currentRoute = currentRoute, + onDestinationClick = { route -> + navController.navigate(route) { + popUpTo(navController.graph.startDestinationId) { + saveState = true + } + launchSingleTop = true + restoreState = true } - launchSingleTop = true - restoreState = true - } - }, - modifier = Modifier.padding(bottom = 48.dp), + }, + modifier = Modifier.padding(bottom = 48.dp), + ) + } + + if (showCampusDialog) { + CampusUpdateDialog( + onDismiss = { showCampusDialog = false }, ) } } @@ -803,8 +859,10 @@ fun MainComposeApp() { @Composable fun AcademicsComposeScreen( - navController: NavHostController, - onOpenDrawer: () -> Unit, + navController: NavHostController, + onOpenDrawer: () -> Unit, + academicsViewModel: AcademicsViewModel, + coursePageViewModel: CoursePageViewModel, ) { val context = LocalContext.current val prefs = remember { context.getSharedPreferences(Constants.USER_INFO, 0) } @@ -834,10 +892,7 @@ fun AcademicsComposeScreen( LaunchedEffect(userResponse) { userResponse?.let { response -> - allCourses = - withContext(Dispatchers.Default) { - extractCoursesFromTimetable(response) - } + allCourses = withContext(Dispatchers.Default) { extractCoursesFromTimetable(response) } isLoading = false } } @@ -845,26 +900,28 @@ fun AcademicsComposeScreen( val profilePictureUrl = remember { prefs.getString(Constants.COMMUNITY_PICTURE, null) } AcademicsScreenContent( - profilePictureUrl = profilePictureUrl, - allCourses = allCourses, - onCourseClick = { course -> - val encodedTitle = URLEncoder.encode(course.title, StandardCharsets.UTF_8.toString()) - val encodedCode = URLEncoder.encode(course.code, StandardCharsets.UTF_8.toString()) - navController.navigate("course_page/$encodedTitle/$encodedCode") - }, - onOpenDrawer = onOpenDrawer, + profilePictureUrl = profilePictureUrl, + allCourses = allCourses, + onCourseClick = { course -> + val encodedTitle = + URLEncoder.encode(course.title, StandardCharsets.UTF_8.toString()) + val encodedCode = URLEncoder.encode(course.code, StandardCharsets.UTF_8.toString()) + navController.navigate("course_page/$encodedTitle/$encodedCode") + }, + onOpenDrawer = onOpenDrawer, + academicsViewModel = academicsViewModel, + coursePageViewModel = coursePageViewModel, ) } @Composable fun ScheduleComposeScreen( - onOpenDrawer: () -> Unit, - navController: NavHostController, + onOpenDrawer: () -> Unit, + navController: NavHostController, ) { ScheduleScreenContent( - onOpenDrawer = onOpenDrawer, - onCardClick = - { title: String, code: String -> + onOpenDrawer = onOpenDrawer, + onCardClick = { title: String, code: String -> val encodedTitle = URLEncoder.encode(title, StandardCharsets.UTF_8.toString()) val encodedCode = URLEncoder.encode(code, StandardCharsets.UTF_8.toString()) navController.navigate("course_page/$encodedTitle/$encodedCode") @@ -874,98 +931,86 @@ fun ScheduleComposeScreen( @Composable fun ConnectComposeScreen( - navController: NavHostController, - connectViewModel: ConnectViewModel, + navController: NavHostController, + connectViewModel: ConnectViewModel, ) { ConnectScreenContent( - onSearchClick = { - navController.navigate("add_friend") - }, - onFriendClick = { friend -> - val friendJson = Gson().toJson(friend) - val encodedFriendData = URLEncoder.encode(friendJson, StandardCharsets.UTF_8.toString()) - navController.navigate("friend_detail/$encodedFriendData") - }, - onCircleClick = { circle, circleMembers -> - val circleJson = Gson().toJson(circle) - val encodedCircleData = URLEncoder.encode(circleJson, StandardCharsets.UTF_8.toString()) - val encodedCircleMembers = - URLEncoder.encode( - Gson().toJson(circleMembers), - StandardCharsets.UTF_8.toString(), - ) + onSearchClick = { navController.navigate("add_friend") }, + onFriendClick = { friend -> + val friendJson = Gson().toJson(friend) + val encodedFriendData = + URLEncoder.encode(friendJson, StandardCharsets.UTF_8.toString()) + navController.navigate("friend_detail/$encodedFriendData") + }, + onCircleClick = { circle, circleMembers -> + val circleJson = Gson().toJson(circle) + val encodedCircleData = + URLEncoder.encode(circleJson, StandardCharsets.UTF_8.toString()) + val encodedCircleMembers = + URLEncoder.encode( + Gson().toJson(circleMembers), + StandardCharsets.UTF_8.toString(), + ) - navController.navigate("circle_detail/$encodedCircleData/$encodedCircleMembers") - }, - onFriendRequestsClick = { - navController.navigate("friend_requests") - }, - onCircleRequestsClick = { - navController.navigate("circle_requests") - }, - connectViewModel = connectViewModel, + navController.navigate("circle_detail/$encodedCircleData/$encodedCircleMembers") + }, + onFriendRequestsClick = { navController.navigate("friend_requests") }, + onCircleRequestsClick = { navController.navigate("circle_requests") }, + connectViewModel = connectViewModel, ) } @Composable fun BottomNavigationBar( - currentRoute: String?, - onDestinationClick: (String) -> Unit, - modifier: Modifier = Modifier, + currentRoute: String?, + onDestinationClick: (String) -> Unit, + modifier: Modifier = Modifier, ) { - val cardScale by animateFloatAsState( - targetValue = 1.0f, - animationSpec = - spring( - dampingRatio = 0.6f, - stiffness = 200f, - ), - label = "cardScale", - ) + val cardScale by + animateFloatAsState( + targetValue = 1.0f, + animationSpec = + spring( + dampingRatio = 0.6f, + stiffness = 200f, + ), + label = "cardScale", + ) Card( - modifier = - modifier - .padding(horizontal = 16.dp) - .scale(cardScale), - shape = RoundedCornerShape(120.dp), - elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), - colors = - CardDefaults.cardColors( - containerColor = Background, - ), - border = BorderStroke(width = 1.dp, color = Secondary), + modifier = modifier.padding(horizontal = 16.dp).scale(cardScale), + shape = RoundedCornerShape(120.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), + colors = + CardDefaults.cardColors( + containerColor = Background, + ), + border = BorderStroke(width = 1.dp, color = Secondary), ) { Row( - modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, ) { NavigationItem( - icon = R.drawable.ic_academics_outline, - text = "Academics", - isSelected = currentRoute == "academics", - onClick = { - onDestinationClick("academics") - }, + icon = R.drawable.ic_academics_outline, + text = "Academics", + isSelected = currentRoute == "academics", + onClick = { onDestinationClick("academics") }, ) NavigationItem( - icon = R.drawable.ic_timetable_outline, - text = "Schedule", - isSelected = currentRoute == "schedule", - onClick = { - onDestinationClick("schedule") - }, + icon = R.drawable.ic_timetable_outline, + text = "Schedule", + isSelected = currentRoute == "schedule", + onClick = { onDestinationClick("schedule") }, ) NavigationItem( - icon = R.drawable.ic_community_outline, - text = "Connect", - isSelected = currentRoute == "connect", - onClick = { - onDestinationClick("connect") - }, + icon = R.drawable.ic_community_outline, + text = "Connect", + isSelected = currentRoute == "connect", + onClick = { onDestinationClick("connect") }, ) } } @@ -973,57 +1018,57 @@ fun BottomNavigationBar( @Composable fun NavigationItem( - icon: Int, - text: String, - isSelected: Boolean, - onClick: () -> Unit, - modifier: Modifier = Modifier, + icon: Int, + text: String, + isSelected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, ) { - val scale by animateFloatAsState( - targetValue = if (isSelected) 1.0f else 0.95f, - animationSpec = tween(150), - label = "scale", - ) + val scale by + animateFloatAsState( + targetValue = if (isSelected) 1.0f else 0.95f, + animationSpec = tween(150), + label = "scale", + ) val backgroundColor = - if (isSelected) { - Secondary - } else { - Color.Transparent - } + if (isSelected) { + Secondary + } else { + Color.Transparent + } Row( - modifier = - modifier - .scale(scale) - .clip(RoundedCornerShape(120.dp)) - .background(backgroundColor) - .clickable( - indication = null, - interactionSource = remember { MutableInteractionSource() }, - ) { onClick() } - .padding(horizontal = 20.dp, vertical = 14.dp), - horizontalArrangement = Arrangement.spacedBy(6.dp), - verticalAlignment = Alignment.CenterVertically, + modifier = + modifier.scale(scale) + .clip(RoundedCornerShape(120.dp)) + .background(backgroundColor) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + ) { onClick() } + .padding(horizontal = 20.dp, vertical = 14.dp), + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalAlignment = Alignment.CenterVertically, ) { Icon( - painter = painterResource(id = icon), - contentDescription = text, - tint = TextColor, - modifier = Modifier.size(24.dp), + painter = painterResource(id = icon), + contentDescription = text, + tint = TextColor, + modifier = Modifier.size(24.dp), ) AnimatedVisibility( - visible = isSelected, - enter = fadeIn(animationSpec = tween(150)), - exit = fadeOut(animationSpec = tween(100)), + visible = isSelected, + enter = fadeIn(animationSpec = tween(150)), + exit = fadeOut(animationSpec = tween(100)), ) { Text( - text = text, - color = TextColor, - fontSize = 16.sp, - fontWeight = FontWeight.Medium, - maxLines = 1, + text = text, + color = TextColor, + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + maxLines = 1, ) } } @@ -1031,460 +1076,493 @@ fun NavigationItem( @Composable fun DrawerContent( - navController: NavHostController, - onCloseDrawer: () -> Unit, + navController: NavHostController, + onCloseDrawer: () -> Unit, ) { val context = LocalContext.current val prefs = remember { context.getSharedPreferences(Constants.USER_INFO, 0) } val profilePictureUrl = remember { prefs.getString(Constants.COMMUNITY_PICTURE, "") } val username = remember { prefs.getString(Constants.COMMUNITY_USERNAME, "") ?: "User" } val name = remember { prefs.getString(Constants.COMMUNITY_NAME, "") ?: "Name" } - val campus = remember { prefs.getString(Constants.COMMUNITY_CAMPUS, "") ?: "Campus" } - var isGhostModeEnabled by remember { mutableStateOf(prefs.getBoolean(Constants.GHOST_MODE, false)) } + var campus by remember { mutableStateOf(prefs.getString(Constants.COMMUNITY_CAMPUS, "") ?: "") } + var isGhostModeEnabled by remember { + mutableStateOf(prefs.getBoolean(Constants.GHOST_MODE, false)) + } DisposableEffect(Unit) { val listener = - SharedPreferences.OnSharedPreferenceChangeListener { _, key -> - if (key == Constants.GHOST_MODE) { - isGhostModeEnabled = prefs.getBoolean(Constants.GHOST_MODE, false) + SharedPreferences.OnSharedPreferenceChangeListener { _, key -> + when (key) { + Constants.GHOST_MODE -> { + isGhostModeEnabled = prefs.getBoolean(Constants.GHOST_MODE, false) + } + Constants.COMMUNITY_CAMPUS -> { + campus = prefs.getString(Constants.COMMUNITY_CAMPUS, "") ?: "" + } + } } - } prefs.registerOnSharedPreferenceChangeListener(listener) - onDispose { - prefs.unregisterOnSharedPreferenceChangeListener(listener) - } + onDispose { prefs.unregisterOnSharedPreferenceChangeListener(listener) } } ModalDrawerSheet( - drawerContainerColor = Secondary, - drawerContentColor = TextColor, - modifier = Modifier.fillMaxWidth(0.8f), + drawerContainerColor = Secondary, + drawerContentColor = TextColor, + modifier = Modifier.fillMaxWidth(0.8f), ) { Column( - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 26.dp, vertical = 28.dp), + modifier = Modifier.fillMaxWidth().padding(horizontal = 26.dp, vertical = 28.dp), ) { Row( - modifier = - Modifier - .fillMaxWidth() - .padding(top = 20.dp), - verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth().padding(top = 20.dp), + verticalAlignment = Alignment.CenterVertically, ) { AsyncImage( - model = profilePictureUrl, - contentDescription = "Profile Picture", - modifier = - Modifier - .size(52.dp) - .clip(CircleShape), - placeholder = painterResource(R.drawable.ic_gdscvit), - error = painterResource(R.drawable.ic_gdscvit), + model = profilePictureUrl, + contentDescription = "Profile Picture", + modifier = Modifier.size(52.dp).clip(CircleShape), + placeholder = painterResource(R.drawable.ic_gdscvit), + error = painterResource(R.drawable.ic_gdscvit), ) } Spacer(modifier = Modifier.height(4.dp)) Text( - text = name, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = - MaterialTheme.typography.labelLarge.copy( - fontWeight = FontWeight.Normal, - ), + text = name, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = + MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.Normal, + ), ) Spacer(modifier = Modifier.height(4.dp)) Text( - text = "@$username", - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = - TextStyle( - fontSize = 14.sp, - lineHeight = 20.sp, - fontWeight = FontWeight.Normal, - letterSpacing = (-.14).sp, - color = Accent, - ), + text = "@$username", + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = + TextStyle( + fontSize = 14.sp, + lineHeight = 20.sp, + fontWeight = FontWeight.Normal, + letterSpacing = (-.14).sp, + color = Accent, + ), ) Spacer(modifier = Modifier.height(32.dp)) HorizontalDivider( - color = Accent, - thickness = 1.dp, + color = Accent, + thickness = 1.dp, ) Spacer(modifier = Modifier.height(24.dp)) if (campus.lowercase() == "vellore") { NavigationDrawerItem( + icon = { + Icon( + painter = painterResource(R.drawable.ic_empty_classroom), + contentDescription = "Find Empty Classroom", + tint = TextColor, + ) + }, + label = { + Text( + modifier = Modifier.padding(start = 24.dp), + text = "Find Empty Classroom", + color = TextColor, + style = + MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.Normal, + ), + ) + }, + selected = false, + onClick = { + onCloseDrawer() + navController.navigate("empty_classrooms") + }, + colors = + NavigationDrawerItemDefaults.colors( + unselectedContainerColor = Color.Transparent, + selectedContainerColor = Secondary, + ), + ) + } + + NavigationDrawerItem( icon = { Icon( - painter = painterResource(R.drawable.ic_empty_classroom), - contentDescription = "Find Empty Classroom", - tint = TextColor, + painter = painterResource(R.drawable.ic_settings), + contentDescription = "Settings", + tint = TextColor, ) }, label = { Text( - modifier = Modifier.padding(start = 24.dp), - text = "Find Empty Classroom", - color = TextColor, - style = - MaterialTheme.typography.labelLarge.copy( - fontWeight = FontWeight.Normal, - ), + modifier = Modifier.padding(start = 24.dp), + style = + MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.Normal, + ), + text = "Settings", + color = TextColor, ) }, selected = false, onClick = { onCloseDrawer() - navController.navigate("empty_classrooms") + context.startActivity(Intent(context, SettingsActivity::class.java)) }, colors = - NavigationDrawerItemDefaults.colors( - unselectedContainerColor = Color.Transparent, - selectedContainerColor = Secondary, - ), - ) - } - - NavigationDrawerItem( - icon = { - Icon( - painter = painterResource(R.drawable.ic_settings), - contentDescription = "Settings", - tint = TextColor, - ) - }, - label = { - Text( - modifier = Modifier.padding(start = 24.dp), - style = - MaterialTheme.typography.labelLarge.copy( - fontWeight = FontWeight.Normal, + NavigationDrawerItemDefaults.colors( + unselectedContainerColor = Color.Transparent, + selectedContainerColor = Secondary, ), - text = "Settings", - color = TextColor, - ) - }, - selected = false, - onClick = { - onCloseDrawer() - context.startActivity(Intent(context, SettingsActivity::class.java)) - }, - colors = - NavigationDrawerItemDefaults.colors( - unselectedContainerColor = Color.Transparent, - selectedContainerColor = Secondary, - ), ) Spacer(modifier = Modifier.height(24.dp)) HorizontalDivider( - color = Accent, - thickness = 1.dp, + color = Accent, + thickness = 1.dp, ) Spacer(modifier = Modifier.height(24.dp)) NavigationDrawerItem( - icon = { - Icon( - painter = painterResource(R.drawable.ic_share), - contentDescription = "Share", - tint = TextColor, - ) - }, - label = { - Text( - modifier = Modifier.padding(start = 24.dp), - style = - MaterialTheme.typography.labelLarge.copy( - fontWeight = FontWeight.Normal, + icon = { + Icon( + painter = painterResource(R.drawable.ic_share), + contentDescription = "Share", + tint = TextColor, + ) + }, + label = { + Text( + modifier = Modifier.padding(start = 24.dp), + style = + MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.Normal, + ), + text = "Share", + color = TextColor, + ) + }, + selected = false, + onClick = { + onCloseDrawer() + val shareIntent = + Intent().apply { + action = Intent.ACTION_SEND + type = "text/plain" + putExtra( + Intent.EXTRA_TEXT, + context.getString(R.string.share_text) + ) + } + context.startActivity(Intent.createChooser(shareIntent, null)) + }, + colors = + NavigationDrawerItemDefaults.colors( + unselectedContainerColor = Color.Transparent, + selectedContainerColor = Secondary, ), - text = "Share", - color = TextColor, - ) - }, - selected = false, - onClick = { - onCloseDrawer() - val shareIntent = - Intent().apply { - action = Intent.ACTION_SEND - type = "text/plain" - putExtra(Intent.EXTRA_TEXT, context.getString(R.string.share_text)) - } - context.startActivity(Intent.createChooser(shareIntent, null)) - }, - colors = - NavigationDrawerItemDefaults.colors( - unselectedContainerColor = Color.Transparent, - selectedContainerColor = Secondary, - ), ) NavigationDrawerItem( - icon = { - Icon( - painter = painterResource(R.drawable.ic_support), - contentDescription = "Support", - tint = TextColor, - ) - }, - label = { - Text( - modifier = Modifier.padding(start = 24.dp), - style = - MaterialTheme.typography.labelLarge.copy( - fontWeight = FontWeight.Normal, + icon = { + Icon( + painter = painterResource(R.drawable.ic_support), + contentDescription = "Support", + tint = TextColor, + ) + }, + label = { + Text( + modifier = Modifier.padding(start = 24.dp), + style = + MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.Normal, + ), + text = "Support", + color = TextColor, + ) + }, + selected = false, + onClick = { + onCloseDrawer() + UtilFunctions.openLink(context, context.getString(R.string.telegram_link)) + }, + colors = + NavigationDrawerItemDefaults.colors( + unselectedContainerColor = Color.Transparent, + selectedContainerColor = Secondary, ), - text = "Support", - color = TextColor, - ) - }, - selected = false, - onClick = { - onCloseDrawer() - UtilFunctions.openLink(context, context.getString(R.string.telegram_link)) - }, - colors = - NavigationDrawerItemDefaults.colors( - unselectedContainerColor = Color.Transparent, - selectedContainerColor = Secondary, - ), ) Spacer(modifier = Modifier.height(24.dp)) HorizontalDivider( - color = Accent, - thickness = 1.dp, + color = Accent, + thickness = 1.dp, ) Spacer(modifier = Modifier.height(24.dp)) Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, ) { Column( - modifier = Modifier.weight(1f), + modifier = Modifier.weight(1f), ) { Text( - text = - buildAnnotatedString { - append("Ghost Mode ") - addStyle( - style = - SpanStyle( - fontWeight = FontWeight.Bold, - fontSize = 12.sp, - letterSpacing = (-0.12).sp, - color = Accent, - ), - start = 0, - end = "Ghost Mode".length, - ) + text = + buildAnnotatedString { + append("Ghost Mode ") + addStyle( + style = + SpanStyle( + fontWeight = FontWeight.Bold, + fontSize = 12.sp, + letterSpacing = (-0.12).sp, + color = Accent, + ), + start = 0, + end = "Ghost Mode".length, + ) - append("(your timetable will be visible only to you)") - addStyle( - style = - SpanStyle( - fontWeight = FontWeight.Medium, - fontSize = 12.sp, - letterSpacing = (-0.12).sp, - color = Accent, - ), - start = "Ghost Mode ".length, - end = "Ghost Mode (your timetable will be visible only to you)".length, - ) - }, + append("(your timetable will be visible only to you)") + addStyle( + style = + SpanStyle( + fontWeight = FontWeight.Medium, + fontSize = 12.sp, + letterSpacing = (-0.12).sp, + color = Accent, + ), + start = "Ghost Mode ".length, + end = + "Ghost Mode (your timetable will be visible only to you)".length, + ) + }, ) } Spacer(modifier = Modifier.width(16.dp)) Switch( - checked = isGhostModeEnabled, - onCheckedChange = { isChecked -> - val token = prefs.getString(Constants.COMMUNITY_TOKEN, "") ?: "" - val currentUsername = prefs.getString(Constants.COMMUNITY_USERNAME, "") ?: "" - - if (token.isNotEmpty() && currentUsername.isNotEmpty()) { - if (isChecked) { - APICommunityRestClient.instance.enableGhostMode( - token = token, - username = currentUsername, - retrofitUserActionListener = - object : RetrofitUserActionListener { - override fun onSuccess( - call: Call?, - response: PostResponse?, - ) { - isGhostModeEnabled = true - prefs.edit { putBoolean(Constants.GHOST_MODE, true) } - Toast.makeText(context, "Ghost mode enabled", Toast.LENGTH_SHORT).show() - } - - override fun onError( - call: Call?, - t: Throwable?, - ) { - isGhostModeEnabled = false - Toast.makeText(context, "Failed to enable ghost mode", Toast.LENGTH_SHORT).show() - } - }, - ) + checked = isGhostModeEnabled, + onCheckedChange = { isChecked -> + val token = prefs.getString(Constants.COMMUNITY_TOKEN, "") ?: "" + val currentUsername = + prefs.getString(Constants.COMMUNITY_USERNAME, "") ?: "" + + if (token.isNotEmpty() && currentUsername.isNotEmpty()) { + if (isChecked) { + APICommunityRestClient.instance.enableGhostMode( + token = token, + username = currentUsername, + retrofitUserActionListener = + object : RetrofitUserActionListener { + override fun onSuccess( + call: Call?, + response: PostResponse?, + ) { + isGhostModeEnabled = true + prefs.edit { + putBoolean( + Constants.GHOST_MODE, + true + ) + } + Toast.makeText( + context, + "Ghost mode enabled", + Toast.LENGTH_SHORT + ) + .show() + } + + override fun onError( + call: Call?, + t: Throwable?, + ) { + isGhostModeEnabled = false + Toast.makeText( + context, + "Failed to enable ghost mode", + Toast.LENGTH_SHORT + ) + .show() + } + }, + ) + } else { + APICommunityRestClient.instance.disableGhostMode( + token = token, + username = currentUsername, + retrofitUserActionListener = + object : RetrofitUserActionListener { + override fun onSuccess( + call: Call?, + response: PostResponse?, + ) { + isGhostModeEnabled = false + prefs.edit { + putBoolean( + Constants.GHOST_MODE, + false + ) + } + Toast.makeText( + context, + "Ghost mode disabled", + Toast.LENGTH_SHORT + ) + .show() + } + + override fun onError( + call: Call?, + t: Throwable?, + ) { + isGhostModeEnabled = true + Toast.makeText( + context, + "Failed to disable ghost mode", + Toast.LENGTH_SHORT + ) + .show() + } + }, + ) + } } else { - APICommunityRestClient.instance.disableGhostMode( - token = token, - username = currentUsername, - retrofitUserActionListener = - object : RetrofitUserActionListener { - override fun onSuccess( - call: Call?, - response: PostResponse?, - ) { - isGhostModeEnabled = false - prefs.edit { putBoolean(Constants.GHOST_MODE, false) } - Toast.makeText(context, "Ghost mode disabled", Toast.LENGTH_SHORT).show() - } - - override fun onError( - call: Call?, - t: Throwable?, - ) { - isGhostModeEnabled = true - Toast.makeText(context, "Failed to disable ghost mode", Toast.LENGTH_SHORT).show() - } - }, - ) + isGhostModeEnabled = isChecked + prefs.edit { putBoolean(Constants.GHOST_MODE, isChecked) } + Toast.makeText( + context, + "Ghost mode updated locally", + Toast.LENGTH_SHORT + ) + .show() } - } else { - isGhostModeEnabled = isChecked - prefs.edit { putBoolean(Constants.GHOST_MODE, isChecked) } - Toast.makeText(context, "Ghost mode updated locally", Toast.LENGTH_SHORT).show() - } - }, - colors = - SwitchDefaults.colors( - checkedThumbColor = Background, - checkedTrackColor = Accent, - uncheckedThumbColor = Color(0xff768EA4), - uncheckedTrackColor = Color(0x33475985), - uncheckedBorderColor = Color.Transparent, - ), + }, + colors = + SwitchDefaults.colors( + checkedThumbColor = Background, + checkedTrackColor = Accent, + uncheckedThumbColor = Color(0xff768EA4), + uncheckedTrackColor = Color(0x33475985), + uncheckedBorderColor = Color.Transparent, + ), ) } Spacer(modifier = Modifier.weight(1f)) NavigationDrawerItem( - icon = { - Icon( - painter = painterResource(R.drawable.ic_logout_2), - contentDescription = "log out", - tint = Color(0xffFF0000), - ) - }, - label = { - Text( - style = - MaterialTheme.typography.labelLarge.copy( - fontWeight = FontWeight.Normal, - ), - text = "log out", - color = Color(0xffFF0000), - ) - }, - selected = false, - onClick = { - onCloseDrawer() + icon = { + Icon( + painter = painterResource(R.drawable.ic_logout_2), + contentDescription = "log out", + tint = Color(0xffFF0000), + ) + }, + label = { + Text( + style = + MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.Normal, + ), + text = "log out", + color = Color(0xffFF0000), + ) + }, + selected = false, + onClick = { + onCloseDrawer() - val activity = context as? Activity - if (activity != null) { - LogoutHelper.logout(context, activity, prefs) - } - }, - colors = - NavigationDrawerItemDefaults.colors( - unselectedContainerColor = Color.Transparent, - selectedContainerColor = Secondary, - ), + val activity = context as? Activity + if (activity != null) { + LogoutHelper.logout(context, activity, prefs) + } + }, + colors = + NavigationDrawerItemDefaults.colors( + unselectedContainerColor = Color.Transparent, + selectedContainerColor = Secondary, + ), ) } } } -private suspend fun loadCachedCourses(prefs: android.content.SharedPreferences): List = - withContext(Dispatchers.IO) { - try { - val cachedData = - prefs.getString(Constants.CACHE_COMMUNITY_TIMETABLE, null) - ?: return@withContext emptyList() - - val userResponse = - Gson().fromJson(cachedData, UserResponse::class.java) - ?: return@withContext emptyList() - - extractCoursesFromTimetable(userResponse) - } catch (e: Exception) { - e.printStackTrace() - emptyList() +private suspend fun loadCachedCourses(prefs: SharedPreferences): List = + withContext(Dispatchers.IO) { + try { + val cachedData = + prefs.getString(Constants.CACHE_COMMUNITY_TIMETABLE, null) + ?: return@withContext emptyList() + + val userResponse = + Gson().fromJson(cachedData, UserResponse::class.java) + ?: return@withContext emptyList() + + extractCoursesFromTimetable(userResponse) + } catch (e: Exception) { + e.printStackTrace() + emptyList() + } } - } private fun extractCoursesFromTimetable(userResponse: UserResponse): List { val timetableData = userResponse.timetable?.data ?: return emptyList() val allLectures = - sequenceOf( - timetableData.Monday, - timetableData.Tuesday, - timetableData.Wednesday, - timetableData.Thursday, - timetableData.Friday, - timetableData.Saturday, - timetableData.Sunday, - ).filterNotNull().flatten() + sequenceOf( + timetableData.Monday, + timetableData.Tuesday, + timetableData.Wednesday, + timetableData.Thursday, + timetableData.Friday, + timetableData.Saturday, + timetableData.Sunday, + ) + .filterNotNull() + .flatten() val currentSemester = SemesterUtils.determineSemester() return allLectures - .groupBy { it.name } - .mapNotNull { (title, lectures) -> - if (title.isNullOrBlank()) return@mapNotNull null - - val uniqueSlots = - lectures - .mapTo(LinkedHashSet()) { it.slot } - .sorted() - .joinToString(" + ") - - val uniqueCodes = - lectures - .mapTo(LinkedHashSet()) { it.code } - .sorted() - .joinToString(" / ") - - Course( - title = title, - slot = uniqueSlots, - code = uniqueCodes, - semester = currentSemester, - isStarred = false, - ) - }.sortedBy { it.title } + .groupBy { it.name } + .mapNotNull { (title, lectures) -> + if (title.isBlank()) return@mapNotNull null + + val uniqueSlots = + lectures.mapTo(LinkedHashSet()) { it.slot }.sorted().joinToString(" + ") + + val uniqueCodes = + lectures.mapTo(LinkedHashSet()) { it.code }.sorted().joinToString(" / ") + + Course( + title = title, + slot = uniqueSlots, + code = uniqueCodes, + semester = currentSemester, + isStarred = false, + ) + } + .sortedBy { it.title } } diff --git a/app/src/main/java/com/dscvit/vitty/ui/settings/SettingsFragment.kt b/app/src/main/java/com/dscvit/vitty/ui/settings/SettingsFragment.kt index 49ea910..a6024d2 100755 --- a/app/src/main/java/com/dscvit/vitty/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/com/dscvit/vitty/ui/settings/SettingsFragment.kt @@ -9,6 +9,7 @@ import android.os.PowerManager import android.provider.Settings import android.view.View import android.widget.Toast +import androidx.core.net.toUri import androidx.preference.ListPreference import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat @@ -21,12 +22,18 @@ import com.dscvit.vitty.util.UtilFunctions.getSatModeCode import com.dscvit.vitty.util.UtilFunctions.reloadWidgets class SettingsFragment : PreferenceFragmentCompat() { - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + override fun onCreatePreferences( + savedInstanceState: Bundle?, + rootKey: String?, + ) { setPreferencesFromResource(R.xml.root_preferences, rootKey) setupPreferences() } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + override fun onViewCreated( + view: View, + savedInstanceState: Bundle?, + ) { super.onViewCreated(view, savedInstanceState) val rv = listView rv.setPadding(24, 0, 24, 0) @@ -56,9 +63,9 @@ class SettingsFragment : PreferenceFragmentCompat() { individualNotification?.setOnPreferenceClickListener { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val settingsIntent: Intent = - Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - .putExtra(Settings.EXTRA_APP_PACKAGE, context?.packageName) + Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .putExtra(Settings.EXTRA_APP_PACKAGE, context?.packageName) startActivity(settingsIntent) } true @@ -83,22 +90,24 @@ class SettingsFragment : PreferenceFragmentCompat() { val pm: PowerManager = context?.getSystemService(Context.POWER_SERVICE) as PowerManager if (pm.isIgnoringBatteryOptimizations(context?.packageName)) { batteryOptimization?.summary = - "Keep the battery optimizations turned off to receive notifications on time" + "Keep the battery optimizations turned off to receive notifications on time" } batteryOptimization?.setOnPreferenceClickListener { if (!pm.isIgnoringBatteryOptimizations(context?.packageName)) { Toast.makeText( - context, - "Please turn off the Battery Optimization Settings for VITTY to receive notifications on time.", - Toast.LENGTH_LONG - ).show() + context, + "Please turn off the Battery Optimization Settings for VITTY to receive notifications on time.", + Toast.LENGTH_LONG, + ) + .show() } else { Toast.makeText( - context, - "Please keep the Battery Optimization Settings for VITTY turned off to receive notifications on time.", - Toast.LENGTH_LONG - ).show() + context, + "Please keep the Battery Optimization Settings for VITTY turned off to receive notifications on time.", + Toast.LENGTH_LONG, + ) + .show() } val pmIntent = Intent() pmIntent.action = Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS @@ -107,15 +116,18 @@ class SettingsFragment : PreferenceFragmentCompat() { } } - private fun openWebsite(key: String, website: String) { + private fun openWebsite( + key: String, + website: String, + ) { val pref: Preference? = findPreference(key) pref?.setOnPreferenceClickListener { try { startActivity( - Intent( - Intent.ACTION_VIEW, - Uri.parse(website) - ) + Intent( + Intent.ACTION_VIEW, + Uri.parse(website), + ), ) true } catch (e: Exception) { @@ -127,8 +139,8 @@ class SettingsFragment : PreferenceFragmentCompat() { private fun setupAccountDetails() { val prefs = requireContext().getSharedPreferences(Constants.USER_INFO, 0) -// val cat: PreferenceCategory? = findPreference("account_title") -// cat?.title = prefs.getString("sign_in_method", "Google") + " Account Details" + // val cat: PreferenceCategory? = findPreference("account_title") + // cat?.title = prefs.getString("sign_in_method", "Google") + " Account Details" val account: Preference? = findPreference("account") account?.setOnPreferenceClickListener { LogoutHelper.logout(requireContext(), requireActivity(), prefs) @@ -142,6 +154,47 @@ class SettingsFragment : PreferenceFragmentCompat() { openWebsite(Constants.CHANGE_TIMETABLE, Constants.VITTY_URL) } + private fun setupSupportAndFeedback() { + + val supportEmail: Preference? = findPreference(Constants.SUPPORT_EMAIL) + supportEmail?.setOnPreferenceClickListener { + try { + val emailIntent = + Intent(Intent.ACTION_SENDTO).apply { + data = "mailto:".toUri() + putExtra(Intent.EXTRA_EMAIL, arrayOf(Constants.SUPPORT_EMAIL_ADDRESS)) + putExtra(Intent.EXTRA_SUBJECT, "Vitty App Support Request") + putExtra( + Intent.EXTRA_TEXT, + "Hi Vitty Support Team,\n\nI need help with:\n\n" + ) + } + startActivity(Intent.createChooser(emailIntent, "Send Email")) + true + } catch (e: Exception) { + + val clipboard = + requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as + android.content.ClipboardManager + val clip = + android.content.ClipData.newPlainText( + "Support Email", + Constants.SUPPORT_EMAIL_ADDRESS + ) + clipboard.setPrimaryClip(clip) + Toast.makeText( + context, + "Email address copied to clipboard: ${Constants.SUPPORT_EMAIL_ADDRESS}", + Toast.LENGTH_LONG + ) + .show() + false + } + } + + openWebsite(Constants.GITHUB_ISSUES, Constants.GITHUB_ISSUES_LINK) + } + private fun setupClass() { val prefs = requireContext().getSharedPreferences(Constants.USER_INFO, 0) val satClass: ListPreference? = findPreference("sat_mode") @@ -157,7 +210,7 @@ class SettingsFragment : PreferenceFragmentCompat() { val pref: Preference? = findPreference("refresh_widgets") pref?.setOnPreferenceClickListener { reloadWidgets(requireContext()) -// Toast.makeText(context, "Refreshed!", Toast.LENGTH_LONG).show() + // Toast.makeText(context, "Refreshed!", Toast.LENGTH_LONG).show() pref.isEnabled = false pref.isSelectable = false pref.summary = "Refreshed!" @@ -169,9 +222,10 @@ class SettingsFragment : PreferenceFragmentCompat() { setupAccountDetails() setupClass() setupNotifications() -// setupEffects() + // setupEffects() setupBattery() setupRefreshWidgets() setupAbout() + setupSupportAndFeedback() } } diff --git a/app/src/main/java/com/dscvit/vitty/util/Analytics.kt b/app/src/main/java/com/dscvit/vitty/util/Analytics.kt index 095141d..2556822 100755 --- a/app/src/main/java/com/dscvit/vitty/util/Analytics.kt +++ b/app/src/main/java/com/dscvit/vitty/util/Analytics.kt @@ -8,6 +8,7 @@ import java.util.Calendar object Analytics { private val firebaseAnalytics: FirebaseAnalytics = Firebase.analytics + fun share(packageName: String) { firebaseAnalytics.logEvent(FirebaseAnalytics.Event.SHARE) { param(FirebaseAnalytics.Param.CONTENT_TYPE, "text") @@ -31,4 +32,12 @@ object Analytics { param("block", block) } } + + fun appNavigation(screen: String) { + firebaseAnalytics.logEvent("app_navigation") { + param(FirebaseAnalytics.Param.CONTENT_TYPE, "text") + param(FirebaseAnalytics.Param.ITEM_ID, Calendar.getInstance().timeInMillis) + param("screen", screen) + } + } } diff --git a/app/src/main/java/com/dscvit/vitty/util/Constants.kt b/app/src/main/java/com/dscvit/vitty/util/Constants.kt index 6246bb2..559acae 100755 --- a/app/src/main/java/com/dscvit/vitty/util/Constants.kt +++ b/app/src/main/java/com/dscvit/vitty/util/Constants.kt @@ -42,6 +42,12 @@ object Constants { const val SAT_MODE = "sat_mode" const val CHANGE_TIMETABLE = "change_timetable" + // Support & Feedback + const val SUPPORT_EMAIL = "support_email" + const val SUPPORT_EMAIL_ADDRESS = "dscvit.vitty@gmail.com" + const val GITHUB_ISSUES = "github_issues" + const val GITHUB_ISSUES_LINK = "https://github.com/GDGVIT/vitty-app/issues" + const val COMMUNITY_TOKEN = "community_token" const val COMMUNITY_USERNAME = "community_username" const val COMMUNITY_NAME = "community_name" @@ -58,6 +64,8 @@ object Constants { const val REMINDER_CHANNEL_ID = "reminder_channel" const val REMINDER_CHANNEL_NAME = "Reminders" const val REMINDER_CHANNEL_DESC = "Notifications for course reminders" + + const val PREF_LAST_REVIEW_REQUEST = "last_review_request" } fun String.urlDecode(): String = diff --git a/app/src/main/java/com/dscvit/vitty/util/LogoutHelper.kt b/app/src/main/java/com/dscvit/vitty/util/LogoutHelper.kt index 0e4edf4..213e5d2 100755 --- a/app/src/main/java/com/dscvit/vitty/util/LogoutHelper.kt +++ b/app/src/main/java/com/dscvit/vitty/util/LogoutHelper.kt @@ -8,6 +8,7 @@ import android.view.LayoutInflater import android.view.View import android.widget.Button import androidx.appcompat.content.res.AppCompatResources +import androidx.core.content.edit import com.dscvit.vitty.R import com.dscvit.vitty.activity.AuthActivity import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -54,13 +55,13 @@ object LogoutHelper { putString(Constants.COMMUNITY_NAME, null) putString(Constants.COMMUNITY_PICTURE, null) putString(Constants.COMMUNITY_REGNO, null) + putString(Constants.COMMUNITY_CAMPUS, null) putBoolean(Constants.COMMUNITY_TIMETABLE_AVAILABLE, false) putString(Constants.CACHE_COMMUNITY_TIMETABLE, null) - putString(Constants.COMMUNITY_CAMPUS, null) apply() } - prefs.edit().clear().apply() + prefs.edit { clear() } FirebaseAuth.getInstance().signOut() val intent = Intent(context, AuthActivity::class.java) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK diff --git a/app/src/main/res/drawable/ic_support.xml b/app/src/main/res/drawable/ic_support.xml index 4a85f15..cb0dcac 100644 --- a/app/src/main/res/drawable/ic_support.xml +++ b/app/src/main/res/drawable/ic_support.xml @@ -3,7 +3,7 @@ android:height="18dp" android:viewportWidth="18" android:viewportHeight="18"> - + + + + + + + + \ No newline at end of file From 3192a08450d8bb96a08b249827f636c3ed297734 Mon Sep 17 00:00:00 2001 From: JothishKamal Date: Wed, 30 Jul 2025 15:51:53 +0530 Subject: [PATCH 4/7] feat: implement active friends fetching and caching mechanism - Added constants for active friends fetched status and list. - Updated MainComposeApp to fetch and cache active friends on launch. - Modified LogoutHelper to reset active friends fetched status on logout. - Created ActiveFriendResponse data class for handling active friends API responses. - Created GhostPostResponse data class for handling ghost mode API responses. - Removed unused imports and cleaned up code for better readability. --- .idea/gradle.xml | 2 +- app/build.gradle | 2 + .../network/api/community/APICommunity.kt | 11 +- .../api/community/APICommunityRestClient.kt | 51 +- .../api/community/CommunityNetworkClient.kt | 33 +- .../community/RetrofitCommunityListener.kt | 26 + .../responses/user/ActiveFriendResponse.kt | 5 + .../responses/user/GhostPostResponse.kt | 5 + .../vitty/ui/connect/ConnectViewModel.kt | 989 +++++++++------- .../ui/connect/FriendDetailScreenContent.kt | 1017 +++++++++-------- .../dscvit/vitty/ui/main/MainComposeApp.kt | 271 +---- .../java/com/dscvit/vitty/util/Constants.kt | 3 + .../com/dscvit/vitty/util/LogoutHelper.kt | 2 + 13 files changed, 1294 insertions(+), 1123 deletions(-) create mode 100644 app/src/main/java/com/dscvit/vitty/network/api/community/responses/user/ActiveFriendResponse.kt create mode 100644 app/src/main/java/com/dscvit/vitty/network/api/community/responses/user/GhostPostResponse.kt diff --git a/.idea/gradle.xml b/.idea/gradle.xml index efacb99..804c374 100755 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -11,9 +11,9 @@ - diff --git a/app/build.gradle b/app/build.gradle index eed91f6..eec18c2 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -85,6 +85,8 @@ dependencies { implementation 'androidx.navigation:navigation-ui-ktx:2.9.2' implementation 'androidx.navigation:navigation-compose:2.9.2' implementation 'androidx.compose.material3:material3-android:1.3.2' + implementation "androidx.compose.material:material-icons-extended:1.7.8" + implementation 'androidx.compose.runtime:runtime-livedata:1.8.3' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.2.1' diff --git a/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunity.kt b/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunity.kt index 94651b7..1861769 100644 --- a/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunity.kt +++ b/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunity.kt @@ -9,8 +9,10 @@ import com.dscvit.vitty.network.api.community.responses.circle.CreateCircleRespo import com.dscvit.vitty.network.api.community.responses.circle.JoinCircleResponse import com.dscvit.vitty.network.api.community.responses.requests.RequestsResponse import com.dscvit.vitty.network.api.community.responses.timetable.TimetableResponse +import com.dscvit.vitty.network.api.community.responses.user.ActiveFriendResponse import com.dscvit.vitty.network.api.community.responses.user.CircleResponse import com.dscvit.vitty.network.api.community.responses.user.FriendResponse +import com.dscvit.vitty.network.api.community.responses.user.GhostPostResponse import com.dscvit.vitty.network.api.community.responses.user.PostResponse import com.dscvit.vitty.network.api.community.responses.user.SignInResponse import com.dscvit.vitty.network.api.community.responses.user.UserResponse @@ -114,13 +116,13 @@ interface APICommunity { fun enableGhostMode( @Header("Authorization") authToken: String, @Path("username") username: String, - ): Call + ): Call @POST("/api/v3/friends/alive/{username}") fun disableGhostMode( @Header("Authorization") authToken: String, @Path("username") username: String, - ): Call + ): Call @GET("/api/v3/circles") fun getCircles( @@ -213,4 +215,9 @@ interface APICommunity { @Header("Authorization") authToken: String, @Query("slot") slot: String, ): Call>> + + @GET(value = "/api/v3/friends/active") + fun getActiveFriends( + @Header("Authorization") authToken: String, + ): Call } diff --git a/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunityRestClient.kt b/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunityRestClient.kt index 4527681..462f29c 100644 --- a/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunityRestClient.kt +++ b/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunityRestClient.kt @@ -11,8 +11,10 @@ import com.dscvit.vitty.network.api.community.responses.circle.CreateCircleRespo import com.dscvit.vitty.network.api.community.responses.circle.JoinCircleResponse import com.dscvit.vitty.network.api.community.responses.requests.RequestsResponse import com.dscvit.vitty.network.api.community.responses.timetable.TimetableResponse +import com.dscvit.vitty.network.api.community.responses.user.ActiveFriendResponse import com.dscvit.vitty.network.api.community.responses.user.CircleResponse import com.dscvit.vitty.network.api.community.responses.user.FriendResponse +import com.dscvit.vitty.network.api.community.responses.user.GhostPostResponse import com.dscvit.vitty.network.api.community.responses.user.PostResponse import com.dscvit.vitty.network.api.community.responses.user.SignInResponse import com.dscvit.vitty.network.api.community.responses.user.UserResponse @@ -184,6 +186,7 @@ class APICommunityRestClient { call: Call, response: Response, ) { + Timber.d("FriendListResponse: ${response.body()}") retrofitFriendListListener.onSuccess(call, response.body()) } @@ -658,23 +661,23 @@ class APICommunityRestClient { fun enableGhostMode( token: String, username: String, - retrofitUserActionListener: RetrofitUserActionListener, + retrofitUserActionListener: RetrofitGhostActionListener, ) { val bearerToken = "Bearer $token" mApiUser = retrofit.create(APICommunity::class.java) val apiGhostModeCall = mApiUser!!.enableGhostMode(bearerToken, username) apiGhostModeCall.enqueue( - object : Callback { + object : Callback { override fun onResponse( - call: Call, - response: Response, + call: Call, + response: Response, ) { retrofitUserActionListener.onSuccess(call, response.body()) } override fun onFailure( - call: Call, + call: Call, t: Throwable, ) { retrofitUserActionListener.onError(call, t) @@ -686,23 +689,23 @@ class APICommunityRestClient { fun disableGhostMode( token: String, username: String, - retrofitUserActionListener: RetrofitUserActionListener, + retrofitUserActionListener: RetrofitGhostActionListener, ) { val bearerToken = "Bearer $token" mApiUser = retrofit.create(APICommunity::class.java) val apiGhostModeCall = mApiUser!!.disableGhostMode(bearerToken, username) apiGhostModeCall.enqueue( - object : Callback { + object : Callback { override fun onResponse( - call: Call, - response: Response, + call: Call, + response: Response, ) { retrofitUserActionListener.onSuccess(call, response.body()) } override fun onFailure( - call: Call, + call: Call, t: Throwable, ) { retrofitUserActionListener.onError(call, t) @@ -1011,4 +1014,32 @@ class APICommunityRestClient { }, ) } + + fun getActiveFriends( + token: String, + retrofitActiveFriendsListener: RetrofitActiveFriendsListener, + ) { + val bearerToken = "Bearer $token" + mApiUser = retrofit.create(APICommunity::class.java) + val apiActiveFriendsCall = mApiUser!!.getActiveFriends(bearerToken) + apiActiveFriendsCall.enqueue( + object : Callback { + override fun onResponse( + call: Call, + response: Response, + ) { + Timber.d("ActiveFriends: ${response.body()}") + retrofitActiveFriendsListener.onSuccess(call, response.body()) + } + + override fun onFailure( + call: Call, + t: Throwable, + ) { + Timber.d("ActiveFriendsError: ${t.message}") + retrofitActiveFriendsListener.onError(call, t) + } + }, + ) + } } diff --git a/app/src/main/java/com/dscvit/vitty/network/api/community/CommunityNetworkClient.kt b/app/src/main/java/com/dscvit/vitty/network/api/community/CommunityNetworkClient.kt index 3df55db..c9b4f93 100644 --- a/app/src/main/java/com/dscvit/vitty/network/api/community/CommunityNetworkClient.kt +++ b/app/src/main/java/com/dscvit/vitty/network/api/community/CommunityNetworkClient.kt @@ -1,28 +1,41 @@ package com.dscvit.vitty.network.api.community - import com.dscvit.vitty.util.WebConstants.COMMUNITY_BASE_URL import com.dscvit.vitty.util.WebConstants.TIMEOUT import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import java.util.concurrent.TimeUnit object CommunityNetworkClient { - private var retrofit: Retrofit? = null val retrofitClientCommunity: Retrofit get() { if (retrofit == null) { - val okHttpClientBuilder = OkHttpClient.Builder() - okHttpClientBuilder.connectTimeout(TIMEOUT.toLong(), TimeUnit.SECONDS) - retrofit = Retrofit.Builder() - .baseUrl(COMMUNITY_BASE_URL) - .addConverterFactory(GsonConverterFactory.create()) - .client(okHttpClientBuilder.build()) - .build() + val loggingInterceptor = + HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + } + + val okHttpClient = + OkHttpClient + .Builder() + .addInterceptor(loggingInterceptor) + .connectTimeout(TIMEOUT.toLong(), TimeUnit.SECONDS) + .readTimeout(TIMEOUT.toLong(), TimeUnit.SECONDS) + .writeTimeout(TIMEOUT.toLong(), TimeUnit.SECONDS) + .build() + + retrofit = + Retrofit + .Builder() + .baseUrl(COMMUNITY_BASE_URL) + .addConverterFactory(GsonConverterFactory.create()) + .client(okHttpClient) + .build() } return retrofit!! } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/dscvit/vitty/network/api/community/RetrofitCommunityListener.kt b/app/src/main/java/com/dscvit/vitty/network/api/community/RetrofitCommunityListener.kt index 1883ac8..41ab0b8 100644 --- a/app/src/main/java/com/dscvit/vitty/network/api/community/RetrofitCommunityListener.kt +++ b/app/src/main/java/com/dscvit/vitty/network/api/community/RetrofitCommunityListener.kt @@ -5,8 +5,10 @@ import com.dscvit.vitty.network.api.community.responses.circle.CreateCircleRespo import com.dscvit.vitty.network.api.community.responses.circle.JoinCircleResponse import com.dscvit.vitty.network.api.community.responses.requests.RequestsResponse import com.dscvit.vitty.network.api.community.responses.timetable.TimetableResponse +import com.dscvit.vitty.network.api.community.responses.user.ActiveFriendResponse import com.dscvit.vitty.network.api.community.responses.user.CircleResponse import com.dscvit.vitty.network.api.community.responses.user.FriendResponse +import com.dscvit.vitty.network.api.community.responses.user.GhostPostResponse import com.dscvit.vitty.network.api.community.responses.user.PostResponse import com.dscvit.vitty.network.api.community.responses.user.SignInResponse import com.dscvit.vitty.network.api.community.responses.user.UserResponse @@ -108,6 +110,18 @@ interface RetrofitUserActionListener { ) } +interface RetrofitGhostActionListener { + fun onSuccess( + call: Call?, + response: GhostPostResponse?, + ) + + fun onError( + call: Call?, + t: Throwable?, + ) +} + interface RetrofitCircleListener { fun onSuccess( call: Call?, @@ -143,3 +157,15 @@ interface RetrofitJoinCircleListener { t: Throwable?, ) } + +interface RetrofitActiveFriendsListener { + fun onSuccess( + call: Call, + response: ActiveFriendResponse?, + ) + + fun onError( + call: Call, + t: Throwable, + ) +} diff --git a/app/src/main/java/com/dscvit/vitty/network/api/community/responses/user/ActiveFriendResponse.kt b/app/src/main/java/com/dscvit/vitty/network/api/community/responses/user/ActiveFriendResponse.kt new file mode 100644 index 0000000..c7a86e3 --- /dev/null +++ b/app/src/main/java/com/dscvit/vitty/network/api/community/responses/user/ActiveFriendResponse.kt @@ -0,0 +1,5 @@ +package com.dscvit.vitty.network.api.community.responses.user + +data class ActiveFriendResponse( + val data: List?, +) diff --git a/app/src/main/java/com/dscvit/vitty/network/api/community/responses/user/GhostPostResponse.kt b/app/src/main/java/com/dscvit/vitty/network/api/community/responses/user/GhostPostResponse.kt new file mode 100644 index 0000000..ca109a4 --- /dev/null +++ b/app/src/main/java/com/dscvit/vitty/network/api/community/responses/user/GhostPostResponse.kt @@ -0,0 +1,5 @@ +package com.dscvit.vitty.network.api.community.responses.user + +data class GhostPostResponse( + val data: String, +) diff --git a/app/src/main/java/com/dscvit/vitty/ui/connect/ConnectViewModel.kt b/app/src/main/java/com/dscvit/vitty/ui/connect/ConnectViewModel.kt index 022edb3..8b05681 100644 --- a/app/src/main/java/com/dscvit/vitty/ui/connect/ConnectViewModel.kt +++ b/app/src/main/java/com/dscvit/vitty/ui/connect/ConnectViewModel.kt @@ -1,22 +1,30 @@ package com.dscvit.vitty.ui.connect +import android.content.SharedPreferences +import androidx.core.content.edit import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import com.dscvit.vitty.network.api.community.APICommunityRestClient +import com.dscvit.vitty.network.api.community.RetrofitActiveFriendsListener import com.dscvit.vitty.network.api.community.RetrofitCircleListener import com.dscvit.vitty.network.api.community.RetrofitCircleRequestListener import com.dscvit.vitty.network.api.community.RetrofitCreateCircleListener import com.dscvit.vitty.network.api.community.RetrofitFriendListListener import com.dscvit.vitty.network.api.community.RetrofitFriendRequestListener +import com.dscvit.vitty.network.api.community.RetrofitGhostActionListener import com.dscvit.vitty.network.api.community.RetrofitJoinCircleListener import com.dscvit.vitty.network.api.community.RetrofitUserActionListener import com.dscvit.vitty.network.api.community.responses.circle.CircleRequestsResponse import com.dscvit.vitty.network.api.community.responses.circle.CreateCircleResponse import com.dscvit.vitty.network.api.community.responses.circle.JoinCircleResponse import com.dscvit.vitty.network.api.community.responses.requests.RequestsResponse +import com.dscvit.vitty.network.api.community.responses.user.ActiveFriendResponse import com.dscvit.vitty.network.api.community.responses.user.CircleResponse import com.dscvit.vitty.network.api.community.responses.user.FriendResponse +import com.dscvit.vitty.network.api.community.responses.user.GhostPostResponse import com.dscvit.vitty.network.api.community.responses.user.PostResponse +import com.dscvit.vitty.util.Constants +import com.google.gson.Gson import retrofit2.Call import timber.log.Timber @@ -40,7 +48,10 @@ class ConnectViewModel : ViewModel() { private val _sentCircleRequests = MutableLiveData() private val _isCircleRequestsLoading = MutableLiveData() private val _circleActionResponse = MutableLiveData() + private val _activeFriends = MutableLiveData>() + private val _ghostModeResponse = MutableLiveData() + val ghostModeResponse: MutableLiveData = _ghostModeResponse val friendList: MutableLiveData = _friendList val friendRequest: MutableLiveData = _friendRequest val requestActionResponse: MutableLiveData = _requestActionResponse @@ -60,137 +71,138 @@ class ConnectViewModel : ViewModel() { val sentCircleRequests: MutableLiveData = _sentCircleRequests val isCircleRequestsLoading: MutableLiveData = _isCircleRequestsLoading val circleActionResponse: MutableLiveData = _circleActionResponse + val activeFriends: MutableLiveData> = _activeFriends fun getFriendList( - token: String, - username: String, + token: String, + username: String, ) { _isLoading.postValue(true) APICommunityRestClient.instance.getFriendList( - token, - username, - object : RetrofitFriendListListener { - override fun onSuccess( - call: Call?, - response: FriendResponse?, - ) { - Timber.d("ConnectFriendList: $response") - _friendList.postValue(response) - _isLoading.postValue(false) - } - - override fun onError( - call: Call?, - t: Throwable?, - ) { - _friendList.postValue(null) - _isLoading.postValue(false) - } - }, + token, + username, + object : RetrofitFriendListListener { + override fun onSuccess( + call: Call?, + response: FriendResponse?, + ) { + Timber.d("ConnectFriendList: $response") + _friendList.postValue(response) + _isLoading.postValue(false) + } + + override fun onError( + call: Call?, + t: Throwable?, + ) { + _friendList.postValue(null) + _isLoading.postValue(false) + } + }, ) } fun refreshFriendList( - token: String, - username: String, + token: String, + username: String, ) { _isRefreshing.postValue(true) APICommunityRestClient.instance.getFriendList( - token, - username, - object : RetrofitFriendListListener { - override fun onSuccess( - call: Call?, - response: FriendResponse?, - ) { - _friendList.postValue(response) - _isRefreshing.postValue(false) - } - - override fun onError( - call: Call?, - t: Throwable?, - ) { - _friendList.postValue(null) - _isRefreshing.postValue(false) - } - }, + token, + username, + object : RetrofitFriendListListener { + override fun onSuccess( + call: Call?, + response: FriendResponse?, + ) { + _friendList.postValue(response) + _isRefreshing.postValue(false) + } + + override fun onError( + call: Call?, + t: Throwable?, + ) { + _friendList.postValue(null) + _isRefreshing.postValue(false) + } + }, ) } fun getFriendRequest(token: String) { APICommunityRestClient.instance.getFriendRequest( - token, - object : RetrofitFriendRequestListener { - override fun onSuccess( - call: Call?, - response: RequestsResponse?, - ) { - Timber.d("ConnectFriendRequest: $response") - _friendRequest.postValue(response) - } - - override fun onError( - call: Call?, - t: Throwable?, - ) { - _friendRequest.postValue(null) - } - }, + token, + object : RetrofitFriendRequestListener { + override fun onSuccess( + call: Call?, + response: RequestsResponse?, + ) { + Timber.d("ConnectFriendRequest: $response") + _friendRequest.postValue(response) + } + + override fun onError( + call: Call?, + t: Throwable?, + ) { + _friendRequest.postValue(null) + } + }, ) } fun sendRequest( - token: String, - username: String, + token: String, + username: String, ) { APICommunityRestClient.instance.sendRequest( - token, - username, - object : RetrofitUserActionListener { - override fun onSuccess( - call: Call?, - response: PostResponse?, - ) { - Timber.d("ConnectSendRequest: $response") - _sendRequestResponse.postValue(response) - } - - override fun onError( - call: Call?, - t: Throwable?, - ) { - Timber.d("ConnectSendRequestError: ${t?.message}") - _sendRequestResponse.postValue(null) - } - }, + token, + username, + object : RetrofitUserActionListener { + override fun onSuccess( + call: Call?, + response: PostResponse?, + ) { + Timber.d("ConnectSendRequest: $response") + _sendRequestResponse.postValue(response) + } + + override fun onError( + call: Call?, + t: Throwable?, + ) { + Timber.d("ConnectSendRequestError: ${t?.message}") + _sendRequestResponse.postValue(null) + } + }, ) } fun unfriend( - token: String, - username: String, + token: String, + username: String, ) { APICommunityRestClient.instance.unfriend( - token, - username, - object : RetrofitUserActionListener { - override fun onSuccess( - call: Call?, - response: PostResponse?, - ) { - Timber.d("ConnectUnfriend: $response") - _unfriendSuccess.postValue(username) - } - - override fun onError( - call: Call?, - t: Throwable?, - ) { - Timber.d("ConnectUnfriendError: ${t?.message}") - _unfriendSuccess.postValue(null) - } - }, + token, + username, + object : RetrofitUserActionListener { + override fun onSuccess( + call: Call?, + response: PostResponse?, + ) { + Timber.d("ConnectUnfriend: $response") + _unfriendSuccess.postValue(username) + } + + override fun onError( + call: Call?, + t: Throwable?, + ) { + Timber.d("ConnectUnfriendError: ${t?.message}") + _unfriendSuccess.postValue(null) + } + }, ) } @@ -207,81 +219,81 @@ class ConnectViewModel : ViewModel() { } fun acceptRequest( - token: String, - username: String, + token: String, + username: String, ) { APICommunityRestClient.instance.acceptRequest( - token, - username, - object : RetrofitUserActionListener { - override fun onSuccess( - call: Call?, - response: PostResponse?, - ) { - Timber.d("AcceptRequest: $response") - _requestActionResponse.postValue(response) - } - - override fun onError( - call: Call?, - t: Throwable?, - ) { - Timber.d("AcceptRequest: ${t?.message}") - _requestActionResponse.postValue(null) - } - }, + token, + username, + object : RetrofitUserActionListener { + override fun onSuccess( + call: Call?, + response: PostResponse?, + ) { + Timber.d("AcceptRequest: $response") + _requestActionResponse.postValue(response) + } + + override fun onError( + call: Call?, + t: Throwable?, + ) { + Timber.d("AcceptRequest: ${t?.message}") + _requestActionResponse.postValue(null) + } + }, ) } fun rejectRequest( - token: String, - username: String, + token: String, + username: String, ) { APICommunityRestClient.instance.rejectRequest( - token, - username, - object : RetrofitUserActionListener { - override fun onSuccess( - call: Call?, - response: PostResponse?, - ) { - Timber.d("RejectRequest: $response") - _requestActionResponse.postValue(response) - } - - override fun onError( - call: Call?, - t: Throwable?, - ) { - Timber.d("RejectRequest: ${t?.message}") - _requestActionResponse.postValue(null) - } - }, + token, + username, + object : RetrofitUserActionListener { + override fun onSuccess( + call: Call?, + response: PostResponse?, + ) { + Timber.d("RejectRequest: $response") + _requestActionResponse.postValue(response) + } + + override fun onError( + call: Call?, + t: Throwable?, + ) { + Timber.d("RejectRequest: ${t?.message}") + _requestActionResponse.postValue(null) + } + }, ) } fun getCircleList(token: String) { _isCircleLoading.postValue(true) APICommunityRestClient.instance.getCircles( - token, - object : RetrofitCircleListener { - override fun onSuccess( - call: Call?, - response: CircleResponse?, - ) { - Timber.d("ConnectCircleList: $response") - _circleList.postValue(response) - _isCircleLoading.postValue(false) - } - - override fun onError( - call: Call?, - t: Throwable?, - ) { - _circleList.postValue(null) - _isCircleLoading.postValue(false) - } - }, + token, + object : RetrofitCircleListener { + override fun onSuccess( + call: Call?, + response: CircleResponse?, + ) { + Timber.d("ConnectCircleList: $response") + _circleList.postValue(response) + _isCircleLoading.postValue(false) + } + + override fun onError( + call: Call?, + t: Throwable?, + ) { + _circleList.postValue(null) + _isCircleLoading.postValue(false) + } + }, ) } @@ -291,144 +303,139 @@ class ConnectViewModel : ViewModel() { _circleMembersLoading.postValue(emptySet()) APICommunityRestClient.instance.getCircles( - token, - object : RetrofitCircleListener { - override fun onSuccess( - call: Call?, - response: CircleResponse?, - ) { - Timber.d("ConnectCircleList: $response") - _circleList.postValue(response) - _isCircleRefreshing.postValue(false) - - response?.data?.forEach { circle -> - getCircleDetails(token, circle.circle_id) - } - } - - override fun onError( - call: Call?, - t: Throwable?, - ) { - _circleList.postValue(null) - _isCircleRefreshing.postValue(false) - } - }, + token, + object : RetrofitCircleListener { + override fun onSuccess( + call: Call?, + response: CircleResponse?, + ) { + Timber.d("ConnectCircleList: $response") + _circleList.postValue(response) + _isCircleRefreshing.postValue(false) + + response?.data?.forEach { circle -> + getCircleDetails(token, circle.circle_id) + } + } + + override fun onError( + call: Call?, + t: Throwable?, + ) { + _circleList.postValue(null) + _isCircleRefreshing.postValue(false) + } + }, ) } fun getCircleDetails( - token: String, - circleId: String, + token: String, + circleId: String, ) { val currentLoading = _circleMembersLoading.value?.toMutableSet() ?: mutableSetOf() currentLoading.add(circleId) _circleMembersLoading.postValue(currentLoading) APICommunityRestClient.instance.getCircleDetails( - token, - circleId, - object : RetrofitFriendListListener { - override fun onSuccess( - call: Call?, - response: FriendResponse?, - ) { - Timber.d("CircleDetails for $circleId: $response") - val currentMembers = _circleMembers.value?.toMutableMap() ?: mutableMapOf() - if (response != null) { - currentMembers[circleId] = response - _circleMembers.postValue(currentMembers) - } - - val updatedLoading = _circleMembersLoading.value?.toMutableSet() ?: mutableSetOf() - updatedLoading.remove(circleId) - _circleMembersLoading.postValue(updatedLoading) - } - - override fun onError( - call: Call?, - t: Throwable?, - ) { - Timber.d("CircleDetailsError for $circleId: ${t?.message}") - - val updatedLoading = _circleMembersLoading.value?.toMutableSet() ?: mutableSetOf() - updatedLoading.remove(circleId) - _circleMembersLoading.postValue(updatedLoading) - } - }, - ) - } + token, + circleId, + object : RetrofitFriendListListener { + override fun onSuccess( + call: Call?, + response: FriendResponse?, + ) { + Timber.d("CircleDetails for $circleId: $response") + val currentMembers = _circleMembers.value?.toMutableMap() ?: mutableMapOf() + if (response != null) { + currentMembers[circleId] = response + _circleMembers.postValue(currentMembers) + } + + val updatedLoading = + _circleMembersLoading.value?.toMutableSet() ?: mutableSetOf() + updatedLoading.remove(circleId) + _circleMembersLoading.postValue(updatedLoading) + } - fun fetchAllCircleDetails(token: String) { - val circles = _circleList.value?.data ?: return - circles.forEach { circle -> - getCircleDetails(token, circle.circle_id) - } + override fun onError( + call: Call?, + t: Throwable?, + ) { + Timber.d("CircleDetailsError for $circleId: ${t?.message}") + + val updatedLoading = + _circleMembersLoading.value?.toMutableSet() ?: mutableSetOf() + updatedLoading.remove(circleId) + _circleMembersLoading.postValue(updatedLoading) + } + }, + ) } fun createCircle( - token: String, - circleName: String, + token: String, + circleName: String, ) { Timber.d( - "ConnectViewModel.createCircle called with token: ${if (token.isNotEmpty()) "exists" else "empty"}, circleName: $circleName", + "ConnectViewModel.createCircle called with token: ${if (token.isNotEmpty()) "exists" else "empty"}, circleName: $circleName", ) APICommunityRestClient.instance.createCircle( - token, - circleName, - object : RetrofitCreateCircleListener { - override fun onSuccess( - call: Call?, - response: CreateCircleResponse?, - ) { - Timber.d("CreateCircle: $response") - _createCircleResponse.postValue(response) - } - - override fun onError( - call: Call?, - t: Throwable?, - ) { - Timber.d("CreateCircle Error: ${t?.message}") - _createCircleResponse.postValue(null) - } - }, + token, + circleName, + object : RetrofitCreateCircleListener { + override fun onSuccess( + call: Call?, + response: CreateCircleResponse?, + ) { + Timber.d("CreateCircle: $response") + _createCircleResponse.postValue(response) + } + + override fun onError( + call: Call?, + t: Throwable?, + ) { + Timber.d("CreateCircle Error: ${t?.message}") + _createCircleResponse.postValue(null) + } + }, ) } fun joinCircleByCode( - token: String, - joinCode: String, + token: String, + joinCode: String, ) { Timber.d( - "ConnectViewModel.joinCircleByCode called with token: ${if (token.isNotEmpty()) "exists" else "empty"}, joinCode: $joinCode", + "ConnectViewModel.joinCircleByCode called with token: ${if (token.isNotEmpty()) "exists" else "empty"}, joinCode: $joinCode", ) _joinCircleResponse.postValue(null) _joinCircleError.postValue(null) APICommunityRestClient.instance.joinCircleByCode( - token, - joinCode, - object : RetrofitJoinCircleListener { - override fun onSuccess( - call: Call?, - response: JoinCircleResponse?, - ) { - Timber.d("JoinCircle Success: $response") - _joinCircleResponse.postValue(response) - } - - override fun onError( - call: Call?, - t: Throwable?, - ) { - val errorMessage = t?.message ?: "Unknown error occurred" - Timber.d("JoinCircle Error: $errorMessage") - _joinCircleError.postValue(errorMessage) - } - }, + token, + joinCode, + object : RetrofitJoinCircleListener { + override fun onSuccess( + call: Call?, + response: JoinCircleResponse?, + ) { + Timber.d("JoinCircle Success: $response") + _joinCircleResponse.postValue(response) + } + + override fun onError( + call: Call?, + t: Throwable?, + ) { + val errorMessage = t?.message ?: "Unknown error occurred" + Timber.d("JoinCircle Error: $errorMessage") + _joinCircleError.postValue(errorMessage) + } + }, ) } @@ -447,52 +454,52 @@ class ConnectViewModel : ViewModel() { fun getReceivedCircleRequests(token: String) { _isCircleRequestsLoading.postValue(true) APICommunityRestClient.instance.getReceivedCircleRequests( - token, - object : RetrofitCircleRequestListener { - override fun onSuccess( - call: Call?, - response: CircleRequestsResponse?, - ) { - Timber.d("ReceivedCircleRequests: $response") - _receivedCircleRequests.postValue(response) - _isCircleRequestsLoading.postValue(false) - } - - override fun onError( - call: Call?, - t: Throwable?, - ) { - Timber.d("ReceivedCircleRequestsError: ${t?.message}") - _receivedCircleRequests.postValue(null) - _isCircleRequestsLoading.postValue(false) - } - }, + token, + object : RetrofitCircleRequestListener { + override fun onSuccess( + call: Call?, + response: CircleRequestsResponse?, + ) { + Timber.d("ReceivedCircleRequests: $response") + _receivedCircleRequests.postValue(response) + _isCircleRequestsLoading.postValue(false) + } + + override fun onError( + call: Call?, + t: Throwable?, + ) { + Timber.d("ReceivedCircleRequestsError: ${t?.message}") + _receivedCircleRequests.postValue(null) + _isCircleRequestsLoading.postValue(false) + } + }, ) } fun getSentCircleRequests(token: String) { _isCircleRequestsLoading.postValue(true) APICommunityRestClient.instance.getSentCircleRequests( - token, - object : RetrofitCircleRequestListener { - override fun onSuccess( - call: Call?, - response: CircleRequestsResponse?, - ) { - Timber.d("SentCircleRequests: $response") - _sentCircleRequests.postValue(response) - _isCircleRequestsLoading.postValue(false) - } - - override fun onError( - call: Call?, - t: Throwable?, - ) { - Timber.d("SentCircleRequestsError: ${t?.message}") - _sentCircleRequests.postValue(null) - _isCircleRequestsLoading.postValue(false) - } - }, + token, + object : RetrofitCircleRequestListener { + override fun onSuccess( + call: Call?, + response: CircleRequestsResponse?, + ) { + Timber.d("SentCircleRequests: $response") + _sentCircleRequests.postValue(response) + _isCircleRequestsLoading.postValue(false) + } + + override fun onError( + call: Call?, + t: Throwable?, + ) { + Timber.d("SentCircleRequestsError: ${t?.message}") + _sentCircleRequests.postValue(null) + _isCircleRequestsLoading.postValue(false) + } + }, ) } @@ -502,56 +509,56 @@ class ConnectViewModel : ViewModel() { } fun deleteCircle( - token: String, - circleId: String, + token: String, + circleId: String, ) { APICommunityRestClient.instance.deleteCircle( - token, - circleId, - object : RetrofitUserActionListener { - override fun onSuccess( - call: Call?, - response: PostResponse?, - ) { - Timber.d("DeleteCircle: $response") - _circleActionResponse.postValue(response) - } - - override fun onError( - call: Call?, - t: Throwable?, - ) { - Timber.d("DeleteCircleError: ${t?.message}") - _circleActionResponse.postValue(null) - } - }, + token, + circleId, + object : RetrofitUserActionListener { + override fun onSuccess( + call: Call?, + response: PostResponse?, + ) { + Timber.d("DeleteCircle: $response") + _circleActionResponse.postValue(response) + } + + override fun onError( + call: Call?, + t: Throwable?, + ) { + Timber.d("DeleteCircleError: ${t?.message}") + _circleActionResponse.postValue(null) + } + }, ) } fun leaveCircle( - token: String, - circleId: String, + token: String, + circleId: String, ) { APICommunityRestClient.instance.leaveCircle( - token, - circleId, - object : RetrofitUserActionListener { - override fun onSuccess( - call: Call?, - response: PostResponse?, - ) { - Timber.d("LeaveCircle: $response") - _circleActionResponse.postValue(response) - } - - override fun onError( - call: Call?, - t: Throwable?, - ) { - Timber.d("LeaveCircleError: ${t?.message}") - _circleActionResponse.postValue(null) - } - }, + token, + circleId, + object : RetrofitUserActionListener { + override fun onSuccess( + call: Call?, + response: PostResponse?, + ) { + Timber.d("LeaveCircle: $response") + _circleActionResponse.postValue(response) + } + + override fun onError( + call: Call?, + t: Throwable?, + ) { + Timber.d("LeaveCircleError: ${t?.message}") + _circleActionResponse.postValue(null) + } + }, ) } @@ -560,116 +567,214 @@ class ConnectViewModel : ViewModel() { } fun acceptCircleRequest( - token: String, - circleId: String, + token: String, + circleId: String, ) { APICommunityRestClient.instance.acceptCircleRequest( - token, - circleId, - object : RetrofitUserActionListener { - override fun onSuccess( - call: Call?, - response: PostResponse?, - ) { - Timber.d("AcceptCircleRequest: $response") - _circleActionResponse.postValue(response) - } - - override fun onError( - call: Call?, - t: Throwable?, - ) { - Timber.d("AcceptCircleRequestError: ${t?.message}") - _circleActionResponse.postValue(null) - } - }, + token, + circleId, + object : RetrofitUserActionListener { + override fun onSuccess( + call: Call?, + response: PostResponse?, + ) { + Timber.d("AcceptCircleRequest: $response") + _circleActionResponse.postValue(response) + } + + override fun onError( + call: Call?, + t: Throwable?, + ) { + Timber.d("AcceptCircleRequestError: ${t?.message}") + _circleActionResponse.postValue(null) + } + }, ) } fun declineCircleRequest( - token: String, - circleId: String, + token: String, + circleId: String, ) { APICommunityRestClient.instance.declineCircleRequest( - token, - circleId, - object : RetrofitUserActionListener { - override fun onSuccess( - call: Call?, - response: PostResponse?, - ) { - Timber.d("DeclineCircleRequest: $response") - _circleActionResponse.postValue(response) - } - - override fun onError( - call: Call?, - t: Throwable?, - ) { - Timber.d("DeclineCircleRequestError: ${t?.message}") - _circleActionResponse.postValue(null) - } - }, + token, + circleId, + object : RetrofitUserActionListener { + override fun onSuccess( + call: Call?, + response: PostResponse?, + ) { + Timber.d("DeclineCircleRequest: $response") + _circleActionResponse.postValue(response) + } + + override fun onError( + call: Call?, + t: Throwable?, + ) { + Timber.d("DeclineCircleRequestError: ${t?.message}") + _circleActionResponse.postValue(null) + } + }, ) } fun unsendCircleRequest( - token: String, - circleId: String, - username: String, + token: String, + circleId: String, + username: String, ) { APICommunityRestClient.instance.unsendCircleRequest( - token, - circleId, - username, - object : RetrofitUserActionListener { - override fun onSuccess( - call: Call?, - response: PostResponse?, - ) { - Timber.d("UnsendCircleRequest: $response") - _circleActionResponse.postValue(response) - } - - override fun onError( - call: Call?, - t: Throwable?, - ) { - Timber.d("UnsendCircleRequestError: ${t?.message}") - _circleActionResponse.postValue(null) - } - }, + token, + circleId, + username, + object : RetrofitUserActionListener { + override fun onSuccess( + call: Call?, + response: PostResponse?, + ) { + Timber.d("UnsendCircleRequest: $response") + _circleActionResponse.postValue(response) + } + + override fun onError( + call: Call?, + t: Throwable?, + ) { + Timber.d("UnsendCircleRequestError: ${t?.message}") + _circleActionResponse.postValue(null) + } + }, ) } fun removeUserFromCircle( - token: String, - circleId: String, - username: String, + token: String, + circleId: String, + username: String, ) { APICommunityRestClient.instance.removeUserFromCircle( - token, - circleId, - username, - object : RetrofitUserActionListener { - override fun onSuccess( - call: Call?, - response: PostResponse?, - ) { - Timber.d("RemoveUserFromCircle: $response") - _circleActionResponse.postValue(response) - - getCircleDetails(token, circleId) - } - - override fun onError( - call: Call?, - t: Throwable?, - ) { - Timber.d("RemoveUserFromCircleError: ${t?.message}") - _circleActionResponse.postValue(null) - } - }, + token, + circleId, + username, + object : RetrofitUserActionListener { + override fun onSuccess( + call: Call?, + response: PostResponse?, + ) { + Timber.d("RemoveUserFromCircle: $response") + _circleActionResponse.postValue(response) + + getCircleDetails(token, circleId) + } + + override fun onError( + call: Call?, + t: Throwable?, + ) { + Timber.d("RemoveUserFromCircleError: ${t?.message}") + _circleActionResponse.postValue(null) + } + }, + ) + } + + fun fetchActiveFriends( + token: String, + prefs: SharedPreferences, + ) { + APICommunityRestClient.instance.getActiveFriends( + token, + object : RetrofitActiveFriendsListener { + override fun onSuccess( + call: Call, + response: ActiveFriendResponse?, + ) { + val list = response?.data ?: emptyList() + _activeFriends.postValue(list) + + prefs.edit { + putBoolean(Constants.ACTIVE_FRIENDS_FETCHED, true) + .putString(Constants.ACTIVE_FRIENDS_LIST, Gson().toJson(list)) + } + } + + override fun onError( + call: Call, + t: Throwable, + ) { + _activeFriends.postValue(emptyList()) + } + }, ) } + + fun toggleGhostMode( + token: String, + username: String, + enable: Boolean, + ) { + _ghostModeResponse.postValue(null) + val api = APICommunityRestClient.instance + if (enable) { + api.enableGhostMode( + token, + username, + object : RetrofitGhostActionListener { + override fun onSuccess( + call: Call?, + response: GhostPostResponse?, + ) { + val success = + response?.data?.contains("hidden", true) == true || + response?.data?.contains("visible", true) == true + _ghostModeResponse.postValue(GhostModeResponse(success)) + } + + override fun onError( + call: Call?, + t: Throwable?, + ) { + _ghostModeResponse.postValue(GhostModeResponse(false)) + } + }, + ) + } else { + api.disableGhostMode( + token, + username, + object : RetrofitGhostActionListener { + override fun onSuccess( + call: Call?, + response: GhostPostResponse?, + ) { + val success = + response?.data?.contains("hidden", true) == true || + response?.data?.contains("visible", true) == true + _ghostModeResponse.postValue(GhostModeResponse(success)) + } + + override fun onError( + call: Call?, + t: Throwable?, + ) { + _ghostModeResponse.postValue(GhostModeResponse(false)) + } + }, + ) + } + } + + fun clearGhostModeResponse() { + _ghostModeResponse.postValue(null) + } + + fun updateActiveFriendsList(data: List?) { + _activeFriends.postValue(data) + } + + data class GhostModeResponse( + val success: Boolean, + ) } diff --git a/app/src/main/java/com/dscvit/vitty/ui/connect/FriendDetailScreenContent.kt b/app/src/main/java/com/dscvit/vitty/ui/connect/FriendDetailScreenContent.kt index e770ab6..1f69c5e 100644 --- a/app/src/main/java/com/dscvit/vitty/ui/connect/FriendDetailScreenContent.kt +++ b/app/src/main/java/com/dscvit/vitty/ui/connect/FriendDetailScreenContent.kt @@ -1,6 +1,7 @@ package com.dscvit.vitty.ui.connect import android.content.Context +import android.widget.Toast import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -35,6 +36,8 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ScrollableTabRow +import androidx.compose.material3.Switch +import androidx.compose.material3.SwitchDefaults import androidx.compose.material3.Tab import androidx.compose.material3.TabRowDefaults import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset @@ -59,6 +62,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.core.content.edit import androidx.lifecycle.viewmodel.compose.viewModel import coil.compose.AsyncImage import com.dscvit.vitty.R @@ -75,26 +79,29 @@ import com.dscvit.vitty.ui.schedule.ScheduleViewModel import com.dscvit.vitty.util.Constants import com.dscvit.vitty.util.Quote import com.dscvit.vitty.widget.parseTimeToTimestamp +import com.google.gson.Gson +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Locale import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber -import java.text.SimpleDateFormat -import java.util.Calendar -import java.util.Locale @OptIn(ExperimentalMaterial3Api::class) @Composable fun FriendDetailScreenContent( - friend: UserResponse, - onBackClick: () -> Unit = {}, - connectViewModel: ConnectViewModel, + friend: UserResponse, + onBackClick: () -> Unit = {}, + connectViewModel: ConnectViewModel, ) { val context = LocalContext.current val scheduleViewModel: ScheduleViewModel = viewModel() val scope = rememberCoroutineScope() var quote by remember { mutableStateOf("") } - var friendTimetableData by remember { mutableStateOf>>(emptyMap()) } + var friendTimetableData by remember { + mutableStateOf>>(emptyMap()) + } var isLoadingTimetable by remember { mutableStateOf(false) } var hasLoadedData by remember { mutableStateOf(false) } var hasUnfriended by remember { mutableStateOf(false) } @@ -102,8 +109,62 @@ fun FriendDetailScreenContent( var hasRequestSent by remember { mutableStateOf(false) } var showUnfriendDialog by remember { mutableStateOf(false) } + var isFriendGhosted by remember { mutableStateOf(false) } + var isTogglingGhostMode by remember { mutableStateOf(false) } + val unfriendSuccess by connectViewModel.unfriendSuccess.observeAsState() val sendRequestResponse by connectViewModel.sendRequestResponse.observeAsState() + val activeFriends by connectViewModel.activeFriends.observeAsState() + val ghostModeResponse by connectViewModel.ghostModeResponse.observeAsState() + + LaunchedEffect(activeFriends) { + Timber.d("Active friends: $activeFriends") + activeFriends?.let { activeList -> isFriendGhosted = !activeList.contains(friend.username) } + } + + LaunchedEffect(ghostModeResponse) { + if (ghostModeResponse != null && isTogglingGhostMode) { + isTogglingGhostMode = false + + val sharedPreferences = + context.getSharedPreferences(Constants.USER_INFO, Context.MODE_PRIVATE) + + if (ghostModeResponse!!.success) { + + val currentActiveFriends = activeFriends?.toMutableList() ?: mutableListOf() + val gson = Gson() + + if (isFriendGhosted) { + + currentActiveFriends.removeAll { it == friend.username } + } else { + + if (!currentActiveFriends.contains(friend.username)) { + currentActiveFriends.add(friend.username) + } + } + + val activeFriendsJson = gson.toJson(currentActiveFriends) + sharedPreferences.edit { + putString(Constants.ACTIVE_FRIENDS_LIST, activeFriendsJson) + } + + Toast.makeText( + context, + if (isFriendGhosted) "${friend.name} is now ghosted" + else "${friend.name} is now visible", + Toast.LENGTH_SHORT, + ) + .show() + connectViewModel.updateActiveFriendsList(currentActiveFriends) + } else { + + isFriendGhosted = !isFriendGhosted + } + + connectViewModel.clearGhostModeResponse() + } + } LaunchedEffect(unfriendSuccess) { if (unfriendSuccess == friend.username) { @@ -119,7 +180,8 @@ fun FriendDetailScreenContent( hasRequestSent = true connectViewModel.clearSendRequestResponse() - val sharedPreferences = context.getSharedPreferences(Constants.USER_INFO, Context.MODE_PRIVATE) + val sharedPreferences = + context.getSharedPreferences(Constants.USER_INFO, Context.MODE_PRIVATE) val token = sharedPreferences.getString(Constants.COMMUNITY_TOKEN, "") ?: "" val username = sharedPreferences.getString(Constants.COMMUNITY_USERNAME, "") ?: "" if (token.isNotEmpty() && username.isNotEmpty()) { @@ -132,31 +194,32 @@ fun FriendDetailScreenContent( } val days = listOf("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun") - val currentDay = - remember { - when (Calendar.getInstance().get(Calendar.DAY_OF_WEEK)) { - Calendar.MONDAY -> 0 - Calendar.TUESDAY -> 1 - Calendar.WEDNESDAY -> 2 - Calendar.THURSDAY -> 3 - Calendar.FRIDAY -> 4 - Calendar.SATURDAY -> 5 - Calendar.SUNDAY -> 6 - else -> 0 - } + val currentDay = remember { + when (Calendar.getInstance().get(Calendar.DAY_OF_WEEK)) { + Calendar.MONDAY -> 0 + Calendar.TUESDAY -> 1 + Calendar.WEDNESDAY -> 2 + Calendar.THURSDAY -> 3 + Calendar.FRIDAY -> 4 + Calendar.SATURDAY -> 5 + Calendar.SUNDAY -> 6 + else -> 0 } + } val pagerState = - rememberPagerState( - initialPage = currentDay, - pageCount = { 7 }, - ) + rememberPagerState( + initialPage = currentDay, + pageCount = { 7 }, + ) + LaunchedEffect(friend.username) { if (hasLoadedData) return@LaunchedEffect quote = Quote.getLine(context) - val sharedPreferences = context.getSharedPreferences(Constants.USER_INFO, Context.MODE_PRIVATE) + val sharedPreferences = + context.getSharedPreferences(Constants.USER_INFO, Context.MODE_PRIVATE) val token = sharedPreferences.getString(Constants.COMMUNITY_TOKEN, "") ?: "" if (token.isNotEmpty()) { @@ -193,100 +256,117 @@ fun FriendDetailScreenContent( } Column( - modifier = - Modifier - .fillMaxSize() - .background(Background), + modifier = Modifier.fillMaxSize().background(Background), ) { CenterAlignedTopAppBar( - title = { - Text( - text = "Friend Profile", - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold, - ) - }, - navigationIcon = { - IconButton(onClick = onBackClick) { - Icon( - painter = painterResource(id = R.drawable.ic_round_chevron_left), - contentDescription = "Back", - tint = TextColor, + title = { + Text( + text = "Friend Profile", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, ) - } - }, - colors = - TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.background, - titleContentColor = MaterialTheme.colorScheme.onBackground, - ), + }, + navigationIcon = { + IconButton(onClick = onBackClick) { + Icon( + painter = painterResource(id = R.drawable.ic_round_chevron_left), + contentDescription = "Back", + tint = TextColor, + ) + } + }, + colors = + TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.background, + titleContentColor = MaterialTheme.colorScheme.onBackground, + ), ) FriendProfileCard( - friend = friend, - hasUnfriended = hasUnfriended, - isSendingRequest = isSendingRequest, - hasRequestSent = hasRequestSent, - onUnfriendClick = { - showUnfriendDialog = true - }, - onSendRequestClick = { - val sharedPreferences = context.getSharedPreferences(Constants.USER_INFO, Context.MODE_PRIVATE) - val token = sharedPreferences.getString(Constants.COMMUNITY_TOKEN, "") ?: "" - if (token.isNotEmpty()) { - isSendingRequest = true - connectViewModel.sendRequest(token, friend.username) - } - }, + friend = friend, + hasUnfriended = hasUnfriended, + isSendingRequest = isSendingRequest, + hasRequestSent = hasRequestSent, + isFriendGhosted = isFriendGhosted, + isTogglingGhostMode = isTogglingGhostMode, + onUnfriendClick = { showUnfriendDialog = true }, + onSendRequestClick = { + val sharedPreferences = + context.getSharedPreferences(Constants.USER_INFO, Context.MODE_PRIVATE) + val token = sharedPreferences.getString(Constants.COMMUNITY_TOKEN, "") ?: "" + if (token.isNotEmpty()) { + isSendingRequest = true + connectViewModel.sendRequest(token, friend.username) + } + }, + onToggleGhostMode = { newValue -> + if (!isTogglingGhostMode) { + isTogglingGhostMode = true + isFriendGhosted = newValue + + val sharedPreferences = + context.getSharedPreferences( + Constants.USER_INFO, + Context.MODE_PRIVATE + ) + val token = sharedPreferences.getString(Constants.COMMUNITY_TOKEN, "") ?: "" + + connectViewModel.toggleGhostMode(token, friend.username, newValue) + } + }, ) Spacer(modifier = Modifier.height(16.dp)) if (!hasUnfriended) { Text( - text = "Schedule", - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold, - color = TextColor, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + text = "Schedule", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + color = TextColor, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), ) ScrollableTabRow( - selectedTabIndex = pagerState.currentPage, - containerColor = MaterialTheme.colorScheme.surface, - contentColor = MaterialTheme.colorScheme.onSurface, - divider = {}, - edgePadding = 0.dp, - indicator = { tabPositions -> - if (pagerState.currentPage < tabPositions.size) { - TabRowDefaults.SecondaryIndicator( - modifier = Modifier.tabIndicatorOffset(tabPositions[pagerState.currentPage]), - color = TextColor, - ) - } - }, + selectedTabIndex = pagerState.currentPage, + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface, + divider = {}, + edgePadding = 0.dp, + indicator = { tabPositions -> + if (pagerState.currentPage < tabPositions.size) { + TabRowDefaults.SecondaryIndicator( + modifier = + Modifier.tabIndicatorOffset( + tabPositions[pagerState.currentPage] + ), + color = TextColor, + ) + } + }, ) { days.forEachIndexed { index, day -> Tab( - selected = pagerState.currentPage == index, - onClick = { - scope.launch { - pagerState.animateScrollToPage(index) - } - }, - text = { - Text( - text = day, - fontFamily = Poppins, - fontWeight = if (pagerState.currentPage == index) FontWeight.Medium else FontWeight.Normal, - fontSize = 20.sp, - lineHeight = (20 * 1.4).sp, - color = if (pagerState.currentPage == index) TextColor else Accent, - textAlign = TextAlign.Center, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - }, + selected = pagerState.currentPage == index, + onClick = { scope.launch { pagerState.animateScrollToPage(index) } }, + text = { + Text( + text = day, + fontFamily = Poppins, + fontWeight = + if (pagerState.currentPage == index) + FontWeight.Medium + else FontWeight.Normal, + fontSize = 20.sp, + lineHeight = (20 * 1.4).sp, + color = + if (pagerState.currentPage == index) TextColor + else Accent, + textAlign = TextAlign.Center, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + }, ) } } @@ -296,56 +376,51 @@ fun FriendDetailScreenContent( Box(modifier = Modifier.weight(1f)) { if (isLoadingTimetable) { Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center, - ) { - CircularProgressIndicator() - } + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { CircularProgressIndicator() } } else { HorizontalPager( - state = pagerState, - modifier = Modifier.fillMaxSize(), + state = pagerState, + modifier = Modifier.fillMaxSize(), ) { page -> DayScheduleContent( - periods = friendTimetableData[page] ?: emptyList(), - dayIndex = page, - quote = quote, + periods = friendTimetableData[page] ?: emptyList(), + dayIndex = page, + quote = quote, ) } } } } else { Box( - modifier = - Modifier - .fillMaxWidth() - .weight(1f) - .padding(32.dp), - contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxWidth().weight(1f).padding(32.dp), + contentAlignment = Alignment.Center, ) { Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp), ) { Icon( - painter = painterResource(R.drawable.ic_community_outline), - contentDescription = "No Access", - modifier = Modifier.size(64.dp), - tint = Accent.copy(alpha = 0.5f), + painter = painterResource(R.drawable.ic_community_outline), + contentDescription = "No Access", + modifier = Modifier.size(64.dp), + tint = Accent.copy(alpha = 0.5f), ) Text( - text = "Schedule Not Available", - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold, - color = TextColor, - textAlign = TextAlign.Center, + text = "Schedule Not Available", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + color = TextColor, + textAlign = TextAlign.Center, ) Text( - text = "You can no longer view ${friend.name}'s schedule as they are not in your friends list.", - style = MaterialTheme.typography.bodyMedium, - color = Accent, - textAlign = TextAlign.Center, - lineHeight = 20.sp, + text = + "You can no longer view ${friend.name}'s schedule as they are not in your friends list.", + style = MaterialTheme.typography.bodyMedium, + color = Accent, + textAlign = TextAlign.Center, + lineHeight = 20.sp, ) } } @@ -354,115 +429,121 @@ fun FriendDetailScreenContent( if (showUnfriendDialog) { AlertDialog( - onDismissRequest = { showUnfriendDialog = false }, - title = { - Text( - text = "Unfriend ${friend.name}?", - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold, - ) - }, - text = { - Text( - text = "Are you sure you want to remove ${friend.name} from your friends list? You won't be able to see their schedule anymore.", - style = - MaterialTheme.typography.bodyMedium.copy( - color = Accent, - ), - ) - }, - containerColor = Secondary, - confirmButton = { - TextButton( - onClick = { - showUnfriendDialog = false - val sharedPreferences = context.getSharedPreferences(Constants.USER_INFO, Context.MODE_PRIVATE) - val token = sharedPreferences.getString(Constants.COMMUNITY_TOKEN, "") ?: "" - val username = sharedPreferences.getString(Constants.COMMUNITY_USERNAME, "") ?: "" - if (token.isNotEmpty()) { - connectViewModel.unfriend(token, friend.username) - connectViewModel.getFriendList(token, username) - } - }, - ) { + onDismissRequest = { showUnfriendDialog = false }, + title = { Text( - text = "Unfriend", - color = Red, - fontWeight = FontWeight.Medium, + text = "Unfriend ${friend.name}?", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, ) - } - }, - dismissButton = { - TextButton( - onClick = { showUnfriendDialog = false }, - ) { + }, + text = { Text( - text = "Cancel", - color = TextColor, - fontWeight = FontWeight.Medium, + text = + "Are you sure you want to remove ${friend.name} from your friends list? " + + "You won't be able to see their schedule anymore.", + style = + MaterialTheme.typography.bodyMedium.copy( + color = Accent, + ), ) - } - }, + }, + containerColor = Secondary, + confirmButton = { + TextButton( + onClick = { + showUnfriendDialog = false + val sharedPreferences = + context.getSharedPreferences( + Constants.USER_INFO, + Context.MODE_PRIVATE + ) + val token = + sharedPreferences.getString(Constants.COMMUNITY_TOKEN, "") + ?: "" + val username = + sharedPreferences.getString( + Constants.COMMUNITY_USERNAME, + "" + ) + ?: "" + if (token.isNotEmpty()) { + connectViewModel.unfriend(token, friend.username) + connectViewModel.getFriendList(token, username) + } + }, + ) { + Text( + text = "Unfriend", + color = Red, + fontWeight = FontWeight.Medium, + ) + } + }, + dismissButton = { + TextButton( + onClick = { showUnfriendDialog = false }, + ) { + Text( + text = "Cancel", + color = TextColor, + fontWeight = FontWeight.Medium, + ) + } + }, ) } } @Composable fun FriendProfileCard( - friend: UserResponse, - hasUnfriended: Boolean, - isSendingRequest: Boolean, - hasRequestSent: Boolean, - onUnfriendClick: () -> Unit, - onSendRequestClick: () -> Unit, + friend: UserResponse, + hasUnfriended: Boolean, + isSendingRequest: Boolean, + hasRequestSent: Boolean, + isFriendGhosted: Boolean, + isTogglingGhostMode: Boolean, + onUnfriendClick: () -> Unit, + onSendRequestClick: () -> Unit, + onToggleGhostMode: (Boolean) -> Unit, ) { Card( - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - colors = CardDefaults.cardColors(containerColor = Secondary), - elevation = CardDefaults.cardElevation(defaultElevation = 4.dp), - shape = RoundedCornerShape(16.dp), + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + colors = CardDefaults.cardColors(containerColor = Secondary), + elevation = CardDefaults.cardElevation(defaultElevation = 4.dp), + shape = RoundedCornerShape(16.dp), ) { Column( - modifier = - Modifier - .fillMaxWidth() - .padding(20.dp), + modifier = Modifier.fillMaxWidth().padding(20.dp), ) { Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, ) { if (friend.picture.isNotEmpty()) { AsyncImage( - model = friend.picture, - contentDescription = null, - modifier = - Modifier - .size(64.dp) - .clip(CircleShape), - placeholder = painterResource(R.drawable.ic_gdscvit), - error = painterResource(R.drawable.ic_gdscvit), + model = friend.picture, + contentDescription = null, + modifier = Modifier.size(64.dp).clip(CircleShape), + placeholder = painterResource(R.drawable.ic_gdscvit), + error = painterResource(R.drawable.ic_gdscvit), ) } else { Box( - modifier = - Modifier - .size(64.dp) - .background(Accent.copy(alpha = 0.2f), CircleShape), - contentAlignment = Alignment.Center, + modifier = + Modifier.size(64.dp) + .background(Accent.copy(alpha = 0.2f), CircleShape), + contentAlignment = Alignment.Center, ) { Text( - text = - friend.name - .take(2) - .map { it.uppercaseChar() } - .joinToString(""), - color = TextColor, - fontWeight = FontWeight.Bold, - fontSize = 22.sp, + text = + friend.name + .take(2) + .map { it.uppercaseChar() } + .joinToString(""), + color = TextColor, + fontWeight = FontWeight.Bold, + fontSize = 22.sp, ) } } @@ -471,114 +552,156 @@ fun FriendProfileCard( Column(modifier = Modifier.weight(1f)) { Text( - text = friend.name, - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold, - color = TextColor, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - lineHeight = 24.sp, + text = friend.name, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + color = TextColor, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + lineHeight = 24.sp, ) Spacer(modifier = Modifier.height(4.dp)) Text( - text = "@${friend.username}", - style = MaterialTheme.typography.bodyMedium, - color = Accent, - maxLines = 1, - overflow = TextOverflow.Ellipsis, + text = "@${friend.username}", + style = MaterialTheme.typography.bodyMedium, + color = Accent, + maxLines = 1, + overflow = TextOverflow.Ellipsis, ) } } Spacer(modifier = Modifier.height(16.dp)) + if (!hasUnfriended) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Column { + Text( + text = + if (isFriendGhosted) "Friend is ghosted" + else "Friend is visible", + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium, + color = + if (isFriendGhosted) Accent.copy(alpha = 0.6f) + else TextColor, + ) + Text( + text = + if (isFriendGhosted) "They won't see your activity" + else "They can see your activity", + style = MaterialTheme.typography.bodySmall, + color = Accent.copy(alpha = 0.7f), + ) + } + + Switch( + checked = isFriendGhosted, + onCheckedChange = onToggleGhostMode, + enabled = !isTogglingGhostMode, + colors = + SwitchDefaults.colors( + checkedThumbColor = MaterialTheme.colorScheme.onPrimary, + checkedTrackColor = Accent, + uncheckedThumbColor = Accent, + uncheckedTrackColor = Secondary, + checkedBorderColor = Accent, + uncheckedBorderColor = Accent, + ), + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + } + if (hasUnfriended) { if (hasRequestSent) { Button( - onClick = {}, - enabled = false, - colors = - ButtonDefaults.buttonColors( - containerColor = Accent.copy(alpha = 0.1f), - contentColor = Accent.copy(alpha = 0.6f), - ), - modifier = - Modifier - .fillMaxWidth() - .height(48.dp), - shape = RoundedCornerShape(12.dp), + onClick = {}, + enabled = false, + colors = + ButtonDefaults.buttonColors( + containerColor = Accent.copy(alpha = 0.1f), + contentColor = Accent.copy(alpha = 0.6f), + ), + modifier = Modifier.fillMaxWidth().height(48.dp), + shape = RoundedCornerShape(12.dp), ) { Text( - text = "Request Sent", - style = MaterialTheme.typography.labelLarge, - fontWeight = FontWeight.Medium, + text = "Request Sent", + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.Medium, ) } } else { Button( - onClick = onSendRequestClick, - enabled = !isSendingRequest, - colors = - ButtonDefaults.buttonColors( - containerColor = if (isSendingRequest) Accent.copy(alpha = 0.1f) else Accent, - contentColor = if (isSendingRequest) Accent.copy(alpha = 0.6f) else MaterialTheme.colorScheme.onPrimary, - ), - modifier = - Modifier - .fillMaxWidth() - .height(48.dp), - shape = RoundedCornerShape(12.dp), + onClick = onSendRequestClick, + enabled = !isSendingRequest, + colors = + ButtonDefaults.buttonColors( + containerColor = + if (isSendingRequest) Accent.copy(alpha = 0.1f) + else Accent, + contentColor = + if (isSendingRequest) Accent.copy(alpha = 0.6f) + else MaterialTheme.colorScheme.onPrimary, + ), + modifier = Modifier.fillMaxWidth().height(48.dp), + shape = RoundedCornerShape(12.dp), ) { if (isSendingRequest) { Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), ) { CircularProgressIndicator( - modifier = Modifier.size(18.dp), - strokeWidth = 2.dp, - color = Accent.copy(alpha = 0.7f), + modifier = Modifier.size(18.dp), + strokeWidth = 2.dp, + color = Accent.copy(alpha = 0.7f), ) Text( - text = "Sending Request...", - style = MaterialTheme.typography.labelLarge, - fontWeight = FontWeight.Medium, + text = "Sending Request...", + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.Medium, ) } } else { Text( - text = "Send Friend Request", - style = MaterialTheme.typography.labelLarge, - fontWeight = FontWeight.Medium, + text = "Send Friend Request", + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.Medium, ) } } } } else { Button( - onClick = onUnfriendClick, - colors = - ButtonDefaults.buttonColors( - containerColor = Red.copy(alpha = 0.1f), - contentColor = Red, - ), - modifier = - Modifier - .fillMaxWidth() - .height(48.dp) - .border( - width = 1.dp, - color = Red, - shape = RoundedCornerShape(12.dp), - ), - shape = RoundedCornerShape(12.dp), + onClick = onUnfriendClick, + colors = + ButtonDefaults.buttonColors( + containerColor = Red.copy(alpha = 0.1f), + contentColor = Red, + ), + modifier = + Modifier.fillMaxWidth() + .height(48.dp) + .border( + width = 1.dp, + color = Red, + shape = RoundedCornerShape(12.dp), + ), + shape = RoundedCornerShape(12.dp), ) { Text( - text = "Unfriend", - style = MaterialTheme.typography.labelLarge, - fontWeight = FontWeight.Medium, + text = "Unfriend", + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.Medium, ) } } @@ -588,60 +711,54 @@ fun FriendProfileCard( @Composable private fun DayScheduleContent( - periods: List, - dayIndex: Int, - quote: String = "Every day is a new opportunity to learn and grow.", + periods: List, + dayIndex: Int, + quote: String = "Every day is a new opportunity to learn and grow.", ) { if (periods.isEmpty()) { Box( - modifier = - Modifier - .fillMaxSize() - .padding(horizontal = 18.dp, vertical = 32.dp), - contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize().padding(horizontal = 18.dp, vertical = 32.dp), + contentAlignment = Alignment.Center, ) { Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp), ) { Icon( - painter = painterResource(R.drawable.ic_timetable_outline), - contentDescription = "No Classes", - modifier = Modifier.size(64.dp), - tint = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.5f), + painter = painterResource(R.drawable.ic_timetable_outline), + contentDescription = "No Classes", + modifier = Modifier.size(64.dp), + tint = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.5f), ) Spacer(modifier = Modifier.height(8.dp)) Text( - text = "No classes today!", - style = MaterialTheme.typography.headlineMedium, - color = MaterialTheme.colorScheme.onBackground, - fontWeight = FontWeight.Bold, + text = "No classes today!", + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.onBackground, + fontWeight = FontWeight.Bold, ) Text( - text = quote, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.7f), - textAlign = TextAlign.Center, - lineHeight = 20.sp, + text = quote, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.7f), + textAlign = TextAlign.Center, + lineHeight = 20.sp, ) } } } else { LazyColumn( - modifier = - Modifier - .fillMaxSize() - .padding(horizontal = 18.dp), - verticalArrangement = Arrangement.spacedBy(16.dp), - contentPadding = PaddingValues(top = 16.dp, bottom = 64.dp), + modifier = Modifier.fillMaxSize().padding(horizontal = 18.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + contentPadding = PaddingValues(top = 16.dp, bottom = 64.dp), ) { items( - items = periods, - key = { period -> "${period.courseCode}_${period.slot}" }, + items = periods, + key = { period -> "${period.courseCode}_${period.slot}" }, ) { period -> FriendPeriodCard( - period = period, - dayIndex = dayIndex, + period = period, + dayIndex = dayIndex, ) } } @@ -650,87 +767,82 @@ private fun DayScheduleContent( @Composable private fun FriendPeriodCard( - period: PeriodDetails, - dayIndex: Int, + period: PeriodDetails, + dayIndex: Int, ) { val timeFormat = remember { SimpleDateFormat("h:mm a", Locale.getDefault()) } val startTimeStr = - remember(period.startTime) { - timeFormat.format(period.startTime.toDate()).uppercase() - } + remember(period.startTime) { timeFormat.format(period.startTime.toDate()).uppercase() } val endTimeStr = - remember(period.endTime) { - timeFormat.format(period.endTime.toDate()).uppercase() - } + remember(period.endTime) { timeFormat.format(period.endTime.toDate()).uppercase() } val now = Calendar.getInstance() val isToday = - remember(dayIndex) { - val todayIndex = - when (now.get(Calendar.DAY_OF_WEEK)) { - Calendar.MONDAY -> 0 - Calendar.TUESDAY -> 1 - Calendar.WEDNESDAY -> 2 - Calendar.THURSDAY -> 3 - Calendar.FRIDAY -> 4 - Calendar.SATURDAY -> 5 - Calendar.SUNDAY -> 6 - else -> -1 - } - dayIndex == todayIndex - } + remember(dayIndex) { + val todayIndex = + when (now.get(Calendar.DAY_OF_WEEK)) { + Calendar.MONDAY -> 0 + Calendar.TUESDAY -> 1 + Calendar.WEDNESDAY -> 2 + Calendar.THURSDAY -> 3 + Calendar.FRIDAY -> 4 + Calendar.SATURDAY -> 5 + Calendar.SUNDAY -> 6 + else -> -1 + } + dayIndex == todayIndex + } val isActive = - if (!isToday) { - false - } else { - val startTime = Calendar.getInstance().apply { time = period.startTime.toDate() } - val endTime = Calendar.getInstance().apply { time = period.endTime.toDate() } - val currentTime = Calendar.getInstance() - - val currentHourMinute = currentTime.get(Calendar.HOUR_OF_DAY) * 60 + currentTime.get(Calendar.MINUTE) - val startHourMinute = startTime.get(Calendar.HOUR_OF_DAY) * 60 + startTime.get(Calendar.MINUTE) - val endHourMinute = endTime.get(Calendar.HOUR_OF_DAY) * 60 + endTime.get(Calendar.MINUTE) - - currentHourMinute in startHourMinute..endHourMinute - } + if (!isToday) { + false + } else { + val startTime = Calendar.getInstance().apply { time = period.startTime.toDate() } + val endTime = Calendar.getInstance().apply { time = period.endTime.toDate() } + val currentTime = Calendar.getInstance() + + val currentHourMinute = + currentTime.get(Calendar.HOUR_OF_DAY) * 60 + + currentTime.get(Calendar.MINUTE) + val startHourMinute = + startTime.get(Calendar.HOUR_OF_DAY) * 60 + startTime.get(Calendar.MINUTE) + val endHourMinute = + endTime.get(Calendar.HOUR_OF_DAY) * 60 + endTime.get(Calendar.MINUTE) + + currentHourMinute in startHourMinute..endHourMinute + } Card( - modifier = - Modifier - .fillMaxWidth(), - colors = CardDefaults.cardColors(containerColor = Secondary), - border = - if (isActive) { - BorderStroke( - 1.dp, - Accent, - ) - } else { - null - }, - shape = RoundedCornerShape(16.dp), + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = Secondary), + border = + if (isActive) { + BorderStroke( + 1.dp, + Accent, + ) + } else { + null + }, + shape = RoundedCornerShape(16.dp), ) { Column( - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp, vertical = 28.dp), + modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp, vertical = 28.dp), ) { Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.Top, + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top, ) { Column(modifier = Modifier.weight(1f)) { Text( - text = period.courseName, - style = MaterialTheme.typography.labelLarge, - fontWeight = FontWeight.Bold, - color = TextColor, - maxLines = 2, - overflow = TextOverflow.Ellipsis, + text = period.courseName, + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.Bold, + color = TextColor, + maxLines = 2, + overflow = TextOverflow.Ellipsis, ) } } @@ -738,47 +850,47 @@ private fun FriendPeriodCard( Spacer(modifier = Modifier.height(4.dp)) Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, ) { Text( - text = "$startTimeStr - $endTimeStr | ${period.slot}", - style = MaterialTheme.typography.bodyMedium, - color = Accent, - fontWeight = FontWeight.Medium, + text = "$startTimeStr - $endTimeStr | ${period.slot}", + style = MaterialTheme.typography.bodyMedium, + color = Accent, + fontWeight = FontWeight.Medium, ) if (period.roomNo.isNotEmpty()) { Box( - modifier = - Modifier - .clip(RoundedCornerShape(9999.dp)) - .border( - width = 1.dp, - color = Accent, - shape = RoundedCornerShape(9999.dp), - ).padding(horizontal = 8.dp, vertical = 4.dp), + modifier = + Modifier.clip(RoundedCornerShape(9999.dp)) + .border( + width = 1.dp, + color = Accent, + shape = RoundedCornerShape(9999.dp), + ) + .padding(horizontal = 8.dp, vertical = 4.dp), ) { Row(verticalAlignment = Alignment.CenterVertically) { Image( - painter = painterResource(id = R.drawable.ic_compass), - contentDescription = "Compass icon", - modifier = Modifier.size(12.dp), - alignment = Alignment.Center, + painter = painterResource(id = R.drawable.ic_compass), + contentDescription = "Compass icon", + modifier = Modifier.size(12.dp), + alignment = Alignment.Center, ) Spacer(modifier = Modifier.width(4.dp)) Text( - text = period.roomNo, - style = - MaterialTheme.typography.bodyMedium.copy( - fontSize = 12.sp, - lineHeight = 12.sp, - letterSpacing = (-0.12).sp, - textAlign = TextAlign.Center, - ), - color = MaterialTheme.colorScheme.primary, - fontWeight = FontWeight.Medium, + text = period.roomNo, + style = + MaterialTheme.typography.bodyMedium.copy( + fontSize = 12.sp, + lineHeight = 12.sp, + letterSpacing = (-0.12).sp, + textAlign = TextAlign.Center, + ), + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.Medium, ) } } @@ -788,41 +900,54 @@ private fun FriendPeriodCard( } } -private suspend fun processFriendTimetableData(friend: TimetableResponse): Map> = - withContext(Dispatchers.Default) { - val timetableData = friend.data - val dayNames = listOf("monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday") - - val result = mutableMapOf>() - - dayNames.forEachIndexed { index, dayName -> - val courses = - when (dayName) { - "monday" -> timetableData.Monday - "tuesday" -> timetableData.Tuesday - "wednesday" -> timetableData.Wednesday - "thursday" -> timetableData.Thursday - "friday" -> timetableData.Friday - "saturday" -> timetableData.Saturday - "sunday" -> timetableData.Sunday - else -> emptyList() - } +private suspend fun processFriendTimetableData( + friend: TimetableResponse +): Map> = + withContext(Dispatchers.Default) { + val timetableData = friend.data + val dayNames = + listOf( + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + "sunday" + ) - val periods = - courses - ?.map { course -> - PeriodDetails( - courseCode = course.code, - courseName = course.name, - startTime = parseTimeToTimestamp(course.start_time), - endTime = parseTimeToTimestamp(course.end_time), - slot = course.slot, - roomNo = course.venue, - ) - }?.sortedBy { it.startTime.toDate() } ?: emptyList() + val result = mutableMapOf>() + + dayNames.forEachIndexed { index, dayName -> + val courses = + when (dayName) { + "monday" -> timetableData.Monday + "tuesday" -> timetableData.Tuesday + "wednesday" -> timetableData.Wednesday + "thursday" -> timetableData.Thursday + "friday" -> timetableData.Friday + "saturday" -> timetableData.Saturday + "sunday" -> timetableData.Sunday + else -> emptyList() + } - result[index] = periods - } + val periods = + courses + ?.map { course -> + PeriodDetails( + courseCode = course.code, + courseName = course.name, + startTime = parseTimeToTimestamp(course.start_time), + endTime = parseTimeToTimestamp(course.end_time), + slot = course.slot, + roomNo = course.venue, + ) + } + ?.sortedBy { it.startTime.toDate() } + ?: emptyList() + + result[index] = periods + } - result - } + result + } diff --git a/app/src/main/java/com/dscvit/vitty/ui/main/MainComposeApp.kt b/app/src/main/java/com/dscvit/vitty/ui/main/MainComposeApp.kt index c0be67b..0437040 100644 --- a/app/src/main/java/com/dscvit/vitty/ui/main/MainComposeApp.kt +++ b/app/src/main/java/com/dscvit/vitty/ui/main/MainComposeApp.kt @@ -3,7 +3,6 @@ package com.dscvit.vitty.ui.main import android.app.Activity import android.content.Intent import android.content.SharedPreferences -import android.widget.Toast import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.spring @@ -25,12 +24,12 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Card @@ -43,8 +42,6 @@ import androidx.compose.material3.ModalDrawerSheet import androidx.compose.material3.ModalNavigationDrawer import androidx.compose.material3.NavigationDrawerItem import androidx.compose.material3.NavigationDrawerItemDefaults -import androidx.compose.material3.Switch -import androidx.compose.material3.SwitchDefaults import androidx.compose.material3.Text import androidx.compose.material3.rememberDrawerState import androidx.compose.runtime.Composable @@ -63,14 +60,11 @@ import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.core.content.edit import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost @@ -80,9 +74,6 @@ import androidx.navigation.compose.rememberNavController import coil.compose.AsyncImage import com.dscvit.vitty.R import com.dscvit.vitty.activity.SettingsActivity -import com.dscvit.vitty.network.api.community.APICommunityRestClient -import com.dscvit.vitty.network.api.community.RetrofitUserActionListener -import com.dscvit.vitty.network.api.community.responses.user.PostResponse import com.dscvit.vitty.network.api.community.responses.user.UserResponse import com.dscvit.vitty.theme.Accent import com.dscvit.vitty.theme.Background @@ -115,13 +106,13 @@ import com.dscvit.vitty.util.LogoutHelper import com.dscvit.vitty.util.SemesterUtils import com.dscvit.vitty.util.UtilFunctions import com.google.gson.Gson +import com.google.gson.reflect.TypeToken import java.net.URLDecoder import java.net.URLEncoder import java.nio.charset.StandardCharsets import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import retrofit2.Call @Composable fun MainComposeApp() { @@ -194,6 +185,29 @@ fun MainComposeApp() { } } + LaunchedEffect(Unit) { + val activeFriendsFetched = prefs.getBoolean(Constants.ACTIVE_FRIENDS_FETCHED, false) + val activeFriendsList = prefs.getString(Constants.ACTIVE_FRIENDS_LIST, "") + val listType = object : TypeToken>() {}.type + val cachedList: List = + try { + Gson().fromJson(activeFriendsList, listType) ?: emptyList() + } catch (e: Exception) { + emptyList() + } + + if (!activeFriendsFetched) { + val token = prefs.getString(Constants.COMMUNITY_TOKEN, null) + if (!token.isNullOrEmpty()) { + connectViewModel.fetchActiveFriends(token, prefs) + } + } + + if (activeFriendsFetched) { + connectViewModel.updateActiveFriendsList(cachedList) + } + } + VittyTheme { ModalNavigationDrawer( drawerState = drawerState, @@ -353,7 +367,7 @@ fun MainComposeApp() { val friendData = URLDecoder.decode( encodedFriendData, - StandardCharsets.UTF_8.toString() + StandardCharsets.UTF_8.toString(), ) val friend = @@ -515,12 +529,12 @@ fun MainComposeApp() { val courseTitle = URLDecoder.decode( encodedCourseTitle, - StandardCharsets.UTF_8.toString() + StandardCharsets.UTF_8.toString(), ) val courseCode = URLDecoder.decode( encodedCourseCode, - StandardCharsets.UTF_8.toString() + StandardCharsets.UTF_8.toString(), ) CoursePageContent( courseTitle = courseTitle, @@ -541,7 +555,7 @@ fun MainComposeApp() { } ?: "new" navController.navigate( - "note_screen/$encodedCourseCodeParam/$encodedNoteId" + "note_screen/$encodedCourseCodeParam/$encodedNoteId", ) }, ) @@ -575,7 +589,7 @@ fun MainComposeApp() { val courseCode = URLDecoder.decode( encodedCourseCode, - StandardCharsets.UTF_8.toString() + StandardCharsets.UTF_8.toString(), ) val noteId = if (encodedNoteId == "new") { @@ -672,12 +686,12 @@ fun MainComposeApp() { val circleData = URLDecoder.decode( encodedCircleData, - StandardCharsets.UTF_8.toString() + StandardCharsets.UTF_8.toString(), ) val circleMembersData = URLDecoder.decode( encodedCircleMembersData, - StandardCharsets.UTF_8.toString() + StandardCharsets.UTF_8.toString(), ) val circle = @@ -686,7 +700,7 @@ fun MainComposeApp() { circleData, com.dscvit.vitty.network.api.community.responses .user.CircleItem::class - .java + .java, ) } catch (e: Exception) { null @@ -714,10 +728,10 @@ fun MainComposeApp() { val encodedMemberData = URLEncoder.encode( memberJson, - StandardCharsets.UTF_8.toString() + StandardCharsets.UTF_8.toString(), ) navController.navigate( - "circle_member_detail/$encodedMemberData/$circleId" + "circle_member_detail/$encodedMemberData/$circleId", ) }, onAddParticipantsClick = { circleId: String -> @@ -758,7 +772,7 @@ fun MainComposeApp() { val memberData = URLDecoder.decode( encodedMemberData, - StandardCharsets.UTF_8.toString() + StandardCharsets.UTF_8.toString(), ) val member = @@ -1086,17 +1100,11 @@ fun DrawerContent( val name = remember { prefs.getString(Constants.COMMUNITY_NAME, "") ?: "Name" } var campus by remember { mutableStateOf(prefs.getString(Constants.COMMUNITY_CAMPUS, "") ?: "") } - var isGhostModeEnabled by remember { - mutableStateOf(prefs.getBoolean(Constants.GHOST_MODE, false)) - } DisposableEffect(Unit) { val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key -> when (key) { - Constants.GHOST_MODE -> { - isGhostModeEnabled = prefs.getBoolean(Constants.GHOST_MODE, false) - } Constants.COMMUNITY_CAMPUS -> { campus = prefs.getString(Constants.COMMUNITY_CAMPUS, "") ?: "" } @@ -1113,7 +1121,10 @@ fun DrawerContent( modifier = Modifier.fillMaxWidth(0.8f), ) { Column( - modifier = Modifier.fillMaxWidth().padding(horizontal = 26.dp, vertical = 28.dp), + modifier = + Modifier.fillMaxWidth() + .fillMaxHeight() + .padding(horizontal = 26.dp, vertical = 28.dp), ) { Row( modifier = Modifier.fillMaxWidth().padding(top = 20.dp), @@ -1136,7 +1147,7 @@ fun DrawerContent( overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.labelLarge.copy( - fontWeight = FontWeight.Normal, + fontWeight = FontWeight.Normal ), ) @@ -1158,10 +1169,7 @@ fun DrawerContent( Spacer(modifier = Modifier.height(32.dp)) - HorizontalDivider( - color = Accent, - thickness = 1.dp, - ) + HorizontalDivider(color = Accent, thickness = 1.dp) Spacer(modifier = Modifier.height(24.dp)) @@ -1181,7 +1189,7 @@ fun DrawerContent( color = TextColor, style = MaterialTheme.typography.labelLarge.copy( - fontWeight = FontWeight.Normal, + fontWeight = FontWeight.Normal ), ) }, @@ -1209,12 +1217,12 @@ fun DrawerContent( label = { Text( modifier = Modifier.padding(start = 24.dp), + text = "Settings", + color = TextColor, style = MaterialTheme.typography.labelLarge.copy( - fontWeight = FontWeight.Normal, + fontWeight = FontWeight.Normal ), - text = "Settings", - color = TextColor, ) }, selected = false, @@ -1231,10 +1239,7 @@ fun DrawerContent( Spacer(modifier = Modifier.height(24.dp)) - HorizontalDivider( - color = Accent, - thickness = 1.dp, - ) + HorizontalDivider(color = Accent, thickness = 1.dp) Spacer(modifier = Modifier.height(24.dp)) @@ -1249,12 +1254,12 @@ fun DrawerContent( label = { Text( modifier = Modifier.padding(start = 24.dp), + text = "Share", + color = TextColor, style = MaterialTheme.typography.labelLarge.copy( - fontWeight = FontWeight.Normal, + fontWeight = FontWeight.Normal ), - text = "Share", - color = TextColor, ) }, selected = false, @@ -1289,12 +1294,12 @@ fun DrawerContent( label = { Text( modifier = Modifier.padding(start = 24.dp), + text = "Support", + color = TextColor, style = MaterialTheme.typography.labelLarge.copy( - fontWeight = FontWeight.Normal, + fontWeight = FontWeight.Normal ), - text = "Support", - color = TextColor, ) }, selected = false, @@ -1311,164 +1316,7 @@ fun DrawerContent( Spacer(modifier = Modifier.height(24.dp)) - HorizontalDivider( - color = Accent, - thickness = 1.dp, - ) - - Spacer(modifier = Modifier.height(24.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Column( - modifier = Modifier.weight(1f), - ) { - Text( - text = - buildAnnotatedString { - append("Ghost Mode ") - addStyle( - style = - SpanStyle( - fontWeight = FontWeight.Bold, - fontSize = 12.sp, - letterSpacing = (-0.12).sp, - color = Accent, - ), - start = 0, - end = "Ghost Mode".length, - ) - - append("(your timetable will be visible only to you)") - addStyle( - style = - SpanStyle( - fontWeight = FontWeight.Medium, - fontSize = 12.sp, - letterSpacing = (-0.12).sp, - color = Accent, - ), - start = "Ghost Mode ".length, - end = - "Ghost Mode (your timetable will be visible only to you)".length, - ) - }, - ) - } - - Spacer(modifier = Modifier.width(16.dp)) - - Switch( - checked = isGhostModeEnabled, - onCheckedChange = { isChecked -> - val token = prefs.getString(Constants.COMMUNITY_TOKEN, "") ?: "" - val currentUsername = - prefs.getString(Constants.COMMUNITY_USERNAME, "") ?: "" - - if (token.isNotEmpty() && currentUsername.isNotEmpty()) { - if (isChecked) { - APICommunityRestClient.instance.enableGhostMode( - token = token, - username = currentUsername, - retrofitUserActionListener = - object : RetrofitUserActionListener { - override fun onSuccess( - call: Call?, - response: PostResponse?, - ) { - isGhostModeEnabled = true - prefs.edit { - putBoolean( - Constants.GHOST_MODE, - true - ) - } - Toast.makeText( - context, - "Ghost mode enabled", - Toast.LENGTH_SHORT - ) - .show() - } - - override fun onError( - call: Call?, - t: Throwable?, - ) { - isGhostModeEnabled = false - Toast.makeText( - context, - "Failed to enable ghost mode", - Toast.LENGTH_SHORT - ) - .show() - } - }, - ) - } else { - APICommunityRestClient.instance.disableGhostMode( - token = token, - username = currentUsername, - retrofitUserActionListener = - object : RetrofitUserActionListener { - override fun onSuccess( - call: Call?, - response: PostResponse?, - ) { - isGhostModeEnabled = false - prefs.edit { - putBoolean( - Constants.GHOST_MODE, - false - ) - } - Toast.makeText( - context, - "Ghost mode disabled", - Toast.LENGTH_SHORT - ) - .show() - } - - override fun onError( - call: Call?, - t: Throwable?, - ) { - isGhostModeEnabled = true - Toast.makeText( - context, - "Failed to disable ghost mode", - Toast.LENGTH_SHORT - ) - .show() - } - }, - ) - } - } else { - isGhostModeEnabled = isChecked - prefs.edit { putBoolean(Constants.GHOST_MODE, isChecked) } - Toast.makeText( - context, - "Ghost mode updated locally", - Toast.LENGTH_SHORT - ) - .show() - } - }, - colors = - SwitchDefaults.colors( - checkedThumbColor = Background, - checkedTrackColor = Accent, - uncheckedThumbColor = Color(0xff768EA4), - uncheckedTrackColor = Color(0x33475985), - uncheckedBorderColor = Color.Transparent, - ), - ) - } + HorizontalDivider(color = Accent, thickness = 1.dp) Spacer(modifier = Modifier.weight(1f)) @@ -1476,24 +1324,23 @@ fun DrawerContent( icon = { Icon( painter = painterResource(R.drawable.ic_logout_2), - contentDescription = "log out", - tint = Color(0xffFF0000), + contentDescription = "Log out", + tint = Color(0xFFFF0000), ) }, label = { Text( + text = "Log out", + color = Color(0xFFFF0000), style = MaterialTheme.typography.labelLarge.copy( - fontWeight = FontWeight.Normal, + fontWeight = FontWeight.Normal ), - text = "log out", - color = Color(0xffFF0000), ) }, selected = false, onClick = { onCloseDrawer() - val activity = context as? Activity if (activity != null) { LogoutHelper.logout(context, activity, prefs) diff --git a/app/src/main/java/com/dscvit/vitty/util/Constants.kt b/app/src/main/java/com/dscvit/vitty/util/Constants.kt index 559acae..e5e2bbd 100755 --- a/app/src/main/java/com/dscvit/vitty/util/Constants.kt +++ b/app/src/main/java/com/dscvit/vitty/util/Constants.kt @@ -66,6 +66,9 @@ object Constants { const val REMINDER_CHANNEL_DESC = "Notifications for course reminders" const val PREF_LAST_REVIEW_REQUEST = "last_review_request" + + const val ACTIVE_FRIENDS_FETCHED = "active_friends_fetched" + const val ACTIVE_FRIENDS_LIST = "active_friends_list" } fun String.urlDecode(): String = diff --git a/app/src/main/java/com/dscvit/vitty/util/LogoutHelper.kt b/app/src/main/java/com/dscvit/vitty/util/LogoutHelper.kt index 213e5d2..43ecd54 100755 --- a/app/src/main/java/com/dscvit/vitty/util/LogoutHelper.kt +++ b/app/src/main/java/com/dscvit/vitty/util/LogoutHelper.kt @@ -58,6 +58,8 @@ object LogoutHelper { putString(Constants.COMMUNITY_CAMPUS, null) putBoolean(Constants.COMMUNITY_TIMETABLE_AVAILABLE, false) putString(Constants.CACHE_COMMUNITY_TIMETABLE, null) + putBoolean(Constants.ACTIVE_FRIENDS_FETCHED, false) + putString(Constants.ACTIVE_FRIENDS_FETCHED, null) apply() } From eba92941d8181199f494a182b9231ef644da92d1 Mon Sep 17 00:00:00 2001 From: JothishKamal Date: Wed, 30 Jul 2025 16:11:53 +0530 Subject: [PATCH 5/7] feat: add publishing configuration for markdown library --- app/build.gradle | 1 - markdowntext/build.gradle | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index eec18c2..816d9c0 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -85,7 +85,6 @@ dependencies { implementation 'androidx.navigation:navigation-ui-ktx:2.9.2' implementation 'androidx.navigation:navigation-compose:2.9.2' implementation 'androidx.compose.material3:material3-android:1.3.2' - implementation "androidx.compose.material:material-icons-extended:1.7.8" implementation 'androidx.compose.runtime:runtime-livedata:1.8.3' testImplementation 'junit:junit:4.13.2' diff --git a/markdowntext/build.gradle b/markdowntext/build.gradle index 123d47f..d48c6df 100644 --- a/markdowntext/build.gradle +++ b/markdowntext/build.gradle @@ -33,6 +33,9 @@ android { sourceCompatibility JavaVersion.VERSION_21 targetCompatibility JavaVersion.VERSION_21 } + publishing { + singleVariant("release") {} + } } dependencies { From aceb86402214ee9b484ecea0b272fb05eb51798f Mon Sep 17 00:00:00 2001 From: JothishKamal Date: Thu, 31 Jul 2025 20:11:02 +0530 Subject: [PATCH 6/7] fix: correct type declaration for activeFriends LiveData --- .../vitty/ui/connect/ConnectViewModel.kt | 1012 ++++++++--------- 1 file changed, 506 insertions(+), 506 deletions(-) diff --git a/app/src/main/java/com/dscvit/vitty/ui/connect/ConnectViewModel.kt b/app/src/main/java/com/dscvit/vitty/ui/connect/ConnectViewModel.kt index 8b05681..c00189e 100644 --- a/app/src/main/java/com/dscvit/vitty/ui/connect/ConnectViewModel.kt +++ b/app/src/main/java/com/dscvit/vitty/ui/connect/ConnectViewModel.kt @@ -48,7 +48,7 @@ class ConnectViewModel : ViewModel() { private val _sentCircleRequests = MutableLiveData() private val _isCircleRequestsLoading = MutableLiveData() private val _circleActionResponse = MutableLiveData() - private val _activeFriends = MutableLiveData>() + private val _activeFriends = MutableLiveData??>() private val _ghostModeResponse = MutableLiveData() val ghostModeResponse: MutableLiveData = _ghostModeResponse @@ -71,138 +71,138 @@ class ConnectViewModel : ViewModel() { val sentCircleRequests: MutableLiveData = _sentCircleRequests val isCircleRequestsLoading: MutableLiveData = _isCircleRequestsLoading val circleActionResponse: MutableLiveData = _circleActionResponse - val activeFriends: MutableLiveData> = _activeFriends + val activeFriends: MutableLiveData??> = _activeFriends fun getFriendList( - token: String, - username: String, + token: String, + username: String, ) { _isLoading.postValue(true) APICommunityRestClient.instance.getFriendList( - token, - username, - object : RetrofitFriendListListener { - override fun onSuccess( - call: Call?, - response: FriendResponse?, - ) { - Timber.d("ConnectFriendList: $response") - _friendList.postValue(response) - _isLoading.postValue(false) - } - - override fun onError( - call: Call?, - t: Throwable?, - ) { - _friendList.postValue(null) - _isLoading.postValue(false) - } - }, + token, + username, + object : RetrofitFriendListListener { + override fun onSuccess( + call: Call?, + response: FriendResponse?, + ) { + Timber.d("ConnectFriendList: $response") + _friendList.postValue(response) + _isLoading.postValue(false) + } + + override fun onError( + call: Call?, + t: Throwable?, + ) { + _friendList.postValue(null) + _isLoading.postValue(false) + } + }, ) } fun refreshFriendList( - token: String, - username: String, + token: String, + username: String, ) { _isRefreshing.postValue(true) APICommunityRestClient.instance.getFriendList( - token, - username, - object : RetrofitFriendListListener { - override fun onSuccess( - call: Call?, - response: FriendResponse?, - ) { - _friendList.postValue(response) - _isRefreshing.postValue(false) - } - - override fun onError( - call: Call?, - t: Throwable?, - ) { - _friendList.postValue(null) - _isRefreshing.postValue(false) - } - }, + token, + username, + object : RetrofitFriendListListener { + override fun onSuccess( + call: Call?, + response: FriendResponse?, + ) { + _friendList.postValue(response) + _isRefreshing.postValue(false) + } + + override fun onError( + call: Call?, + t: Throwable?, + ) { + _friendList.postValue(null) + _isRefreshing.postValue(false) + } + }, ) } fun getFriendRequest(token: String) { APICommunityRestClient.instance.getFriendRequest( - token, - object : RetrofitFriendRequestListener { - override fun onSuccess( - call: Call?, - response: RequestsResponse?, - ) { - Timber.d("ConnectFriendRequest: $response") - _friendRequest.postValue(response) - } - - override fun onError( - call: Call?, - t: Throwable?, - ) { - _friendRequest.postValue(null) - } - }, + token, + object : RetrofitFriendRequestListener { + override fun onSuccess( + call: Call?, + response: RequestsResponse?, + ) { + Timber.d("ConnectFriendRequest: $response") + _friendRequest.postValue(response) + } + + override fun onError( + call: Call?, + t: Throwable?, + ) { + _friendRequest.postValue(null) + } + }, ) } fun sendRequest( - token: String, - username: String, + token: String, + username: String, ) { APICommunityRestClient.instance.sendRequest( - token, - username, - object : RetrofitUserActionListener { - override fun onSuccess( - call: Call?, - response: PostResponse?, - ) { - Timber.d("ConnectSendRequest: $response") - _sendRequestResponse.postValue(response) - } - - override fun onError( - call: Call?, - t: Throwable?, - ) { - Timber.d("ConnectSendRequestError: ${t?.message}") - _sendRequestResponse.postValue(null) - } - }, + token, + username, + object : RetrofitUserActionListener { + override fun onSuccess( + call: Call?, + response: PostResponse?, + ) { + Timber.d("ConnectSendRequest: $response") + _sendRequestResponse.postValue(response) + } + + override fun onError( + call: Call?, + t: Throwable?, + ) { + Timber.d("ConnectSendRequestError: ${t?.message}") + _sendRequestResponse.postValue(null) + } + }, ) } fun unfriend( - token: String, - username: String, + token: String, + username: String, ) { APICommunityRestClient.instance.unfriend( - token, - username, - object : RetrofitUserActionListener { - override fun onSuccess( - call: Call?, - response: PostResponse?, - ) { - Timber.d("ConnectUnfriend: $response") - _unfriendSuccess.postValue(username) - } - - override fun onError( - call: Call?, - t: Throwable?, - ) { - Timber.d("ConnectUnfriendError: ${t?.message}") - _unfriendSuccess.postValue(null) - } - }, + token, + username, + object : RetrofitUserActionListener { + override fun onSuccess( + call: Call?, + response: PostResponse?, + ) { + Timber.d("ConnectUnfriend: $response") + _unfriendSuccess.postValue(username) + } + + override fun onError( + call: Call?, + t: Throwable?, + ) { + Timber.d("ConnectUnfriendError: ${t?.message}") + _unfriendSuccess.postValue(null) + } + }, ) } @@ -219,81 +219,81 @@ class ConnectViewModel : ViewModel() { } fun acceptRequest( - token: String, - username: String, + token: String, + username: String, ) { APICommunityRestClient.instance.acceptRequest( - token, - username, - object : RetrofitUserActionListener { - override fun onSuccess( - call: Call?, - response: PostResponse?, - ) { - Timber.d("AcceptRequest: $response") - _requestActionResponse.postValue(response) - } - - override fun onError( - call: Call?, - t: Throwable?, - ) { - Timber.d("AcceptRequest: ${t?.message}") - _requestActionResponse.postValue(null) - } - }, + token, + username, + object : RetrofitUserActionListener { + override fun onSuccess( + call: Call?, + response: PostResponse?, + ) { + Timber.d("AcceptRequest: $response") + _requestActionResponse.postValue(response) + } + + override fun onError( + call: Call?, + t: Throwable?, + ) { + Timber.d("AcceptRequest: ${t?.message}") + _requestActionResponse.postValue(null) + } + }, ) } fun rejectRequest( - token: String, - username: String, + token: String, + username: String, ) { APICommunityRestClient.instance.rejectRequest( - token, - username, - object : RetrofitUserActionListener { - override fun onSuccess( - call: Call?, - response: PostResponse?, - ) { - Timber.d("RejectRequest: $response") - _requestActionResponse.postValue(response) - } - - override fun onError( - call: Call?, - t: Throwable?, - ) { - Timber.d("RejectRequest: ${t?.message}") - _requestActionResponse.postValue(null) - } - }, + token, + username, + object : RetrofitUserActionListener { + override fun onSuccess( + call: Call?, + response: PostResponse?, + ) { + Timber.d("RejectRequest: $response") + _requestActionResponse.postValue(response) + } + + override fun onError( + call: Call?, + t: Throwable?, + ) { + Timber.d("RejectRequest: ${t?.message}") + _requestActionResponse.postValue(null) + } + }, ) } fun getCircleList(token: String) { _isCircleLoading.postValue(true) APICommunityRestClient.instance.getCircles( - token, - object : RetrofitCircleListener { - override fun onSuccess( - call: Call?, - response: CircleResponse?, - ) { - Timber.d("ConnectCircleList: $response") - _circleList.postValue(response) - _isCircleLoading.postValue(false) - } - - override fun onError( - call: Call?, - t: Throwable?, - ) { - _circleList.postValue(null) - _isCircleLoading.postValue(false) - } - }, + token, + object : RetrofitCircleListener { + override fun onSuccess( + call: Call?, + response: CircleResponse?, + ) { + Timber.d("ConnectCircleList: $response") + _circleList.postValue(response) + _isCircleLoading.postValue(false) + } + + override fun onError( + call: Call?, + t: Throwable?, + ) { + _circleList.postValue(null) + _isCircleLoading.postValue(false) + } + }, ) } @@ -303,139 +303,139 @@ class ConnectViewModel : ViewModel() { _circleMembersLoading.postValue(emptySet()) APICommunityRestClient.instance.getCircles( - token, - object : RetrofitCircleListener { - override fun onSuccess( - call: Call?, - response: CircleResponse?, - ) { - Timber.d("ConnectCircleList: $response") - _circleList.postValue(response) - _isCircleRefreshing.postValue(false) - - response?.data?.forEach { circle -> - getCircleDetails(token, circle.circle_id) - } - } - - override fun onError( - call: Call?, - t: Throwable?, - ) { - _circleList.postValue(null) - _isCircleRefreshing.postValue(false) - } - }, + token, + object : RetrofitCircleListener { + override fun onSuccess( + call: Call?, + response: CircleResponse?, + ) { + Timber.d("ConnectCircleList: $response") + _circleList.postValue(response) + _isCircleRefreshing.postValue(false) + + response?.data?.forEach { circle -> + getCircleDetails(token, circle.circle_id) + } + } + + override fun onError( + call: Call?, + t: Throwable?, + ) { + _circleList.postValue(null) + _isCircleRefreshing.postValue(false) + } + }, ) } fun getCircleDetails( - token: String, - circleId: String, + token: String, + circleId: String, ) { val currentLoading = _circleMembersLoading.value?.toMutableSet() ?: mutableSetOf() currentLoading.add(circleId) _circleMembersLoading.postValue(currentLoading) APICommunityRestClient.instance.getCircleDetails( - token, - circleId, - object : RetrofitFriendListListener { - override fun onSuccess( - call: Call?, - response: FriendResponse?, - ) { - Timber.d("CircleDetails for $circleId: $response") - val currentMembers = _circleMembers.value?.toMutableMap() ?: mutableMapOf() - if (response != null) { - currentMembers[circleId] = response - _circleMembers.postValue(currentMembers) - } - - val updatedLoading = - _circleMembersLoading.value?.toMutableSet() ?: mutableSetOf() - updatedLoading.remove(circleId) - _circleMembersLoading.postValue(updatedLoading) - } - - override fun onError( - call: Call?, - t: Throwable?, - ) { - Timber.d("CircleDetailsError for $circleId: ${t?.message}") - - val updatedLoading = - _circleMembersLoading.value?.toMutableSet() ?: mutableSetOf() - updatedLoading.remove(circleId) - _circleMembersLoading.postValue(updatedLoading) - } - }, + token, + circleId, + object : RetrofitFriendListListener { + override fun onSuccess( + call: Call?, + response: FriendResponse?, + ) { + Timber.d("CircleDetails for $circleId: $response") + val currentMembers = _circleMembers.value?.toMutableMap() ?: mutableMapOf() + if (response != null) { + currentMembers[circleId] = response + _circleMembers.postValue(currentMembers) + } + + val updatedLoading = + _circleMembersLoading.value?.toMutableSet() ?: mutableSetOf() + updatedLoading.remove(circleId) + _circleMembersLoading.postValue(updatedLoading) + } + + override fun onError( + call: Call?, + t: Throwable?, + ) { + Timber.d("CircleDetailsError for $circleId: ${t?.message}") + + val updatedLoading = + _circleMembersLoading.value?.toMutableSet() ?: mutableSetOf() + updatedLoading.remove(circleId) + _circleMembersLoading.postValue(updatedLoading) + } + }, ) } fun createCircle( - token: String, - circleName: String, + token: String, + circleName: String, ) { Timber.d( - "ConnectViewModel.createCircle called with token: ${if (token.isNotEmpty()) "exists" else "empty"}, circleName: $circleName", + "ConnectViewModel.createCircle called with token: ${if (token.isNotEmpty()) "exists" else "empty"}, circleName: $circleName", ) APICommunityRestClient.instance.createCircle( - token, - circleName, - object : RetrofitCreateCircleListener { - override fun onSuccess( - call: Call?, - response: CreateCircleResponse?, - ) { - Timber.d("CreateCircle: $response") - _createCircleResponse.postValue(response) - } - - override fun onError( - call: Call?, - t: Throwable?, - ) { - Timber.d("CreateCircle Error: ${t?.message}") - _createCircleResponse.postValue(null) - } - }, + token, + circleName, + object : RetrofitCreateCircleListener { + override fun onSuccess( + call: Call?, + response: CreateCircleResponse?, + ) { + Timber.d("CreateCircle: $response") + _createCircleResponse.postValue(response) + } + + override fun onError( + call: Call?, + t: Throwable?, + ) { + Timber.d("CreateCircle Error: ${t?.message}") + _createCircleResponse.postValue(null) + } + }, ) } fun joinCircleByCode( - token: String, - joinCode: String, + token: String, + joinCode: String, ) { Timber.d( - "ConnectViewModel.joinCircleByCode called with token: ${if (token.isNotEmpty()) "exists" else "empty"}, joinCode: $joinCode", + "ConnectViewModel.joinCircleByCode called with token: ${if (token.isNotEmpty()) "exists" else "empty"}, joinCode: $joinCode", ) _joinCircleResponse.postValue(null) _joinCircleError.postValue(null) APICommunityRestClient.instance.joinCircleByCode( - token, - joinCode, - object : RetrofitJoinCircleListener { - override fun onSuccess( - call: Call?, - response: JoinCircleResponse?, - ) { - Timber.d("JoinCircle Success: $response") - _joinCircleResponse.postValue(response) - } - - override fun onError( - call: Call?, - t: Throwable?, - ) { - val errorMessage = t?.message ?: "Unknown error occurred" - Timber.d("JoinCircle Error: $errorMessage") - _joinCircleError.postValue(errorMessage) - } - }, + token, + joinCode, + object : RetrofitJoinCircleListener { + override fun onSuccess( + call: Call?, + response: JoinCircleResponse?, + ) { + Timber.d("JoinCircle Success: $response") + _joinCircleResponse.postValue(response) + } + + override fun onError( + call: Call?, + t: Throwable?, + ) { + val errorMessage = t?.message ?: "Unknown error occurred" + Timber.d("JoinCircle Error: $errorMessage") + _joinCircleError.postValue(errorMessage) + } + }, ) } @@ -454,52 +454,52 @@ class ConnectViewModel : ViewModel() { fun getReceivedCircleRequests(token: String) { _isCircleRequestsLoading.postValue(true) APICommunityRestClient.instance.getReceivedCircleRequests( - token, - object : RetrofitCircleRequestListener { - override fun onSuccess( - call: Call?, - response: CircleRequestsResponse?, - ) { - Timber.d("ReceivedCircleRequests: $response") - _receivedCircleRequests.postValue(response) - _isCircleRequestsLoading.postValue(false) - } - - override fun onError( - call: Call?, - t: Throwable?, - ) { - Timber.d("ReceivedCircleRequestsError: ${t?.message}") - _receivedCircleRequests.postValue(null) - _isCircleRequestsLoading.postValue(false) - } - }, + token, + object : RetrofitCircleRequestListener { + override fun onSuccess( + call: Call?, + response: CircleRequestsResponse?, + ) { + Timber.d("ReceivedCircleRequests: $response") + _receivedCircleRequests.postValue(response) + _isCircleRequestsLoading.postValue(false) + } + + override fun onError( + call: Call?, + t: Throwable?, + ) { + Timber.d("ReceivedCircleRequestsError: ${t?.message}") + _receivedCircleRequests.postValue(null) + _isCircleRequestsLoading.postValue(false) + } + }, ) } fun getSentCircleRequests(token: String) { _isCircleRequestsLoading.postValue(true) APICommunityRestClient.instance.getSentCircleRequests( - token, - object : RetrofitCircleRequestListener { - override fun onSuccess( - call: Call?, - response: CircleRequestsResponse?, - ) { - Timber.d("SentCircleRequests: $response") - _sentCircleRequests.postValue(response) - _isCircleRequestsLoading.postValue(false) - } - - override fun onError( - call: Call?, - t: Throwable?, - ) { - Timber.d("SentCircleRequestsError: ${t?.message}") - _sentCircleRequests.postValue(null) - _isCircleRequestsLoading.postValue(false) - } - }, + token, + object : RetrofitCircleRequestListener { + override fun onSuccess( + call: Call?, + response: CircleRequestsResponse?, + ) { + Timber.d("SentCircleRequests: $response") + _sentCircleRequests.postValue(response) + _isCircleRequestsLoading.postValue(false) + } + + override fun onError( + call: Call?, + t: Throwable?, + ) { + Timber.d("SentCircleRequestsError: ${t?.message}") + _sentCircleRequests.postValue(null) + _isCircleRequestsLoading.postValue(false) + } + }, ) } @@ -509,56 +509,56 @@ class ConnectViewModel : ViewModel() { } fun deleteCircle( - token: String, - circleId: String, + token: String, + circleId: String, ) { APICommunityRestClient.instance.deleteCircle( - token, - circleId, - object : RetrofitUserActionListener { - override fun onSuccess( - call: Call?, - response: PostResponse?, - ) { - Timber.d("DeleteCircle: $response") - _circleActionResponse.postValue(response) - } - - override fun onError( - call: Call?, - t: Throwable?, - ) { - Timber.d("DeleteCircleError: ${t?.message}") - _circleActionResponse.postValue(null) - } - }, + token, + circleId, + object : RetrofitUserActionListener { + override fun onSuccess( + call: Call?, + response: PostResponse?, + ) { + Timber.d("DeleteCircle: $response") + _circleActionResponse.postValue(response) + } + + override fun onError( + call: Call?, + t: Throwable?, + ) { + Timber.d("DeleteCircleError: ${t?.message}") + _circleActionResponse.postValue(null) + } + }, ) } fun leaveCircle( - token: String, - circleId: String, + token: String, + circleId: String, ) { APICommunityRestClient.instance.leaveCircle( - token, - circleId, - object : RetrofitUserActionListener { - override fun onSuccess( - call: Call?, - response: PostResponse?, - ) { - Timber.d("LeaveCircle: $response") - _circleActionResponse.postValue(response) - } - - override fun onError( - call: Call?, - t: Throwable?, - ) { - Timber.d("LeaveCircleError: ${t?.message}") - _circleActionResponse.postValue(null) - } - }, + token, + circleId, + object : RetrofitUserActionListener { + override fun onSuccess( + call: Call?, + response: PostResponse?, + ) { + Timber.d("LeaveCircle: $response") + _circleActionResponse.postValue(response) + } + + override fun onError( + call: Call?, + t: Throwable?, + ) { + Timber.d("LeaveCircleError: ${t?.message}") + _circleActionResponse.postValue(null) + } + }, ) } @@ -567,201 +567,201 @@ class ConnectViewModel : ViewModel() { } fun acceptCircleRequest( - token: String, - circleId: String, + token: String, + circleId: String, ) { APICommunityRestClient.instance.acceptCircleRequest( - token, - circleId, - object : RetrofitUserActionListener { - override fun onSuccess( - call: Call?, - response: PostResponse?, - ) { - Timber.d("AcceptCircleRequest: $response") - _circleActionResponse.postValue(response) - } - - override fun onError( - call: Call?, - t: Throwable?, - ) { - Timber.d("AcceptCircleRequestError: ${t?.message}") - _circleActionResponse.postValue(null) - } - }, + token, + circleId, + object : RetrofitUserActionListener { + override fun onSuccess( + call: Call?, + response: PostResponse?, + ) { + Timber.d("AcceptCircleRequest: $response") + _circleActionResponse.postValue(response) + } + + override fun onError( + call: Call?, + t: Throwable?, + ) { + Timber.d("AcceptCircleRequestError: ${t?.message}") + _circleActionResponse.postValue(null) + } + }, ) } fun declineCircleRequest( - token: String, - circleId: String, + token: String, + circleId: String, ) { APICommunityRestClient.instance.declineCircleRequest( - token, - circleId, - object : RetrofitUserActionListener { - override fun onSuccess( - call: Call?, - response: PostResponse?, - ) { - Timber.d("DeclineCircleRequest: $response") - _circleActionResponse.postValue(response) - } - - override fun onError( - call: Call?, - t: Throwable?, - ) { - Timber.d("DeclineCircleRequestError: ${t?.message}") - _circleActionResponse.postValue(null) - } - }, + token, + circleId, + object : RetrofitUserActionListener { + override fun onSuccess( + call: Call?, + response: PostResponse?, + ) { + Timber.d("DeclineCircleRequest: $response") + _circleActionResponse.postValue(response) + } + + override fun onError( + call: Call?, + t: Throwable?, + ) { + Timber.d("DeclineCircleRequestError: ${t?.message}") + _circleActionResponse.postValue(null) + } + }, ) } fun unsendCircleRequest( - token: String, - circleId: String, - username: String, + token: String, + circleId: String, + username: String, ) { APICommunityRestClient.instance.unsendCircleRequest( - token, - circleId, - username, - object : RetrofitUserActionListener { - override fun onSuccess( - call: Call?, - response: PostResponse?, - ) { - Timber.d("UnsendCircleRequest: $response") - _circleActionResponse.postValue(response) - } - - override fun onError( - call: Call?, - t: Throwable?, - ) { - Timber.d("UnsendCircleRequestError: ${t?.message}") - _circleActionResponse.postValue(null) - } - }, + token, + circleId, + username, + object : RetrofitUserActionListener { + override fun onSuccess( + call: Call?, + response: PostResponse?, + ) { + Timber.d("UnsendCircleRequest: $response") + _circleActionResponse.postValue(response) + } + + override fun onError( + call: Call?, + t: Throwable?, + ) { + Timber.d("UnsendCircleRequestError: ${t?.message}") + _circleActionResponse.postValue(null) + } + }, ) } fun removeUserFromCircle( - token: String, - circleId: String, - username: String, + token: String, + circleId: String, + username: String, ) { APICommunityRestClient.instance.removeUserFromCircle( + token, + circleId, + username, + object : RetrofitUserActionListener { + override fun onSuccess( + call: Call?, + response: PostResponse?, + ) { + Timber.d("RemoveUserFromCircle: $response") + _circleActionResponse.postValue(response) + + getCircleDetails(token, circleId) + } + + override fun onError( + call: Call?, + t: Throwable?, + ) { + Timber.d("RemoveUserFromCircleError: ${t?.message}") + _circleActionResponse.postValue(null) + } + }, + ) + } + + fun fetchActiveFriends( + token: String, + prefs: SharedPreferences, + ) { + APICommunityRestClient.instance.getActiveFriends( + token, + object : RetrofitActiveFriendsListener { + override fun onSuccess( + call: Call, + response: ActiveFriendResponse?, + ) { + val list = response?.data ?: emptyList() + _activeFriends.postValue(list) + + prefs.edit { + putBoolean(Constants.ACTIVE_FRIENDS_FETCHED, true) + .putString(Constants.ACTIVE_FRIENDS_LIST, Gson().toJson(list)) + } + } + + override fun onError( + call: Call, + t: Throwable, + ) { + _activeFriends.postValue(emptyList()) + } + }, + ) + } + + fun toggleGhostMode( + token: String, + username: String, + enable: Boolean, + ) { + _ghostModeResponse.postValue(null) + val api = APICommunityRestClient.instance + if (enable) { + api.enableGhostMode( token, - circleId, username, - object : RetrofitUserActionListener { + object : RetrofitGhostActionListener { override fun onSuccess( - call: Call?, - response: PostResponse?, + call: Call?, + response: GhostPostResponse?, ) { - Timber.d("RemoveUserFromCircle: $response") - _circleActionResponse.postValue(response) - - getCircleDetails(token, circleId) + val success = + response?.data?.contains("hidden", true) == true || + response?.data?.contains("visible", true) == true + _ghostModeResponse.postValue(GhostModeResponse(success)) } override fun onError( - call: Call?, - t: Throwable?, + call: Call?, + t: Throwable?, ) { - Timber.d("RemoveUserFromCircleError: ${t?.message}") - _circleActionResponse.postValue(null) + _ghostModeResponse.postValue(GhostModeResponse(false)) } }, - ) - } - - fun fetchActiveFriends( - token: String, - prefs: SharedPreferences, - ) { - APICommunityRestClient.instance.getActiveFriends( + ) + } else { + api.disableGhostMode( token, - object : RetrofitActiveFriendsListener { + username, + object : RetrofitGhostActionListener { override fun onSuccess( - call: Call, - response: ActiveFriendResponse?, + call: Call?, + response: GhostPostResponse?, ) { - val list = response?.data ?: emptyList() - _activeFriends.postValue(list) - - prefs.edit { - putBoolean(Constants.ACTIVE_FRIENDS_FETCHED, true) - .putString(Constants.ACTIVE_FRIENDS_LIST, Gson().toJson(list)) - } + val success = + response?.data?.contains("hidden", true) == true || + response?.data?.contains("visible", true) == true + _ghostModeResponse.postValue(GhostModeResponse(success)) } override fun onError( - call: Call, - t: Throwable, + call: Call?, + t: Throwable?, ) { - _activeFriends.postValue(emptyList()) + _ghostModeResponse.postValue(GhostModeResponse(false)) } }, - ) - } - - fun toggleGhostMode( - token: String, - username: String, - enable: Boolean, - ) { - _ghostModeResponse.postValue(null) - val api = APICommunityRestClient.instance - if (enable) { - api.enableGhostMode( - token, - username, - object : RetrofitGhostActionListener { - override fun onSuccess( - call: Call?, - response: GhostPostResponse?, - ) { - val success = - response?.data?.contains("hidden", true) == true || - response?.data?.contains("visible", true) == true - _ghostModeResponse.postValue(GhostModeResponse(success)) - } - - override fun onError( - call: Call?, - t: Throwable?, - ) { - _ghostModeResponse.postValue(GhostModeResponse(false)) - } - }, - ) - } else { - api.disableGhostMode( - token, - username, - object : RetrofitGhostActionListener { - override fun onSuccess( - call: Call?, - response: GhostPostResponse?, - ) { - val success = - response?.data?.contains("hidden", true) == true || - response?.data?.contains("visible", true) == true - _ghostModeResponse.postValue(GhostModeResponse(success)) - } - - override fun onError( - call: Call?, - t: Throwable?, - ) { - _ghostModeResponse.postValue(GhostModeResponse(false)) - } - }, ) } } @@ -775,6 +775,6 @@ class ConnectViewModel : ViewModel() { } data class GhostModeResponse( - val success: Boolean, + val success: Boolean, ) } From 6176539780a40b7e84f1b5243319b8a73b7d88c6 Mon Sep 17 00:00:00 2001 From: JothishKamal Date: Sat, 2 Aug 2025 22:55:23 +0530 Subject: [PATCH 7/7] fix: new friends ghosted by default --- .../vitty/ui/connect/ConnectViewModel.kt | 4 + .../ui/connect/FriendDetailScreenContent.kt | 1011 +++++++++-------- .../ui/connect/FriendRequestsScreenContent.kt | 2 +- build.gradle | 2 +- 4 files changed, 521 insertions(+), 498 deletions(-) diff --git a/app/src/main/java/com/dscvit/vitty/ui/connect/ConnectViewModel.kt b/app/src/main/java/com/dscvit/vitty/ui/connect/ConnectViewModel.kt index c00189e..1e31f21 100644 --- a/app/src/main/java/com/dscvit/vitty/ui/connect/ConnectViewModel.kt +++ b/app/src/main/java/com/dscvit/vitty/ui/connect/ConnectViewModel.kt @@ -182,6 +182,7 @@ class ConnectViewModel : ViewModel() { fun unfriend( token: String, username: String, + prefs: SharedPreferences, ) { APICommunityRestClient.instance.unfriend( token, @@ -192,6 +193,7 @@ class ConnectViewModel : ViewModel() { response: PostResponse?, ) { Timber.d("ConnectUnfriend: $response") + fetchActiveFriends(token, prefs) _unfriendSuccess.postValue(username) } @@ -221,6 +223,7 @@ class ConnectViewModel : ViewModel() { fun acceptRequest( token: String, username: String, + prefs: SharedPreferences, ) { APICommunityRestClient.instance.acceptRequest( token, @@ -231,6 +234,7 @@ class ConnectViewModel : ViewModel() { response: PostResponse?, ) { Timber.d("AcceptRequest: $response") + fetchActiveFriends(token, prefs) _requestActionResponse.postValue(response) } diff --git a/app/src/main/java/com/dscvit/vitty/ui/connect/FriendDetailScreenContent.kt b/app/src/main/java/com/dscvit/vitty/ui/connect/FriendDetailScreenContent.kt index 1f69c5e..086e683 100644 --- a/app/src/main/java/com/dscvit/vitty/ui/connect/FriendDetailScreenContent.kt +++ b/app/src/main/java/com/dscvit/vitty/ui/connect/FriendDetailScreenContent.kt @@ -80,20 +80,20 @@ import com.dscvit.vitty.util.Constants import com.dscvit.vitty.util.Quote import com.dscvit.vitty.widget.parseTimeToTimestamp import com.google.gson.Gson -import java.text.SimpleDateFormat -import java.util.Calendar -import java.util.Locale import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Locale @OptIn(ExperimentalMaterial3Api::class) @Composable fun FriendDetailScreenContent( - friend: UserResponse, - onBackClick: () -> Unit = {}, - connectViewModel: ConnectViewModel, + friend: UserResponse, + onBackClick: () -> Unit = {}, + connectViewModel: ConnectViewModel, ) { val context = LocalContext.current val scheduleViewModel: ScheduleViewModel = viewModel() @@ -127,18 +127,15 @@ fun FriendDetailScreenContent( isTogglingGhostMode = false val sharedPreferences = - context.getSharedPreferences(Constants.USER_INFO, Context.MODE_PRIVATE) + context.getSharedPreferences(Constants.USER_INFO, Context.MODE_PRIVATE) if (ghostModeResponse!!.success) { - val currentActiveFriends = activeFriends?.toMutableList() ?: mutableListOf() val gson = Gson() if (isFriendGhosted) { - currentActiveFriends.removeAll { it == friend.username } } else { - if (!currentActiveFriends.contains(friend.username)) { currentActiveFriends.add(friend.username) } @@ -149,16 +146,18 @@ fun FriendDetailScreenContent( putString(Constants.ACTIVE_FRIENDS_LIST, activeFriendsJson) } - Toast.makeText( - context, - if (isFriendGhosted) "${friend.name} is now ghosted" - else "${friend.name} is now visible", - Toast.LENGTH_SHORT, - ) - .show() + Toast + .makeText( + context, + if (isFriendGhosted) { + "${friend.name} is now ghosted" + } else { + "${friend.name} is now visible" + }, + Toast.LENGTH_SHORT, + ).show() connectViewModel.updateActiveFriendsList(currentActiveFriends) } else { - isFriendGhosted = !isFriendGhosted } @@ -181,7 +180,7 @@ fun FriendDetailScreenContent( connectViewModel.clearSendRequestResponse() val sharedPreferences = - context.getSharedPreferences(Constants.USER_INFO, Context.MODE_PRIVATE) + context.getSharedPreferences(Constants.USER_INFO, Context.MODE_PRIVATE) val token = sharedPreferences.getString(Constants.COMMUNITY_TOKEN, "") ?: "" val username = sharedPreferences.getString(Constants.COMMUNITY_USERNAME, "") ?: "" if (token.isNotEmpty() && username.isNotEmpty()) { @@ -194,24 +193,25 @@ fun FriendDetailScreenContent( } val days = listOf("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun") - val currentDay = remember { - when (Calendar.getInstance().get(Calendar.DAY_OF_WEEK)) { - Calendar.MONDAY -> 0 - Calendar.TUESDAY -> 1 - Calendar.WEDNESDAY -> 2 - Calendar.THURSDAY -> 3 - Calendar.FRIDAY -> 4 - Calendar.SATURDAY -> 5 - Calendar.SUNDAY -> 6 - else -> 0 + val currentDay = + remember { + when (Calendar.getInstance().get(Calendar.DAY_OF_WEEK)) { + Calendar.MONDAY -> 0 + Calendar.TUESDAY -> 1 + Calendar.WEDNESDAY -> 2 + Calendar.THURSDAY -> 3 + Calendar.FRIDAY -> 4 + Calendar.SATURDAY -> 5 + Calendar.SUNDAY -> 6 + else -> 0 + } } - } val pagerState = - rememberPagerState( - initialPage = currentDay, - pageCount = { 7 }, - ) + rememberPagerState( + initialPage = currentDay, + pageCount = { 7 }, + ) LaunchedEffect(friend.username) { if (hasLoadedData) return@LaunchedEffect @@ -219,7 +219,7 @@ fun FriendDetailScreenContent( quote = Quote.getLine(context) val sharedPreferences = - context.getSharedPreferences(Constants.USER_INFO, Context.MODE_PRIVATE) + context.getSharedPreferences(Constants.USER_INFO, Context.MODE_PRIVATE) val token = sharedPreferences.getString(Constants.COMMUNITY_TOKEN, "") ?: "" if (token.isNotEmpty()) { @@ -256,117 +256,122 @@ fun FriendDetailScreenContent( } Column( - modifier = Modifier.fillMaxSize().background(Background), + modifier = Modifier.fillMaxSize().background(Background), ) { CenterAlignedTopAppBar( - title = { - Text( - text = "Friend Profile", - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold, + title = { + Text( + text = "Friend Profile", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + ) + }, + navigationIcon = { + IconButton(onClick = onBackClick) { + Icon( + painter = painterResource(id = R.drawable.ic_round_chevron_left), + contentDescription = "Back", + tint = TextColor, ) - }, - navigationIcon = { - IconButton(onClick = onBackClick) { - Icon( - painter = painterResource(id = R.drawable.ic_round_chevron_left), - contentDescription = "Back", - tint = TextColor, - ) - } - }, - colors = - TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.background, - titleContentColor = MaterialTheme.colorScheme.onBackground, - ), + } + }, + colors = + TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.background, + titleContentColor = MaterialTheme.colorScheme.onBackground, + ), ) FriendProfileCard( - friend = friend, - hasUnfriended = hasUnfriended, - isSendingRequest = isSendingRequest, - hasRequestSent = hasRequestSent, - isFriendGhosted = isFriendGhosted, - isTogglingGhostMode = isTogglingGhostMode, - onUnfriendClick = { showUnfriendDialog = true }, - onSendRequestClick = { + friend = friend, + hasUnfriended = hasUnfriended, + isSendingRequest = isSendingRequest, + hasRequestSent = hasRequestSent, + isFriendGhosted = isFriendGhosted, + isTogglingGhostMode = isTogglingGhostMode, + onUnfriendClick = { showUnfriendDialog = true }, + onSendRequestClick = { + val sharedPreferences = + context.getSharedPreferences(Constants.USER_INFO, Context.MODE_PRIVATE) + val token = sharedPreferences.getString(Constants.COMMUNITY_TOKEN, "") ?: "" + if (token.isNotEmpty()) { + isSendingRequest = true + connectViewModel.sendRequest(token, friend.username) + } + }, + onToggleGhostMode = { newValue -> + if (!isTogglingGhostMode) { + isTogglingGhostMode = true + isFriendGhosted = newValue + val sharedPreferences = - context.getSharedPreferences(Constants.USER_INFO, Context.MODE_PRIVATE) + context.getSharedPreferences( + Constants.USER_INFO, + Context.MODE_PRIVATE, + ) val token = sharedPreferences.getString(Constants.COMMUNITY_TOKEN, "") ?: "" - if (token.isNotEmpty()) { - isSendingRequest = true - connectViewModel.sendRequest(token, friend.username) - } - }, - onToggleGhostMode = { newValue -> - if (!isTogglingGhostMode) { - isTogglingGhostMode = true - isFriendGhosted = newValue - val sharedPreferences = - context.getSharedPreferences( - Constants.USER_INFO, - Context.MODE_PRIVATE - ) - val token = sharedPreferences.getString(Constants.COMMUNITY_TOKEN, "") ?: "" - - connectViewModel.toggleGhostMode(token, friend.username, newValue) - } - }, + connectViewModel.toggleGhostMode(token, friend.username, newValue) + } + }, ) Spacer(modifier = Modifier.height(16.dp)) if (!hasUnfriended) { Text( - text = "Schedule", - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold, - color = TextColor, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + text = "Schedule", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + color = TextColor, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), ) ScrollableTabRow( - selectedTabIndex = pagerState.currentPage, - containerColor = MaterialTheme.colorScheme.surface, - contentColor = MaterialTheme.colorScheme.onSurface, - divider = {}, - edgePadding = 0.dp, - indicator = { tabPositions -> - if (pagerState.currentPage < tabPositions.size) { - TabRowDefaults.SecondaryIndicator( - modifier = - Modifier.tabIndicatorOffset( - tabPositions[pagerState.currentPage] - ), - color = TextColor, - ) - } - }, + selectedTabIndex = pagerState.currentPage, + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface, + divider = {}, + edgePadding = 0.dp, + indicator = { tabPositions -> + if (pagerState.currentPage < tabPositions.size) { + TabRowDefaults.SecondaryIndicator( + modifier = + Modifier.tabIndicatorOffset( + tabPositions[pagerState.currentPage], + ), + color = TextColor, + ) + } + }, ) { days.forEachIndexed { index, day -> Tab( - selected = pagerState.currentPage == index, - onClick = { scope.launch { pagerState.animateScrollToPage(index) } }, - text = { - Text( - text = day, - fontFamily = Poppins, - fontWeight = - if (pagerState.currentPage == index) - FontWeight.Medium - else FontWeight.Normal, - fontSize = 20.sp, - lineHeight = (20 * 1.4).sp, - color = - if (pagerState.currentPage == index) TextColor - else Accent, - textAlign = TextAlign.Center, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - }, + selected = pagerState.currentPage == index, + onClick = { scope.launch { pagerState.animateScrollToPage(index) } }, + text = { + Text( + text = day, + fontFamily = Poppins, + fontWeight = + if (pagerState.currentPage == index) { + FontWeight.Medium + } else { + FontWeight.Normal + }, + fontSize = 20.sp, + lineHeight = (20 * 1.4).sp, + color = + if (pagerState.currentPage == index) { + TextColor + } else { + Accent + }, + textAlign = TextAlign.Center, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + }, ) } } @@ -376,51 +381,51 @@ fun FriendDetailScreenContent( Box(modifier = Modifier.weight(1f)) { if (isLoadingTimetable) { Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, ) { CircularProgressIndicator() } } else { HorizontalPager( - state = pagerState, - modifier = Modifier.fillMaxSize(), + state = pagerState, + modifier = Modifier.fillMaxSize(), ) { page -> DayScheduleContent( - periods = friendTimetableData[page] ?: emptyList(), - dayIndex = page, - quote = quote, + periods = friendTimetableData[page] ?: emptyList(), + dayIndex = page, + quote = quote, ) } } } } else { Box( - modifier = Modifier.fillMaxWidth().weight(1f).padding(32.dp), - contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxWidth().weight(1f).padding(32.dp), + contentAlignment = Alignment.Center, ) { Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp), ) { Icon( - painter = painterResource(R.drawable.ic_community_outline), - contentDescription = "No Access", - modifier = Modifier.size(64.dp), - tint = Accent.copy(alpha = 0.5f), + painter = painterResource(R.drawable.ic_community_outline), + contentDescription = "No Access", + modifier = Modifier.size(64.dp), + tint = Accent.copy(alpha = 0.5f), ) Text( - text = "Schedule Not Available", - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold, - color = TextColor, - textAlign = TextAlign.Center, + text = "Schedule Not Available", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + color = TextColor, + textAlign = TextAlign.Center, ) Text( - text = - "You can no longer view ${friend.name}'s schedule as they are not in your friends list.", - style = MaterialTheme.typography.bodyMedium, - color = Accent, - textAlign = TextAlign.Center, - lineHeight = 20.sp, + text = + "You can no longer view ${friend.name}'s schedule as they are not in your friends list.", + style = MaterialTheme.typography.bodyMedium, + color = Accent, + textAlign = TextAlign.Center, + lineHeight = 20.sp, ) } } @@ -429,121 +434,122 @@ fun FriendDetailScreenContent( if (showUnfriendDialog) { AlertDialog( - onDismissRequest = { showUnfriendDialog = false }, - title = { + onDismissRequest = { showUnfriendDialog = false }, + title = { + Text( + text = "Unfriend ${friend.name}?", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + ) + }, + text = { + Text( + text = + "Are you sure you want to remove ${friend.name} from your friends list? " + + "You won't be able to see their schedule anymore.", + style = + MaterialTheme.typography.bodyMedium.copy( + color = Accent, + ), + ) + }, + containerColor = Secondary, + confirmButton = { + TextButton( + onClick = { + showUnfriendDialog = false + val sharedPreferences = + context.getSharedPreferences( + Constants.USER_INFO, + Context.MODE_PRIVATE, + ) + val token = + sharedPreferences.getString(Constants.COMMUNITY_TOKEN, "") + ?: "" + val username = + sharedPreferences.getString( + Constants.COMMUNITY_USERNAME, + "", + ) + ?: "" + if (token.isNotEmpty()) { + connectViewModel.unfriend(token, friend.username, sharedPreferences) + connectViewModel.getFriendList(token, username) + } + }, + ) { Text( - text = "Unfriend ${friend.name}?", - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold, + text = "Unfriend", + color = Red, + fontWeight = FontWeight.Medium, ) - }, - text = { + } + }, + dismissButton = { + TextButton( + onClick = { showUnfriendDialog = false }, + ) { Text( - text = - "Are you sure you want to remove ${friend.name} from your friends list? " + - "You won't be able to see their schedule anymore.", - style = - MaterialTheme.typography.bodyMedium.copy( - color = Accent, - ), + text = "Cancel", + color = TextColor, + fontWeight = FontWeight.Medium, ) - }, - containerColor = Secondary, - confirmButton = { - TextButton( - onClick = { - showUnfriendDialog = false - val sharedPreferences = - context.getSharedPreferences( - Constants.USER_INFO, - Context.MODE_PRIVATE - ) - val token = - sharedPreferences.getString(Constants.COMMUNITY_TOKEN, "") - ?: "" - val username = - sharedPreferences.getString( - Constants.COMMUNITY_USERNAME, - "" - ) - ?: "" - if (token.isNotEmpty()) { - connectViewModel.unfriend(token, friend.username) - connectViewModel.getFriendList(token, username) - } - }, - ) { - Text( - text = "Unfriend", - color = Red, - fontWeight = FontWeight.Medium, - ) - } - }, - dismissButton = { - TextButton( - onClick = { showUnfriendDialog = false }, - ) { - Text( - text = "Cancel", - color = TextColor, - fontWeight = FontWeight.Medium, - ) - } - }, + } + }, ) } } @Composable fun FriendProfileCard( - friend: UserResponse, - hasUnfriended: Boolean, - isSendingRequest: Boolean, - hasRequestSent: Boolean, - isFriendGhosted: Boolean, - isTogglingGhostMode: Boolean, - onUnfriendClick: () -> Unit, - onSendRequestClick: () -> Unit, - onToggleGhostMode: (Boolean) -> Unit, + friend: UserResponse, + hasUnfriended: Boolean, + isSendingRequest: Boolean, + hasRequestSent: Boolean, + isFriendGhosted: Boolean, + isTogglingGhostMode: Boolean, + onUnfriendClick: () -> Unit, + onSendRequestClick: () -> Unit, + onToggleGhostMode: (Boolean) -> Unit, ) { Card( - modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), - colors = CardDefaults.cardColors(containerColor = Secondary), - elevation = CardDefaults.cardElevation(defaultElevation = 4.dp), - shape = RoundedCornerShape(16.dp), + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + colors = CardDefaults.cardColors(containerColor = Secondary), + elevation = CardDefaults.cardElevation(defaultElevation = 4.dp), + shape = RoundedCornerShape(16.dp), ) { Column( - modifier = Modifier.fillMaxWidth().padding(20.dp), + modifier = Modifier.fillMaxWidth().padding(20.dp), ) { Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, ) { if (friend.picture.isNotEmpty()) { AsyncImage( - model = friend.picture, - contentDescription = null, - modifier = Modifier.size(64.dp).clip(CircleShape), - placeholder = painterResource(R.drawable.ic_gdscvit), - error = painterResource(R.drawable.ic_gdscvit), + model = friend.picture, + contentDescription = null, + modifier = Modifier.size(64.dp).clip(CircleShape), + placeholder = painterResource(R.drawable.ic_gdscvit), + error = painterResource(R.drawable.ic_gdscvit), ) } else { Box( - modifier = - Modifier.size(64.dp) - .background(Accent.copy(alpha = 0.2f), CircleShape), - contentAlignment = Alignment.Center, + modifier = + Modifier + .size(64.dp) + .background(Accent.copy(alpha = 0.2f), CircleShape), + contentAlignment = Alignment.Center, ) { Text( - text = - friend.name - .take(2) - .map { it.uppercaseChar() } - .joinToString(""), - color = TextColor, - fontWeight = FontWeight.Bold, - fontSize = 22.sp, + text = + friend.name + .take(2) + .map { it.uppercaseChar() } + .joinToString(""), + color = TextColor, + fontWeight = FontWeight.Bold, + fontSize = 22.sp, ) } } @@ -552,23 +558,23 @@ fun FriendProfileCard( Column(modifier = Modifier.weight(1f)) { Text( - text = friend.name, - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold, - color = TextColor, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - lineHeight = 24.sp, + text = friend.name, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + color = TextColor, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + lineHeight = 24.sp, ) Spacer(modifier = Modifier.height(4.dp)) Text( - text = "@${friend.username}", - style = MaterialTheme.typography.bodyMedium, - color = Accent, - maxLines = 1, - overflow = TextOverflow.Ellipsis, + text = "@${friend.username}", + style = MaterialTheme.typography.bodyMedium, + color = Accent, + maxLines = 1, + overflow = TextOverflow.Ellipsis, ) } } @@ -577,43 +583,52 @@ fun FriendProfileCard( if (!hasUnfriended) { Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, ) { Column { Text( - text = - if (isFriendGhosted) "Friend is ghosted" - else "Friend is visible", - style = MaterialTheme.typography.bodyMedium, - fontWeight = FontWeight.Medium, - color = - if (isFriendGhosted) Accent.copy(alpha = 0.6f) - else TextColor, + text = + if (isFriendGhosted) { + "Friend is ghosted" + } else { + "Friend is visible" + }, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium, + color = + if (isFriendGhosted) { + Accent.copy(alpha = 0.6f) + } else { + TextColor + }, ) Text( - text = - if (isFriendGhosted) "They won't see your activity" - else "They can see your activity", - style = MaterialTheme.typography.bodySmall, - color = Accent.copy(alpha = 0.7f), + text = + if (isFriendGhosted) { + "They won't see your activity" + } else { + "They can see your activity" + }, + style = MaterialTheme.typography.bodySmall, + color = Accent.copy(alpha = 0.7f), ) } Switch( - checked = isFriendGhosted, - onCheckedChange = onToggleGhostMode, - enabled = !isTogglingGhostMode, - colors = - SwitchDefaults.colors( - checkedThumbColor = MaterialTheme.colorScheme.onPrimary, - checkedTrackColor = Accent, - uncheckedThumbColor = Accent, - uncheckedTrackColor = Secondary, - checkedBorderColor = Accent, - uncheckedBorderColor = Accent, - ), + checked = isFriendGhosted, + onCheckedChange = onToggleGhostMode, + enabled = !isTogglingGhostMode, + colors = + SwitchDefaults.colors( + checkedThumbColor = MaterialTheme.colorScheme.onPrimary, + checkedTrackColor = Accent, + uncheckedThumbColor = Accent, + uncheckedTrackColor = Secondary, + checkedBorderColor = Accent, + uncheckedBorderColor = Accent, + ), ) } @@ -623,85 +638,92 @@ fun FriendProfileCard( if (hasUnfriended) { if (hasRequestSent) { Button( - onClick = {}, - enabled = false, - colors = - ButtonDefaults.buttonColors( - containerColor = Accent.copy(alpha = 0.1f), - contentColor = Accent.copy(alpha = 0.6f), - ), - modifier = Modifier.fillMaxWidth().height(48.dp), - shape = RoundedCornerShape(12.dp), + onClick = {}, + enabled = false, + colors = + ButtonDefaults.buttonColors( + containerColor = Accent.copy(alpha = 0.1f), + contentColor = Accent.copy(alpha = 0.6f), + ), + modifier = Modifier.fillMaxWidth().height(48.dp), + shape = RoundedCornerShape(12.dp), ) { Text( - text = "Request Sent", - style = MaterialTheme.typography.labelLarge, - fontWeight = FontWeight.Medium, + text = "Request Sent", + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.Medium, ) } } else { Button( - onClick = onSendRequestClick, - enabled = !isSendingRequest, - colors = - ButtonDefaults.buttonColors( - containerColor = - if (isSendingRequest) Accent.copy(alpha = 0.1f) - else Accent, - contentColor = - if (isSendingRequest) Accent.copy(alpha = 0.6f) - else MaterialTheme.colorScheme.onPrimary, - ), - modifier = Modifier.fillMaxWidth().height(48.dp), - shape = RoundedCornerShape(12.dp), + onClick = onSendRequestClick, + enabled = !isSendingRequest, + colors = + ButtonDefaults.buttonColors( + containerColor = + if (isSendingRequest) { + Accent.copy(alpha = 0.1f) + } else { + Accent + }, + contentColor = + if (isSendingRequest) { + Accent.copy(alpha = 0.6f) + } else { + MaterialTheme.colorScheme.onPrimary + }, + ), + modifier = Modifier.fillMaxWidth().height(48.dp), + shape = RoundedCornerShape(12.dp), ) { if (isSendingRequest) { Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), ) { CircularProgressIndicator( - modifier = Modifier.size(18.dp), - strokeWidth = 2.dp, - color = Accent.copy(alpha = 0.7f), + modifier = Modifier.size(18.dp), + strokeWidth = 2.dp, + color = Accent.copy(alpha = 0.7f), ) Text( - text = "Sending Request...", - style = MaterialTheme.typography.labelLarge, - fontWeight = FontWeight.Medium, + text = "Sending Request...", + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.Medium, ) } } else { Text( - text = "Send Friend Request", - style = MaterialTheme.typography.labelLarge, - fontWeight = FontWeight.Medium, + text = "Send Friend Request", + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.Medium, ) } } } } else { Button( - onClick = onUnfriendClick, - colors = - ButtonDefaults.buttonColors( - containerColor = Red.copy(alpha = 0.1f), - contentColor = Red, - ), - modifier = - Modifier.fillMaxWidth() - .height(48.dp) - .border( - width = 1.dp, - color = Red, - shape = RoundedCornerShape(12.dp), - ), - shape = RoundedCornerShape(12.dp), + onClick = onUnfriendClick, + colors = + ButtonDefaults.buttonColors( + containerColor = Red.copy(alpha = 0.1f), + contentColor = Red, + ), + modifier = + Modifier + .fillMaxWidth() + .height(48.dp) + .border( + width = 1.dp, + color = Red, + shape = RoundedCornerShape(12.dp), + ), + shape = RoundedCornerShape(12.dp), ) { Text( - text = "Unfriend", - style = MaterialTheme.typography.labelLarge, - fontWeight = FontWeight.Medium, + text = "Unfriend", + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.Medium, ) } } @@ -711,54 +733,54 @@ fun FriendProfileCard( @Composable private fun DayScheduleContent( - periods: List, - dayIndex: Int, - quote: String = "Every day is a new opportunity to learn and grow.", + periods: List, + dayIndex: Int, + quote: String = "Every day is a new opportunity to learn and grow.", ) { if (periods.isEmpty()) { Box( - modifier = Modifier.fillMaxSize().padding(horizontal = 18.dp, vertical = 32.dp), - contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize().padding(horizontal = 18.dp, vertical = 32.dp), + contentAlignment = Alignment.Center, ) { Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp), ) { Icon( - painter = painterResource(R.drawable.ic_timetable_outline), - contentDescription = "No Classes", - modifier = Modifier.size(64.dp), - tint = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.5f), + painter = painterResource(R.drawable.ic_timetable_outline), + contentDescription = "No Classes", + modifier = Modifier.size(64.dp), + tint = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.5f), ) Spacer(modifier = Modifier.height(8.dp)) Text( - text = "No classes today!", - style = MaterialTheme.typography.headlineMedium, - color = MaterialTheme.colorScheme.onBackground, - fontWeight = FontWeight.Bold, + text = "No classes today!", + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.onBackground, + fontWeight = FontWeight.Bold, ) Text( - text = quote, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.7f), - textAlign = TextAlign.Center, - lineHeight = 20.sp, + text = quote, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.7f), + textAlign = TextAlign.Center, + lineHeight = 20.sp, ) } } } else { LazyColumn( - modifier = Modifier.fillMaxSize().padding(horizontal = 18.dp), - verticalArrangement = Arrangement.spacedBy(16.dp), - contentPadding = PaddingValues(top = 16.dp, bottom = 64.dp), + modifier = Modifier.fillMaxSize().padding(horizontal = 18.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + contentPadding = PaddingValues(top = 16.dp, bottom = 64.dp), ) { items( - items = periods, - key = { period -> "${period.courseCode}_${period.slot}" }, + items = periods, + key = { period -> "${period.courseCode}_${period.slot}" }, ) { period -> FriendPeriodCard( - period = period, - dayIndex = dayIndex, + period = period, + dayIndex = dayIndex, ) } } @@ -767,82 +789,82 @@ private fun DayScheduleContent( @Composable private fun FriendPeriodCard( - period: PeriodDetails, - dayIndex: Int, + period: PeriodDetails, + dayIndex: Int, ) { val timeFormat = remember { SimpleDateFormat("h:mm a", Locale.getDefault()) } val startTimeStr = - remember(period.startTime) { timeFormat.format(period.startTime.toDate()).uppercase() } + remember(period.startTime) { timeFormat.format(period.startTime.toDate()).uppercase() } val endTimeStr = - remember(period.endTime) { timeFormat.format(period.endTime.toDate()).uppercase() } + remember(period.endTime) { timeFormat.format(period.endTime.toDate()).uppercase() } val now = Calendar.getInstance() val isToday = - remember(dayIndex) { - val todayIndex = - when (now.get(Calendar.DAY_OF_WEEK)) { - Calendar.MONDAY -> 0 - Calendar.TUESDAY -> 1 - Calendar.WEDNESDAY -> 2 - Calendar.THURSDAY -> 3 - Calendar.FRIDAY -> 4 - Calendar.SATURDAY -> 5 - Calendar.SUNDAY -> 6 - else -> -1 - } - dayIndex == todayIndex - } + remember(dayIndex) { + val todayIndex = + when (now.get(Calendar.DAY_OF_WEEK)) { + Calendar.MONDAY -> 0 + Calendar.TUESDAY -> 1 + Calendar.WEDNESDAY -> 2 + Calendar.THURSDAY -> 3 + Calendar.FRIDAY -> 4 + Calendar.SATURDAY -> 5 + Calendar.SUNDAY -> 6 + else -> -1 + } + dayIndex == todayIndex + } val isActive = - if (!isToday) { - false - } else { - val startTime = Calendar.getInstance().apply { time = period.startTime.toDate() } - val endTime = Calendar.getInstance().apply { time = period.endTime.toDate() } - val currentTime = Calendar.getInstance() - - val currentHourMinute = - currentTime.get(Calendar.HOUR_OF_DAY) * 60 + - currentTime.get(Calendar.MINUTE) - val startHourMinute = - startTime.get(Calendar.HOUR_OF_DAY) * 60 + startTime.get(Calendar.MINUTE) - val endHourMinute = - endTime.get(Calendar.HOUR_OF_DAY) * 60 + endTime.get(Calendar.MINUTE) - - currentHourMinute in startHourMinute..endHourMinute - } + if (!isToday) { + false + } else { + val startTime = Calendar.getInstance().apply { time = period.startTime.toDate() } + val endTime = Calendar.getInstance().apply { time = period.endTime.toDate() } + val currentTime = Calendar.getInstance() + + val currentHourMinute = + currentTime.get(Calendar.HOUR_OF_DAY) * 60 + + currentTime.get(Calendar.MINUTE) + val startHourMinute = + startTime.get(Calendar.HOUR_OF_DAY) * 60 + startTime.get(Calendar.MINUTE) + val endHourMinute = + endTime.get(Calendar.HOUR_OF_DAY) * 60 + endTime.get(Calendar.MINUTE) + + currentHourMinute in startHourMinute..endHourMinute + } Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors(containerColor = Secondary), - border = - if (isActive) { - BorderStroke( - 1.dp, - Accent, - ) - } else { - null - }, - shape = RoundedCornerShape(16.dp), + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = Secondary), + border = + if (isActive) { + BorderStroke( + 1.dp, + Accent, + ) + } else { + null + }, + shape = RoundedCornerShape(16.dp), ) { Column( - modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp, vertical = 28.dp), + modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp, vertical = 28.dp), ) { Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.Top, + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top, ) { Column(modifier = Modifier.weight(1f)) { Text( - text = period.courseName, - style = MaterialTheme.typography.labelLarge, - fontWeight = FontWeight.Bold, - color = TextColor, - maxLines = 2, - overflow = TextOverflow.Ellipsis, + text = period.courseName, + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.Bold, + color = TextColor, + maxLines = 2, + overflow = TextOverflow.Ellipsis, ) } } @@ -850,47 +872,47 @@ private fun FriendPeriodCard( Spacer(modifier = Modifier.height(4.dp)) Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, ) { Text( - text = "$startTimeStr - $endTimeStr | ${period.slot}", - style = MaterialTheme.typography.bodyMedium, - color = Accent, - fontWeight = FontWeight.Medium, + text = "$startTimeStr - $endTimeStr | ${period.slot}", + style = MaterialTheme.typography.bodyMedium, + color = Accent, + fontWeight = FontWeight.Medium, ) if (period.roomNo.isNotEmpty()) { Box( - modifier = - Modifier.clip(RoundedCornerShape(9999.dp)) - .border( - width = 1.dp, - color = Accent, - shape = RoundedCornerShape(9999.dp), - ) - .padding(horizontal = 8.dp, vertical = 4.dp), + modifier = + Modifier + .clip(RoundedCornerShape(9999.dp)) + .border( + width = 1.dp, + color = Accent, + shape = RoundedCornerShape(9999.dp), + ).padding(horizontal = 8.dp, vertical = 4.dp), ) { Row(verticalAlignment = Alignment.CenterVertically) { Image( - painter = painterResource(id = R.drawable.ic_compass), - contentDescription = "Compass icon", - modifier = Modifier.size(12.dp), - alignment = Alignment.Center, + painter = painterResource(id = R.drawable.ic_compass), + contentDescription = "Compass icon", + modifier = Modifier.size(12.dp), + alignment = Alignment.Center, ) Spacer(modifier = Modifier.width(4.dp)) Text( - text = period.roomNo, - style = - MaterialTheme.typography.bodyMedium.copy( - fontSize = 12.sp, - lineHeight = 12.sp, - letterSpacing = (-0.12).sp, - textAlign = TextAlign.Center, - ), - color = MaterialTheme.colorScheme.primary, - fontWeight = FontWeight.Medium, + text = period.roomNo, + style = + MaterialTheme.typography.bodyMedium.copy( + fontSize = 12.sp, + lineHeight = 12.sp, + letterSpacing = (-0.12).sp, + textAlign = TextAlign.Center, + ), + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.Medium, ) } } @@ -900,54 +922,51 @@ private fun FriendPeriodCard( } } -private suspend fun processFriendTimetableData( - friend: TimetableResponse -): Map> = - withContext(Dispatchers.Default) { - val timetableData = friend.data - val dayNames = - listOf( - "monday", - "tuesday", - "wednesday", - "thursday", - "friday", - "saturday", - "sunday" - ) +private suspend fun processFriendTimetableData(friend: TimetableResponse): Map> = + withContext(Dispatchers.Default) { + val timetableData = friend.data + val dayNames = + listOf( + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + "sunday", + ) - val result = mutableMapOf>() - - dayNames.forEachIndexed { index, dayName -> - val courses = - when (dayName) { - "monday" -> timetableData.Monday - "tuesday" -> timetableData.Tuesday - "wednesday" -> timetableData.Wednesday - "thursday" -> timetableData.Thursday - "friday" -> timetableData.Friday - "saturday" -> timetableData.Saturday - "sunday" -> timetableData.Sunday - else -> emptyList() - } + val result = mutableMapOf>() + + dayNames.forEachIndexed { index, dayName -> + val courses = + when (dayName) { + "monday" -> timetableData.Monday + "tuesday" -> timetableData.Tuesday + "wednesday" -> timetableData.Wednesday + "thursday" -> timetableData.Thursday + "friday" -> timetableData.Friday + "saturday" -> timetableData.Saturday + "sunday" -> timetableData.Sunday + else -> emptyList() + } - val periods = - courses - ?.map { course -> - PeriodDetails( - courseCode = course.code, - courseName = course.name, - startTime = parseTimeToTimestamp(course.start_time), - endTime = parseTimeToTimestamp(course.end_time), - slot = course.slot, - roomNo = course.venue, - ) - } - ?.sortedBy { it.startTime.toDate() } - ?: emptyList() - - result[index] = periods - } + val periods = + courses + ?.map { course -> + PeriodDetails( + courseCode = course.code, + courseName = course.name, + startTime = parseTimeToTimestamp(course.start_time), + endTime = parseTimeToTimestamp(course.end_time), + slot = course.slot, + roomNo = course.venue, + ) + }?.sortedBy { it.startTime.toDate() } + ?: emptyList() - result + result[index] = periods } + + result + } diff --git a/app/src/main/java/com/dscvit/vitty/ui/connect/FriendRequestsScreenContent.kt b/app/src/main/java/com/dscvit/vitty/ui/connect/FriendRequestsScreenContent.kt index 22a124c..6142d02 100644 --- a/app/src/main/java/com/dscvit/vitty/ui/connect/FriendRequestsScreenContent.kt +++ b/app/src/main/java/com/dscvit/vitty/ui/connect/FriendRequestsScreenContent.kt @@ -180,7 +180,7 @@ fun FriendRequestsScreenContent( val token = sharedPreferences.getString(Constants.COMMUNITY_TOKEN, "") ?: "" if (token.isNotEmpty()) { processedRequests = processedRequests + user.username - connectViewModel.acceptRequest(token, user.username) + connectViewModel.acceptRequest(token, user.username, sharedPreferences) } }, onReject = { user -> diff --git a/build.gradle b/build.gradle index 796e432..32c055b 100755 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { maven { url "https://jitpack.io" } } dependencies { - classpath 'com.android.tools.build:gradle:8.11.1' + classpath 'com.android.tools.build:gradle:8.12.0' classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.21' classpath 'androidx.navigation:navigation-safe-args-gradle-plugin:2.9.2' classpath 'com.google.gms:google-services:4.4.3'