diff --git a/Public API v1.yaml b/Public API v1.yaml index 1b1b350..5b711a1 100644 --- a/Public API v1.yaml +++ b/Public API v1.yaml @@ -2764,27 +2764,30 @@ components: type: object properties: type: - type: string - text: - type: string + $ref: '#/components/schemas/NotificationType' uid: type: string + user_uid: + type: string action: - $ref: '#/components/schemas/NotificationAction' + type: array + items: + $ref: '#/components/schemas/NotificationAction' read: type: boolean data: type: object NotificationAction: - required: - - type - - icon - type: object - properties: - type: - type: string - icon: - type: string + type: string + enum: + - play + - retry_download + - view_download_error + NotificationType: + type: string + enum: + - download_complete + - download_error BaseChangePermissionsRequest: required: - permission diff --git a/backend/downloader.js b/backend/downloader.js index b2d87c5..4309691 100644 --- a/backend/downloader.js +++ b/backend/downloader.js @@ -14,6 +14,7 @@ const { create } = require('xmlbuilder2'); const categories_api = require('./categories'); const utils = require('./utils'); const db_api = require('./db'); +const notifications_api = require('./notifications'); const mutex = new Mutex(); let should_check_downloads = true; @@ -341,6 +342,8 @@ async function downloadQueuedFile(download_uid) { // registers file in DB const file_obj = await db_api.registerFileDB(full_file_path, type, download['user_uid'], category, download['sub_id'] ? download['sub_id'] : null, options.cropFileSettings); + notifications_api.sendDownloadNotification(file_obj, download['user_uid']); + file_objs.push(file_obj); } diff --git a/backend/notifications.js b/backend/notifications.js index d673367..954c5d4 100644 --- a/backend/notifications.js +++ b/backend/notifications.js @@ -1,13 +1,32 @@ -const utils = require('./utils'); -const logger = require('./logger'); +const { uuid } = require('uuidv4'); const db_api = require('./db'); -exports.sendNotification = async () => { +exports.sendNotification = async (notification) => { // TODO: hook into third party service + await db_api.insertRecordIntoTable('notifications', notification); + return notification; +} - const notification = {} +exports.sendDownloadNotification = async (file, user_uid) => { + const data = {file_uid: file.uid, file_title: file.title}; + const notification = exports.createNotification('download_complete', ['play'], data, user_uid); + return await exports.sendNotification(notification); +} - await db_api.insertRecordIntoTable('notifications', notification); +exports.sendDownloadErrorNotification = async (download, user_uid) => { + const data = {download_uid: download.uid, download_url: download.url}; + const notification = exports.createNotification('download_error', ['view_download_error', 'retry_download'], data, user_uid); + return await exports.sendNotification(notification); +} +exports.createNotification = (type, actions, data, user_uid) => { + const notification = { + type: type, + actions: actions, + data: data, + user_uid: user_uid, + uid: uuid(), + read: false + } return notification; -} \ No newline at end of file +} diff --git a/src/api-types/index.ts b/src/api-types/index.ts index 8af2a0d..5819a8c 100644 --- a/src/api-types/index.ts +++ b/src/api-types/index.ts @@ -80,7 +80,8 @@ export type { inline_response_200_15 } from './models/inline_response_200_15'; export type { LoginRequest } from './models/LoginRequest'; export type { LoginResponse } from './models/LoginResponse'; export type { Notification } from './models/Notification'; -export type { NotificationAction } from './models/NotificationAction'; +export { NotificationAction } from './models/NotificationAction'; +export { NotificationType } from './models/NotificationType'; export type { Playlist } from './models/Playlist'; export type { RegisterRequest } from './models/RegisterRequest'; export type { RegisterResponse } from './models/RegisterResponse'; diff --git a/src/api-types/models/Notification.ts b/src/api-types/models/Notification.ts index e4bfd78..704533c 100644 --- a/src/api-types/models/Notification.ts +++ b/src/api-types/models/Notification.ts @@ -3,12 +3,13 @@ /* eslint-disable */ import type { NotificationAction } from './NotificationAction'; +import type { NotificationType } from './NotificationType'; export type Notification = { - type: string; - text: string; + type: NotificationType; uid: string; - action?: NotificationAction; + user_uid?: string; + action?: Array; read: boolean; data?: any; }; \ No newline at end of file diff --git a/src/api-types/models/NotificationAction.ts b/src/api-types/models/NotificationAction.ts index fcb4db4..785f7d2 100644 --- a/src/api-types/models/NotificationAction.ts +++ b/src/api-types/models/NotificationAction.ts @@ -2,7 +2,8 @@ /* tslint:disable */ /* eslint-disable */ -export type NotificationAction = { - type: string; - icon: string; -}; \ No newline at end of file +export enum NotificationAction { + PLAY = 'play', + RETRY_DOWNLOAD = 'retry_download', + VIEW_DOWNLOAD_ERROR = 'view_download_error', +} \ No newline at end of file diff --git a/src/api-types/models/NotificationType.ts b/src/api-types/models/NotificationType.ts new file mode 100644 index 0000000..6ef911a --- /dev/null +++ b/src/api-types/models/NotificationType.ts @@ -0,0 +1,8 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export enum NotificationType { + DOWNLOAD_COMPLETE = 'download_complete', + DOWNLOAD_ERROR = 'download_error', +} \ No newline at end of file diff --git a/src/app/app.component.html b/src/app/app.component.html index c2d79a2..9f75838 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -11,7 +11,11 @@ {{topBarTitle}} -
+
+ + + + - + + + +
+
\ No newline at end of file diff --git a/src/app/components/notifications-list/notifications-list.component.scss b/src/app/components/notifications-list/notifications-list.component.scss new file mode 100644 index 0000000..8190694 --- /dev/null +++ b/src/app/components/notifications-list/notifications-list.component.scss @@ -0,0 +1,4 @@ +.notification-divider { + margin-bottom: 10px; + margin-top: 10px; +} diff --git a/src/app/components/notifications-list/notifications-list.component.spec.ts b/src/app/components/notifications-list/notifications-list.component.spec.ts new file mode 100644 index 0000000..e4eb195 --- /dev/null +++ b/src/app/components/notifications-list/notifications-list.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NotificationsListComponent } from './notifications-list.component'; + +describe('NotificationsListComponent', () => { + let component: NotificationsListComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ NotificationsListComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(NotificationsListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/notifications-list/notifications-list.component.ts b/src/app/components/notifications-list/notifications-list.component.ts new file mode 100644 index 0000000..845773a --- /dev/null +++ b/src/app/components/notifications-list/notifications-list.component.ts @@ -0,0 +1,52 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { Notification } from 'api-types'; +import { NotificationAction } from 'api-types/models/NotificationAction'; +import { NotificationType } from 'api-types/models/NotificationType'; + +@Component({ + selector: 'app-notifications-list', + templateUrl: './notifications-list.component.html', + styleUrls: ['./notifications-list.component.scss'] +}) +export class NotificationsListComponent { + @Input() notifications = null; + @Output() deleteNotification = new EventEmitter(); + @Output() notificationAction = new EventEmitter<{notification: Notification, action: NotificationAction}>(); + + NOTIFICATION_PREFIX: { [key in NotificationType]: string } = { + download_complete: $localize`Finished downloading:`, + download_error: $localize`Download failed:` + } + + // Attaches string to the end of the notification text + NOTIFICATION_SUFFIX_KEY: { [key in NotificationType]: string } = { + download_complete: 'file_title', + download_error: 'file_url' + } + + NOTIFICATION_ACTION_TO_STRING: { [key in NotificationAction]: string } = { + play: $localize`Play`, + retry_download: $localize`Retry download`, + view_download_error: $localize`View error` + } + + NOTIFICATION_COLOR: { [key in NotificationAction]: string } = { + play: 'primary', + retry_download: 'primary', + view_download_error: 'warn' + } + + NOTIFICATION_ICON: { [key in NotificationAction]: string } = { + play: 'smart_display', + retry_download: 'restart_alt', + view_download_error: 'warning' + } + + emitNotificationAction(notification: Notification, action: NotificationAction): void { + this.notificationAction.emit({notification: notification, action: action}); + } + + emitDeleteNotification(uid: string): void { + this.deleteNotification.emit(uid); + } +} diff --git a/src/app/components/notifications/notifications.component.css b/src/app/components/notifications/notifications.component.css index fff13f4..7dce8d9 100644 --- a/src/app/components/notifications/notifications.component.css +++ b/src/app/components/notifications/notifications.component.css @@ -1,8 +1,3 @@ -.notification-divider { - margin-bottom: 10px; - margin-top: 10px; -} - .notification-title { margin-bottom: 6px; text-align: center diff --git a/src/app/components/notifications/notifications.component.html b/src/app/components/notifications/notifications.component.html index 387401d..901485b 100644 --- a/src/app/components/notifications/notifications.component.html +++ b/src/app/components/notifications/notifications.component.html @@ -1,32 +1,10 @@ -

No notifications available

+

No notifications available

-

New notifications

-
- -
- -
-
- {{notification.title}} -
-
- -
-
+

New notifications

+
-

Old notifications

-
- -
- -
-
- {{notification.title}} -
-
- -
-
+

Old notifications

+
\ No newline at end of file diff --git a/src/app/components/notifications/notifications.component.ts b/src/app/components/notifications/notifications.component.ts index ebd4418..6667a1e 100644 --- a/src/app/components/notifications/notifications.component.ts +++ b/src/app/components/notifications/notifications.component.ts @@ -1,11 +1,9 @@ import { Component, ElementRef, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { MatMenu, MatMenuTrigger } from '@angular/material/menu'; +import { MatMenuTrigger } from '@angular/material/menu'; import { Router } from '@angular/router'; import { PostsService } from 'app/posts.services'; import { Notification } from 'api-types'; - -// TODO: fill this out -const NOTIFICATION_ACTION_TO_STRING = {} +import { NotificationAction } from 'api-types/models/NotificationAction'; @Component({ selector: 'app-notifications', @@ -17,7 +15,6 @@ export class NotificationsComponent implements OnInit { notifications = null; read_notifications = null; - @Input() menu: MatMenuTrigger; @Output() notificationCount = new EventEmitter(); constructor(public postsService: PostsService, private router: Router, private elRef: ElementRef) { } @@ -37,21 +34,35 @@ export class NotificationsComponent implements OnInit { getNotifications(): void { this.postsService.getNotifications().subscribe(res => { - this.notifications = res['notifications'].filter(notification => notification.read == false); - this.read_notifications = res['notifications'].filter(notification => notification.read == true); + this.notifications = res['notifications'].filter(notification => !notification.read); + this.read_notifications = res['notifications'].filter(notification => notification.read); this.notificationCount.emit(this.notifications.length); }); } - notificationAction(notification: Notification): void { - // TODO: implement + notificationAction(action_info: {notification: Notification, action: NotificationAction}): void { + switch (action_info['action']) { + case NotificationAction.PLAY: + this.router.navigate(['player', {uid: action_info['notification']['data']['file_uid']}]); + break; + case NotificationAction.VIEW_DOWNLOAD_ERROR: + this.router.navigate(['downloads']); + break; + case NotificationAction.RETRY_DOWNLOAD: + this.postsService.restartDownload(action_info['notification']['data']['download_uid']) + break; + default: + console.error(`Notification action ${action_info['action']} does not exist!`); + break; + } } - - deleteNotification(uid: string, index: number): void { + + deleteNotification(uid: string): void { this.postsService.deleteNotification(uid).subscribe(res => { - console.log(res); - // TODO: remove from array + this.notifications.filter(notification => notification['uid'] !== uid); + this.read_notifications.filter(read_notification => read_notification['uid'] !== uid); this.notificationCount.emit(this.notifications.length); + this.getNotifications(); }); } @@ -73,4 +84,10 @@ export class NotificationsComponent implements OnInit { this.notificationCount.emit(0); } + notificationMenuClosed(): void { + if (this.notifications.length > 0) { + this.setNotificationsToRead(); + } + } + }