Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions css/main-B6-Z0GiM.chunk.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion css/main-sYSSLlZi.chunk.css

This file was deleted.

2 changes: 1 addition & 1 deletion css/notifications-main.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
/* extracted by css-entry-points-plugin */
@import './main-sYSSLlZi.chunk.css';
@import './main-B6-Z0GiM.chunk.css';
@import './style-KKPrO-hg.chunk.css';
1 change: 0 additions & 1 deletion js/NotificationsApp-DAfgvAAe.chunk.mjs.map

This file was deleted.

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions js/NotificationsApp-qhWOuxsc.chunk.mjs.map

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions js/index-CX7FiwID.chunk.mjs → js/index-BGXfQziE.chunk.mjs

Large diffs are not rendered by default.

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions js/notifications-main.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=[window.OC.filePath('notifications', '', 'js/NotificationsApp-DAfgvAAe.chunk.mjs'),window.OC.filePath('notifications', '', 'js/_plugin-vue_export-helper-kGAHXdKN.chunk.mjs'),window.OC.filePath('notifications', '', 'js/style-D-cXWuxW.chunk.mjs'),window.OC.filePath('notifications', '', 'css/style-KKPrO-hg.chunk.css'),window.OC.filePath('notifications', '', 'css/_plugin-vue_export-helper-DyKWSVv8.chunk.css'),window.OC.filePath('notifications', '', 'js/vite-preload-helper-DxYC2qmj.chunk.mjs'),window.OC.filePath('notifications', '', 'js/mdi-CpchYUUV-BTMM3-Z9.chunk.mjs'),window.OC.filePath('notifications', '', 'js/BrowserStorage-DnCSoRcY.chunk.mjs'),window.OC.filePath('notifications', '', 'css/NotificationsApp-CQwuD_xN.chunk.css')])))=>i.map(i=>d[i]);
import{_ as o}from"./vite-preload-helper-DxYC2qmj.chunk.mjs";import{c as i,d as n}from"./style-D-cXWuxW.chunk.mjs";const m=n(()=>o(()=>import("./NotificationsApp-DAfgvAAe.chunk.mjs").then(t=>t.N),__vite__mapDeps([0,1,2,3,4,5,6,7,8]),import.meta.url));i(m).mount("#notifications");
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=[window.OC.filePath('notifications', '', 'js/NotificationsApp-qhWOuxsc.chunk.mjs'),window.OC.filePath('notifications', '', 'js/_plugin-vue_export-helper-kGAHXdKN.chunk.mjs'),window.OC.filePath('notifications', '', 'js/style-D-cXWuxW.chunk.mjs'),window.OC.filePath('notifications', '', 'css/style-KKPrO-hg.chunk.css'),window.OC.filePath('notifications', '', 'css/_plugin-vue_export-helper-DyKWSVv8.chunk.css'),window.OC.filePath('notifications', '', 'js/vite-preload-helper-DxYC2qmj.chunk.mjs'),window.OC.filePath('notifications', '', 'js/mdi-CpchYUUV-BTMM3-Z9.chunk.mjs'),window.OC.filePath('notifications', '', 'js/BrowserStorage-DnCSoRcY.chunk.mjs'),window.OC.filePath('notifications', '', 'css/NotificationsApp-C2WboTqW.chunk.css')])))=>i.map(i=>d[i]);
import{_ as o}from"./vite-preload-helper-DxYC2qmj.chunk.mjs";import{c as i,d as n}from"./style-D-cXWuxW.chunk.mjs";const m=n(()=>o(()=>import("./NotificationsApp-qhWOuxsc.chunk.mjs").then(t=>t.N),__vite__mapDeps([0,1,2,3,4,5,6,7,8]),import.meta.url));i(m).mount("#notifications");
//# sourceMappingURL=notifications-main.mjs.map
4 changes: 2 additions & 2 deletions src/Components/NotificationItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -139,10 +139,10 @@ export default {

components: {
ActionButton,
NcButton,
NcDateTime,
IconClose,
IconMessageOutline,
NcButton,
NcDateTime,
NcRichText,
},

Expand Down
43 changes: 41 additions & 2 deletions src/NotificationsApp.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
<NotificationItem
v-for="(notification, index) in notifications"
:key="notification.notificationId"
:class="{ 'notification--new': notification.notificationId > lastOpenMaxId }"
:notification="notification"
@remove="onRemove(index)" />
</transition-group>
Expand All @@ -44,7 +45,22 @@
:name="emptyContentMessage"
:description="emptyContentDescription">
<template #icon>
<IconBellOutline v-if="!hasThrottledPushNotifications" />
<div v-if="showInboxZero && !hasThrottledPushNotifications" class="inbox-zero-celebrate">
<span
v-for="i in 8"
:key="i"
class="confetti-dot"
:style="`--confetti-i: ${i - 1}`" />
<svg class="inbox-zero-check" viewBox="0 0 52 52" xmlns="http://www.w3.org/2000/svg">
<circle
class="inbox-zero-circle"
cx="26"
cy="26"
r="22" />
<polyline class="inbox-zero-checkmark" points="14,26 22,34 38,18" />
</svg>
</div>
<IconBellOutline v-else-if="!hasThrottledPushNotifications" />
<span v-else class="icon icon-alert-outline" />
</template>

Expand Down Expand Up @@ -187,6 +203,11 @@ export default {
pushEndpoints: null,

open: false,

/** Highest notification ID seen when the popup was last opened; items above this are "new" */
lastOpenMaxId: 0,
/** True for 5 s after all notifications are cleared — inbox zero celebration */
showInboxZero: false,
}
},

Expand Down Expand Up @@ -219,6 +240,17 @@ export default {
},
},

watch: {
'notifications.length': function(newLen, oldLen) {
if (newLen === 0 && oldLen > 0) {
this.showInboxZero = true
setTimeout(() => {
this.showInboxZero = false
}, 5000)
}
},
},

mounted() {
this.tabId = getRequestToken() || ('' + Math.random())
this._oldcount = 0
Expand Down Expand Up @@ -296,6 +328,9 @@ export default {
},

async onOpen() {
// Capture the current max ID before fetching so newly arrived items are marked "new"
this.lastOpenMaxId = this.notifications.reduce((max, n) => Math.max(max, n.notificationId), 0)

if (this.webNotificationsGranted === null) {
this.requestWebNotificationPermissions()
.then((granted) => {
Expand Down Expand Up @@ -334,7 +369,9 @@ export default {
.delete(generateOcsUrl('apps/notifications/api/v2/notifications'))
.then(() => {
this.notifications = []
this.open = false
setTimeout(() => {
this.open = false
}, 3_000) // confetti-burst animation length + a bit of extra time
setCurrentTabAsActive(this.tabId)
})
.catch(() => {
Expand Down Expand Up @@ -406,6 +443,8 @@ export default {

/**
* Performs the AJAX request to retrieve the notifications
*
* @param {bool} force Refresh the list of notifications right away
*/
async _fetch(force = false) {
if (this.notifications.length && this.notifications[0].notificationId > this.webNotificationsThresholdId) {
Expand Down
136 changes: 132 additions & 4 deletions src/styles/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,35 @@
}
}

// Frosted glass notification popup
#notifications .header-menu__wrapper {
background-color: var(--color-main-background-blur, var(--color-main-background));
backdrop-filter: var(--filter-background-blur, none);
-webkit-backdrop-filter: var(--filter-background-blur, none);
transform-origin: center top;
}
Comment thread
nickvergessen marked this conversation as resolved.

#notifications.header-menu--opened .header-menu__caret,
#notifications.header-menu--opened .header-menu__wrapper {
Comment thread
nickvergessen marked this conversation as resolved.
animation: nc-notif-panel-open 0.45s cubic-bezier(0.2, 0, 0, 1) both;
}

@keyframes nc-notif-panel-open {
from {
opacity: 0;
transform: translateY(-10px) scale(0.97);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}

#notifications .header-menu__caret {
border-bottom-color: var(--color-main-background-blur, var(--color-main-background));
transform-origin: center bottom;
}
Comment thread
nickvergessen marked this conversation as resolved.

svg {
@keyframes pulse {
0% {
Expand All @@ -52,6 +81,23 @@ svg {
border-bottom: 1px solid var(--color-border);
}

// "New since last open" indicator — primary-colored bar on the inline-start edge
&.notification--new {
position: relative;
background-color: color-mix(in srgb, var(--color-primary-element) 16%, transparent) !important;

&::before {
content: '';
position: absolute;
inset-block: calc(var(--default-grid-baseline, 4px) * 2);
inset-inline-start: 0;
width: 3px;
background-color: var(--color-primary-element);
border-radius: 999px;
animation: nc-nav-stripe-in var(--animation-quick, 200ms) ease-out;
}
}

.notification-heading {
display: flex;
align-items: center; // Else children will stretch in height as container is absolutely-positioned.
Expand Down Expand Up @@ -80,8 +126,16 @@ svg {
display: flex;
align-items: center;

// Rounded avatar-style icon container
& > .image {
align-self: flex-start;
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 44px;
height: 44px;
border-radius: 50%;
background: var(--color-background-hover);
}

& > span.subject,
Expand All @@ -95,17 +149,20 @@ svg {

.notification-message,
.notification-full-message {
padding-inline-start: 42px; // 32px icon + 10px title padding
padding-inline-start: 54px; // 44px icon container + 10px title padding
color: var(--color-text-maxcontrast);

& > .collapsed {
overflow: hidden;
max-height: 70px;
// Fade the bottom of clipped messages by masking the text itself,
// so the fade works on any backdrop including the frosted-glass panel.
-webkit-mask-image: linear-gradient(to bottom, #000 70%, transparent 100%);
mask-image: linear-gradient(to bottom, #000 70%, transparent 100%);
}

& > .notification-overflow {
box-shadow: 0 0 20px 20px var(--color-main-background);
position: relative;
display: none;
}
}

Expand All @@ -123,3 +180,74 @@ svg {
}
}
}

// Larger icon size to fit inside the rounded container
img.notification-icon {
width: 26px !important;
height: 26px !important;
}

// Inbox-zero celebration: animated checkmark + confetti burst
.inbox-zero-celebrate {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 64px;
height: 64px;
}

.inbox-zero-check {
width: 52px;
height: 52px;
position: relative;
z-index: 1;
}

.inbox-zero-circle {
fill: none;
stroke: var(--color-element-success);
stroke-width: 3;
stroke-dasharray: 138;
stroke-dashoffset: 138;
animation: inbox-zero-circle-draw 0.8s cubic-bezier(0.65, 0, 0.35, 1) 0.2s both;
}

.inbox-zero-checkmark {
fill: none;
stroke: var(--color-element-success);
stroke-width: 3;
stroke-linecap: round;
stroke-linejoin: round;
stroke-dasharray: 36;
stroke-dashoffset: 36;
animation: inbox-zero-check-draw 0.6s cubic-bezier(0.65, 0, 0.35, 1) 0.9s both;
}

@keyframes inbox-zero-circle-draw { to { stroke-dashoffset: 0; } }
@keyframes inbox-zero-check-draw { to { stroke-dashoffset: 0; } }

// Confetti dots burst outward in 8 directions
.confetti-dot {
position: absolute;
top: calc(50% - 3px);
left: calc(50% - 3px);
width: 6px;
height: 6px;
border-radius: 50%;
animation: confetti-burst 1s ease-out calc(0.5s + var(--confetti-i, 0) * 0.1s) both;

&:nth-child(1) { background: #ff5555; }
&:nth-child(2) { background: #ff9900; }
&:nth-child(3) { background: #ffcc00; }
&:nth-child(4) { background: #55cc55; }
&:nth-child(5) { background: #00679e; }
&:nth-child(6) { background: #aa55ff; }
&:nth-child(7) { background: #ff55aa; }
&:nth-child(8) { background: #55cccc; }
}

@keyframes confetti-burst {
from { transform: rotate(calc(45deg * var(--confetti-i, 0))) translateY(0) scale(1); opacity: 1; }
to { transform: rotate(calc(45deg * var(--confetti-i, 0))) translateY(-34px) scale(0); opacity: .8; }
}
Loading