feat: notification on mentions
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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"
|
||||||
}, {
|
}, {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user