From 8e18cf8004dbb2628cf08ebbfd0d6b453b4147e2 Mon Sep 17 00:00:00 2001 From: JustAGhost23 Date: Sun, 25 Feb 2024 22:17:44 +0530 Subject: [PATCH 1/5] Add Notification model --- .../crux/bphc/cms/models/core/Notification.kt | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 app/src/main/java/crux/bphc/cms/models/core/Notification.kt diff --git a/app/src/main/java/crux/bphc/cms/models/core/Notification.kt b/app/src/main/java/crux/bphc/cms/models/core/Notification.kt new file mode 100644 index 00000000..0e0d6961 --- /dev/null +++ b/app/src/main/java/crux/bphc/cms/models/core/Notification.kt @@ -0,0 +1,19 @@ +package crux.bphc.cms.models.core + +import com.google.gson.annotations.SerializedName +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +/** + * Model class to represent the response from [crux.bphc.cms.network.MoodleServices.fetchNotifications] + */ + +open class Notification( + @PrimaryKey @SerializedName("id") var notificationId: Int = 0, + @SerializedName("useridto") var userIdTo: Int = 0, + @SerializedName("subject") var subject: String = "", + @SerializedName("timecreatedpretty") var timeCreated: String = "", + @SerializedName("smallmessage") var message: String = "", + @SerializedName("read") var read: Boolean = false, + @SerializedName("contexturl") var url: String? = null +): RealmObject() \ No newline at end of file From 1eebfe8e896a49176eee75457c31dba5edeba8fb Mon Sep 17 00:00:00 2001 From: JustAGhost23 Date: Sun, 25 Feb 2024 22:20:52 +0530 Subject: [PATCH 2/5] Add notification related moodle services to MoodleServices.java --- .../crux/bphc/cms/network/MoodleServices.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/app/src/main/java/crux/bphc/cms/network/MoodleServices.java b/app/src/main/java/crux/bphc/cms/network/MoodleServices.java index 702e2c68..29c65fd2 100644 --- a/app/src/main/java/crux/bphc/cms/network/MoodleServices.java +++ b/app/src/main/java/crux/bphc/cms/network/MoodleServices.java @@ -101,6 +101,26 @@ Call getForumDiscussions(@Query("wstoken") String token, @GET("webservice/rest/server.php?wsfunction=core_message_mark_all_notifications_as_read&moodlewsrestformat=json") Call markAllNotificationsAsRead(@Query("wstoken") String token, @Query("useridto") int userid); + /** + * Mark a notification as read for a user + * + * @param token A valid Moodle Web Service token + * @param notificationId Id of the user whose number of unread notifications are needed + */ + + @GET("webservice/rest/server.php?wsfunction=core_message_mark_notification_read&moodlewsrestformat=json") + Call markNotificationRead(@Query("wstoken") String token, @Query("notificationid") int notificationId); + + /** + * Fetches recent popup notifications of a user + * + * @param token A valid Moodle Web Service token + * @param userid Id of the user whose notifications are needed + */ + + @GET("webservice/rest/server.php?wsfunction=message_popup_get_popup_notifications&moodlewsrestformat=json") + Call fetchNotifications(@Query("wstoken") String token, @Query("useridto") int userid); + @GET("webservice/rest/server.php?wsfunction=core_user_add_user_device&moodlewsrestformat=json") Call registerUserDevice(@Query("wstoken") @NotNull String token, @Query("appid") @NotNull String appid, From 897a34e2d2c286a93ce177cf2fe331050450d70a Mon Sep 17 00:00:00 2001 From: JustAGhost23 Date: Sun, 25 Feb 2024 22:29:24 +0530 Subject: [PATCH 3/5] Add and update helper functions for notification use cases --- .../bphc/cms/helper/CourseDataHandler.java | 46 ++++++++++++++- .../bphc/cms/helper/CourseRequestHandler.java | 56 +++++++++++++++++++ 2 files changed, 100 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/crux/bphc/cms/helper/CourseDataHandler.java b/app/src/main/java/crux/bphc/cms/helper/CourseDataHandler.java index 5c7ef208..1dcdb647 100644 --- a/app/src/main/java/crux/bphc/cms/helper/CourseDataHandler.java +++ b/app/src/main/java/crux/bphc/cms/helper/CourseDataHandler.java @@ -13,6 +13,7 @@ import java.util.stream.Collectors; import crux.bphc.cms.models.UserAccount; +import crux.bphc.cms.models.core.Notification; import crux.bphc.cms.models.course.Content; import crux.bphc.cms.models.course.Course; import crux.bphc.cms.models.course.CourseSection; @@ -78,6 +79,44 @@ public void replaceCourses(@NotNull List courses) { } } + @NotNull + public List getNotifications() { + if (realm != null) { + return realm.copyFromRealm(realm.where(Notification.class).findAll()); + } else { + throw new NullPointerException("Realm instance is null"); + } + } + + public void replaceNotifications(@NotNull List notifications) { + if (realm != null) { + realm.beginTransaction(); + realm.delete(Notification.class); + realm.copyToRealm(notifications); + realm.commitTransaction(); + } else { + throw new NullPointerException("Realm instance is null"); + } + } + + public void markNotificationAsRead(Notification notification) { + notification.setRead(true); + realm.executeTransaction(r -> { + Notification foundNotification = realm.where(Notification.class) + .equalTo("notificationId", notification.getNotificationId()) + .findFirst(); + if(foundNotification != null) { + foundNotification.setRead(true); + } + }); + } + + public int getUnreadNotificationCount() { + return realm.where(Notification.class) + .equalTo("read", false) + .findAll().size(); + } + /** * Isolates and returns all Course instances from * courses. @@ -346,7 +385,10 @@ public void markCourseAsRead(int courseId) { * Mark all modules across all courses as read. */ public void markAllAsRead() { - realm.executeTransaction(r -> r.where(Module.class).findAll().setBoolean("isUnread", false)); + realm.executeTransaction(r -> { + r.where(Module.class).findAll().setBoolean("isUnread", false); + r.where(Notification.class).findAll().setBoolean("read", true); + }); } public void markModuleAsRead(Module module) { @@ -368,7 +410,7 @@ public void markModuleAsUnread(Module module) { public String getCourseName(int courseId) { Course course = realm.where(Course.class).equalTo("id", courseId).findFirst(); if (course == null) return ""; - return course.getShortName(); + return course.getFullName(); } public String getCourseNameForActionBarTitle(int courseId){ diff --git a/app/src/main/java/crux/bphc/cms/helper/CourseRequestHandler.java b/app/src/main/java/crux/bphc/cms/helper/CourseRequestHandler.java index c0f43651..0c971c96 100644 --- a/app/src/main/java/crux/bphc/cms/helper/CourseRequestHandler.java +++ b/app/src/main/java/crux/bphc/cms/helper/CourseRequestHandler.java @@ -12,6 +12,8 @@ import com.google.gson.reflect.TypeToken; import org.jetbrains.annotations.NotNull; +import org.json.JSONException; +import org.json.JSONObject; import java.io.IOException; import java.util.ArrayList; @@ -21,6 +23,7 @@ import crux.bphc.cms.exceptions.InvalidTokenException; import crux.bphc.cms.models.UserAccount; +import crux.bphc.cms.models.core.Notification; import crux.bphc.cms.models.course.Content; import crux.bphc.cms.models.course.Course; import crux.bphc.cms.models.course.CourseSection; @@ -229,6 +232,59 @@ public void onFailure(@NotNull Call> call, @NotNull Throwabl }); } + public List fetchNotificationListSync() throws IOException, RuntimeException, InvalidTokenException { + + Call notificationCall = moodleServices.fetchNotifications(userAccount.getToken(), userAccount.getUserID()); + + try { + Response response = notificationCall.execute(); + if (response.code() != 200) { // Moodle returns 200 for all API calls + HttpException e = new HttpException(response); + Log.e(TAG, "Response code not 200!", e); + throw e; + } + + if (response.body() == null) { + throw new RuntimeException("Response body is null"); + } + + String responseString = response.body().string(); + if (responseString.contains("Invalid token")) { + throw new InvalidTokenException(); + } + JSONObject json = new JSONObject(responseString); + Gson gson = new Gson(); + return gson.fromJson( + json.getJSONArray("notifications").toString(), new TypeToken>() {}.getType()); + } catch (IOException e) { + Log.e(TAG, "IOException when fetching Notification List", e); + throw e; + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + + public void markNotificationAsRead(int notificationId) throws IOException, RuntimeException { + Call notificationCall = moodleServices.markNotificationRead(userAccount.getToken(), notificationId); + + try { + Response response = notificationCall.execute(); + + if (response.code() != 200) { // Moodle returns 200 for all API calls + HttpException e = new HttpException(response); + Log.e(TAG, "Response code not 200!", e); + throw e; + } + + if (response.body() == null) { + throw new RuntimeException("Response body is null"); + } + } catch (IOException e) { + Log.e(TAG, "IOException when fetching unread notification count", e); + throw e; + } + } + public Boolean markAllNotificationsAsRead() throws IOException, RuntimeException { Call notificationCall = moodleServices.markAllNotificationsAsRead(userAccount.getToken(), userAccount.getUserID()); From d6d6eccf0eef15e766635b078e95e8d22836e2ab Mon Sep 17 00:00:00 2001 From: JustAGhost23 Date: Sun, 25 Feb 2024 22:37:59 +0530 Subject: [PATCH 4/5] Add layouts for notification screen --- .../res/layout/fragment_notifications.xml | 38 +++++++++ app/src/main/res/layout/row_notification.xml | 85 +++++++++++++++++++ app/src/main/res/menu/my_courses_menu.xml | 7 +- app/src/main/res/values/strings.xml | 4 + 4 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 app/src/main/res/layout/fragment_notifications.xml create mode 100644 app/src/main/res/layout/row_notification.xml diff --git a/app/src/main/res/layout/fragment_notifications.xml b/app/src/main/res/layout/fragment_notifications.xml new file mode 100644 index 00000000..c1a06fef --- /dev/null +++ b/app/src/main/res/layout/fragment_notifications.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/row_notification.xml b/app/src/main/res/layout/row_notification.xml new file mode 100644 index 00000000..68991f20 --- /dev/null +++ b/app/src/main/res/layout/row_notification.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/my_courses_menu.xml b/app/src/main/res/menu/my_courses_menu.xml index c451cdda..f28605a4 100644 --- a/app/src/main/res/menu/my_courses_menu.xml +++ b/app/src/main/res/menu/my_courses_menu.xml @@ -1,8 +1,13 @@ + + app:showAsAction="always" /> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d64f6fba..29e542a8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -31,6 +31,10 @@ %d Courses updated + + open notifications + No Notifications to Display + open page in browser mark all as read From 602bb77dd445bfe35f4a9335ae5d250564a06f28 Mon Sep 17 00:00:00 2001 From: JustAGhost23 Date: Sun, 25 Feb 2024 22:51:35 +0530 Subject: [PATCH 5/5] Add NotificationsFragment and update MyCoursesFragment to display notifications to users --- .../bphc/cms/fragments/MyCoursesFragment.kt | 100 +++++++- .../cms/fragments/NotificationsFragment.kt | 239 ++++++++++++++++++ 2 files changed, 336 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/crux/bphc/cms/fragments/NotificationsFragment.kt diff --git a/app/src/main/java/crux/bphc/cms/fragments/MyCoursesFragment.kt b/app/src/main/java/crux/bphc/cms/fragments/MyCoursesFragment.kt index 792bd1ae..3c2f126e 100644 --- a/app/src/main/java/crux/bphc/cms/fragments/MyCoursesFragment.kt +++ b/app/src/main/java/crux/bphc/cms/fragments/MyCoursesFragment.kt @@ -11,15 +11,20 @@ import android.view.* import android.view.inputmethod.InputMethodManager import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.OptIn +import androidx.appcompat.app.AppCompatActivity import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.badge.BadgeDrawable +import com.google.android.material.badge.BadgeUtils +import com.google.android.material.badge.ExperimentalBadgeUtils import com.google.android.material.dialog.MaterialAlertDialogBuilder import crux.bphc.cms.R import crux.bphc.cms.activities.CourseDetailActivity +import crux.bphc.cms.activities.MainActivity import crux.bphc.cms.databinding.FragmentMyCoursesBinding import crux.bphc.cms.databinding.RowCourseBinding import crux.bphc.cms.exceptions.InvalidTokenException @@ -29,8 +34,6 @@ import crux.bphc.cms.helper.CourseRequestHandler import crux.bphc.cms.interfaces.ClickListener import crux.bphc.cms.models.course.Course import crux.bphc.cms.models.course.CourseSection -import crux.bphc.cms.network.APIClient -import crux.bphc.cms.network.MoodleServices import crux.bphc.cms.utils.UserUtils import io.realm.Realm import kotlinx.coroutines.* @@ -67,6 +70,7 @@ class MyCoursesFragment : Fragment() { override fun onStart() { super.onStart() requireActivity().title = "My Courses" + badge = null } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, @@ -76,33 +80,59 @@ class MyCoursesFragment : Fragment() { return binding.root } + private fun pushView(fragment: Fragment, tag: String) { + try { + val activity = requireActivity() as MainActivity + activity.pushView(fragment, tag, false) + } catch (e: Exception) { + Toast.makeText(requireContext(), "Failed to launch fragment $tag", Toast.LENGTH_SHORT) + .show() + e.printStackTrace() + } + } + + @ExperimentalBadgeUtils + @OptIn(markerClass = [ExperimentalBadgeUtils::class]) override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { super.onCreateOptionsMenu(menu, inflater) + attachBatchDrawable() inflater.inflate(R.menu.my_courses_menu, menu) } + @ExperimentalBadgeUtils + @OptIn(markerClass = [ExperimentalBadgeUtils::class]) override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.mark_all_as_read -> { CoroutineScope(Dispatchers.Default).launch { courseRequestHandler = CourseRequestHandler() - courseRequestHandler.markAllNotificationsAsRead() + val isSuccessful = courseRequestHandler.markAllNotificationsAsRead() val realm = Realm.getDefaultInstance() val courseDataHandler = CourseDataHandler(realm) courseDataHandler.markAllAsRead() realm.close() CoroutineScope(Dispatchers.Main).launch { + if(isSuccessful) { + detachBadgeDrawable() + } Toast.makeText(requireActivity(), "Marked all as read", Toast.LENGTH_SHORT).show() mAdapter.courses = this@MyCoursesFragment.courseDataHandler.courseList } } true } + R.id.current_notifications -> { + detachBadgeDrawable() + pushView(NotificationsFragment(), "notifications") + true + } else -> false } } + @ExperimentalBadgeUtils + @OptIn(markerClass = [ExperimentalBadgeUtils::class]) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) courseDataHandler = CourseDataHandler(realm) @@ -193,6 +223,7 @@ class MyCoursesFragment : Fragment() { binding.swipeRefreshLayout.setOnRefreshListener { binding.swipeRefreshLayout.isRefreshing = true refreshCourses() + refreshNotifications() } checkEmpty() @@ -283,6 +314,69 @@ class MyCoursesFragment : Fragment() { } } } + @ExperimentalBadgeUtils + @OptIn(markerClass = [ExperimentalBadgeUtils::class]) + private fun refreshNotifications() { + lifecycleScope.launch { + launch(Dispatchers.IO) { // lifecycle scope allows cancellation of this scope + val courseRequestHandler = CourseRequestHandler() + try { + val notificationList = courseRequestHandler.fetchNotificationListSync() + Log.i(NotificationsFragment.TAG, "${notificationList.size} notifications") + val realm = Realm.getDefaultInstance() // tie a realm instance to this thread + val courseDataHandler = CourseDataHandler(realm) + courseDataHandler.replaceNotifications(notificationList) + realm.close() + attachBatchDrawable() + } catch (e: Exception) { + Log.e(NotificationsFragment.TAG, "", e) + withContext(Dispatchers.Main) { + Toast.makeText(requireActivity(), "Error: ${e.message}", Toast.LENGTH_SHORT).show() + if (e is InvalidTokenException) { + UserUtils.logout() + UserUtils.clearBackStackAndLaunchTokenActivity(requireActivity()) + } + } + } finally { + withContext(Dispatchers.Main) { + binding.swipeRefreshLayout?.isRefreshing = false + } + } + } + } + } + + @ExperimentalBadgeUtils + @OptIn(markerClass = [ExperimentalBadgeUtils::class]) + private fun attachBatchDrawable() { + val realm = Realm.getDefaultInstance() + val courseDataHandler = CourseDataHandler(realm) + val notifCount: Int = courseDataHandler.unreadNotificationCount + val visibility: Boolean = courseDataHandler.unreadNotificationCount > 0 + badge = BadgeDrawable.create(requireContext()).apply { + badgeGravity = BadgeDrawable.TOP_END + number = notifCount + isVisible = visibility + } + BadgeUtils.attachBadgeDrawable( + badge!!, + (activity as AppCompatActivity).findViewById(R.id.toolbar), + R.id.current_notifications + ) + realm.close() + } + @ExperimentalBadgeUtils + @OptIn(markerClass = [ExperimentalBadgeUtils::class]) + private fun detachBadgeDrawable() { + if(badge != null) { + BadgeUtils.detachBadgeDrawable( + badge!!, + (activity as AppCompatActivity).findViewById(R.id.toolbar), + R.id.current_notifications + ) + badge = null + } + } override fun onDestroyView() { super.onDestroyView() diff --git a/app/src/main/java/crux/bphc/cms/fragments/NotificationsFragment.kt b/app/src/main/java/crux/bphc/cms/fragments/NotificationsFragment.kt new file mode 100644 index 00000000..10d10320 --- /dev/null +++ b/app/src/main/java/crux/bphc/cms/fragments/NotificationsFragment.kt @@ -0,0 +1,239 @@ +package crux.bphc.cms.fragments + +import android.content.Context +import android.os.Bundle +import android.util.Log +import android.util.TypedValue +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import crux.bphc.cms.R +import crux.bphc.cms.databinding.FragmentNotificationsBinding +import crux.bphc.cms.databinding.RowNotificationBinding +import crux.bphc.cms.exceptions.InvalidTokenException +import crux.bphc.cms.helper.CourseDataHandler +import crux.bphc.cms.helper.CourseRequestHandler +import crux.bphc.cms.interfaces.ClickListener +import crux.bphc.cms.models.core.Notification +import crux.bphc.cms.utils.UserUtils +import crux.bphc.cms.utils.Utils +import io.realm.Realm +import kotlinx.coroutines.* +import java.util.* + +class NotificationsFragment : Fragment() { + private lateinit var realm: Realm + private lateinit var courseDataHandler: CourseDataHandler + private lateinit var courseRequestHandler: CourseRequestHandler + private lateinit var binding: FragmentNotificationsBinding + + private var notifications: MutableList = ArrayList() + + private lateinit var mAdapter: NotificationsFragment.Adapter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = FragmentNotificationsBinding.inflate(layoutInflater) + + setHasOptionsMenu(true) + } + + override fun onStart() { + super.onStart() + requireActivity().title = "Notifications" + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + realm = Realm.getDefaultInstance() + + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + courseDataHandler = CourseDataHandler(realm) + courseRequestHandler = CourseRequestHandler() + notifications = courseDataHandler.notifications + + // Set up the adapter + mAdapter = Adapter(requireActivity(), notifications) + mAdapter.notifications = notifications + mAdapter.clickListener = ClickListener { `object`: Any, position: Int -> + val notification = `object` as Notification + if(notification.url != null) { + Utils.openURLInBrowser(requireActivity(), notification.url) + } + else { + val message = "No URL associated with this notification" + Toast.makeText(activity, message, Toast.LENGTH_SHORT).show() + } + return@ClickListener true + } + + binding.notificationRecyclerView.adapter = mAdapter + binding.notificationRecyclerView.layoutManager = LinearLayoutManager(activity) + + binding.notificationSwipeRefreshLayout.setOnRefreshListener { + binding.notificationSwipeRefreshLayout.isRefreshing = true + refreshNotifications() + } + lifecycleScope.launch { + launch(Dispatchers.IO) { + updateNotificationContent() + } + } + checkEmpty() + } + + private fun checkEmpty() { + if (notifications.isEmpty()) { + binding.notificationEmpty?.visibility = View.VISIBLE + } else { + binding.notificationEmpty?.visibility = View.GONE + } + } + + private fun refreshNotifications() { + lifecycleScope.launch { + launch(Dispatchers.IO) { // lifecycle scope allows cancellation of this scope + val courseRequestHandler = CourseRequestHandler() + try { + val notificationList = courseRequestHandler.fetchNotificationListSync() + notifications.clear() + notifications.addAll(notificationList) + Log.i(TAG, "${notificationList.size} notifications") + val realm = Realm.getDefaultInstance() // tie a realm instance to this thread + val courseDataHandler = CourseDataHandler(realm) + courseDataHandler.replaceNotifications(notificationList) + realm.close() + withContext(Dispatchers.Main) { + checkEmpty() + } + updateNotificationContent() + withContext(Dispatchers.Main) { + val message = "Notifications are up to date" + Toast.makeText(activity, message, Toast.LENGTH_SHORT).show() + } + } catch (e: Exception) { + Log.e(TAG, "", e) + withContext(Dispatchers.Main) { + Toast.makeText(requireActivity(), "Error: ${e.message}", Toast.LENGTH_SHORT).show() + if (e is InvalidTokenException) { + UserUtils.logout() + UserUtils.clearBackStackAndLaunchTokenActivity(requireActivity()) + } + } + } finally { + withContext(Dispatchers.Main) { + binding.notificationSwipeRefreshLayout?.isRefreshing = false + } + } + } + } + } + + private suspend fun updateNotificationContent() { + withContext(Dispatchers.Main) { + Log.i(MyCoursesFragment.TAG, "Fetching notifications") + binding.notificationSwipeRefreshLayout?.isRefreshing = false + mAdapter.filterNotifications(notifications) + } + } + + override fun onDestroyView() { + super.onDestroyView() + realm.close() + } + + private inner class Adapter constructor( + val context: Context, + notificationList: List + ) : RecyclerView.Adapter() { + private val inflater: LayoutInflater = LayoutInflater.from(context) + + var clickListener: ClickListener? = null + var notifications: List = ArrayList() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder { + val itemBinding = RowNotificationBinding.inflate(inflater, parent, false) + return MyViewHolder(itemBinding) + } + + override fun onBindViewHolder(holder: MyViewHolder, position: Int) { + val notification = notifications[position] + setLayoutTheme(holder, notification) + holder.bind(notification) + } + + override fun getItemCount(): Int { + return notifications.size + } + + private fun setLayoutTheme(vh: NotificationsFragment.Adapter.MyViewHolder, notification: Notification) { + val value = TypedValue() + if (!notification.read) { + activity?.theme?.resolveAttribute(R.attr.unReadModule, value, true) + } else { + activity?.theme?.resolveAttribute(R.attr.cardBgColor, value, true) + } + vh.itemView.findViewById(R.id.layout_wrapper).setBackgroundColor(value.data) + } + + fun filterNotifications(notificationList: List) { + val filteredCourses: MutableList = notificationList as MutableList + notifications = filteredCourses + notifyDataSetChanged() + } + + inner class MyViewHolder(val itemBinding: RowNotificationBinding) : RecyclerView.ViewHolder(itemBinding.root) { + fun bind(notification: Notification) { + itemBinding.notificationSubject.text = notification.subject + itemBinding.notificationMessage.text = notification.message + itemBinding.markAsReadButton.isVisible = notification.read == false + itemBinding.timeCreated.text = notification.timeCreated + } + + fun markNotificationAsRead() { + val notificationId = notifications[layoutPosition].notificationId + lifecycleScope.launch { + launch(Dispatchers.IO) { + courseRequestHandler.markNotificationAsRead(notificationId) + } + } + courseDataHandler.markNotificationAsRead(notifications[layoutPosition]) + notifyItemChanged(layoutPosition) + + Toast.makeText(activity, "Marked as read", Toast.LENGTH_SHORT).show() + } + + init { + itemBinding.notificationCard.setOnClickListener { + if (clickListener != null) { + val pos = layoutPosition + clickListener!!.onClick(notifications[pos], pos) + } + } + itemBinding.markAsReadButton.setOnClickListener { markNotificationAsRead() } + } + } + + init { + this.notifications = notificationList + } + } + + companion object { + @JvmStatic + fun newInstance() = NotificationsFragment() + + @JvmStatic + val TAG = "NotificationsFragment" + } +} \ No newline at end of file