Skip to content

Conversation

@AlexVelezLl
Copy link
Member

@AlexVelezLl AlexVelezLl commented Dec 17, 2025

Summary

  • Added Notifications page.
    • Displays this page as a StudioImmersivePage.
    • To persist the state when reloading the page, I have implemented this so that it is opened if a Modal=NOTIFICATIONS query param is set in the route. Since this modal will be shown across the entire application, I chose this option rather than creating a new route for this page on all app routers.
    • Built the NotificationsList component in a way that each row corresponds to a notification type, and each notification type has its own renderer. So that we can extend this later if we want to support more notification types.
  • Ended up implementing the "red dot" by adding two new datetime fields in the User model: last_read_notification_date and newest_notification_date, the first representing the date of the last notification read by the user, the second the date of the most recent notification. If newest_notification_date > last_read_notification_date, the user has a new notification :).
  • Composables
    • Moved useFilter, useFetch, useKeywordSearch, and useQueryParams to the shared folder.
    • Modified useFilter composable to comply with the current KSelect API.
    • Added useSnackbar as a wrapper of the current Vuex Snackbar module, until this gets migrated.
    • Added useStore as sugar syntax to get the store from the currentInstance.
    • Added useCommunityLibraryUpdates composable to manage the data fetching and transformation of submissions into updates/notifications.
Grabacion.de.pantalla.2025-12-17.a.la.s.10.41.42.a.m.mov

References

Closes #5457
Closes #5458

Reviewer guidance

  • Check the new notifications page on the profile menu in the Appbar or by opening the main menu.
  • To create new notifications, create new Submissions.
  • If you want to see approved submissions/rejected submissions notifications, you can use the admin_communitylibrary_submission/{id}/resolve endpoint.

Tech debt introduced

  • Right now, notification filter selections don't close automatically when clicking outside the select input. This is a bug due to some weird interaction of VDialog and Popper, I did not want to spend too much time trying to solve this because we will end up replacing Vuetify in Studio anyways, but noting that this bug will be present until then. We can coordinate later if we can prioritize migrating the ImmersivePage into Studio before releasing the Community Library, or if we prefer spending more time trying to fix this bug. There is now a StudioImmersivePage! (Thanks @MisRob 😄 )

@AlexVelezLl AlexVelezLl force-pushed the notifications-page branch 3 times, most recently from c7df896 to 9a59b96 Compare December 19, 2025 16:13
@AlexVelezLl AlexVelezLl requested a review from rtibbles December 19, 2025 16:13
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests were moved to be their own test files

grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 6px 12px;

/* TODO: Open a KDS follow up to fix KTextbox feedback message alignment */
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

},
UPDATE_SESSION_FROM_INDEXEDDB(state, { id, ...mods }) {
if (id === state.currentUser.id) {
UPDATE_SESSION_FROM_INDEXEDDB(state, { CURRENT_USER, ...mods }) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a weird error, the key field in the session indexedDB table is CURRENT_USER and its only value could be CURRENT_USER so it doesnt make sense to look at the id and if id === state.currentUser.id for this table. Therefore, until now, this function wasn't doing anything.

@MisRob
Copy link
Member

MisRob commented Jan 15, 2026

Hi @AlexVelezLl, I didn't review, just noticed the note about FullscreenModal (assuming you refer to shared/views/FullscreenModal.vue). If so, would you instead please use non-Vuetify alternative StudioImmersiveModal? We built it recently as a replacement for FullscreenModal. Of course, feel free to adjust StudioImmersiveModal to this new use-case if it's needed.

If there are any other places like that, please let me know and we can chat. This tracker should be quite up-to-date so it may too help.

@MisRob
Copy link
Member

MisRob commented Jan 15, 2026

And also StudioPage + StudioOfflineAlert will be handy :) The first one takes care of consistent paddings on all screens - I hope we get to using it everywhere for these kinds of layouts in Studio.

@AlexVelezLl
Copy link
Member Author

Thanks a lot @MisRob! I did look for an alternative on Studio, but didn't find it. Will use the StudioImmersiveModal instead. Thanks!

@MisRob
Copy link
Member

MisRob commented Jan 15, 2026

Yeah the naming doesn't help - when possible I try to rename in line with Kolibri conventions :) Glad it helps.

Copy link

@rtibblesbot rtibblesbot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work on this notifications feature! This is a well-structured PR with clean composable architecture, good test coverage, and thoughtful design decisions (like using query params for modal persistence and the extensible notification type renderer pattern). The video demo looks polished.

There are a few issues to address before merging:

Blocking issues:

  • Variable shadowing bug in useFetch.js — the catch (error) parameter shadows the outer error ref, so error state is never written to the ref
  • notify_update_to_channel_editors() is called before super().save() for new submissions, but date_updated is auto_now=True and hasn't been set yet — the notification date will be None
  • notify_users() reads user.newest_notification_date from stale in-memory objects after .update() — the change events will push the old values
  • Approval/rejection notification components pass notification.date_created instead of notification.date to NotificationBase, showing the wrong date
  • NotificationFilters receives a lastReadFilter prop from the parent but never declares or uses it

Visual inspection: The UI video shows proper layout, working tabs, filters, and notification list rendering. The immersive modal and responsive design look good.

CI: All checks pass.

Looking forward to the updates — this is a great addition to Studio!

error.value = caughtError;
throw caughtError;
} catch (error) {
error.value = error;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blocking: Variable shadowing bug. The catch (error) parameter on line 17 shadows the outer const error = ref(null) from line 7. On line 18, error.value = error attempts to set .value on the caught error object (a plain Error), not on the Vue ref. The ref is never updated in the error path.

The original code used catch (caughtError) which avoided this. Consider renaming the catch parameter back:

} catch (caughtError) {
  error.value = caughtError;
  data.value = null;
  throw caughtError;
}

channel_version.new_token()

# Notify channel editors of new submission
self.notify_update_to_channel_editors()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blocking: notify_update_to_channel_editors() is called before super().save(), but self.date_updated is an auto_now=True field (line 2869). Django only sets auto_now fields during save(), so at this point self.date_updated will be None for a newly created submission.

This means User.notify_users(editors, date=None) is called, which will set newest_notification_date to None via Greatest(F('newest_notification_date'), Value(None)) — potentially clearing existing notification dates.

Consider moving this call after super().save() so date_updated is populated, or explicitly passing timezone.now() as the date.

generate_update_event(
"CURRENT_USER",
SESSION,
{"newest_notification_date": user.newest_notification_date},

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blocking: After users_queryset.update(...) on line 695, the queryset objects in memory are stale — Django's update() operates at the SQL level and doesn't refresh Python objects. So user.newest_notification_date on this line reads the old value, not the newly computed one.

The change events pushed to the frontend will contain stale newest_notification_date values, which means the red dot indicator may not appear for users until they refresh.

Consider re-querying after the update:

users_queryset.update(...)
refreshed = cls.objects.filter(pk__in=users_queryset.values_list('pk', flat=True))
Change.create_changes(
    [generate_update_event(..., {"newest_notification_date": user.newest_notification_date}, ...)
     for user in refreshed],
    applied=True,
)


<NotificationBase
:title="title"
:date="notification.date_created"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blocking: This passes notification.date_created to NotificationBase, but for approval notifications the composable sets date to submission.date_updated (the approval date). The displayed date will show the original submission creation date instead of the approval date.

Should be :date="notification.date" to use the correct event date from the composable's transformation. The same issue exists in CommunityLibrarySubmissionRejection.vue and CommunityLibrarySubmissionCreation.vue.

>
<NotificationFilters
:disabled="isBusy"
:lastReadFilter="lastReadFilter"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blocking: lastReadFilter is passed as a prop here, but NotificationFilters never declares it in its defineProps. This prop is silently ignored. If this is intended to pass the lastRead timestamp to the filters component, it needs to be declared and used there — or the filtering logic needs to be adjusted.

*/
const getSubmissionsUpdates = (submissions = []) => {
let updates = [];
for (const submission of submissions) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: This mutates the submission objects in-place (submission.date_created = new Date(...)) before spreading them. Since these objects come from the API response, mutating them directly could cause unexpected behavior if the same objects are referenced elsewhere. Consider creating new Date objects without mutation:

const dateCreated = new Date(submission.date_created);
const dateUpdated = submission.date_updated && new Date(submission.date_updated);
// Then use dateCreated/dateUpdated in the spread

<ul>
<component
:is="NotificationTypeToComponent[notification.type]"
v-for="(notification, index) in notifications"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Using index as :key can cause rendering issues when the list changes (e.g., when loading more items or re-sorting). Consider using a unique identifier like notification.id + '-' + notification.type to ensure Vue correctly tracks each notification across re-renders.

today: {
label: todayLabel$(),
params: {
date_updated__gte: new Date().toISOString().split('T')[0],

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: The DateFilterMap computes dates at module evaluation time (when the component is first created). If a user keeps the notifications modal open across midnight, the "Today" filter won't update to reflect the new day. Consider making these computed or recalculating on each filter open, especially for today.

const filters = channelStatusFilters.value;
channelStatusFilter.value = filters.length ? filters[0].key : null;
const options = channelStatusOptions.value;
channelStatusFilter.value = options.length ? options[0].key : null;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: After the useFilter refactor, options[0].key works here only because key and value are always the same string. For clarity and consistency with the new API, consider using options[0].value.

try {
const [newestNotification] = notifications.value;
if (newestNotification) {
// Add 1 second to avoid precisision issues

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: Minor typo: "precisision" should be "precision".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement red dot in username and sidebar to flag new notifications Implement Community Library notifications page

4 participants