From 2f1548fda32c3ba9e53913270394eedfacd55986 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 8 Jan 2019 11:26:41 +0100 Subject: Add notifications in the client --- client/package.json | 2 + .../my-account-notifications.component.html | 7 + .../my-account-notifications.component.scss | 23 ++++ .../my-account-notifications.component.ts | 14 ++ .../app/+my-account/my-account-routing.module.ts | 10 ++ .../my-account-notification-preferences/index.ts | 1 + ...account-notification-preferences.component.html | 19 +++ ...account-notification-preferences.component.scss | 25 ++++ ...y-account-notification-preferences.component.ts | 99 +++++++++++++ .../my-account-settings.component.html | 5 +- client/src/app/+my-account/my-account.component.ts | 4 + client/src/app/+my-account/my-account.module.ts | 6 +- .../video-channel-videos.component.ts | 2 +- .../video-channels-routing.module.ts | 2 +- .../+video-channels/video-channels.component.ts | 4 +- client/src/app/app.module.ts | 4 +- .../app/menu/avatar-notification.component.html | 23 ++++ .../app/menu/avatar-notification.component.scss | 82 +++++++++++ .../src/app/menu/avatar-notification.component.ts | 64 +++++++++ client/src/app/menu/index.ts | 2 + client/src/app/menu/menu.component.html | 6 +- client/src/app/menu/menu.component.scss | 7 - client/src/app/shared/misc/help.component.html | 1 + client/src/app/shared/misc/help.component.scss | 22 +-- .../app/shared/rest/component-pagination.model.ts | 11 ++ .../src/app/shared/rest/rest-extractor.service.ts | 1 + client/src/app/shared/shared.module.ts | 8 +- client/src/app/shared/users/index.ts | 1 + .../app/shared/users/user-notification.model.ts | 153 +++++++++++++++++++++ .../app/shared/users/user-notification.service.ts | 110 +++++++++++++++ .../shared/users/user-notifications.component.html | 61 ++++++++ .../shared/users/user-notifications.component.scss | 30 ++++ .../shared/users/user-notifications.component.ts | 82 +++++++++++ client/src/app/shared/users/user.model.ts | 6 +- .../comment/video-comments.component.ts | 15 +- client/src/sass/include/_bootstrap-variables.scss | 3 +- client/src/sass/primeng-custom.scss | 4 +- client/yarn.lock | 53 ++++++- 38 files changed, 925 insertions(+), 47 deletions(-) create mode 100644 client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html create mode 100644 client/src/app/+my-account/my-account-notifications/my-account-notifications.component.scss create mode 100644 client/src/app/+my-account/my-account-notifications/my-account-notifications.component.ts create mode 100644 client/src/app/+my-account/my-account-settings/my-account-notification-preferences/index.ts create mode 100644 client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html create mode 100644 client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.scss create mode 100644 client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts create mode 100644 client/src/app/menu/avatar-notification.component.html create mode 100644 client/src/app/menu/avatar-notification.component.scss create mode 100644 client/src/app/menu/avatar-notification.component.ts create mode 100644 client/src/app/shared/users/user-notification.model.ts create mode 100644 client/src/app/shared/users/user-notification.service.ts create mode 100644 client/src/app/shared/users/user-notifications.component.html create mode 100644 client/src/app/shared/users/user-notifications.component.scss create mode 100644 client/src/app/shared/users/user-notifications.component.ts (limited to 'client') diff --git a/client/package.json b/client/package.json index 81422f05f..5fe1f3d5f 100644 --- a/client/package.json +++ b/client/package.json @@ -94,6 +94,7 @@ "@types/markdown-it": "^0.0.5", "@types/node": "^10.9.2", "@types/sanitize-html": "1.18.0", + "@types/socket.io-client": "^1.4.32", "@types/video.js": "^7.2.5", "@types/webtorrent": "^0.98.4", "angular2-hotkeys": "^2.1.2", @@ -141,6 +142,7 @@ "sanitize-html": "^1.18.4", "sass-loader": "^7.1.0", "sass-resources-loader": "^2.0.0", + "socket.io-client": "^2.2.0", "stream-browserify": "^2.0.1", "stream-http": "^3.0.0", "terser-webpack-plugin": "^1.1.0", diff --git a/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html new file mode 100644 index 000000000..d2810c343 --- /dev/null +++ b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html @@ -0,0 +1,7 @@ +
+ Notification preferences + + +
+ + diff --git a/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.scss b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.scss new file mode 100644 index 000000000..86ac094c5 --- /dev/null +++ b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.scss @@ -0,0 +1,23 @@ +@import '_variables'; +@import '_mixins'; + +.header { + display: flex; + justify-content: space-between; + font-size: 15px; + margin-bottom: 10px; + + a { + @include peertube-button-link; + @include grey-button; + } + + button { + @include peertube-button; + @include grey-button; + } +} + +my-user-notifications { + font-size: 15px; +} diff --git a/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.ts b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.ts new file mode 100644 index 000000000..3e197088d --- /dev/null +++ b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.ts @@ -0,0 +1,14 @@ +import { Component, ViewChild } from '@angular/core' +import { UserNotificationsComponent } from '@app/shared' + +@Component({ + templateUrl: './my-account-notifications.component.html', + styleUrls: [ './my-account-notifications.component.scss' ] +}) +export class MyAccountNotificationsComponent { + @ViewChild('userNotification') userNotification: UserNotificationsComponent + + markAllAsRead () { + this.userNotification.markAllAsRead() + } +} diff --git a/client/src/app/+my-account/my-account-routing.module.ts b/client/src/app/+my-account/my-account-routing.module.ts index a2cbeaffc..9996218ca 100644 --- a/client/src/app/+my-account/my-account-routing.module.ts +++ b/client/src/app/+my-account/my-account-routing.module.ts @@ -14,6 +14,7 @@ import { MyAccountOwnershipComponent } from '@app/+my-account/my-account-ownersh import { MyAccountBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-blocklist.component' import { MyAccountServerBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-server-blocklist.component' import { MyAccountHistoryComponent } from '@app/+my-account/my-account-history/my-account-history.component' +import { MyAccountNotificationsComponent } from '@app/+my-account/my-account-notifications/my-account-notifications.component' const myAccountRoutes: Routes = [ { @@ -124,6 +125,15 @@ const myAccountRoutes: Routes = [ title: 'Videos history' } } + }, + { + path: 'notifications', + component: MyAccountNotificationsComponent, + data: { + meta: { + title: 'Notifications' + } + } } ] } diff --git a/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/index.ts b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/index.ts new file mode 100644 index 000000000..5e1d51339 --- /dev/null +++ b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/index.ts @@ -0,0 +1 @@ +export * from './my-account-notification-preferences.component' diff --git a/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html new file mode 100644 index 000000000..59422d682 --- /dev/null +++ b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html @@ -0,0 +1,19 @@ +
+
Activities
+
Web
+
Email
+
+ +
+ +
{{ labelNotifications[notificationType] }}
+ +
+ +
+ +
+ +
+
+
diff --git a/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.scss b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.scss new file mode 100644 index 000000000..6feb16ab1 --- /dev/null +++ b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.scss @@ -0,0 +1,25 @@ +@import '_variables'; +@import '_mixins'; + +.custom-row { + display: flex; + align-items: center; + border-bottom: 1px solid rgba(0, 0, 0, 0.10); + + &:first-child { + font-size: 16px; + + & > div { + font-weight: $font-semibold; + } + } + + & > div { + width: 350px; + } + + & > div { + padding: 10px + } +} + diff --git a/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts new file mode 100644 index 000000000..519bdfab4 --- /dev/null +++ b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts @@ -0,0 +1,99 @@ +import { Component, Input, OnInit } from '@angular/core' +import { User } from '@app/shared' +import { I18n } from '@ngx-translate/i18n-polyfill' +import { Subject } from 'rxjs' +import { UserNotificationSetting, UserNotificationSettingValue, UserRight } from '../../../../../../shared' +import { Notifier, ServerService } from '@app/core' +import { debounce } from 'lodash-es' +import { UserNotificationService } from '@app/shared/users/user-notification.service' + +@Component({ + selector: 'my-account-notification-preferences', + templateUrl: './my-account-notification-preferences.component.html', + styleUrls: [ './my-account-notification-preferences.component.scss' ] +}) +export class MyAccountNotificationPreferencesComponent implements OnInit { + @Input() user: User = null + @Input() userInformationLoaded: Subject + + notificationSettingKeys: (keyof UserNotificationSetting)[] = [] + emailNotifications: { [ id in keyof UserNotificationSetting ]: boolean } = {} as any + webNotifications: { [ id in keyof UserNotificationSetting ]: boolean } = {} as any + labelNotifications: { [ id in keyof UserNotificationSetting ]: string } = {} as any + rightNotifications: { [ id in keyof Partial ]: UserRight } = {} as any + emailEnabled: boolean + + private savePreferences = debounce(this.savePreferencesImpl.bind(this), 500) + + constructor ( + private i18n: I18n, + private userNotificationService: UserNotificationService, + private serverService: ServerService, + private notifier: Notifier + ) { + this.labelNotifications = { + newVideoFromSubscription: this.i18n('New video from your subscriptions'), + newCommentOnMyVideo: this.i18n('New comment on your video'), + videoAbuseAsModerator: this.i18n('New video abuse on local video'), + blacklistOnMyVideo: this.i18n('One of your video is blacklisted/unblacklisted'), + myVideoPublished: this.i18n('Video published (after transcoding/scheduled update)'), + myVideoImportFinished: this.i18n('Video import finished'), + newUserRegistration: this.i18n('A new user registered on your instance'), + newFollow: this.i18n('You or your channel(s) has a new follower'), + commentMention: this.i18n('Someone mentioned you in video comments') + } + this.notificationSettingKeys = Object.keys(this.labelNotifications) as (keyof UserNotificationSetting)[] + + this.rightNotifications = { + videoAbuseAsModerator: UserRight.MANAGE_VIDEO_ABUSES, + newUserRegistration: UserRight.MANAGE_USERS + } + + this.emailEnabled = this.serverService.getConfig().email.enabled + } + + ngOnInit () { + this.userInformationLoaded.subscribe(() => this.loadNotificationSettings()) + } + + hasUserRight (field: keyof UserNotificationSetting) { + const rightToHave = this.rightNotifications[field] + if (!rightToHave) return true // No rights needed + + return this.user.hasRight(rightToHave) + } + + updateEmailSetting (field: keyof UserNotificationSetting, value: boolean) { + if (value === true) this.user.notificationSettings[field] |= UserNotificationSettingValue.EMAIL + else this.user.notificationSettings[field] &= ~UserNotificationSettingValue.EMAIL + + this.savePreferences() + } + + updateWebSetting (field: keyof UserNotificationSetting, value: boolean) { + if (value === true) this.user.notificationSettings[field] |= UserNotificationSettingValue.WEB + else this.user.notificationSettings[field] &= ~UserNotificationSettingValue.WEB + + this.savePreferences() + } + + private savePreferencesImpl () { + this.userNotificationService.updateNotificationSettings(this.user, this.user.notificationSettings) + .subscribe( + () => { + this.notifier.success(this.i18n('Preferences saved'), undefined, 2000) + }, + + err => this.notifier.error(err.message) + ) + } + + private loadNotificationSettings () { + for (const key of Object.keys(this.user.notificationSettings)) { + const value = this.user.notificationSettings[key] + this.emailNotifications[key] = value & UserNotificationSettingValue.EMAIL + + this.webNotifications[key] = value & UserNotificationSettingValue.WEB + } + } +} diff --git a/client/src/app/+my-account/my-account-settings/my-account-settings.component.html b/client/src/app/+my-account/my-account-settings/my-account-settings.component.html index c7e23cd1f..2eb7dd56e 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-settings.component.html +++ b/client/src/app/+my-account/my-account-settings/my-account-settings.component.html @@ -9,6 +9,9 @@ + + + @@ -16,4 +19,4 @@ - \ No newline at end of file + diff --git a/client/src/app/+my-account/my-account.component.ts b/client/src/app/+my-account/my-account.component.ts index 1bac9547d..8a4102d80 100644 --- a/client/src/app/+my-account/my-account.component.ts +++ b/client/src/app/+my-account/my-account.component.ts @@ -68,6 +68,10 @@ export class MyAccountComponent { label: this.i18n('My settings'), routerLink: '/my-account/settings' }, + { + label: this.i18n('My notifications'), + routerLink: '/my-account/notifications' + }, libraryEntries, miscEntries ] diff --git a/client/src/app/+my-account/my-account.module.ts b/client/src/app/+my-account/my-account.module.ts index 80d9f0cf7..18f51f171 100644 --- a/client/src/app/+my-account/my-account.module.ts +++ b/client/src/app/+my-account/my-account.module.ts @@ -23,6 +23,8 @@ import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-sub import { MyAccountBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-blocklist.component' import { MyAccountServerBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-server-blocklist.component' import { MyAccountHistoryComponent } from '@app/+my-account/my-account-history/my-account-history.component' +import { MyAccountNotificationsComponent } from '@app/+my-account/my-account-notifications/my-account-notifications.component' +import { MyAccountNotificationPreferencesComponent } from '@app/+my-account/my-account-settings/my-account-notification-preferences' @NgModule({ imports: [ @@ -53,7 +55,9 @@ import { MyAccountHistoryComponent } from '@app/+my-account/my-account-history/m MyAccountSubscriptionsComponent, MyAccountBlocklistComponent, MyAccountServerBlocklistComponent, - MyAccountHistoryComponent + MyAccountHistoryComponent, + MyAccountNotificationsComponent, + MyAccountNotificationPreferencesComponent ], exports: [ diff --git a/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts b/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts index 70c4374e0..dea378a6e 100644 --- a/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts +++ b/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts @@ -55,7 +55,7 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On this.videoChannelSub = this.videoChannelService.videoChannelLoaded .subscribe(videoChannel => { this.videoChannel = videoChannel - this.currentRoute = '/video-channels/' + this.videoChannel.uuid + '/videos' + this.currentRoute = '/video-channels/' + this.videoChannel.nameWithHost + '/videos' this.reloadVideos() this.generateSyndicationList() diff --git a/client/src/app/+video-channels/video-channels-routing.module.ts b/client/src/app/+video-channels/video-channels-routing.module.ts index 935578d2a..3ac3533d9 100644 --- a/client/src/app/+video-channels/video-channels-routing.module.ts +++ b/client/src/app/+video-channels/video-channels-routing.module.ts @@ -7,7 +7,7 @@ import { VideoChannelAboutComponent } from './video-channel-about/video-channel- const videoChannelsRoutes: Routes = [ { - path: ':videoChannelId', + path: ':videoChannelName', component: VideoChannelsComponent, canActivateChild: [ MetaGuard ], children: [ diff --git a/client/src/app/+video-channels/video-channels.component.ts b/client/src/app/+video-channels/video-channels.component.ts index 0c5c814c7..41ff82e98 100644 --- a/client/src/app/+video-channels/video-channels.component.ts +++ b/client/src/app/+video-channels/video-channels.component.ts @@ -34,9 +34,9 @@ export class VideoChannelsComponent implements OnInit, OnDestroy { ngOnInit () { this.routeSub = this.route.params .pipe( - map(params => params[ 'videoChannelId' ]), + map(params => params[ 'videoChannelName' ]), distinctUntilChanged(), - switchMap(videoChannelId => this.videoChannelService.getVideoChannel(videoChannelId)), + switchMap(videoChannelName => this.videoChannelService.getVideoChannel(videoChannelName)), catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 404 ])) ) .subscribe(videoChannel => this.videoChannel = videoChannel) diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts index 371199442..0bbc2e08b 100644 --- a/client/src/app/app.module.ts +++ b/client/src/app/app.module.ts @@ -12,13 +12,12 @@ import { AppComponent } from './app.component' import { CoreModule } from './core' import { HeaderComponent } from './header' import { LoginModule } from './login' -import { MenuComponent } from './menu' +import { AvatarNotificationComponent, LanguageChooserComponent, MenuComponent } from './menu' import { SharedModule } from './shared' import { SignupModule } from './signup' import { VideosModule } from './videos' import { buildFileLocale, getCompleteLocale, isDefaultLocale } from '../../../shared/models/i18n' import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils' -import { LanguageChooserComponent } from '@app/menu/language-chooser.component' import { SearchModule } from '@app/search' export function metaFactory (serverService: ServerService): MetaLoader { @@ -40,6 +39,7 @@ export function metaFactory (serverService: ServerService): MetaLoader { MenuComponent, LanguageChooserComponent, + AvatarNotificationComponent, HeaderComponent ], imports: [ diff --git a/client/src/app/menu/avatar-notification.component.html b/client/src/app/menu/avatar-notification.component.html new file mode 100644 index 000000000..2f0b7c669 --- /dev/null +++ b/client/src/app/menu/avatar-notification.component.html @@ -0,0 +1,23 @@ +
+
{{ unreadNotifications }}
+ + Avatar +
+ + +
+
Notifications
+ + +
+ + + + See all your notifications +
diff --git a/client/src/app/menu/avatar-notification.component.scss b/client/src/app/menu/avatar-notification.component.scss new file mode 100644 index 000000000..c86667469 --- /dev/null +++ b/client/src/app/menu/avatar-notification.component.scss @@ -0,0 +1,82 @@ +@import '_variables'; +@import '_mixins'; + +/deep/ { + .popover-notifications.popover { + max-width: 400px; + + .popover-body { + padding: 0; + font-size: 14px; + font-family: $main-fonts; + overflow-y: auto; + max-height: 500px; + box-shadow: 0 6px 14px rgba(0, 0, 0, 0.30); + + .notifications-header { + display: flex; + justify-content: space-between; + + background-color: rgba(0, 0, 0, 0.10); + align-items: center; + padding: 0 10px; + font-size: 16px; + height: 50px; + + a { + @include disable-default-a-behaviour; + + color: rgba(20, 20, 20, 0.5); + + &:hover { + color: rgba(20, 20, 20, 0.8); + } + } + } + + .all-notifications { + display: flex; + align-items: center; + justify-content: center; + font-weight: $font-semibold; + color: var(--mainForegroundColor); + height: 30px; + } + } + } +} + +.notification-avatar { + cursor: pointer; + position: relative; + + img, + .unread-notifications { + margin-left: 20px; + } + + img { + @include avatar(34px); + + margin-right: 10px; + } + + .unread-notifications { + position: absolute; + top: -5px; + left: -5px; + + display: flex; + align-items: center; + justify-content: center; + + background-color: var(--mainColor); + color: var(--mainBackgroundColor); + font-size: 10px; + font-weight: $font-semibold; + + border-radius: 15px; + width: 15px; + height: 15px; + } +} diff --git a/client/src/app/menu/avatar-notification.component.ts b/client/src/app/menu/avatar-notification.component.ts new file mode 100644 index 000000000..60e090726 --- /dev/null +++ b/client/src/app/menu/avatar-notification.component.ts @@ -0,0 +1,64 @@ +import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core' +import { User } from '../shared/users/user.model' +import { UserNotificationService } from '@app/shared/users/user-notification.service' +import { Subscription } from 'rxjs' +import { Notifier } from '@app/core' +import { NgbPopover } from '@ng-bootstrap/ng-bootstrap' +import { NavigationEnd, Router } from '@angular/router' +import { filter } from 'rxjs/operators' + +@Component({ + selector: 'my-avatar-notification', + templateUrl: './avatar-notification.component.html', + styleUrls: [ './avatar-notification.component.scss' ] +}) +export class AvatarNotificationComponent implements OnInit, OnDestroy { + @ViewChild('popover') popover: NgbPopover + @Input() user: User + + unreadNotifications = 0 + + private notificationSub: Subscription + private routeSub: Subscription + + constructor ( + private userNotificationService: UserNotificationService, + private notifier: Notifier, + private router: Router + ) {} + + ngOnInit () { + this.userNotificationService.countUnreadNotifications() + .subscribe( + result => { + this.unreadNotifications = Math.min(result, 99) // Limit number to 99 + this.subscribeToNotifications() + }, + + err => this.notifier.error(err.message) + ) + + this.routeSub = this.router.events + .pipe(filter(event => event instanceof NavigationEnd)) + .subscribe(() => this.closePopover()) + } + + ngOnDestroy () { + if (this.notificationSub) this.notificationSub.unsubscribe() + if (this.routeSub) this.routeSub.unsubscribe() + } + + closePopover () { + this.popover.close() + } + + private subscribeToNotifications () { + this.notificationSub = this.userNotificationService.getMyNotificationsSocket() + .subscribe(data => { + if (data.type === 'new') return this.unreadNotifications++ + if (data.type === 'read') return this.unreadNotifications-- + if (data.type === 'read-all') return this.unreadNotifications = 0 + }) + } + +} diff --git a/client/src/app/menu/index.ts b/client/src/app/menu/index.ts index 421271c12..39dbde750 100644 --- a/client/src/app/menu/index.ts +++ b/client/src/app/menu/index.ts @@ -1 +1,3 @@ +export * from './language-chooser.component' +export * from './avatar-notification.component' export * from './menu.component' diff --git a/client/src/app/menu/menu.component.html b/client/src/app/menu/menu.component.html index e04bdf3d6..aa5bfa9c9 100644 --- a/client/src/app/menu/menu.component.html +++ b/client/src/app/menu/menu.component.html @@ -2,9 +2,7 @@ - \ No newline at end of file + diff --git a/client/src/app/menu/menu.component.scss b/client/src/app/menu/menu.component.scss index b271ebfd2..a4aaadc7f 100644 --- a/client/src/app/menu/menu.component.scss +++ b/client/src/app/menu/menu.component.scss @@ -39,13 +39,6 @@ menu { justify-content: center; margin-bottom: 35px; - img { - @include avatar(34px); - - margin-left: 20px; - margin-right: 10px; - } - .logged-in-info { flex-grow: 1; diff --git a/client/src/app/shared/misc/help.component.html b/client/src/app/shared/misc/help.component.html index 28ccb1e26..08a2fc367 100644 --- a/client/src/app/shared/misc/help.component.html +++ b/client/src/app/shared/misc/help.component.html @@ -18,6 +18,7 @@ container="body" title="Get help" i18n-title + popoverClass="help-popover" [attr.aria-pressed]="isPopoverOpened" [ngbPopover]="tooltipTemplate" [placement]="tooltipPlacement" diff --git a/client/src/app/shared/misc/help.component.scss b/client/src/app/shared/misc/help.component.scss index 5c73a8031..6a5c3b1fa 100644 --- a/client/src/app/shared/misc/help.component.scss +++ b/client/src/app/shared/misc/help.component.scss @@ -12,19 +12,21 @@ } /deep/ { - .popover-body { - text-align: left; - padding: 10px; + .popover-help.popover { max-width: 300px; - font-size: 13px; - font-family: $main-fonts; - background-color: #fff; - color: #000; - box-shadow: 0 0 6px rgba(0, 0, 0, 0.5); + .popover-body { + text-align: left; + padding: 10px; + font-size: 13px; + font-family: $main-fonts; + background-color: #fff; + color: #000; + box-shadow: 0 0 6px rgba(0, 0, 0, 0.5); - ul { - padding-left: 20px; + ul { + padding-left: 20px; + } } } } diff --git a/client/src/app/shared/rest/component-pagination.model.ts b/client/src/app/shared/rest/component-pagination.model.ts index 0b8ecc318..85160d445 100644 --- a/client/src/app/shared/rest/component-pagination.model.ts +++ b/client/src/app/shared/rest/component-pagination.model.ts @@ -3,3 +3,14 @@ export interface ComponentPagination { itemsPerPage: number totalItems?: number } + +export function hasMoreItems (componentPagination: ComponentPagination) { + // No results + if (componentPagination.totalItems === 0) return false + + // Not loaded yet + if (!componentPagination.totalItems) return true + + const maxPage = componentPagination.totalItems / componentPagination.itemsPerPage + return maxPage > componentPagination.currentPage +} diff --git a/client/src/app/shared/rest/rest-extractor.service.ts b/client/src/app/shared/rest/rest-extractor.service.ts index f149569ef..e6518dd1d 100644 --- a/client/src/app/shared/rest/rest-extractor.service.ts +++ b/client/src/app/shared/rest/rest-extractor.service.ts @@ -80,6 +80,7 @@ export class RestExtractor { errorMessage = errorMessage ? errorMessage : 'Unknown error.' console.error(`Backend returned code ${err.status}, errorMessage is: ${errorMessage}`) } else { + console.error(err) errorMessage = err } diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts index 4a5d664db..c99c87c00 100644 --- a/client/src/app/shared/shared.module.ts +++ b/client/src/app/shared/shared.module.ts @@ -63,6 +63,8 @@ import { UserModerationDropdownComponent } from '@app/shared/moderation/user-mod import { BlocklistService } from '@app/shared/blocklist' import { TopMenuDropdownComponent } from '@app/shared/menu/top-menu-dropdown.component' import { UserHistoryService } from '@app/shared/users/user-history.service' +import { UserNotificationService } from '@app/shared/users/user-notification.service' +import { UserNotificationsComponent } from '@app/shared/users/user-notifications.component' @NgModule({ imports: [ @@ -105,7 +107,8 @@ import { UserHistoryService } from '@app/shared/users/user-history.service' InstanceFeaturesTableComponent, UserBanModalComponent, UserModerationDropdownComponent, - TopMenuDropdownComponent + TopMenuDropdownComponent, + UserNotificationsComponent ], exports: [ @@ -145,6 +148,7 @@ import { UserHistoryService } from '@app/shared/users/user-history.service' UserBanModalComponent, UserModerationDropdownComponent, TopMenuDropdownComponent, + UserNotificationsComponent, NumberFormatterPipe, ObjectLengthPipe, @@ -187,6 +191,8 @@ import { UserHistoryService } from '@app/shared/users/user-history.service' I18nPrimengCalendarService, ScreenService, + UserNotificationService, + I18n ] }) diff --git a/client/src/app/shared/users/index.ts b/client/src/app/shared/users/index.ts index 7b5a67bc7..ebd715fb1 100644 --- a/client/src/app/shared/users/index.ts +++ b/client/src/app/shared/users/index.ts @@ -1,2 +1,3 @@ export * from './user.model' export * from './user.service' +export * from './user-notifications.component' diff --git a/client/src/app/shared/users/user-notification.model.ts b/client/src/app/shared/users/user-notification.model.ts new file mode 100644 index 000000000..5ff816fb8 --- /dev/null +++ b/client/src/app/shared/users/user-notification.model.ts @@ -0,0 +1,153 @@ +import { UserNotification as UserNotificationServer, UserNotificationType, VideoInfo } from '../../../../../shared' + +export class UserNotification implements UserNotificationServer { + id: number + type: UserNotificationType + read: boolean + + video?: VideoInfo & { + channel: { + id: number + displayName: string + } + } + + videoImport?: { + id: number + video?: VideoInfo + torrentName?: string + magnetUri?: string + targetUrl?: string + } + + comment?: { + id: number + threadId: number + account: { + id: number + displayName: string + } + video: VideoInfo + } + + videoAbuse?: { + id: number + video: VideoInfo + } + + videoBlacklist?: { + id: number + video: VideoInfo + } + + account?: { + id: number + displayName: string + name: string + } + + actorFollow?: { + id: number + follower: { + name: string + displayName: string + } + following: { + type: 'account' | 'channel' + name: string + displayName: string + } + } + + createdAt: string + updatedAt: string + + // Additional fields + videoUrl?: string + commentUrl?: any[] + videoAbuseUrl?: string + accountUrl?: string + videoImportIdentifier?: string + videoImportUrl?: string + + constructor (hash: UserNotificationServer) { + this.id = hash.id + this.type = hash.type + this.read = hash.read + + this.video = hash.video + this.videoImport = hash.videoImport + this.comment = hash.comment + this.videoAbuse = hash.videoAbuse + this.videoBlacklist = hash.videoBlacklist + this.account = hash.account + this.actorFollow = hash.actorFollow + + this.createdAt = hash.createdAt + this.updatedAt = hash.updatedAt + + switch (this.type) { + case UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION: + this.videoUrl = this.buildVideoUrl(this.video) + break + + case UserNotificationType.UNBLACKLIST_ON_MY_VIDEO: + this.videoUrl = this.buildVideoUrl(this.video) + break + + case UserNotificationType.NEW_COMMENT_ON_MY_VIDEO: + case UserNotificationType.COMMENT_MENTION: + this.commentUrl = [ this.buildVideoUrl(this.comment.video), { threadId: this.comment.threadId } ] + break + + case UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS: + this.videoAbuseUrl = '/admin/moderation/video-abuses/list' + this.videoUrl = this.buildVideoUrl(this.videoAbuse.video) + break + + case UserNotificationType.BLACKLIST_ON_MY_VIDEO: + this.videoUrl = this.buildVideoUrl(this.videoBlacklist.video) + break + + case UserNotificationType.MY_VIDEO_PUBLISHED: + this.videoUrl = this.buildVideoUrl(this.video) + break + + case UserNotificationType.MY_VIDEO_IMPORT_SUCCESS: + this.videoImportUrl = this.buildVideoImportUrl() + this.videoImportIdentifier = this.buildVideoImportIdentifier(this.videoImport) + this.videoUrl = this.buildVideoUrl(this.videoImport.video) + break + + case UserNotificationType.MY_VIDEO_IMPORT_ERROR: + this.videoImportUrl = this.buildVideoImportUrl() + this.videoImportIdentifier = this.buildVideoImportIdentifier(this.videoImport) + break + + case UserNotificationType.NEW_USER_REGISTRATION: + this.accountUrl = this.buildAccountUrl(this.account) + break + + case UserNotificationType.NEW_FOLLOW: + this.accountUrl = this.buildAccountUrl(this.actorFollow.follower) + break + } + } + + private buildVideoUrl (video: { uuid: string }) { + return '/videos/watch/' + video.uuid + } + + private buildAccountUrl (account: { name: string }) { + return '/accounts/' + account.name + } + + private buildVideoImportUrl () { + return '/my-account/video-imports' + } + + private buildVideoImportIdentifier (videoImport: { targetUrl?: string, magnetUri?: string, torrentName?: string }) { + return videoImport.targetUrl || videoImport.magnetUri || videoImport.torrentName + } + +} diff --git a/client/src/app/shared/users/user-notification.service.ts b/client/src/app/shared/users/user-notification.service.ts new file mode 100644 index 000000000..2dfee8060 --- /dev/null +++ b/client/src/app/shared/users/user-notification.service.ts @@ -0,0 +1,110 @@ +import { Injectable } from '@angular/core' +import { HttpClient, HttpParams } from '@angular/common/http' +import { RestExtractor, RestService } from '@app/shared/rest' +import { catchError, map, tap } from 'rxjs/operators' +import { environment } from '../../../environments/environment' +import { ResultList, UserNotification as UserNotificationServer, UserNotificationSetting } from '../../../../../shared' +import { UserNotification } from '@app/shared/users/user-notification.model' +import { Subject } from 'rxjs' +import * as io from 'socket.io-client' +import { AuthService } from '@app/core' +import { ComponentPagination } from '@app/shared/rest/component-pagination.model' +import { User } from '@app/shared' + +@Injectable() +export class UserNotificationService { + static BASE_NOTIFICATIONS_URL = environment.apiUrl + '/api/v1/users/me/notifications' + static BASE_NOTIFICATION_SETTINGS = environment.apiUrl + '/api/v1/users/me/notification-settings' + + private notificationSubject = new Subject<{ type: 'new' | 'read' | 'read-all', notification?: UserNotification }>() + + private socket: SocketIOClient.Socket + + constructor ( + private auth: AuthService, + private authHttp: HttpClient, + private restExtractor: RestExtractor, + private restService: RestService + ) {} + + listMyNotifications (pagination: ComponentPagination, unread?: boolean, ignoreLoadingBar = false) { + let params = new HttpParams() + params = this.restService.addRestGetParams(params, this.restService.componentPaginationToRestPagination(pagination)) + + if (unread) params = params.append('unread', `${unread}`) + + const headers = ignoreLoadingBar ? { ignoreLoadingBar: '' } : undefined + + return this.authHttp.get>(UserNotificationService.BASE_NOTIFICATIONS_URL, { params, headers }) + .pipe( + map(res => this.restExtractor.convertResultListDateToHuman(res)), + map(res => this.restExtractor.applyToResultListData(res, this.formatNotification.bind(this))), + catchError(err => this.restExtractor.handleError(err)) + ) + } + + countUnreadNotifications () { + return this.listMyNotifications({ currentPage: 1, itemsPerPage: 0 }, true) + .pipe(map(n => n.total)) + } + + getMyNotificationsSocket () { + const socket = this.getSocket() + + socket.on('new-notification', (n: UserNotificationServer) => { + this.notificationSubject.next({ type: 'new', notification: new UserNotification(n) }) + }) + + return this.notificationSubject.asObservable() + } + + markAsRead (notification: UserNotification) { + const url = UserNotificationService.BASE_NOTIFICATIONS_URL + '/read' + + const body = { ids: [ notification.id ] } + const headers = { ignoreLoadingBar: '' } + + return this.authHttp.post(url, body, { headers }) + .pipe( + map(this.restExtractor.extractDataBool), + tap(() => this.notificationSubject.next({ type: 'read' })), + catchError(res => this.restExtractor.handleError(res)) + ) + } + + markAllAsRead () { + const url = UserNotificationService.BASE_NOTIFICATIONS_URL + '/read-all' + const headers = { ignoreLoadingBar: '' } + + return this.authHttp.post(url, {}, { headers }) + .pipe( + map(this.restExtractor.extractDataBool), + tap(() => this.notificationSubject.next({ type: 'read-all' })), + catchError(res => this.restExtractor.handleError(res)) + ) + } + + updateNotificationSettings (user: User, settings: UserNotificationSetting) { + const url = UserNotificationService.BASE_NOTIFICATION_SETTINGS + + return this.authHttp.put(url, settings) + .pipe( + map(this.restExtractor.extractDataBool), + catchError(res => this.restExtractor.handleError(res)) + ) + } + + private getSocket () { + if (this.socket) return this.socket + + this.socket = io(environment.apiUrl + '/user-notifications', { + query: { accessToken: this.auth.getAccessToken() } + }) + + return this.socket + } + + private formatNotification (notification: UserNotificationServer) { + return new UserNotification(notification) + } +} diff --git a/client/src/app/shared/users/user-notifications.component.html b/client/src/app/shared/users/user-notifications.component.html new file mode 100644 index 000000000..86379d941 --- /dev/null +++ b/client/src/app/shared/users/user-notifications.component.html @@ -0,0 +1,61 @@ +
You don't have notifications.
+ +
+
+ +
+ + {{ notification.video.channel.displayName }} published a new video + + + + Your video {{ notification.video.name }} has been unblacklisted + + + + Your video {{ notification.videoBlacklist.video.name }} has been blacklisted + + + + A new video abuse has been created on video {{ notification.videoAbuse.video.name }} + + + + {{ notification.comment.account.displayName }} commented your video {{ notification.comment.video.name }} + + + + Your video {{ notification.video.name }} has been published + + + + Your video import {{ notification.videoImportIdentifier }} succeeded + + + + Your video import {{ notification.videoImportIdentifier }} failed + + + + User {{ notification.account.name }} registered on your instance + + + + {{ notification.actorFollow.follower.displayName }} is following + + + your channel {{ notification.actorFollow.following.displayName }} + + your account + + + + {{ notification.comment.account.displayName }} mentioned you on video {{ notification.comment.video.name }} + +
+ +
+
+
+
+
diff --git a/client/src/app/shared/users/user-notifications.component.scss b/client/src/app/shared/users/user-notifications.component.scss new file mode 100644 index 000000000..0493b10d9 --- /dev/null +++ b/client/src/app/shared/users/user-notifications.component.scss @@ -0,0 +1,30 @@ +.notification { + display: flex; + justify-content: space-between; + align-items: center; + font-size: inherit; + padding: 15px 10px; + border-bottom: 1px solid rgba(0, 0, 0, 0.10); + + .mark-as-read { + min-width: 35px; + + .glyphicon { + display: none; + cursor: pointer; + color: rgba(20, 20, 20, 0.5) + } + } + + &.unread { + background-color: rgba(0, 0, 0, 0.05); + + &:hover .mark-as-read .glyphicon { + display: block; + + &:hover { + color: rgba(20, 20, 20, 0.8); + } + } + } +} diff --git a/client/src/app/shared/users/user-notifications.component.ts b/client/src/app/shared/users/user-notifications.component.ts new file mode 100644 index 000000000..682116226 --- /dev/null +++ b/client/src/app/shared/users/user-notifications.component.ts @@ -0,0 +1,82 @@ +import { Component, Input, OnInit } from '@angular/core' +import { UserNotificationService } from '@app/shared/users/user-notification.service' +import { UserNotificationType } from '../../../../../shared' +import { ComponentPagination, hasMoreItems } from '@app/shared/rest/component-pagination.model' +import { Notifier } from '@app/core' +import { UserNotification } from '@app/shared/users/user-notification.model' + +@Component({ + selector: 'my-user-notifications', + templateUrl: 'user-notifications.component.html', + styleUrls: [ 'user-notifications.component.scss' ] +}) +export class UserNotificationsComponent implements OnInit { + @Input() ignoreLoadingBar = false + @Input() infiniteScroll = true + + notifications: UserNotification[] = [] + + // So we can access it in the template + UserNotificationType = UserNotificationType + + componentPagination: ComponentPagination = { + currentPage: 1, + itemsPerPage: 10, + totalItems: null + } + + constructor ( + private userNotificationService: UserNotificationService, + private notifier: Notifier + ) { } + + ngOnInit () { + this.loadMoreNotifications() + } + + loadMoreNotifications () { + this.userNotificationService.listMyNotifications(this.componentPagination, undefined, this.ignoreLoadingBar) + .subscribe( + result => { + this.notifications = this.notifications.concat(result.data) + this.componentPagination.totalItems = result.total + }, + + err => this.notifier.error(err.message) + ) + } + + onNearOfBottom () { + if (this.infiniteScroll === false) return + + this.componentPagination.currentPage++ + + if (hasMoreItems(this.componentPagination)) { + this.loadMoreNotifications() + } + } + + markAsRead (notification: UserNotification) { + this.userNotificationService.markAsRead(notification) + .subscribe( + () => { + notification.read = true + }, + + err => this.notifier.error(err.message) + ) + } + + markAllAsRead () { + this.userNotificationService.markAllAsRead() + .subscribe( + () => { + for (const notification of this.notifications) { + notification.read = true + } + }, + + err => this.notifier.error(err.message) + ) + } +} diff --git a/client/src/app/shared/users/user.model.ts b/client/src/app/shared/users/user.model.ts index 3663a7b61..c15f1de8c 100644 --- a/client/src/app/shared/users/user.model.ts +++ b/client/src/app/shared/users/user.model.ts @@ -1,4 +1,4 @@ -import { hasUserRight, User as UserServerModel, UserRight, UserRole, VideoChannel } from '../../../../../shared' +import { hasUserRight, User as UserServerModel, UserNotificationSetting, UserRight, UserRole, VideoChannel } from '../../../../../shared' import { NSFWPolicyType } from '../../../../../shared/models/videos/nsfw-policy.type' import { Account } from '@app/shared/account/account.model' import { Avatar } from '../../../../../shared/models/avatars/avatar.model' @@ -24,6 +24,8 @@ export class User implements UserServerModel { blocked: boolean blockedReason?: string + notificationSettings?: UserNotificationSetting + constructor (hash: Partial) { this.id = hash.id this.username = hash.username @@ -41,6 +43,8 @@ export class User implements UserServerModel { this.blocked = hash.blocked this.blockedReason = hash.blockedReason + this.notificationSettings = hash.notificationSettings + if (hash.account !== undefined) { this.account = new Account(hash.account) } diff --git a/client/src/app/videos/+video-watch/comment/video-comments.component.ts b/client/src/app/videos/+video-watch/comment/video-comments.component.ts index 957c17bbf..dc62fe5ae 100644 --- a/client/src/app/videos/+video-watch/comment/video-comments.component.ts +++ b/client/src/app/videos/+video-watch/comment/video-comments.component.ts @@ -4,7 +4,7 @@ import { ConfirmService, Notifier } from '@app/core' import { Subscription } from 'rxjs' import { VideoCommentThreadTree } from '../../../../../../shared/models/videos/video-comment.model' import { AuthService } from '../../../core/auth' -import { ComponentPagination } from '../../../shared/rest/component-pagination.model' +import { ComponentPagination, hasMoreItems } from '../../../shared/rest/component-pagination.model' import { User } from '../../../shared/users' import { VideoSortField } from '../../../shared/video/sort-field.type' import { VideoDetails } from '../../../shared/video/video-details.model' @@ -165,22 +165,11 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy { onNearOfBottom () { this.componentPagination.currentPage++ - if (this.hasMoreComments()) { + if (hasMoreItems(this.componentPagination)) { this.loadMoreComments() } } - private hasMoreComments () { - // No results - if (this.componentPagination.totalItems === 0) return false - - // Not loaded yet - if (!this.componentPagination.totalItems) return true - - const maxPage = this.componentPagination.totalItems / this.componentPagination.itemsPerPage - return maxPage > this.componentPagination.currentPage - } - private deleteLocalCommentThread (parentComment: VideoCommentThreadTree, commentToDelete: VideoComment) { for (const commentChild of parentComment.children) { if (commentChild.comment.id === commentToDelete.id) { diff --git a/client/src/sass/include/_bootstrap-variables.scss b/client/src/sass/include/_bootstrap-variables.scss index 77a20cfe1..7f413836b 100644 --- a/client/src/sass/include/_bootstrap-variables.scss +++ b/client/src/sass/include/_bootstrap-variables.scss @@ -31,4 +31,5 @@ $input-focus-border-color: #ced4da; $nav-pills-link-active-bg: #F0F0F0; $nav-pills-link-active-color: #000; -$zindex-dropdown: 10000; \ No newline at end of file +$zindex-dropdown: 10000; +$zindex-popover: 10000; diff --git a/client/src/sass/primeng-custom.scss b/client/src/sass/primeng-custom.scss index 05db2a2cb..58a6a0004 100644 --- a/client/src/sass/primeng-custom.scss +++ b/client/src/sass/primeng-custom.scss @@ -326,6 +326,8 @@ p-toast { .notification-block { display: flex; + align-items: center; + padding: 5px; .message { flex-grow: 1; @@ -336,12 +338,12 @@ p-toast { p { font-size: 15px; + margin-bottom: 0; } } .glyphicon { font-size: 32px; - margin-top: 15px; margin-right: 5px; } } diff --git a/client/yarn.lock b/client/yarn.lock index 3c7ba2d25..5ed43117a 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -510,6 +510,11 @@ dependencies: "@types/node" "*" +"@types/socket.io-client@^1.4.32": + version "1.4.32" + resolved "https://registry.yarnpkg.com/@types/socket.io-client/-/socket.io-client-1.4.32.tgz#988a65a0386c274b1c22a55377fab6a30789ac14" + integrity sha512-Vs55Kq8F+OWvy1RLA31rT+cAyemzgm0EWNeax6BWF8H7QiiOYMJIdcwSDdm5LVgfEkoepsWkS+40+WNb7BUMbg== + "@types/video.js@^7.2.5": version "7.2.5" resolved "https://registry.yarnpkg.com/@types/video.js/-/video.js-7.2.5.tgz#20896c81141d3517c3a89bb6eb97c6a191aa5d4c" @@ -3195,6 +3200,23 @@ engine.io-client@~3.2.0: xmlhttprequest-ssl "~1.5.4" yeast "0.1.2" +engine.io-client@~3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.3.1.tgz#afedb4a07b2ea48b7190c3136bfea98fdd4f0f03" + integrity sha512-q66JBFuQcy7CSlfAz9L3jH+v7DTT3i6ZEadYcVj2pOs8/0uJHLxKX3WBkGTvULJMdz0tUCyJag0aKT/dpXL9BQ== + dependencies: + component-emitter "1.2.1" + component-inherit "0.0.3" + debug "~3.1.0" + engine.io-parser "~2.1.1" + has-cors "1.1.0" + indexof "0.0.1" + parseqs "0.0.5" + parseuri "0.0.5" + ws "~6.1.0" + xmlhttprequest-ssl "~1.5.4" + yeast "0.1.2" + engine.io-parser@~2.1.0, engine.io-parser@~2.1.1: version "2.1.3" resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-2.1.3.tgz#757ab970fbf2dfb32c7b74b033216d5739ef79a6" @@ -8981,6 +9003,26 @@ socket.io-client@2.1.1: socket.io-parser "~3.2.0" to-array "0.1.4" +socket.io-client@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-2.2.0.tgz#84e73ee3c43d5020ccc1a258faeeb9aec2723af7" + integrity sha512-56ZrkTDbdTLmBIyfFYesgOxsjcLnwAKoN4CiPyTVkMQj3zTUh0QAx3GbvIvLpFEOvQWu92yyWICxB0u7wkVbYA== + dependencies: + backo2 "1.0.2" + base64-arraybuffer "0.1.5" + component-bind "1.0.0" + component-emitter "1.2.1" + debug "~3.1.0" + engine.io-client "~3.3.1" + has-binary2 "~1.0.2" + has-cors "1.1.0" + indexof "0.0.1" + object-component "0.0.3" + parseqs "0.0.5" + parseuri "0.0.5" + socket.io-parser "~3.3.0" + to-array "0.1.4" + socket.io-parser@~3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.2.0.tgz#e7c6228b6aa1f814e6148aea325b51aa9499e077" @@ -8990,6 +9032,15 @@ socket.io-parser@~3.2.0: debug "~3.1.0" isarray "2.0.1" +socket.io-parser@~3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.3.0.tgz#2b52a96a509fdf31440ba40fed6094c7d4f1262f" + integrity sha512-hczmV6bDgdaEbVqhAeVMM/jfUfzuEZHsQg6eOmLgJht6G3mPKMxYm75w2+qhAQZ+4X+1+ATZ+QFKeOZD5riHng== + dependencies: + component-emitter "1.2.1" + debug "~3.1.0" + isarray "2.0.1" + socket.io@2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-2.1.1.tgz#a069c5feabee3e6b214a75b40ce0652e1cfb9980" @@ -10671,7 +10722,7 @@ ws@^5.2.0: dependencies: async-limiter "~1.0.0" -ws@^6.0.0: +ws@^6.0.0, ws@~6.1.0: version "6.1.2" resolved "https://registry.yarnpkg.com/ws/-/ws-6.1.2.tgz#3cc7462e98792f0ac679424148903ded3b9c3ad8" integrity sha512-rfUqzvz0WxmSXtJpPMX2EeASXabOrSMk1ruMOV3JBTBjo4ac2lDjGGsbQSyxj8Odhw5fBib8ZKEjDNvgouNKYw== -- cgit v1.2.3