From 5e08ca004acf0774f73b9d6b86ea31915e55d385 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Thu, 24 Nov 2022 14:54:08 -0500 Subject: [PATCH] Added notifications - (WIP, boilerplate) --- Public API v1.yaml | 53 +++++++++++++ backend/app.js | 38 ++++++++++ backend/db.js | 4 + backend/notifications.js | 13 ++++ src/api-types/index.ts | 5 ++ .../models/DeleteNotificationRequest.ts | 7 ++ .../models/GetNotificationsResponse.ts | 9 +++ src/api-types/models/Notification.ts | 14 ++++ src/api-types/models/NotificationAction.ts | 8 ++ .../models/SetNotificationsToReadRequest.ts | 7 ++ src/app/app.module.ts | 4 +- .../notifications/notifications.component.css | 9 +++ .../notifications.component.html | 32 ++++++++ .../notifications.component.spec.ts | 25 ++++++ .../notifications/notifications.component.ts | 76 +++++++++++++++++++ src/app/posts.services.ts | 33 +++++++- 16 files changed, 333 insertions(+), 4 deletions(-) create mode 100644 backend/notifications.js create mode 100644 src/api-types/models/DeleteNotificationRequest.ts create mode 100644 src/api-types/models/GetNotificationsResponse.ts create mode 100644 src/api-types/models/Notification.ts create mode 100644 src/api-types/models/NotificationAction.ts create mode 100644 src/api-types/models/SetNotificationsToReadRequest.ts create mode 100644 src/app/components/notifications/notifications.component.css create mode 100644 src/app/components/notifications/notifications.component.html create mode 100644 src/app/components/notifications/notifications.component.spec.ts create mode 100644 src/app/components/notifications/notifications.component.ts diff --git a/Public API v1.yaml b/Public API v1.yaml index 9f335f8..1b1b350 100644 --- a/Public API v1.yaml +++ b/Public API v1.yaml @@ -2755,6 +2755,36 @@ components: type: string date: type: string + Notification: + required: + - uid + - type + - text + - read + type: object + properties: + type: + type: string + text: + type: string + uid: + type: string + action: + $ref: '#/components/schemas/NotificationAction' + read: + type: boolean + data: + type: object + NotificationAction: + required: + - type + - icon + type: object + properties: + type: + type: string + icon: + type: string BaseChangePermissionsRequest: required: - permission @@ -2886,6 +2916,29 @@ components: type: array items: $ref: '#/components/schemas/UserPermission' + DeleteNotificationRequest: + required: + - uid + type: object + properties: + uid: + type: string + SetNotificationsToReadRequest: + required: + - uids + type: object + properties: + uids: + type: array + items: + type: string + GetNotificationsResponse: + type: object + properties: + notifications: + type: array + items: + $ref: '#/components/schemas/Notification' securitySchemes: Auth query parameter: name: apiKey diff --git a/backend/app.js b/backend/app.js index 48c0b95..d2123c4 100644 --- a/backend/app.js +++ b/backend/app.js @@ -1992,6 +1992,44 @@ app.post('/api/changeRolePermissions', optionalJwt, async (req, res) => { res.send({success: success}); }); +// notifications + +app.post('/api/getNotifications', optionalJwt, async (req, res) => { + const uuid = req.user.uid; + + const notifications = await db_api.getRecords('notifications', {user_uid: uuid}); + + res.send({notifications: notifications}); +}); + +// set notifications to read +app.post('/api/setNotificationsToRead', optionalJwt, async (req, res) => { + const uids = req.body.uids; + + // TODO: do a bulk update + const success = true; // await db_api.updateRecords('notifications', {user_uid: uuid}); + + res.send({success: success}); +}); + +app.post('/api/deleteNotification', optionalJwt, async (req, res) => { + const uid = req.body.uid; + + const success = await db_api.removeRecord('notifications', {uid: uid}); + + res.send({success: success}); +}); + +app.post('/api/deleteAllNotifications', optionalJwt, async (req, res) => { + const uuid = req.user.uid; + + const success = await db_api.removeAllRecords('notifications', {user_uid: uuid}); + + res.send({success: success}); +}); + +// web server + app.use(function(req, res, next) { //if the request is not html then move along var accept = req.accepts('html', 'json', 'xml'); diff --git a/backend/db.js b/backend/db.js index 2171a09..beae2fa 100644 --- a/backend/db.js +++ b/backend/db.js @@ -58,6 +58,10 @@ const tables = { name: 'tasks', primary_key: 'key' }, + notifications: { + name: 'notifications', + primary_key: 'uid' + }, test: { name: 'test' } diff --git a/backend/notifications.js b/backend/notifications.js new file mode 100644 index 0000000..d673367 --- /dev/null +++ b/backend/notifications.js @@ -0,0 +1,13 @@ +const utils = require('./utils'); +const logger = require('./logger'); +const db_api = require('./db'); + +exports.sendNotification = async () => { + // TODO: hook into third party service + + const notification = {} + + await db_api.insertRecordIntoTable('notifications', notification); + + return notification; +} \ No newline at end of file diff --git a/src/api-types/index.ts b/src/api-types/index.ts index 973aeca..8af2a0d 100644 --- a/src/api-types/index.ts +++ b/src/api-types/index.ts @@ -28,6 +28,7 @@ export type { DBInfoResponse } from './models/DBInfoResponse'; export type { DeleteAllFilesResponse } from './models/DeleteAllFilesResponse'; export type { DeleteCategoryRequest } from './models/DeleteCategoryRequest'; export type { DeleteMp3Mp4Request } from './models/DeleteMp3Mp4Request'; +export type { DeleteNotificationRequest } from './models/DeleteNotificationRequest'; export type { DeletePlaylistRequest } from './models/DeletePlaylistRequest'; export type { DeleteSubscriptionFileRequest } from './models/DeleteSubscriptionFileRequest'; export type { DeleteUserRequest } from './models/DeleteUserRequest'; @@ -63,6 +64,7 @@ export type { GetLogsRequest } from './models/GetLogsRequest'; export type { GetLogsResponse } from './models/GetLogsResponse'; export type { GetMp3sResponse } from './models/GetMp3sResponse'; export type { GetMp4sResponse } from './models/GetMp4sResponse'; +export type { GetNotificationsResponse } from './models/GetNotificationsResponse'; export type { GetPlaylistRequest } from './models/GetPlaylistRequest'; export type { GetPlaylistResponse } from './models/GetPlaylistResponse'; export type { GetPlaylistsRequest } from './models/GetPlaylistsRequest'; @@ -77,12 +79,15 @@ export type { IncrementViewCountRequest } from './models/IncrementViewCountReque 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 type { Playlist } from './models/Playlist'; export type { RegisterRequest } from './models/RegisterRequest'; export type { RegisterResponse } from './models/RegisterResponse'; export type { RestoreDBBackupRequest } from './models/RestoreDBBackupRequest'; export { Schedule } from './models/Schedule'; export type { SetConfigRequest } from './models/SetConfigRequest'; +export type { SetNotificationsToReadRequest } from './models/SetNotificationsToReadRequest'; export type { SharingToggle } from './models/SharingToggle'; export type { Sort } from './models/Sort'; export type { SubscribeRequest } from './models/SubscribeRequest'; diff --git a/src/api-types/models/DeleteNotificationRequest.ts b/src/api-types/models/DeleteNotificationRequest.ts new file mode 100644 index 0000000..c554c6d --- /dev/null +++ b/src/api-types/models/DeleteNotificationRequest.ts @@ -0,0 +1,7 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type DeleteNotificationRequest = { + uid: string; +}; \ No newline at end of file diff --git a/src/api-types/models/GetNotificationsResponse.ts b/src/api-types/models/GetNotificationsResponse.ts new file mode 100644 index 0000000..1a018cd --- /dev/null +++ b/src/api-types/models/GetNotificationsResponse.ts @@ -0,0 +1,9 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { Notification } from './Notification'; + +export type GetNotificationsResponse = { + notifications?: Array; +}; \ No newline at end of file diff --git a/src/api-types/models/Notification.ts b/src/api-types/models/Notification.ts new file mode 100644 index 0000000..e4bfd78 --- /dev/null +++ b/src/api-types/models/Notification.ts @@ -0,0 +1,14 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { NotificationAction } from './NotificationAction'; + +export type Notification = { + type: string; + text: string; + uid: string; + action?: NotificationAction; + 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 new file mode 100644 index 0000000..fcb4db4 --- /dev/null +++ b/src/api-types/models/NotificationAction.ts @@ -0,0 +1,8 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type NotificationAction = { + type: string; + icon: string; +}; \ No newline at end of file diff --git a/src/api-types/models/SetNotificationsToReadRequest.ts b/src/api-types/models/SetNotificationsToReadRequest.ts new file mode 100644 index 0000000..83f5948 --- /dev/null +++ b/src/api-types/models/SetNotificationsToReadRequest.ts @@ -0,0 +1,7 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type SetNotificationsToReadRequest = { + uids: Array; +}; \ No newline at end of file diff --git a/src/app/app.module.ts b/src/app/app.module.ts index ef53d84..66528a8 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -87,6 +87,7 @@ import { SkipAdButtonComponent } from './components/skip-ad-button/skip-ad-butto import { TasksComponent } from './components/tasks/tasks.component'; import { UpdateTaskScheduleDialogComponent } from './dialogs/update-task-schedule-dialog/update-task-schedule-dialog.component'; import { RestoreDbDialogComponent } from './dialogs/restore-db-dialog/restore-db-dialog.component'; +import { NotificationsComponent } from './components/notifications/notifications.component'; registerLocaleData(es, 'es'); @@ -132,7 +133,8 @@ registerLocaleData(es, 'es'); SkipAdButtonComponent, TasksComponent, UpdateTaskScheduleDialogComponent, - RestoreDbDialogComponent + RestoreDbDialogComponent, + NotificationsComponent ], imports: [ CommonModule, diff --git a/src/app/components/notifications/notifications.component.css b/src/app/components/notifications/notifications.component.css new file mode 100644 index 0000000..fff13f4 --- /dev/null +++ b/src/app/components/notifications/notifications.component.css @@ -0,0 +1,9 @@ +.notification-divider { + margin-bottom: 10px; + margin-top: 10px; +} + +.notification-title { + margin-bottom: 6px; + text-align: center +} \ No newline at end of file diff --git a/src/app/components/notifications/notifications.component.html b/src/app/components/notifications/notifications.component.html new file mode 100644 index 0000000..387401d --- /dev/null +++ b/src/app/components/notifications/notifications.component.html @@ -0,0 +1,32 @@ +

No notifications available

+
+

New notifications

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

Old notifications

+
+ +
+ +
+
+ {{notification.title}} +
+
+ +
+
+
\ No newline at end of file diff --git a/src/app/components/notifications/notifications.component.spec.ts b/src/app/components/notifications/notifications.component.spec.ts new file mode 100644 index 0000000..0147b0d --- /dev/null +++ b/src/app/components/notifications/notifications.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NotificationsComponent } from './notifications.component'; + +describe('NotificationsComponent', () => { + let component: NotificationsComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ NotificationsComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(NotificationsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/notifications/notifications.component.ts b/src/app/components/notifications/notifications.component.ts new file mode 100644 index 0000000..ebd4418 --- /dev/null +++ b/src/app/components/notifications/notifications.component.ts @@ -0,0 +1,76 @@ +import { Component, ElementRef, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { MatMenu, 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 = {} + +@Component({ + selector: 'app-notifications', + templateUrl: './notifications.component.html', + styleUrls: ['./notifications.component.css'] +}) +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) { } + + ngOnInit(): void { + // wait for init + if (this.postsService.initialized) { + this.getNotifications(); + } else { + this.postsService.service_initialized.subscribe(init => { + if (init) { + this.getNotifications(); + } + }); + } + } + + 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.notificationCount.emit(this.notifications.length); + }); + } + + notificationAction(notification: Notification): void { + // TODO: implement + } + + deleteNotification(uid: string, index: number): void { + this.postsService.deleteNotification(uid).subscribe(res => { + console.log(res); + // TODO: remove from array + this.notificationCount.emit(this.notifications.length); + }); + } + + deleteAllNotifications(): void { + this.postsService.deleteAllNotifications().subscribe(res => { + console.log(res); + this.notifications = []; + this.read_notifications = []; + this.getNotifications(); + }); + this.notificationCount.emit(0); + } + + setNotificationsToRead(): void { + const uids = this.notifications.map(notification => notification.uid); + this.postsService.setNotificationsToRead(uids).subscribe(res => { + console.log(res); + }); + this.notificationCount.emit(0); + } + +} diff --git a/src/app/posts.services.ts b/src/app/posts.services.ts index 8e74bed..cad691d 100644 --- a/src/app/posts.services.ts +++ b/src/app/posts.services.ts @@ -6,7 +6,7 @@ import 'rxjs/add/observable/throw'; import { THEMES_CONFIG } from '../themes'; import { Router, CanActivate, ActivatedRouteSnapshot } from '@angular/router'; import { DOCUMENT } from '@angular/common'; -import { BehaviorSubject } from 'rxjs'; +import { BehaviorSubject, Observable } from 'rxjs'; import { MatSnackBar } from '@angular/material/snack-bar'; import * as Fingerprint2 from 'fingerprintjs2'; import { @@ -101,7 +101,10 @@ import { Sort, FileTypeFilter, GetAllFilesRequest, - GetAllTasksResponse + GetAllTasksResponse, + DeleteNotificationRequest, + SetNotificationsToReadRequest, + GetNotificationsResponse } from '../api-types'; import { isoLangs } from './settings/locales_list'; import { Title } from '@angular/platform-browser'; @@ -833,7 +836,31 @@ export class PostsService implements CanActivate { return this.http.get(sponsor_block_api_path + `skipSegments/${id_hash}`); } - public openSnackBar(message: string, action: string = '') { + // notifications + + getNotifications(): Observable { + return this.http.post(this.path + 'getNotifications', {}, + this.httpOptions); + } + + setNotificationsToRead(uids: string[]): Observable { + const body: SetNotificationsToReadRequest = {uids: uids}; + return this.http.post(this.path + 'setNotificationsToRead', body, + this.httpOptions); + } + + deleteNotification(uid: string): Observable { + const body: DeleteNotificationRequest = {uid: uid}; + return this.http.post(this.path + 'deleteNotification', body, + this.httpOptions); + } + + deleteAllNotifications(): Observable { + return this.http.post(this.path + 'deleteNotifications', {}, + this.httpOptions); + } + + public openSnackBar(message: string, action = ''): void { this.snackBar.open(message, action, { duration: 2000, });