From 1803b51ed61c97f5d524eec2fd9dc30d84932bb2 Mon Sep 17 00:00:00 2001 From: alperenDagi <111731140+alperenDagi@users.noreply.github.com> Date: Tue, 21 Nov 2023 16:11:19 +0300 Subject: [PATCH 1/2] Notification Screen created Notification Viewmodel created Notification UseCase created Notification Api created --- .../resq/data/models/NotificationItem.kt | 9 ++ .../resq/data/remote/NotificationApi.kt | 37 +++++ .../resq/domain/NotificationUseCase.kt | 25 ++++ .../ui/views/screens/NotificationScreen.kt | 131 ++++++++++++++++++ .../resq/viewmodels/NotificationViewModel.kt | 38 +++++ 5 files changed, 240 insertions(+) create mode 100644 resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/data/models/NotificationItem.kt create mode 100644 resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/data/remote/NotificationApi.kt create mode 100644 resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/domain/NotificationUseCase.kt create mode 100644 resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/ui/views/screens/NotificationScreen.kt create mode 100644 resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/viewmodels/NotificationViewModel.kt diff --git a/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/data/models/NotificationItem.kt b/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/data/models/NotificationItem.kt new file mode 100644 index 00000000..31d29f1c --- /dev/null +++ b/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/data/models/NotificationItem.kt @@ -0,0 +1,9 @@ +package com.cmpe451.resq.data.models + +data class NotificationItem( + val id: Int, + val title: String, + val subtitle: String, + val time: String, + val isActionable: Boolean +) \ No newline at end of file diff --git a/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/data/remote/NotificationApi.kt b/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/data/remote/NotificationApi.kt new file mode 100644 index 00000000..9dff2503 --- /dev/null +++ b/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/data/remote/NotificationApi.kt @@ -0,0 +1,37 @@ +package com.cmpe451.resq.data.remote + +import com.cmpe451.resq.data.models.NotificationItem +import retrofit2.Response +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.http.Body +import retrofit2.http.GET + + +data class NotificationRequest( + val userID: String, + val password: String +) +interface NotificationService { + @GET("notification/getUserNotifications") + suspend fun getNotifications(@Body request: NotificationRequest): Response> +} +class NotificationApi { + + private val notificationService: NotificationService + + init { + val retrofit = Retrofit.Builder() + .baseUrl("http://16.16.63.194/resq/api/v1/") // TODO: Replace with backend URL + .addConverterFactory(GsonConverterFactory.create()) + .build() + + notificationService = retrofit.create(NotificationService::class.java) + } + + + suspend fun getNotifications(notificationRequest: NotificationRequest): Response> { + return notificationService.getNotifications(notificationRequest) + } + +} diff --git a/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/domain/NotificationUseCase.kt b/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/domain/NotificationUseCase.kt new file mode 100644 index 00000000..9e4e1bb3 --- /dev/null +++ b/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/domain/NotificationUseCase.kt @@ -0,0 +1,25 @@ +package com.cmpe451.resq.domain + +import com.cmpe451.resq.data.remote.AuthApi +import com.cmpe451.resq.data.remote.RegisterRequest + +class NotificationUseCase() { + + private val authApi = AuthApi() + + suspend fun execute( + name: String, + surname: String, + email: String, + password: String + ): Result { + + val response = authApi.register(RegisterRequest(name, surname, email, password)) + if (response.isSuccessful) { + response.body()?.let { + return Result.success(it.string()) + } + } + return Result.failure(Throwable(response.message())) + } +} \ No newline at end of file diff --git a/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/ui/views/screens/NotificationScreen.kt b/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/ui/views/screens/NotificationScreen.kt new file mode 100644 index 00000000..316ce72b --- /dev/null +++ b/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/ui/views/screens/NotificationScreen.kt @@ -0,0 +1,131 @@ +package com.cmpe451.resq.ui.views.screens + +import android.annotation.SuppressLint +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.Button +import androidx.compose.material.Card +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.material.TopAppBar +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccountCircle +import androidx.compose.material.icons.filled.Notifications +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.cmpe451.resq.data.models.NotificationItem +import com.cmpe451.resq.viewmodels.NotificationViewModel + +@SuppressLint("UnusedMaterialScaffoldPaddingParameter") +@Composable +fun NotificationScreen() { + val viewModel = NotificationViewModel() + val notifications by viewModel.notificationItems + + Scaffold( + topBar = { + TopAppBar( + title = { Text("Notifications") }, + navigationIcon = { + Icon( + Icons.Default.Notifications, + contentDescription = "Notifications" + ) + }, + backgroundColor = Color.White, + contentColor = Color.Black + ) + } + ) { + NotificationList(listOf( + NotificationItem(1, "Request #1", "is included by Facilitator Justin Westervelt", "9:01 am", false), + NotificationItem(2, "Resource #345", "arrived to Facilitator Lindsey Culhane", "9:01 am", true) + // Add more mock notifications or fetch from a repository + )) + } +} + +@Composable +fun NotificationList(notificationItems: List) { + LazyColumn { + items(notificationItems) { item -> + NotificationItemCard(item) + } + } +} + +@Composable +fun NotificationItemCard(notification: NotificationItem) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + elevation = 2.dp + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + // Replace this with an actual image using Coil or other image loading library + Icon( + Icons.Default.AccountCircle, + contentDescription = "Profile", + modifier = Modifier.size(40.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Column { + Text( + text = notification.title, + fontWeight = FontWeight.Bold + ) + Text( + text = notification.subtitle, + style = MaterialTheme.typography.body2 + ) + } + } + Spacer(modifier = Modifier.height(8.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = notification.time, + style = MaterialTheme.typography.body2 + ) + if (notification.isActionable) { + Button(onClick = { /* TODO: Handle View action */ }) { + Text("VIEW") + } + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +fun DefaultPreview() { + NotificationScreen() +} \ No newline at end of file diff --git a/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/viewmodels/NotificationViewModel.kt b/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/viewmodels/NotificationViewModel.kt new file mode 100644 index 00000000..b542a263 --- /dev/null +++ b/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/viewmodels/NotificationViewModel.kt @@ -0,0 +1,38 @@ +package com.cmpe451.resq.viewmodels + +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.cmpe451.resq.data.models.NotificationItem +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class NotificationViewModel : ViewModel() { + + private val _notificationItems by lazy { mutableStateOf>(listOf()) } + val notificationItems: State> = _notificationItems + + + init { + fetchNotifications() + } + + private fun fetchNotifications() { + viewModelScope.launch { + val notifications = withContext(Dispatchers.IO) { + // Simulate network call or database call to fetch notifications + // This is where you would normally use a repository to get your data + listOf( + NotificationItem(1, "Request #1", "is included by Facilitator Justin Westervelt", "9:01 am", false), + NotificationItem(2, "Resource #345", "arrived to Facilitator Lindsey Culhane", "9:01 am", true) + // Add more mock notifications or fetch from a repository + ) + } + _notificationItems.value = notifications + } + } + + // Add any additional functions that your UI may need to interact with the data +} \ No newline at end of file From 8af726e0bfb68ed31b2197a063578a50949eb975 Mon Sep 17 00:00:00 2001 From: alperenDagi <111731140+alperenDagi@users.noreply.github.com> Date: Sat, 23 Dec 2023 19:04:32 +0300 Subject: [PATCH 2/2] NotificationScreen Implementation --- .../java/com/cmpe451/resq/MainActivity.kt | 3 +- .../resq/data/models/NotificationItem.kt | 10 +++- .../resq/data/remote/NotificationApi.kt | 37 ------------ .../cmpe451/resq/data/remote/ResqService.kt | 22 +++++++ .../resq/domain/NotificationUseCase.kt | 25 -------- .../ui/views/screens/NotificationScreen.kt | 26 +++----- .../resq/viewmodels/NotificationViewModel.kt | 59 ++++++++++++++----- 7 files changed, 85 insertions(+), 97 deletions(-) delete mode 100644 resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/data/remote/NotificationApi.kt delete mode 100644 resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/domain/NotificationUseCase.kt diff --git a/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/MainActivity.kt b/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/MainActivity.kt index e6d83495..7b42bc63 100644 --- a/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/MainActivity.kt +++ b/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/MainActivity.kt @@ -35,6 +35,7 @@ import com.cmpe451.resq.ui.theme.LightGreen import com.cmpe451.resq.ui.theme.ResQTheme import com.cmpe451.resq.ui.views.screens.LoginScreen import com.cmpe451.resq.ui.views.screens.MapScreen +import com.cmpe451.resq.ui.views.screens.NotificationScreen import com.cmpe451.resq.ui.views.screens.OngoingTasksScreen import com.cmpe451.resq.ui.views.screens.ProfileScreen import com.cmpe451.resq.ui.views.screens.RegistrationScreen @@ -143,7 +144,7 @@ fun NavGraph( ProfileScreen(navController, appContext) } composable(NavigationItem.Notifications.route) { - //NotificationsScreen(navController) + NotificationScreen(navController, appContext) } composable(NavigationItem.Settings.route) { SettingsScreen(navController, appContext) diff --git a/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/data/models/NotificationItem.kt b/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/data/models/NotificationItem.kt index 31d29f1c..38171279 100644 --- a/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/data/models/NotificationItem.kt +++ b/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/data/models/NotificationItem.kt @@ -2,8 +2,12 @@ package com.cmpe451.resq.data.models data class NotificationItem( val id: Int, + val createdAt: String, + val modifiedAt: String, + val userId: Int, val title: String, - val subtitle: String, - val time: String, - val isActionable: Boolean + val body: String, + val relatedEntityId: Int, + val notificationType: String, + val read: Boolean ) \ No newline at end of file diff --git a/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/data/remote/NotificationApi.kt b/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/data/remote/NotificationApi.kt deleted file mode 100644 index 9dff2503..00000000 --- a/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/data/remote/NotificationApi.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.cmpe451.resq.data.remote - -import com.cmpe451.resq.data.models.NotificationItem -import retrofit2.Response -import retrofit2.Retrofit -import retrofit2.converter.gson.GsonConverterFactory -import retrofit2.http.Body -import retrofit2.http.GET - - -data class NotificationRequest( - val userID: String, - val password: String -) -interface NotificationService { - @GET("notification/getUserNotifications") - suspend fun getNotifications(@Body request: NotificationRequest): Response> -} -class NotificationApi { - - private val notificationService: NotificationService - - init { - val retrofit = Retrofit.Builder() - .baseUrl("http://16.16.63.194/resq/api/v1/") // TODO: Replace with backend URL - .addConverterFactory(GsonConverterFactory.create()) - .build() - - notificationService = retrofit.create(NotificationService::class.java) - } - - - suspend fun getNotifications(notificationRequest: NotificationRequest): Response> { - return notificationService.getNotifications(notificationRequest) - } - -} diff --git a/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/data/remote/ResqService.kt b/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/data/remote/ResqService.kt index c06aeff9..f77caa80 100644 --- a/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/data/remote/ResqService.kt +++ b/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/data/remote/ResqService.kt @@ -2,6 +2,7 @@ package com.cmpe451.resq.data.remote import android.content.Context import android.os.Build +import android.util.Log import androidx.annotation.RequiresApi import com.cmpe451.resq.data.Constants import com.cmpe451.resq.data.manager.UserSessionManager @@ -11,6 +12,7 @@ import com.cmpe451.resq.data.models.CreateResourceRequestBody import com.cmpe451.resq.data.models.LoginRequestBody import com.cmpe451.resq.data.models.LoginResponse import com.cmpe451.resq.data.models.Need +import com.cmpe451.resq.data.models.NotificationItem import com.cmpe451.resq.data.models.ProfileData import com.cmpe451.resq.data.models.RegisterRequestBody import com.cmpe451.resq.data.models.UserInfoRequest @@ -96,6 +98,14 @@ interface ProfileService { } +interface NotificationService { + @GET("notification/viewAllNotifications") + suspend fun getNotifications( + @Query("userId") userId: Int, + @Header("Authorization") jwtToken: String + ): Response> +} + class ResqService(appContext: Context) { var gson = GsonBuilder() @@ -112,6 +122,7 @@ class ResqService(appContext: Context) { private val needService: NeedService = retrofit.create(NeedService::class.java) private val authService: AuthService = retrofit.create(AuthService::class.java) private val profileService: ProfileService = retrofit.create(ProfileService::class.java) + private val notificationService: NotificationService = retrofit.create(NotificationService::class.java) private val userSessionManager: UserSessionManager = UserSessionManager.getInstance(appContext) @@ -267,4 +278,15 @@ class ResqService(appContext: Context) { return response } + + suspend fun getNotifications(): Response> { + val userId = userSessionManager.getUserId() + val token = userSessionManager.getUserToken() ?: "" + val response = notificationService.getNotifications( + userId = userId, + jwtToken = "Bearer $token", + ) + Log.d("AAA", "getNotifications: ${response.isSuccessful}") + return response + } } \ No newline at end of file diff --git a/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/domain/NotificationUseCase.kt b/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/domain/NotificationUseCase.kt deleted file mode 100644 index 9e4e1bb3..00000000 --- a/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/domain/NotificationUseCase.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.cmpe451.resq.domain - -import com.cmpe451.resq.data.remote.AuthApi -import com.cmpe451.resq.data.remote.RegisterRequest - -class NotificationUseCase() { - - private val authApi = AuthApi() - - suspend fun execute( - name: String, - surname: String, - email: String, - password: String - ): Result { - - val response = authApi.register(RegisterRequest(name, surname, email, password)) - if (response.isSuccessful) { - response.body()?.let { - return Result.success(it.string()) - } - } - return Result.failure(Throwable(response.message())) - } -} \ No newline at end of file diff --git a/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/ui/views/screens/NotificationScreen.kt b/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/ui/views/screens/NotificationScreen.kt index 316ce72b..15aa513f 100644 --- a/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/ui/views/screens/NotificationScreen.kt +++ b/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/ui/views/screens/NotificationScreen.kt @@ -1,6 +1,7 @@ package com.cmpe451.resq.ui.views.screens import android.annotation.SuppressLint +import android.content.Context import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -12,7 +13,6 @@ 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.material.Button import androidx.compose.material.Card import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme @@ -28,15 +28,15 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.navigation.NavController import com.cmpe451.resq.data.models.NotificationItem import com.cmpe451.resq.viewmodels.NotificationViewModel @SuppressLint("UnusedMaterialScaffoldPaddingParameter") @Composable -fun NotificationScreen() { - val viewModel = NotificationViewModel() +fun NotificationScreen(navController: NavController, appContext: Context) { + val viewModel = NotificationViewModel(appContext) val notifications by viewModel.notificationItems Scaffold( @@ -54,11 +54,7 @@ fun NotificationScreen() { ) } ) { - NotificationList(listOf( - NotificationItem(1, "Request #1", "is included by Facilitator Justin Westervelt", "9:01 am", false), - NotificationItem(2, "Resource #345", "arrived to Facilitator Lindsey Culhane", "9:01 am", true) - // Add more mock notifications or fetch from a repository - )) + NotificationList(notifications) } } @@ -100,7 +96,7 @@ fun NotificationItemCard(notification: NotificationItem) { fontWeight = FontWeight.Bold ) Text( - text = notification.subtitle, + text = notification.createdAt, style = MaterialTheme.typography.body2 ) } @@ -111,21 +107,17 @@ fun NotificationItemCard(notification: NotificationItem) { horizontalArrangement = Arrangement.SpaceBetween ) { Text( - text = notification.time, + text = notification.body, style = MaterialTheme.typography.body2 ) + /* if (notification.isActionable) { Button(onClick = { /* TODO: Handle View action */ }) { Text("VIEW") } } + */ } } } -} - -@Preview(showBackground = true) -@Composable -fun DefaultPreview() { - NotificationScreen() } \ No newline at end of file diff --git a/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/viewmodels/NotificationViewModel.kt b/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/viewmodels/NotificationViewModel.kt index b542a263..5cbfec45 100644 --- a/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/viewmodels/NotificationViewModel.kt +++ b/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/viewmodels/NotificationViewModel.kt @@ -1,38 +1,69 @@ package com.cmpe451.resq.viewmodels +import android.content.Context import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.cmpe451.resq.data.models.NotificationItem -import kotlinx.coroutines.Dispatchers +import com.cmpe451.resq.data.remote.ResqService import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -class NotificationViewModel : ViewModel() { +class NotificationViewModel(appContext: Context) : ViewModel() { private val _notificationItems by lazy { mutableStateOf>(listOf()) } val notificationItems: State> = _notificationItems - init { - fetchNotifications() + fetchNotifications(appContext) } - private fun fetchNotifications() { + private fun fetchNotifications(appContext: Context) { viewModelScope.launch { - val notifications = withContext(Dispatchers.IO) { - // Simulate network call or database call to fetch notifications - // This is where you would normally use a repository to get your data + /* + val notifications = getNotifications(appContext) + notifications.getOrNull()?.let { + _notificationItems.value = it + } + */ + val notifications = listOf( - NotificationItem(1, "Request #1", "is included by Facilitator Justin Westervelt", "9:01 am", false), - NotificationItem(2, "Resource #345", "arrived to Facilitator Lindsey Culhane", "9:01 am", true) - // Add more mock notifications or fetch from a repository + NotificationItem( + id = 1, + createdAt = "2021-05-01T00:00:00.000Z", + modifiedAt = "2021-05-01T00:00:00.000Z", + userId = 1, + title = "Notification Title", + body = "Notification Body", + relatedEntityId = 1, + notificationType = "Notification Type", + read = false + ), + NotificationItem( + id = 2, + createdAt = "2021-05-01T00:00:00.000Z", + modifiedAt = "2021-05-01T00:00:00.000Z", + userId = 1, + title = "Notification Title", + body = "Notification Body", + relatedEntityId = 1, + notificationType = "Notification Type", + read = false + ), ) - } _notificationItems.value = notifications + } } - // Add any additional functions that your UI may need to interact with the data + suspend fun getNotifications(appContext: Context): Result> { + val api = ResqService(appContext) + val response = api.getNotifications() + if (response.isSuccessful) { + response.body()?.let { + return Result.success(it) + } + } + return Result.failure(Throwable(response.message())) + } } \ No newline at end of file