Add notifications in the client
This commit is contained in:
parent
f7cc67b455
commit
2f1548fda3
14
.travis.yml
14
.travis.yml
|
@ -48,12 +48,12 @@ matrix:
|
|||
- env: TEST_SUITE=jest
|
||||
|
||||
script:
|
||||
- travis_retry npm run travis -- "$TEST_SUITE"
|
||||
- NODE_PENDING_JOB_WAIT=1000 travis_retry npm run travis -- "$TEST_SUITE"
|
||||
|
||||
after_failure:
|
||||
- cat test1/logs/all-logs.log
|
||||
- cat test2/logs/all-logs.log
|
||||
- cat test3/logs/all-logs.log
|
||||
- cat test4/logs/all-logs.log
|
||||
- cat test5/logs/all-logs.log
|
||||
- cat test6/logs/all-logs.log
|
||||
- cat test1/logs/peertube.log
|
||||
- cat test2/logs/peertube.log
|
||||
- cat test3/logs/peertube.log
|
||||
- cat test4/logs/peertube.log
|
||||
- cat test5/logs/peertube.log
|
||||
- cat test6/logs/peertube.log
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
<div class="header">
|
||||
<a routerLink="/my-account/settings" i18n>Notification preferences</a>
|
||||
|
||||
<button (click)="markAllAsRead()" i18n>Mark all as read</button>
|
||||
</div>
|
||||
|
||||
<my-user-notifications #userNotification></my-user-notifications>
|
|
@ -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;
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export * from './my-account-notification-preferences.component'
|
|
@ -0,0 +1,19 @@
|
|||
<div class="custom-row">
|
||||
<div i18n>Activities</div>
|
||||
<div i18n>Web</div>
|
||||
<div i18n *ngIf="emailEnabled">Email</div>
|
||||
</div>
|
||||
|
||||
<div class="custom-row" *ngFor="let notificationType of notificationSettingKeys">
|
||||
<ng-container *ngIf="hasUserRight(notificationType)">
|
||||
<div>{{ labelNotifications[notificationType] }}</div>
|
||||
|
||||
<div>
|
||||
<p-inputSwitch [(ngModel)]="webNotifications[notificationType]" (onChange)="updateWebSetting(notificationType, $event.checked)"></p-inputSwitch>
|
||||
</div>
|
||||
|
||||
<div *ngIf="emailEnabled">
|
||||
<p-inputSwitch [(ngModel)]="emailNotifications[notificationType]" (onChange)="updateEmailSetting(notificationType, $event.checked)"></p-inputSwitch>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<any>
|
||||
|
||||
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<UserNotificationSetting> ]: 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,6 +9,9 @@
|
|||
<my-account-profile [user]="user" [userInformationLoaded]="userInformationLoaded"></my-account-profile>
|
||||
</ng-template>
|
||||
|
||||
<div i18n class="account-title" id="notifications">Notifications</div>
|
||||
<my-account-notification-preferences [user]="user" [userInformationLoaded]="userInformationLoaded"></my-account-notification-preferences>
|
||||
|
||||
<div i18n class="account-title">Password</div>
|
||||
<my-account-change-password></my-account-change-password>
|
||||
|
||||
|
|
|
@ -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
|
||||
]
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -7,7 +7,7 @@ import { VideoChannelAboutComponent } from './video-channel-about/video-channel-
|
|||
|
||||
const videoChannelsRoutes: Routes = [
|
||||
{
|
||||
path: ':videoChannelId',
|
||||
path: ':videoChannelName',
|
||||
component: VideoChannelsComponent,
|
||||
canActivateChild: [ MetaGuard ],
|
||||
children: [
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
<div
|
||||
[ngbPopover]="popContent" autoClose="outside" placement="bottom-left" container="body" popoverClass="popover-notifications"
|
||||
i18n-title title="View your notifications" class="notification-avatar" #popover="ngbPopover"
|
||||
>
|
||||
<div *ngIf="unreadNotifications > 0" class="unread-notifications">{{ unreadNotifications }}</div>
|
||||
|
||||
<img [src]="user.accountAvatarUrl" alt="Avatar" />
|
||||
</div>
|
||||
|
||||
<ng-template #popContent>
|
||||
<div class="notifications-header">
|
||||
<div i18n>Notifications</div>
|
||||
|
||||
<a
|
||||
i18n-title title="Update your notification preferences" class="glyphicon glyphicon-cog"
|
||||
routerLink="/my-account/settings" fragment="notifications"
|
||||
></a>
|
||||
</div>
|
||||
|
||||
<my-user-notifications [ignoreLoadingBar]="true" [infiniteScroll]="false"></my-user-notifications>
|
||||
|
||||
<a class="all-notifications" routerLink="/my-account/notifications" i18n>See all your notifications</a>
|
||||
</ng-template>
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
||||
}
|
|
@ -1 +1,3 @@
|
|||
export * from './language-chooser.component'
|
||||
export * from './avatar-notification.component'
|
||||
export * from './menu.component'
|
||||
|
|
|
@ -2,9 +2,7 @@
|
|||
<menu>
|
||||
<div class="top-menu">
|
||||
<div *ngIf="isLoggedIn" class="logged-in-block">
|
||||
<a routerLink="/my-account/settings">
|
||||
<img [src]="user.accountAvatarUrl" alt="Avatar" />
|
||||
</a>
|
||||
<my-avatar-notification [user]="user"></my-avatar-notification>
|
||||
|
||||
<div class="logged-in-info">
|
||||
<a routerLink="/my-account/settings" class="logged-in-username">{{ user.account?.displayName }}</a>
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
container="body"
|
||||
title="Get help"
|
||||
i18n-title
|
||||
popoverClass="help-popover"
|
||||
[attr.aria-pressed]="isPopoverOpened"
|
||||
[ngbPopover]="tooltipTemplate"
|
||||
[placement]="tooltipPlacement"
|
||||
|
|
|
@ -12,11 +12,12 @@
|
|||
}
|
||||
|
||||
/deep/ {
|
||||
.popover-help.popover {
|
||||
max-width: 300px;
|
||||
|
||||
.popover-body {
|
||||
text-align: left;
|
||||
padding: 10px;
|
||||
max-width: 300px;
|
||||
|
||||
font-size: 13px;
|
||||
font-family: $main-fonts;
|
||||
background-color: #fff;
|
||||
|
@ -27,4 +28,5 @@
|
|||
padding-left: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
]
|
||||
})
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
export * from './user.model'
|
||||
export * from './user.service'
|
||||
export * from './user-notifications.component'
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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<ResultList<UserNotification>>(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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
<div *ngIf="componentPagination.totalItems === 0" class="no-notification" i18n>You don't have notifications.</div>
|
||||
|
||||
<div class="notifications" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()">
|
||||
<div *ngFor="let notification of notifications" class="notification" [ngClass]="{ unread: !notification.read }">
|
||||
|
||||
<div [ngSwitch]="notification.type">
|
||||
<ng-container i18n *ngSwitchCase="UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION">
|
||||
{{ notification.video.channel.displayName }} published a <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">new video</a>
|
||||
</ng-container>
|
||||
|
||||
<ng-container i18n *ngSwitchCase="UserNotificationType.UNBLACKLIST_ON_MY_VIDEO">
|
||||
Your video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.video.name }}</a> has been unblacklisted
|
||||
</ng-container>
|
||||
|
||||
<ng-container i18n *ngSwitchCase="UserNotificationType.BLACKLIST_ON_MY_VIDEO">
|
||||
Your video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.videoBlacklist.video.name }}</a> has been blacklisted
|
||||
</ng-container>
|
||||
|
||||
<ng-container i18n *ngSwitchCase="UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS">
|
||||
<a (click)="markAsRead(notification)" [routerLink]="notification.videoAbuseUrl">A new video abuse</a> has been created on video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.videoAbuse.video.name }}</a>
|
||||
</ng-container>
|
||||
|
||||
<ng-container i18n *ngSwitchCase="UserNotificationType.NEW_COMMENT_ON_MY_VIDEO">
|
||||
{{ notification.comment.account.displayName }} commented your video <a (click)="markAsRead(notification)" [routerLink]="notification.commentUrl">{{ notification.comment.video.name }}</a>
|
||||
</ng-container>
|
||||
|
||||
<ng-container i18n *ngSwitchCase="UserNotificationType.MY_VIDEO_PUBLISHED">
|
||||
Your video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.video.name }}</a> has been published
|
||||
</ng-container>
|
||||
|
||||
<ng-container i18n *ngSwitchCase="UserNotificationType.MY_VIDEO_IMPORT_SUCCESS">
|
||||
<a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">Your video import</a> {{ notification.videoImportIdentifier }} succeeded
|
||||
</ng-container>
|
||||
|
||||
<ng-container i18n *ngSwitchCase="UserNotificationType.MY_VIDEO_IMPORT_ERROR">
|
||||
<a (click)="markAsRead(notification)" [routerLink]="notification.videoImportUrl">Your video import</a> {{ notification.videoImportIdentifier }} failed
|
||||
</ng-container>
|
||||
|
||||
<ng-container i18n *ngSwitchCase="UserNotificationType.NEW_USER_REGISTRATION">
|
||||
User <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.account.name }} registered</a> on your instance
|
||||
</ng-container>
|
||||
|
||||
<ng-container i18n *ngSwitchCase="UserNotificationType.NEW_FOLLOW">
|
||||
<a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.actorFollow.follower.displayName }}</a> is following
|
||||
|
||||
<ng-container *ngIf="notification.actorFollow.following.type === 'channel'">
|
||||
your channel {{ notification.actorFollow.following.displayName }}
|
||||
</ng-container>
|
||||
<ng-container *ngIf="notification.actorFollow.following.type === 'account'">your account</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-container i18n *ngSwitchCase="UserNotificationType.COMMENT_MENTION">
|
||||
{{ notification.comment.account.displayName }} mentioned you on <a (click)="markAsRead(notification)" [routerLink]="notification.commentUrl">video {{ notification.comment.video.name }}</a>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div i18n title="Mark as read" class="mark-as-read">
|
||||
<div class="glyphicon glyphicon-ok" (click)="markAsRead(notification)"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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<UserServerModel>) {
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -32,3 +32,4 @@ $nav-pills-link-active-bg: #F0F0F0;
|
|||
$nav-pills-link-active-color: #000;
|
||||
|
||||
$zindex-dropdown: 10000;
|
||||
$zindex-popover: 10000;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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==
|
||||
|
|
|
@ -13,7 +13,7 @@ recreateDB () {
|
|||
}
|
||||
|
||||
removeFiles () {
|
||||
rm -rf "./test$1" "./config/local-test.json" "./config/local-test-$1.json"
|
||||
rm -rf "./test$1" "./config/local-test-$1.json"
|
||||
}
|
||||
|
||||
dropRedis () {
|
||||
|
|
|
@ -45,6 +45,11 @@ myNotificationsRouter.post('/me/notifications/read',
|
|||
asyncMiddleware(markAsReadUserNotifications)
|
||||
)
|
||||
|
||||
myNotificationsRouter.post('/me/notifications/read-all',
|
||||
authenticate,
|
||||
asyncMiddleware(markAsReadAllUserNotifications)
|
||||
)
|
||||
|
||||
export {
|
||||
myNotificationsRouter
|
||||
}
|
||||
|
@ -70,7 +75,7 @@ async function updateNotificationSettings (req: express.Request, res: express.Re
|
|||
myVideoImportFinished: body.myVideoImportFinished,
|
||||
newFollow: body.newFollow,
|
||||
newUserRegistration: body.newUserRegistration,
|
||||
commentMention: body.commentMention,
|
||||
commentMention: body.commentMention
|
||||
}
|
||||
|
||||
await UserNotificationSettingModel.update(values, query)
|
||||
|
@ -93,3 +98,11 @@ async function markAsReadUserNotifications (req: express.Request, res: express.R
|
|||
|
||||
return res.status(204).end()
|
||||
}
|
||||
|
||||
async function markAsReadAllUserNotifications (req: express.Request, res: express.Response) {
|
||||
const user: UserModel = res.locals.oauth.token.User
|
||||
|
||||
await UserNotificationModel.markAllAsRead(user.id)
|
||||
|
||||
return res.status(204).end()
|
||||
}
|
||||
|
|
|
@ -9,8 +9,8 @@ function isArray (value: any) {
|
|||
return Array.isArray(value)
|
||||
}
|
||||
|
||||
function isIntArray (value: any) {
|
||||
return Array.isArray(value) && value.every(v => validator.isInt('' + v))
|
||||
function isNotEmptyIntArray (value: any) {
|
||||
return Array.isArray(value) && value.every(v => validator.isInt('' + v)) && value.length !== 0
|
||||
}
|
||||
|
||||
function isDateValid (value: string) {
|
||||
|
@ -82,7 +82,7 @@ function isFileValid (
|
|||
|
||||
export {
|
||||
exists,
|
||||
isIntArray,
|
||||
isNotEmptyIntArray,
|
||||
isArray,
|
||||
isIdValid,
|
||||
isUUIDValid,
|
||||
|
|
|
@ -9,8 +9,12 @@ function isUserNotificationTypeValid (value: any) {
|
|||
|
||||
function isUserNotificationSettingValid (value: any) {
|
||||
return exists(value) &&
|
||||
validator.isInt('' + value) &&
|
||||
UserNotificationSettingValue[ value ] !== undefined
|
||||
validator.isInt('' + value) && (
|
||||
value === UserNotificationSettingValue.NONE ||
|
||||
value === UserNotificationSettingValue.WEB ||
|
||||
value === UserNotificationSettingValue.EMAIL ||
|
||||
value === (UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL)
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
|
|
|
@ -31,7 +31,7 @@ PRIMARY KEY ("id"))
|
|||
'("newVideoFromSubscription", "newCommentOnMyVideo", "videoAbuseAsModerator", "blacklistOnMyVideo", ' +
|
||||
'"myVideoPublished", "myVideoImportFinished", "newUserRegistration", "newFollow", "commentMention", ' +
|
||||
'"userId", "createdAt", "updatedAt") ' +
|
||||
'(SELECT 2, 2, 4, 4, 2, 2, 2, 2, 2, id, NOW(), NOW() FROM "user")'
|
||||
'(SELECT 1, 1, 3, 3, 1, 1, 1, 1, 1, id, NOW(), NOW() FROM "user")'
|
||||
|
||||
await utils.sequelize.query(query)
|
||||
}
|
||||
|
|
|
@ -436,11 +436,11 @@ class Notifier {
|
|||
private isEmailEnabled (user: UserModel, value: UserNotificationSettingValue) {
|
||||
if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION === true && user.emailVerified !== true) return false
|
||||
|
||||
return value === UserNotificationSettingValue.EMAIL || value === UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL
|
||||
return value & UserNotificationSettingValue.EMAIL
|
||||
}
|
||||
|
||||
private isWebNotificationEnabled (value: UserNotificationSettingValue) {
|
||||
return value === UserNotificationSettingValue.WEB_NOTIFICATION || value === UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL
|
||||
return value & UserNotificationSettingValue.WEB
|
||||
}
|
||||
|
||||
static get Instance () {
|
||||
|
|
|
@ -98,15 +98,15 @@ export {
|
|||
function createDefaultUserNotificationSettings (user: UserModel, t: Sequelize.Transaction | undefined) {
|
||||
const values: UserNotificationSetting & { userId: number } = {
|
||||
userId: user.id,
|
||||
newVideoFromSubscription: UserNotificationSettingValue.WEB_NOTIFICATION,
|
||||
newCommentOnMyVideo: UserNotificationSettingValue.WEB_NOTIFICATION,
|
||||
myVideoImportFinished: UserNotificationSettingValue.WEB_NOTIFICATION,
|
||||
myVideoPublished: UserNotificationSettingValue.WEB_NOTIFICATION,
|
||||
videoAbuseAsModerator: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL,
|
||||
blacklistOnMyVideo: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL,
|
||||
newUserRegistration: UserNotificationSettingValue.WEB_NOTIFICATION,
|
||||
commentMention: UserNotificationSettingValue.WEB_NOTIFICATION,
|
||||
newFollow: UserNotificationSettingValue.WEB_NOTIFICATION
|
||||
newVideoFromSubscription: UserNotificationSettingValue.WEB,
|
||||
newCommentOnMyVideo: UserNotificationSettingValue.WEB,
|
||||
myVideoImportFinished: UserNotificationSettingValue.WEB,
|
||||
myVideoPublished: UserNotificationSettingValue.WEB,
|
||||
videoAbuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
newUserRegistration: UserNotificationSettingValue.WEB,
|
||||
commentMention: UserNotificationSettingValue.WEB,
|
||||
newFollow: UserNotificationSettingValue.WEB
|
||||
}
|
||||
|
||||
return UserNotificationSettingModel.create(values, { transaction: t })
|
||||
|
|
|
@ -4,7 +4,7 @@ import { body, query } from 'express-validator/check'
|
|||
import { logger } from '../../helpers/logger'
|
||||
import { areValidationErrors } from './utils'
|
||||
import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications'
|
||||
import { isIntArray } from '../../helpers/custom-validators/misc'
|
||||
import { isNotEmptyIntArray } from '../../helpers/custom-validators/misc'
|
||||
|
||||
const listUserNotificationsValidator = [
|
||||
query('unread')
|
||||
|
@ -42,7 +42,8 @@ const updateNotificationSettingsValidator = [
|
|||
|
||||
const markAsReadUserNotificationsValidator = [
|
||||
body('ids')
|
||||
.custom(isIntArray).withMessage('Should have a valid notification ids to mark as read'),
|
||||
.optional()
|
||||
.custom(isNotEmptyIntArray).withMessage('Should have a valid notification ids to mark as read'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
logger.debug('Checking markAsReadUserNotificationsValidator parameters', { parameters: req.body })
|
||||
|
|
|
@ -290,6 +290,12 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
|
|||
return UserNotificationModel.update({ read: true }, query)
|
||||
}
|
||||
|
||||
static markAllAsRead (userId: number) {
|
||||
const query = { where: { userId } }
|
||||
|
||||
return UserNotificationModel.update({ read: true }, query)
|
||||
}
|
||||
|
||||
toFormattedJSON (): UserNotification {
|
||||
const video = this.Video ? Object.assign(this.formatVideo(this.Video), {
|
||||
channel: {
|
||||
|
|
|
@ -96,6 +96,16 @@ describe('Test user notifications API validators', function () {
|
|||
statusCodeExpected: 400
|
||||
})
|
||||
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: {
|
||||
ids: [ ]
|
||||
},
|
||||
token: server.accessToken,
|
||||
statusCodeExpected: 400
|
||||
})
|
||||
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
|
@ -131,18 +141,39 @@ describe('Test user notifications API validators', function () {
|
|||
})
|
||||
})
|
||||
|
||||
describe('When marking as read my notifications', function () {
|
||||
const path = '/api/v1/users/me/notifications/read-all'
|
||||
|
||||
it('Should fail with a non authenticated user', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
statusCodeExpected: 401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
statusCodeExpected: 204
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When updating my notification settings', function () {
|
||||
const path = '/api/v1/users/me/notification-settings'
|
||||
const correctFields: UserNotificationSetting = {
|
||||
newVideoFromSubscription: UserNotificationSettingValue.WEB_NOTIFICATION,
|
||||
newCommentOnMyVideo: UserNotificationSettingValue.WEB_NOTIFICATION,
|
||||
videoAbuseAsModerator: UserNotificationSettingValue.WEB_NOTIFICATION,
|
||||
blacklistOnMyVideo: UserNotificationSettingValue.WEB_NOTIFICATION,
|
||||
myVideoImportFinished: UserNotificationSettingValue.WEB_NOTIFICATION,
|
||||
myVideoPublished: UserNotificationSettingValue.WEB_NOTIFICATION,
|
||||
commentMention: UserNotificationSettingValue.WEB_NOTIFICATION,
|
||||
newFollow: UserNotificationSettingValue.WEB_NOTIFICATION,
|
||||
newUserRegistration: UserNotificationSettingValue.WEB_NOTIFICATION
|
||||
newVideoFromSubscription: UserNotificationSettingValue.WEB,
|
||||
newCommentOnMyVideo: UserNotificationSettingValue.WEB,
|
||||
videoAbuseAsModerator: UserNotificationSettingValue.WEB,
|
||||
blacklistOnMyVideo: UserNotificationSettingValue.WEB,
|
||||
myVideoImportFinished: UserNotificationSettingValue.WEB,
|
||||
myVideoPublished: UserNotificationSettingValue.WEB,
|
||||
commentMention: UserNotificationSettingValue.WEB,
|
||||
newFollow: UserNotificationSettingValue.WEB,
|
||||
newUserRegistration: UserNotificationSettingValue.WEB
|
||||
}
|
||||
|
||||
it('Should fail with missing fields', async function () {
|
||||
|
@ -150,7 +181,7 @@ describe('Test user notifications API validators', function () {
|
|||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
fields: { newVideoFromSubscription: UserNotificationSettingValue.WEB_NOTIFICATION },
|
||||
fields: { newVideoFromSubscription: UserNotificationSettingValue.WEB },
|
||||
statusCodeExpected: 400
|
||||
})
|
||||
})
|
||||
|
|
|
@ -485,11 +485,10 @@ describe('Test users API validators', function () {
|
|||
email: 'email@example.com',
|
||||
emailVerified: true,
|
||||
videoQuota: 42,
|
||||
role: UserRole.MODERATOR
|
||||
role: UserRole.USER
|
||||
}
|
||||
|
||||
await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields, statusCodeExpected: 204 })
|
||||
userAccessToken = await userLogin(server, user)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -37,7 +37,8 @@ import {
|
|||
getLastNotification,
|
||||
getUserNotifications,
|
||||
markAsReadNotifications,
|
||||
updateMyNotificationSettings
|
||||
updateMyNotificationSettings,
|
||||
markAsReadAllNotifications
|
||||
} from '../../../../shared/utils/users/user-notifications'
|
||||
import {
|
||||
User,
|
||||
|
@ -88,15 +89,15 @@ describe('Test users notifications', function () {
|
|||
let channelId: number
|
||||
|
||||
const allNotificationSettings: UserNotificationSetting = {
|
||||
newVideoFromSubscription: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL,
|
||||
newCommentOnMyVideo: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL,
|
||||
videoAbuseAsModerator: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL,
|
||||
blacklistOnMyVideo: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL,
|
||||
myVideoImportFinished: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL,
|
||||
myVideoPublished: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL,
|
||||
commentMention: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL,
|
||||
newFollow: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL,
|
||||
newUserRegistration: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL
|
||||
newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
newCommentOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
videoAbuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
myVideoImportFinished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
myVideoPublished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
commentMention: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
newFollow: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
newUserRegistration: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
|
||||
}
|
||||
|
||||
before(async function () {
|
||||
|
@ -174,7 +175,10 @@ describe('Test users notifications', function () {
|
|||
})
|
||||
|
||||
it('Should send a new video notification if the user follows the local video publisher', async function () {
|
||||
this.timeout(10000)
|
||||
|
||||
await addUserSubscription(servers[0].url, userAccessToken, 'root_channel@localhost:9001')
|
||||
await waitJobs(servers)
|
||||
|
||||
const { name, uuid } = await uploadVideoByLocalAccount(servers)
|
||||
await checkNewVideoFromSubscription(baseParams, name, uuid, 'presence')
|
||||
|
@ -184,6 +188,7 @@ describe('Test users notifications', function () {
|
|||
this.timeout(50000) // Server 2 has transcoding enabled
|
||||
|
||||
await addUserSubscription(servers[0].url, userAccessToken, 'root_channel@localhost:9002')
|
||||
await waitJobs(servers)
|
||||
|
||||
const { name, uuid } = await uploadVideoByRemoteAccount(servers)
|
||||
await checkNewVideoFromSubscription(baseParams, name, uuid, 'presence')
|
||||
|
@ -822,8 +827,9 @@ describe('Test users notifications', function () {
|
|||
})
|
||||
|
||||
it('Should notify when a local channel is following one of our channel', async function () {
|
||||
await addUserSubscription(servers[0].url, servers[0].accessToken, 'user_1_channel@localhost:9001')
|
||||
this.timeout(10000)
|
||||
|
||||
await addUserSubscription(servers[0].url, servers[0].accessToken, 'user_1_channel@localhost:9001')
|
||||
await waitJobs(servers)
|
||||
|
||||
await checkNewActorFollow(baseParams, 'channel', 'root', 'super root name', myChannelName, 'presence')
|
||||
|
@ -832,8 +838,9 @@ describe('Test users notifications', function () {
|
|||
})
|
||||
|
||||
it('Should notify when a remote channel is following one of our channel', async function () {
|
||||
await addUserSubscription(servers[1].url, servers[1].accessToken, 'user_1_channel@localhost:9001')
|
||||
this.timeout(10000)
|
||||
|
||||
await addUserSubscription(servers[1].url, servers[1].accessToken, 'user_1_channel@localhost:9001')
|
||||
await waitJobs(servers)
|
||||
|
||||
await checkNewActorFollow(baseParams, 'channel', 'root', 'super root 2 name', myChannelName, 'presence')
|
||||
|
@ -895,6 +902,15 @@ describe('Test users notifications', function () {
|
|||
expect(notification.read).to.be.false
|
||||
}
|
||||
})
|
||||
|
||||
it('Should mark as read all notifications', async function () {
|
||||
await markAsReadAllNotifications(servers[ 0 ].url, userAccessToken)
|
||||
|
||||
const res = await getUserNotifications(servers[ 0 ].url, userAccessToken, 0, 10, true)
|
||||
|
||||
expect(res.body.total).to.equal(0)
|
||||
expect(res.body.data).to.have.lengthOf(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Notification settings', function () {
|
||||
|
@ -928,13 +944,13 @@ describe('Test users notifications', function () {
|
|||
|
||||
it('Should only have web notifications', async function () {
|
||||
await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, {
|
||||
newVideoFromSubscription: UserNotificationSettingValue.WEB_NOTIFICATION
|
||||
newVideoFromSubscription: UserNotificationSettingValue.WEB
|
||||
}))
|
||||
|
||||
{
|
||||
const res = await getMyUserInformation(servers[0].url, userAccessToken)
|
||||
const info = res.body as User
|
||||
expect(info.notificationSettings.newVideoFromSubscription).to.equal(UserNotificationSettingValue.WEB_NOTIFICATION)
|
||||
expect(info.notificationSettings.newVideoFromSubscription).to.equal(UserNotificationSettingValue.WEB)
|
||||
}
|
||||
|
||||
const { name, uuid } = await uploadVideoByLocalAccount(servers)
|
||||
|
@ -976,13 +992,15 @@ describe('Test users notifications', function () {
|
|||
|
||||
it('Should have email and web notifications', async function () {
|
||||
await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, {
|
||||
newVideoFromSubscription: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL
|
||||
newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
|
||||
}))
|
||||
|
||||
{
|
||||
const res = await getMyUserInformation(servers[0].url, userAccessToken)
|
||||
const info = res.body as User
|
||||
expect(info.notificationSettings.newVideoFromSubscription).to.equal(UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL)
|
||||
expect(info.notificationSettings.newVideoFromSubscription).to.equal(
|
||||
UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
|
||||
)
|
||||
}
|
||||
|
||||
const { name, uuid } = await uploadVideoByLocalAccount(servers)
|
||||
|
|
|
@ -501,10 +501,6 @@ describe('Test users', function () {
|
|||
accessTokenUser = await userLogin(server, user)
|
||||
})
|
||||
|
||||
it('Should not be able to delete a user by a moderator', async function () {
|
||||
await removeUser(server.url, 2, accessTokenUser, 403)
|
||||
})
|
||||
|
||||
it('Should be able to list video blacklist by a moderator', async function () {
|
||||
await getBlacklistedVideosList(server.url, accessTokenUser)
|
||||
})
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
export enum UserNotificationSettingValue {
|
||||
NONE = 1,
|
||||
WEB_NOTIFICATION = 2,
|
||||
EMAIL = 3,
|
||||
WEB_NOTIFICATION_AND_EMAIL = 4
|
||||
NONE = 0,
|
||||
WEB = 1 << 0,
|
||||
EMAIL = 1 << 1
|
||||
}
|
||||
|
||||
export interface UserNotificationSetting {
|
||||
|
|
|
@ -2,11 +2,15 @@ export enum UserNotificationType {
|
|||
NEW_VIDEO_FROM_SUBSCRIPTION = 1,
|
||||
NEW_COMMENT_ON_MY_VIDEO = 2,
|
||||
NEW_VIDEO_ABUSE_FOR_MODERATORS = 3,
|
||||
|
||||
BLACKLIST_ON_MY_VIDEO = 4,
|
||||
UNBLACKLIST_ON_MY_VIDEO = 5,
|
||||
|
||||
MY_VIDEO_PUBLISHED = 6,
|
||||
|
||||
MY_VIDEO_IMPORT_SUCCESS = 7,
|
||||
MY_VIDEO_IMPORT_ERROR = 8,
|
||||
|
||||
NEW_USER_REGISTRATION = 9,
|
||||
NEW_FOLLOW = 10,
|
||||
COMMENT_MENTION = 11
|
||||
|
|
|
@ -29,6 +29,7 @@ function getJobsListPaginationAndSort (url: string, accessToken: string, state:
|
|||
}
|
||||
|
||||
async function waitJobs (serversArg: ServerInfo[] | ServerInfo) {
|
||||
const pendingJobWait = process.env.NODE_PENDING_JOB_WAIT ? parseInt(process.env.NODE_PENDING_JOB_WAIT, 10) : 2000
|
||||
let servers: ServerInfo[]
|
||||
|
||||
if (Array.isArray(serversArg) === false) servers = [ serversArg as ServerInfo ]
|
||||
|
@ -62,7 +63,7 @@ async function waitJobs (serversArg: ServerInfo[] | ServerInfo) {
|
|||
|
||||
// Retry, in case of new jobs were created
|
||||
if (pendingRequests === false) {
|
||||
await wait(2000)
|
||||
await wait(pendingJobWait)
|
||||
await Promise.all(tasksBuilder())
|
||||
}
|
||||
|
||||
|
|
|
@ -54,6 +54,16 @@ function markAsReadNotifications (url: string, token: string, ids: number[], sta
|
|||
statusCodeExpected
|
||||
})
|
||||
}
|
||||
function markAsReadAllNotifications (url: string, token: string, statusCodeExpected = 204) {
|
||||
const path = '/api/v1/users/me/notifications/read-all'
|
||||
|
||||
return makePostBodyRequest({
|
||||
url,
|
||||
path,
|
||||
token,
|
||||
statusCodeExpected
|
||||
})
|
||||
}
|
||||
|
||||
async function getLastNotification (serverUrl: string, accessToken: string) {
|
||||
const res = await getUserNotifications(serverUrl, accessToken, 0, 1, undefined, '-createdAt')
|
||||
|
@ -409,6 +419,7 @@ export {
|
|||
CheckerBaseParams,
|
||||
CheckerType,
|
||||
checkNotification,
|
||||
markAsReadAllNotifications,
|
||||
checkMyVideoImportIsFinished,
|
||||
checkUserRegistered,
|
||||
checkVideoIsPublished,
|
||||
|
|
Loading…
Reference in New Issue