Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/nextcloud widgets #740

Merged
merged 19 commits into from
Jun 20, 2022
Merged
Show file tree
Hide file tree
Changes from 12 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
71 changes: 71 additions & 0 deletions src/assets/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,77 @@
"remaining": "Remaining",
"up": "Up",
"down": "Down"
},
"nextcloud": {
"active": "active",
"and": "and",
"applications": "applications",
"available": "available",
"away": "Away",
"cache-full": "CACHE FULL",
"chat-room": "chat room",
"delete-all": "Deleta all",
"delete-notification": "Delete notification",
"disabled": "disabled",
"disk-quota": "Disk Quota",
"disk-space": "Disk Space",
"dnd": "Do Not Distrub",
"email": "email",
"enabled": "enabled",
"federated-shares-ucfirst": "Federated shares",
"federated-shares": "federated shares",
"files": "file{plural}",
"free": "free",
"groups": "groups",
"hit-rate": "hit rate",
"hits": "hits",
"home": "home",
"in": "in",
"keys": "keys",
"last-24-hours": "last 24 hours",
"last-5-minutes": "in the last 5 minutes",
"last-hour": "in the last hour",
"last-login": "Last login",
"last-restart": "Last restart",
"load-averages": "Load Averages over all CPU cores",
"local-shares": "Local shares",
"local": "local",
"max-keys": "max keys",
"memory-used": "memory used",
"memory-utilisation": "memory utilisation",
"memory": "memory",
"misses": "misses",
"no-notifications": "No notifications",
"no-pending-updates": "no pending updates",
"nothing-to-show": "Nothing to show here at this time",
"of-which": "of which",
"of": "of",
"offline": "Offline",
"online": "Online",
"other": "other",
"overall": "Ovarall",
"private-link": "private link",
"public-link": "public link",
"quota-enabled": "Disk Quota is {not}enabled for this user",
"received": "received",
"scripts": "scripts",
"sent": "sent",
"started": "Started",
"storages-by-type": "Storages by type",
"storages": "storage{plural}",
"strings-use": "strings use",
"tasks": "Tasks",
"total-files": "total files",
"total-users": "total users",
"total": "total",
"until": "Until",
"updates-available-for": "Updates are available for",
"updates-available": "update{plural} available",
"used": "used",
"user": "user",
"using": "using",
"version": "version",
"wasted": "wasted"
}
}
}
212 changes: 212 additions & 0 deletions src/components/Widgets/NextcloudNotifications.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
<template>
<div class="nextcloud-widget nextcloud-status-wrapper">
<div v-if="notifications.length" class="sep">
<!-- group actions: delete all -->
<p v-if="canDeleteNotification('delete-all')">
<span class="action group-action" @click="deleteNotifications">{{ tt('delete-all') }}</span>
</p>
<hr/>
<!-- notifications list -->
<div v-for="(notification, idx) in notifications" :key="idx" class="notification">
<div><img :src="notificationIcon(notification.icon)" /></div>
<div>
<p>
<small class="date" v-tooltip="dateTooltip(notification)">
{{ getTimeAgo(Date.parse(notification.datetime)) }}
</small> <span v-tooltip="subjectTooltip(notification)">{{ notification.subject }} </span>
<!-- notifications item: action links -->
<span v-if="notification.actions.length">
<span v-for="(action, idx) in notification.actions" :key="idx">
<a :href="action.link" class="action" target="_blank">{{ action.label }}</a>
</span>
</span>
<span v-if="canDeleteNotification('delete')">
<a @click="deleteNotification(notification.notification_id)"
class="action secondary">{{ tt('delete-notification') }}</a>
</span>
</p>
</div>
<hr/>
</div>
</div>
<!-- empty notifications list -->
<div v-else class="sep">
<p>{{ tt('no-notifications') }}</p>
</div>
</div>
</template>
<script>
import WidgetMixin from '@/mixins/WidgetMixin';
import NextcloudMixin from '@/mixins/NextcloudMixin';

/**
* NextcloudNotifications widget - Displays the user's notifications
* Used endpoints
* - capabilities: to determine if the User Notification API is enabled
* - notifications: to fetch list of notifications, delete all or a single notification
*/
export default {
mixins: [WidgetMixin, NextcloudMixin],
components: {},
data() {
return {
notifications: [],
};
},
computed: {
/* Parse the limit user option to either an integer or to an integer + 'm', 'h' or 'd' */
limit() {
const lim = this.options.limit;
const defaultLimit = [0, false];
if (typeof lim === 'string') {
const k = { m: 60, h: 60 * 60, d: 60 * 60 * 24 };
const m = lim.match(/(\d+)([hmd])/);
if (m.length !== 3) return defaultLimit;
return [false, m[1] * k[m[2]] * 1000];
}
if (typeof lim === 'number') {
return [parseInt(this.options.limit, 10) || 0, false];
}
return defaultLimit;
},
},
methods: {
allowedStatuscodes() {
return [100, 200];
},
async fetchData() {
if (!this.hasValidCredentials()) return;
await this.loadCapabilities();
if (!this.capabilities?.notifications?.enabled) {
this.error('This Nextcloud server doesn\'t support the Notifications API');
return;
}
this.makeRequest(this.endpoint('notifications'), this.headers)
.then(this.processNotifications)
.finally(this.finishLoading);
},
processNotifications(response) {
const notifications = this.validateResponse(response);
const [limitCount, limitTime] = this.limit;
this.notifications = [];
notifications.forEach((notification) => {
if (limitCount && this.notifications.length === limitCount) return; // count limit
const notiDate = Date.parse(notification.datetime);
const now = new Date().getTime();
if (limitTime && notiDate && now - notiDate > limitTime) { // time limit
return;
}
this.notifications.push(notification);
});
},
/* Transform icon URL to SVG Color API request URL
* @see https://docs.nextcloud.com/server/latest/developer_manual/html_css_design/icons.html */
notificationIcon(url) {
const color = this.getValueFromCss('widget-text-color').replace('#', '');
return url.replace('core/img', 'svg/core')
.replace(/extra-apps\/([^/]+)\/img/, 'svg/$1')
.replace(/apps\/([^/]+)\/img/, 'svg/$1')
.replace('.svg', `?color=${color}`);
},
/* Notification actions */
canDeleteNotification(deleteTarget) {
const capNotif = this.capabilities?.notifications?.features;
return Array.isArray(capNotif) && capNotif.includes(deleteTarget);
},
deleteNotifications() {
this.makeRequest(this.endpoint('notifications'), this.headers, 'DELETE')
.then(() => {
this.notifications = [];
});
},
deleteNotification(id) {
this.makeRequest(`${this.endpoint('notifications')}/${id}`, this.headers, 'DELETE')
.then(this.fetchData);
},
/* Tooltip generators */
subjectTooltip(notification) {
const content = notification.message;
return {
content, trigger: 'hover focus', delay: 250, classes: 'nc-tooltip',
};
},
dateTooltip(notification) {
const content = new Date(Date.parse(notification.datetime)).toLocaleString();
return {
content, trigger: 'hover focus', delay: 250, classes: 'nc-tooltip',
};
},
},
created() {
this.overrideUpdateInterval = 60;
},
};
</script>

<style scoped lang="scss">
@import '@/styles/widgets/nextcloud-shared.scss';
.nextcloud-status-wrapper {

div p small i {
position: relative;
top: .25rem;
}
small.date {
background: var(--widget-text-color);
color: var(--widget-accent-color);
border-radius: 4px;
padding: .15rem .3rem;
margin: .25rem .25rem .25rem 0;
display: inline-block;
font-weight: bold;
opacity: .66;
}
span.group-action {
float: right;
}
span.action, span a.action {
cursor: pointer;
margin: .1rem 0 .1rem .5rem;
padding: .15rem;
border-radius: 4px;
white-space: nowrap;
}
span.action:hover, span a.action:hover {
background: var(--widget-text-color);
color: var(--widget-accent-color);
text-decoration: underline;
}
.secondary {
opacity: .5;
font-size: 75%;
margin-left: .2rem;
}
div.notification {
display: table;
width: 100%;
> div:first-child {
float: right;
}
> div:nth-child(2) {
float: left;
width: 93%;
}
> div {
display: table-cell;
text-align: left;
> img {
float: right;
width: 16px;
height: 16px;
position: relative;
top: 1rem;
opacity: .75;
}
}
}
div hr {
margin-top: 0.3rem;
margin-bottom: 0;
}
}
</style>
Loading