Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
b51654e
feat(notifications): discussion, space and away notifications with em…
Jeesha09 Sep 18, 2026
0f03839
feat(notifications): bell menu is the three states only
Jeesha09 Sep 19, 2026
4c31ffa
fix(notifications): hide the filters on the Read tab under five rows too
Jeesha09 Sep 21, 2026
7dba81f
feat(notifications): space toggles in the header menu and the Spaces …
Jeesha09 Sep 21, 2026
3a7af46
feat(notifications): timezone preference and a tighter notifications …
Jeesha09 Sep 21, 2026
95576d4
fix(notifications): closing mail as active hours end; midnight start …
Jeesha09 Sep 21, 2026
ee5fc66
feat(notifications): one participation setting, plainer settings copy
Jeesha09 Sep 22, 2026
a164fb6
feat(spaces): the notification bell sits beside the space's menu
Jeesha09 Sep 22, 2026
4c34297
feat(notifications): a catch-up mail replaces the while-you-were-away…
Jeesha09 Sep 22, 2026
2b7dc38
fix(notifications): the catch-up window in the reader's timezone, and…
Jeesha09 Sep 23, 2026
4f27285
refactor(notifications): drop the comments this branch introduced
Jeesha09 Sep 23, 2026
d835c80
test(cascade): derive the reordered cascade from the real one
Jeesha09 Sep 23, 2026
f8f5609
refactor(notifications): drop what nothing uses, and fold four patche…
Jeesha09 Sep 24, 2026
63669af
fix(spaces): stop writing the team column GP Space Subscription no lo…
Jeesha09 Sep 25, 2026
f80d0f9
Merge upstream/develop into discuss_notifications
Jeesha09 Sep 25, 2026
9b3b779
fix(notifications): refuse rows for other users, and keep merged even…
Jeesha09 Sep 25, 2026
46457e2
fix(discussions): read-only mode reached the template as the global, …
Jeesha09 Sep 25, 2026
8bf21b4
test(notifications): seed the filter spec past the count that shows t…
Jeesha09 Sep 25, 2026
54dd2ba
fix(notifications): away stretches, the Space a row is judged by, and…
Jeesha09 Sep 28, 2026
4d504f0
fix(notifications): one row for a discussion that names you, and a tr…
Jeesha09 Sep 28, 2026
75c25e4
fix(notifications): keep a filter the reader set, and stop the bell a…
Jeesha09 Sep 28, 2026
91dcb9e
fix(notifications): count a backlog instead of dropping it, and say w…
Jeesha09 Sep 28, 2026
f21c918
refactor(notifications): close the review findings and the duplicatio…
Jeesha09 Sep 29, 2026
e7a2d73
fix(notifications): walk the space backfill in batches
Jeesha09 Sep 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions frontend/cypress/e2e/notifications/notification-filters.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { resetData } from '../../support/seed'

describe('Notification filters', () => {
let alpha: string
let beta: string
let alphaSpace: string
let betaSpace: string

const mentionHtml =
'<p>Ping <span data-type="mention" data-id="member@example.com" data-label="Member">@Member</span></p>'

beforeEach(() => {
resetData('two_communities').then((ids) => {
;[alpha, beta] = (ids.communities as string[]).map(String)
;[alphaSpace, betaSpace] = (ids.spaces as string[]).map(String)
})
cy.then(() => {
for (const [space, title] of [
[alphaSpace, 'Alpha thread'],
[betaSpace, 'Beta thread'],
[alphaSpace, 'Padding one'],
[alphaSpace, 'Padding two'],
[alphaSpace, 'Padding three'],
]) {
cy.task('requestAsUser', {
user: 'member2@example.com',
method: 'POST',
path: '/api/v2/document/GP Discussion',
body: { title, project: space, content: mentionHtml },
})
}
})
cy.loginAs('member')
cy.intercept('GET', '**/api/v2/document/GP%20Notification*').as('notificationList')
cy.visit('/g/notifications')
cy.wait('@notificationList')
})

it('lists both mentions under a day label', () => {
cy.contains('[data-slot="list-group-header"]', 'Today').should('be.visible')
cy.contains('Alpha thread').should('be.visible')
cy.contains('Beta thread').should('be.visible')
})

it('narrows to the picked community', () => {
cy.button('All communities').click()
cy.contains('[role="option"]', 'Alpha').click()
cy.get('body').type('{esc}')
cy.wait('@notificationList')

cy.contains('Alpha thread').should('be.visible')
cy.contains('Beta thread').should('not.exist')
cy.button('Alpha').should('be.visible').click()
cy.contains('[role="option"]', 'Beta').click()
cy.get('body').type('{esc}')
cy.button('2 communities').should('be.visible')

cy.button('Clear').click()
cy.wait('@notificationList')
cy.contains('Beta thread').should('be.visible')
})

it('narrows to the picked space', () => {
cy.button('All spaces').click()
cy.contains('[role="option"]', 'Beta Space').click()
cy.get('body').type('{esc}')
cy.wait('@notificationList')

cy.contains('Beta thread').should('be.visible')
cy.contains('Alpha thread').should('not.exist')
})

it('keeps today’s rows under the Today filter and shows the day on an empty day', () => {
cy.button('All dates').click()
cy.button('Today').click()
cy.wait('@notificationList')
cy.button('Today').should('be.visible')
cy.contains('Alpha thread').should('be.visible')
cy.contains('Beta thread').should('be.visible')

cy.window().then((win) => {
win.localStorage.setItem(
'gameplan:notificationFilters',
JSON.stringify({
communities: [],
spaces: [],
date: { kind: 'range', from: '2020-01-01', to: '2020-01-01' },
}),
)
})
cy.reload()
cy.wait('@notificationList')
cy.contains('1 January 2020').should('be.visible')
cy.contains('Nothing here').should('be.visible')
})
})
35 changes: 35 additions & 0 deletions frontend/src/components/DiscussionNotificationBell.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<template>
<Dropdown
:options="options"
align="end"
:button="{
icon: discussionStateIcon[state],
variant: 'ghost',
label: `Notifications: ${state}`,
tooltip: `Notifications: ${state}`,
loading,
}"
/>
</template>

<script setup lang="ts">
import { computed } from 'vue'
import { Dropdown } from 'frappe-ui'
import {
discussionNotificationOptions,
discussionStateIcon,
type DiscussionNotificationChoice,
type DiscussionNotificationState,
} from '@/data/notificationPreferences'

const props = defineProps<{
state: DiscussionNotificationState
loading?: boolean
}>()

const emit = defineEmits<{ (e: 'select', choice: DiscussionNotificationChoice): void }>()

const options = computed(() =>
discussionNotificationOptions(props.state, (choice) => emit('select', choice)),
)
</script>
68 changes: 67 additions & 1 deletion frontend/src/components/DiscussionView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,24 @@
<template #prefix>
<PageHeaderBackButton :to="backRoute" />
</template>
<template #suffix>
<template v-if="showHeaderActions">
<DiscussionNotificationBell
:state="discussion.doc!.notification_state!"
:loading="discussion.setNotificationState.loading"
@select="setNotificationState"
/>
<Dropdown
align="end"
:button="{
icon: 'lucide-more-horizontal',
variant: 'ghost',
label: 'Discussion Options',
}"
:options="headerActions"
/>
</template>
</template>
</PageHeaderMobile>
<PageHeader class="hidden sm:flex">
<SpaceBreadcrumbs
Expand All @@ -14,6 +32,22 @@
<span class="hidden text-lg-medium text-ink-gray-8 print:inline">
{{ [communityTitle, space?.title].filter(Boolean).join(' / ') }}
</span>
<div v-if="showHeaderActions" class="flex items-center gap-2 print:hidden">
<DiscussionNotificationBell
:state="discussion.doc!.notification_state!"
:loading="discussion.setNotificationState.loading"
@select="setNotificationState"
/>
<Dropdown
align="end"
:button="{
icon: 'lucide-more-horizontal',
variant: 'ghost',
label: 'Discussion Options',
}"
:options="headerActions"
/>
</div>
</PageHeader>
<div class="discussion-container">
<div v-if="discussion.loading">
Expand Down Expand Up @@ -65,6 +99,7 @@
padding puts its contents back where they were.
-->
<div
ref="postActionsRow"
class="flex items-center bg-surface-base pb-2 pt-2"
:class="
editingPost
Expand Down Expand Up @@ -98,6 +133,12 @@
</Tooltip>
</div>
<div class="ml-auto flex space-x-2 print:hidden">
<DiscussionNotificationBell
v-if="!readOnlyMode && discussion.doc.notification_state"
:state="discussion.doc.notification_state"
:loading="discussion.setNotificationState.loading"
@select="setNotificationState"
/>
<Dropdown
v-if="!readOnlyMode"
class="ml-auto"
Expand Down Expand Up @@ -351,9 +392,10 @@ import {
Switch,
dialog,
} from 'frappe-ui'
import { until, useEventListener } from '@vueuse/core'
import { until, useEventListener, useIntersectionObserver } from '@vueuse/core'
import type { Editor } from '@tiptap/vue-3'
import Reactions from './Reactions.vue'
import DiscussionNotificationBell from './DiscussionNotificationBell.vue'
import UserAvatarWithHover from './UserAvatarWithHover.vue'
import CommentsArea from '@/components/CommentsArea.vue'
import DiscussionViewEditor from './editor/DiscussionViewEditor.vue'
Expand All @@ -367,6 +409,7 @@ import { getSpace, useSpace } from '@/data/spaces'
import { useCommunity } from '@/data/communities'
import { useGroupedSpaceOptions } from '@/data/groupedSpaces'
import { useDiscussion } from '@/data/discussions'
import type { DiscussionNotificationChoice } from '@/data/notificationPreferences'
import { useDraftSync } from '@/data/useDraftSync'
import { tags } from '@/data/tags'
import { shellScrollContainer, useShellScrolled } from 'frappe-ui'
Expand Down Expand Up @@ -849,6 +892,20 @@ const canMoveOrPinDiscussion = computed(() =>
canMoveOrPinContent(discussion.doc, space.value, useSessionUser()),
)

const postActionsRow = useTemplateRef<HTMLElement>('postActionsRow')
const postActionsVisible = ref(true)
useIntersectionObserver(postActionsRow, ([entry]) => {
postActionsVisible.value = entry?.isIntersecting ?? true
})
const showHeaderActions = computed(
() =>
!postActionsVisible.value && !props.readOnlyMode && Boolean(discussion.doc?.notification_state),
)
const HEADER_MENU_EXCLUDES = new Set(['Edit', 'Revisions'])
const headerActions = computed(() =>
actions.value.filter((action) => !HEADER_MENU_EXCLUDES.has(action.label)),
)

const actions = computed(() => [
{
label: 'Edit',
Expand Down Expand Up @@ -977,6 +1034,15 @@ const actions = computed(() => [
},
])

async function setNotificationState(choice: DiscussionNotificationChoice) {
await discussion.setNotificationState.submit({ state: choice })
const result = discussion.setNotificationState.data
if (discussion.doc && result) {
discussion.doc.notification_state = result.notification_state
discussion.doc.notification_state_is_explicit = result.notification_state_is_explicit
}
}

useCommandPaletteCommands(
computed(() => {
if (props.readOnlyMode || !discussion.doc) return []
Expand Down
22 changes: 22 additions & 0 deletions frontend/src/components/NotificationFilterTrigger.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<template>
<Button :variant="active ? 'subtle' : 'outline'" :label="label" :icon-left="icon">
<template #suffix>
<span
class="lucide-chevron-down size-4 transition-transform"
:class="{ 'rotate-180': open }"
aria-hidden="true"
/>
</template>
</Button>
</template>

<script setup lang="ts">
import { Button } from 'frappe-ui'

defineProps<{
label: string
open?: boolean
active?: boolean
icon?: string
}>()
</script>
Loading
Loading