feat: notification on mentions

This commit is contained in:
Jannat Patel
2024-05-23 21:25:22 +05:30
parent f38aebbc9c
commit a748e2c2db
8 changed files with 212 additions and 40 deletions

View File

@@ -10,7 +10,7 @@
<UserDropdown class="p-2" :isCollapsed="isSidebarCollapsed" /> <UserDropdown class="p-2" :isCollapsed="isSidebarCollapsed" />
<div class="flex flex-col overflow-y-auto"> <div class="flex flex-col overflow-y-auto">
<SidebarLink <SidebarLink
v-for="link in links" v-for="link in sidebarLinks"
:link="link" :link="link"
:isCollapsed="isSidebarCollapsed" :isCollapsed="isSidebarCollapsed"
class="mx-2 my-0.5" class="mx-2 my-0.5"
@@ -42,22 +42,53 @@ import UserDropdown from '@/components/UserDropdown.vue'
import CollapseSidebar from '@/components/Icons/CollapseSidebar.vue' import CollapseSidebar from '@/components/Icons/CollapseSidebar.vue'
import SidebarLink from '@/components/SidebarLink.vue' import SidebarLink from '@/components/SidebarLink.vue'
import { useStorage } from '@vueuse/core' import { useStorage } from '@vueuse/core'
import { ref } from 'vue' import { ref, onMounted, inject, computed } from 'vue'
import { getSidebarLinks } from '../utils' import { getSidebarLinks } from '../utils'
import { sessionStore } from '@/stores/session' import { sessionStore } from '@/stores/session'
import { Bell } from 'lucide-vue-next' import { Bell } from 'lucide-vue-next'
import { createResource } from 'frappe-ui'
const { user } = sessionStore() const { user } = sessionStore()
const links = getSidebarLinks() const socket = inject('$socket')
const unreadCount = ref(0)
if (user) { onMounted(() => {
links.push({ socket.on('publish_lms_notifications', (data) => {
label: 'Notifications', unreadNotifications.reload()
icon: Bell,
to: 'Notifications',
activeFor: ['Notifications'],
}) })
} })
const unreadNotifications = createResource({
cache: 'Unread Notifications Count',
url: 'frappe.client.get_count',
makeParams(values) {
return {
doctype: 'Notification Log',
filters: {
for_user: user,
read: 0,
},
}
},
onSuccess(data) {
unreadCount.value = data
},
auto: true,
})
const sidebarLinks = computed(() => {
const links = getSidebarLinks()
if (user) {
links.push({
label: 'Notifications',
icon: Bell,
to: 'Notifications',
activeFor: ['Notifications'],
count: unreadCount.value,
})
}
return links
})
const getSidebarFromStorage = () => { const getSidebarFromStorage = () => {
return useStorage('sidebar_is_collapsed', false) return useStorage('sidebar_is_collapsed', false)

View File

@@ -151,7 +151,16 @@ const newReplyResource = createResource({
}) })
const mentionUsers = computed(() => { const mentionUsers = computed(() => {
return allUsers.data /* [{ console.log(allUsers.data['jannat@frappe.io'])
let users = Object.values(allUsers.data).map((user) => {
return {
value: user.name,
label: user.full_name,
}
})
return users
/* [{
label: "jannat", label: "jannat",
value: "jannat" value: "jannat"
}, { }, {

View File

@@ -6,7 +6,7 @@
@click="handleClick" @click="handleClick"
> >
<div <div
class="flex items-center duration-300 ease-in-out" class="flex items-center w-full duration-300 ease-in-out"
:class="isCollapsed ? 'p-1' : 'px-2 py-1'" :class="isCollapsed ? 'p-1' : 'px-2 py-1'"
> >
<Tooltip :text="link.label" placement="right"> <Tooltip :text="link.label" placement="right">
@@ -29,6 +29,9 @@
> >
{{ link.label }} {{ link.label }}
</span> </span>
<span v-if="link.count" class="!ml-auto block text-xs text-gray-600">
{{ link.count }}
</span>
</div> </div>
</button> </button>
</template> </template>

View File

@@ -57,6 +57,14 @@
v-if="tab.courses && tab.courses.value.length" v-if="tab.courses && tab.courses.value.length"
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5 my-5 mx-5" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5 my-5 mx-5"
> >
<!-- <div v-for="course in tab.courses.value">
{{ course.membership }}
{{ course.current_lesson }}
<div v-if="course.current_lesson">
{{ course.current_lesson.split('-')[0] }}
{{ course.current_lesson.split('-')[1] }}
</div>
</div> -->
<router-link <router-link
v-for="course in tab.courses.value" v-for="course in tab.courses.value"
:to=" :to="
@@ -65,8 +73,8 @@
name: 'Lesson', name: 'Lesson',
params: { params: {
courseName: course.name, courseName: course.name,
chapterNumber: course.current_lesson.split('.')[0], chapterNumber: course.current_lesson.split('-')[0],
lessonNumber: course.current_lesson.split('.')[1], lessonNumber: course.current_lesson.split('-')[1],
}, },
} }
: course.membership : course.membership

View File

@@ -3,48 +3,136 @@
class="sticky top-0 z-10 flex flex-col md:flex-row md:items-center justify-between border-b bg-white px-3 py-2.5 sm:px-5" class="sticky top-0 z-10 flex flex-col md:flex-row md:items-center justify-between border-b bg-white px-3 py-2.5 sm:px-5"
> >
<Breadcrumbs :items="breadcrumbs" /> <Breadcrumbs :items="breadcrumbs" />
<div class="flex items-center space-x-2">
<Button
@click="markAllAsRead.submit"
:loading="markAllAsRead.loading"
v-if="activeTab === 'Unread' && unReadNotifications.data?.length > 0"
>
{{ __('Mark all as read') }}
</Button>
<TabButtons
class="inline-block"
:buttons="[{ label: 'Unread', active: true }, { label: 'Read' }]"
v-model="activeTab"
/>
</div>
</header> </header>
<div class="w-3/4 mx-auto"> <div class="w-3/4 mx-auto px-5 pt-6 divide-y">
<div <div
v-for="log in notifications.data" v-if="notifications?.length"
class="flex items-center border-b py-2 justify-between" v-for="log in notifications"
class="flex items-center py-2 justify-between"
> >
<div class="flex items-center"> <div class="flex items-center">
<UserAvatar :user="allUsers.data[log.from_user]" class="mr-2" /> <UserAvatar :user="allUsers.data[log.from_user]" class="mr-2" />
<div class="notification" v-html="log.subject"></div> <div class="notification" v-html="log.subject"></div>
</div> </div>
<Link <div class="flex items-center space-x-2">
v-if="log.link" <Link
:to="log.link" v-if="log.link"
class="text-gray-600 font-medium text-sm hover:text-gray-700" :to="log.link"
> @click="markAsRead.submit({ name: log.name })"
{{ __('View') }} class="text-gray-600 font-medium text-sm hover:text-gray-700"
</Link> >
{{ __('View') }}
</Link>
<Tooltip :text="__('Mark as read')">
<Button
variant="ghost"
v-if="!log.read"
@click="markAsRead.submit({ name: log.name })"
>
<template #icon>
<X class="h-4 w-4 text-gray-700 stroke-1.5" />
</template>
</Button>
</Tooltip>
</div>
</div>
<div v-else class="text-gray-600">
{{ __('Nothing to see here.') }}
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { createResource, Breadcrumbs, Link } from 'frappe-ui' import {
import { computed, inject } from 'vue' createListResource,
createResource,
Breadcrumbs,
Link,
TabButtons,
Button,
Tooltip,
} from 'frappe-ui'
import { computed, inject, ref, onMounted } from 'vue'
import UserAvatar from '@/components/UserAvatar.vue' import UserAvatar from '@/components/UserAvatar.vue'
import { useRouter } from 'vue-router'
import { X } from 'lucide-vue-next'
const user = inject('$user') const user = inject('$user')
const socket = inject('$socket')
const allUsers = inject('$allUsers') const allUsers = inject('$allUsers')
const activeTab = ref('Unread')
const router = useRouter()
const notifications = createResource({ onMounted(() => {
url: 'frappe.client.get_list', if (!user.data) router.push({ name: 'Courses' })
makeParams: (values) => {
socket.on('publish_lms_notifications', (data) => {
unReadNotifications.reload()
})
})
const notifications = computed(() => {
return activeTab.value === 'Unread'
? unReadNotifications.data
: readNotifications.data
})
const unReadNotifications = createListResource({
doctype: 'Notification Log',
fields: ['subject', 'from_user', 'link', 'read', 'name'],
filters: {
for_user: user.data?.name,
read: 0,
},
orderBy: 'creation desc',
auto: true,
cache: 'Unread Notifications',
})
const readNotifications = createListResource({
doctype: 'Notification Log',
fields: ['subject', 'from_user', 'link', 'read', 'name'],
filters: {
for_user: user.data?.name,
read: 1,
},
orderBy: 'creation desc',
auto: true,
cache: 'Read Notifications',
})
const markAsRead = createResource({
url: 'frappe.desk.doctype.notification_log.notification_log.mark_as_read',
makeParams(values) {
return { return {
doctype: 'Notification Log', docname: values.name,
fields: ['subject', 'from_user', 'link'],
filters: {
for_user: user.data?.name,
},
order_by: 'creation desc',
} }
}, },
auto: true, onSuccess(data) {
cache: user.data?.name, unReadNotifications.reload()
readNotifications.reload()
},
})
const markAllAsRead = createResource({
url: 'frappe.desk.doctype.notification_log.notification_log.mark_all_as_read',
onSuccess(data) {
unReadNotifications.reload()
readNotifications.reload()
},
}) })
const breadcrumbs = computed(() => { const breadcrumbs = computed(() => {

View File

@@ -103,6 +103,7 @@ doc_events = {
] ]
}, },
"Discussion Reply": {"after_insert": "lms.lms.utils.handle_notifications"}, "Discussion Reply": {"after_insert": "lms.lms.utils.handle_notifications"},
"Notification Log": {"after_insert": "lms.lms.utils.publish_notifications"},
} }
# Scheduled Tasks # Scheduled Tasks

View File

@@ -89,7 +89,6 @@ class CourseLesson(Document):
@frappe.whitelist() @frappe.whitelist()
def save_progress(lesson, course): def save_progress(lesson, course):
print("save progress")
membership = frappe.db.exists( membership = frappe.db.exists(
"LMS Enrollment", {"course": course, "member": frappe.session.user} "LMS Enrollment", {"course": course, "member": frappe.session.user}
) )

View File

@@ -640,7 +640,8 @@ def handle_notifications(doc, method):
if topic.reference_doctype not in ["Course Lesson", "LMS Batch"]: if topic.reference_doctype not in ["Course Lesson", "LMS Batch"]:
return return
create_notification_log(doc, topic) create_notification_log(doc, topic)
notify_mentions(doc, topic) notify_mentions_on_portal(doc, topic)
notify_mentions_via_email(doc, topic)
def create_notification_log(doc, topic): def create_notification_log(doc, topic):
@@ -674,7 +675,33 @@ def create_notification_log(doc, topic):
make_notification_logs(notification, users) make_notification_logs(notification, users)
def notify_mentions(doc, topic): def notify_mentions_on_portal(doc, topic):
mentions = extract_mentions(doc.reply)
if not mentions:
return
from_user_name = get_fullname(doc.owner)
course = frappe.db.get_value("Course Lesson", topic.reference_docname, "course")
for user in mentions:
notification = frappe._dict(
{
"subject": _("{0} mentioned you in a comment in {1}").format(
from_user_name, topic.title
),
"email_content": doc.reply,
"document_type": topic.reference_doctype,
"document_name": topic.reference_docname,
"for_user": user,
"from_user": doc.owner,
"type": "Alert",
"link": get_lesson_url(course, get_lesson_index(topic.reference_docname)),
}
)
make_notification_logs(notification, user)
def notify_mentions_via_email(doc, topic):
outgoing_email_account = frappe.get_cached_value( outgoing_email_account = frappe.get_cached_value(
"Email Account", {"default_outgoing": 1, "enable_outgoing": 1}, "name" "Email Account", {"default_outgoing": 1, "enable_outgoing": 1}, "name"
) )
@@ -1828,3 +1855,9 @@ def get_roles(name):
"batch_evaluator": has_course_evaluator_role(name), "batch_evaluator": has_course_evaluator_role(name),
"lms_student": has_student_role(name), "lms_student": has_student_role(name),
} }
def publish_notifications(doc, method):
frappe.publish_realtime(
"publish_lms_notifications", user=doc.for_user, after_commit=True
)