aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/shared/users
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app/shared/users')
-rw-r--r--client/src/app/shared/users/index.ts1
-rw-r--r--client/src/app/shared/users/user-notification.model.ts153
-rw-r--r--client/src/app/shared/users/user-notification.service.ts110
-rw-r--r--client/src/app/shared/users/user-notifications.component.html61
-rw-r--r--client/src/app/shared/users/user-notifications.component.scss30
-rw-r--r--client/src/app/shared/users/user-notifications.component.ts82
-rw-r--r--client/src/app/shared/users/user.model.ts6
7 files changed, 442 insertions, 1 deletions
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 @@
1export * from './user.model' 1export * from './user.model'
2export * from './user.service' 2export * from './user.service'
3export * 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 @@
1import { UserNotification as UserNotificationServer, UserNotificationType, VideoInfo } from '../../../../../shared'
2
3export class UserNotification implements UserNotificationServer {
4 id: number
5 type: UserNotificationType
6 read: boolean
7
8 video?: VideoInfo & {
9 channel: {
10 id: number
11 displayName: string
12 }
13 }
14
15 videoImport?: {
16 id: number
17 video?: VideoInfo
18 torrentName?: string
19 magnetUri?: string
20 targetUrl?: string
21 }
22
23 comment?: {
24 id: number
25 threadId: number
26 account: {
27 id: number
28 displayName: string
29 }
30 video: VideoInfo
31 }
32
33 videoAbuse?: {
34 id: number
35 video: VideoInfo
36 }
37
38 videoBlacklist?: {
39 id: number
40 video: VideoInfo
41 }
42
43 account?: {
44 id: number
45 displayName: string
46 name: string
47 }
48
49 actorFollow?: {
50 id: number
51 follower: {
52 name: string
53 displayName: string
54 }
55 following: {
56 type: 'account' | 'channel'
57 name: string
58 displayName: string
59 }
60 }
61
62 createdAt: string
63 updatedAt: string
64
65 // Additional fields
66 videoUrl?: string
67 commentUrl?: any[]
68 videoAbuseUrl?: string
69 accountUrl?: string
70 videoImportIdentifier?: string
71 videoImportUrl?: string
72
73 constructor (hash: UserNotificationServer) {
74 this.id = hash.id
75 this.type = hash.type
76 this.read = hash.read
77
78 this.video = hash.video
79 this.videoImport = hash.videoImport
80 this.comment = hash.comment
81 this.videoAbuse = hash.videoAbuse
82 this.videoBlacklist = hash.videoBlacklist
83 this.account = hash.account
84 this.actorFollow = hash.actorFollow
85
86 this.createdAt = hash.createdAt
87 this.updatedAt = hash.updatedAt
88
89 switch (this.type) {
90 case UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION:
91 this.videoUrl = this.buildVideoUrl(this.video)
92 break
93
94 case UserNotificationType.UNBLACKLIST_ON_MY_VIDEO:
95 this.videoUrl = this.buildVideoUrl(this.video)
96 break
97
98 case UserNotificationType.NEW_COMMENT_ON_MY_VIDEO:
99 case UserNotificationType.COMMENT_MENTION:
100 this.commentUrl = [ this.buildVideoUrl(this.comment.video), { threadId: this.comment.threadId } ]
101 break
102
103 case UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS:
104 this.videoAbuseUrl = '/admin/moderation/video-abuses/list'
105 this.videoUrl = this.buildVideoUrl(this.videoAbuse.video)
106 break
107
108 case UserNotificationType.BLACKLIST_ON_MY_VIDEO:
109 this.videoUrl = this.buildVideoUrl(this.videoBlacklist.video)
110 break
111
112 case UserNotificationType.MY_VIDEO_PUBLISHED:
113 this.videoUrl = this.buildVideoUrl(this.video)
114 break
115
116 case UserNotificationType.MY_VIDEO_IMPORT_SUCCESS:
117 this.videoImportUrl = this.buildVideoImportUrl()
118 this.videoImportIdentifier = this.buildVideoImportIdentifier(this.videoImport)
119 this.videoUrl = this.buildVideoUrl(this.videoImport.video)
120 break
121
122 case UserNotificationType.MY_VIDEO_IMPORT_ERROR:
123 this.videoImportUrl = this.buildVideoImportUrl()
124 this.videoImportIdentifier = this.buildVideoImportIdentifier(this.videoImport)
125 break
126
127 case UserNotificationType.NEW_USER_REGISTRATION:
128 this.accountUrl = this.buildAccountUrl(this.account)
129 break
130
131 case UserNotificationType.NEW_FOLLOW:
132 this.accountUrl = this.buildAccountUrl(this.actorFollow.follower)
133 break
134 }
135 }
136
137 private buildVideoUrl (video: { uuid: string }) {
138 return '/videos/watch/' + video.uuid
139 }
140
141 private buildAccountUrl (account: { name: string }) {
142 return '/accounts/' + account.name
143 }
144
145 private buildVideoImportUrl () {
146 return '/my-account/video-imports'
147 }
148
149 private buildVideoImportIdentifier (videoImport: { targetUrl?: string, magnetUri?: string, torrentName?: string }) {
150 return videoImport.targetUrl || videoImport.magnetUri || videoImport.torrentName
151 }
152
153}
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 @@
1import { Injectable } from '@angular/core'
2import { HttpClient, HttpParams } from '@angular/common/http'
3import { RestExtractor, RestService } from '@app/shared/rest'
4import { catchError, map, tap } from 'rxjs/operators'
5import { environment } from '../../../environments/environment'
6import { ResultList, UserNotification as UserNotificationServer, UserNotificationSetting } from '../../../../../shared'
7import { UserNotification } from '@app/shared/users/user-notification.model'
8import { Subject } from 'rxjs'
9import * as io from 'socket.io-client'
10import { AuthService } from '@app/core'
11import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
12import { User } from '@app/shared'
13
14@Injectable()
15export class UserNotificationService {
16 static BASE_NOTIFICATIONS_URL = environment.apiUrl + '/api/v1/users/me/notifications'
17 static BASE_NOTIFICATION_SETTINGS = environment.apiUrl + '/api/v1/users/me/notification-settings'
18
19 private notificationSubject = new Subject<{ type: 'new' | 'read' | 'read-all', notification?: UserNotification }>()
20
21 private socket: SocketIOClient.Socket
22
23 constructor (
24 private auth: AuthService,
25 private authHttp: HttpClient,
26 private restExtractor: RestExtractor,
27 private restService: RestService
28 ) {}
29
30 listMyNotifications (pagination: ComponentPagination, unread?: boolean, ignoreLoadingBar = false) {
31 let params = new HttpParams()
32 params = this.restService.addRestGetParams(params, this.restService.componentPaginationToRestPagination(pagination))
33
34 if (unread) params = params.append('unread', `${unread}`)
35
36 const headers = ignoreLoadingBar ? { ignoreLoadingBar: '' } : undefined
37
38 return this.authHttp.get<ResultList<UserNotification>>(UserNotificationService.BASE_NOTIFICATIONS_URL, { params, headers })
39 .pipe(
40 map(res => this.restExtractor.convertResultListDateToHuman(res)),
41 map(res => this.restExtractor.applyToResultListData(res, this.formatNotification.bind(this))),
42 catchError(err => this.restExtractor.handleError(err))
43 )
44 }
45
46 countUnreadNotifications () {
47 return this.listMyNotifications({ currentPage: 1, itemsPerPage: 0 }, true)
48 .pipe(map(n => n.total))
49 }
50
51 getMyNotificationsSocket () {
52 const socket = this.getSocket()
53
54 socket.on('new-notification', (n: UserNotificationServer) => {
55 this.notificationSubject.next({ type: 'new', notification: new UserNotification(n) })
56 })
57
58 return this.notificationSubject.asObservable()
59 }
60
61 markAsRead (notification: UserNotification) {
62 const url = UserNotificationService.BASE_NOTIFICATIONS_URL + '/read'
63
64 const body = { ids: [ notification.id ] }
65 const headers = { ignoreLoadingBar: '' }
66
67 return this.authHttp.post(url, body, { headers })
68 .pipe(
69 map(this.restExtractor.extractDataBool),
70 tap(() => this.notificationSubject.next({ type: 'read' })),
71 catchError(res => this.restExtractor.handleError(res))
72 )
73 }
74
75 markAllAsRead () {
76 const url = UserNotificationService.BASE_NOTIFICATIONS_URL + '/read-all'
77 const headers = { ignoreLoadingBar: '' }
78
79 return this.authHttp.post(url, {}, { headers })
80 .pipe(
81 map(this.restExtractor.extractDataBool),
82 tap(() => this.notificationSubject.next({ type: 'read-all' })),
83 catchError(res => this.restExtractor.handleError(res))
84 )
85 }
86
87 updateNotificationSettings (user: User, settings: UserNotificationSetting) {
88 const url = UserNotificationService.BASE_NOTIFICATION_SETTINGS
89
90 return this.authHttp.put(url, settings)
91 .pipe(
92 map(this.restExtractor.extractDataBool),
93 catchError(res => this.restExtractor.handleError(res))
94 )
95 }
96
97 private getSocket () {
98 if (this.socket) return this.socket
99
100 this.socket = io(environment.apiUrl + '/user-notifications', {
101 query: { accessToken: this.auth.getAccessToken() }
102 })
103
104 return this.socket
105 }
106
107 private formatNotification (notification: UserNotificationServer) {
108 return new UserNotification(notification)
109 }
110}
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 @@
1<div *ngIf="componentPagination.totalItems === 0" class="no-notification" i18n>You don't have notifications.</div>
2
3<div class="notifications" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()">
4 <div *ngFor="let notification of notifications" class="notification" [ngClass]="{ unread: !notification.read }">
5
6 <div [ngSwitch]="notification.type">
7 <ng-container i18n *ngSwitchCase="UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION">
8 {{ notification.video.channel.displayName }} published a <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">new video</a>
9 </ng-container>
10
11 <ng-container i18n *ngSwitchCase="UserNotificationType.UNBLACKLIST_ON_MY_VIDEO">
12 Your video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.video.name }}</a> has been unblacklisted
13 </ng-container>
14
15 <ng-container i18n *ngSwitchCase="UserNotificationType.BLACKLIST_ON_MY_VIDEO">
16 Your video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.videoBlacklist.video.name }}</a> has been blacklisted
17 </ng-container>
18
19 <ng-container i18n *ngSwitchCase="UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS">
20 <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>
21 </ng-container>
22
23 <ng-container i18n *ngSwitchCase="UserNotificationType.NEW_COMMENT_ON_MY_VIDEO">
24 {{ notification.comment.account.displayName }} commented your video <a (click)="markAsRead(notification)" [routerLink]="notification.commentUrl">{{ notification.comment.video.name }}</a>
25 </ng-container>
26
27 <ng-container i18n *ngSwitchCase="UserNotificationType.MY_VIDEO_PUBLISHED">
28 Your video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.video.name }}</a> has been published
29 </ng-container>
30
31 <ng-container i18n *ngSwitchCase="UserNotificationType.MY_VIDEO_IMPORT_SUCCESS">
32 <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">Your video import</a> {{ notification.videoImportIdentifier }} succeeded
33 </ng-container>
34
35 <ng-container i18n *ngSwitchCase="UserNotificationType.MY_VIDEO_IMPORT_ERROR">
36 <a (click)="markAsRead(notification)" [routerLink]="notification.videoImportUrl">Your video import</a> {{ notification.videoImportIdentifier }} failed
37 </ng-container>
38
39 <ng-container i18n *ngSwitchCase="UserNotificationType.NEW_USER_REGISTRATION">
40 User <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.account.name }} registered</a> on your instance
41 </ng-container>
42
43 <ng-container i18n *ngSwitchCase="UserNotificationType.NEW_FOLLOW">
44 <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.actorFollow.follower.displayName }}</a> is following
45
46 <ng-container *ngIf="notification.actorFollow.following.type === 'channel'">
47 your channel {{ notification.actorFollow.following.displayName }}
48 </ng-container>
49 <ng-container *ngIf="notification.actorFollow.following.type === 'account'">your account</ng-container>
50 </ng-container>
51
52 <ng-container i18n *ngSwitchCase="UserNotificationType.COMMENT_MENTION">
53 {{ notification.comment.account.displayName }} mentioned you on <a (click)="markAsRead(notification)" [routerLink]="notification.commentUrl">video {{ notification.comment.video.name }}</a>
54 </ng-container>
55 </div>
56
57 <div i18n title="Mark as read" class="mark-as-read">
58 <div class="glyphicon glyphicon-ok" (click)="markAsRead(notification)"></div>
59 </div>
60 </div>
61</div>
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 @@
1.notification {
2 display: flex;
3 justify-content: space-between;
4 align-items: center;
5 font-size: inherit;
6 padding: 15px 10px;
7 border-bottom: 1px solid rgba(0, 0, 0, 0.10);
8
9 .mark-as-read {
10 min-width: 35px;
11
12 .glyphicon {
13 display: none;
14 cursor: pointer;
15 color: rgba(20, 20, 20, 0.5)
16 }
17 }
18
19 &.unread {
20 background-color: rgba(0, 0, 0, 0.05);
21
22 &:hover .mark-as-read .glyphicon {
23 display: block;
24
25 &:hover {
26 color: rgba(20, 20, 20, 0.8);
27 }
28 }
29 }
30}
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 @@
1import { Component, Input, OnInit } from '@angular/core'
2import { UserNotificationService } from '@app/shared/users/user-notification.service'
3import { UserNotificationType } from '../../../../../shared'
4import { ComponentPagination, hasMoreItems } from '@app/shared/rest/component-pagination.model'
5import { Notifier } from '@app/core'
6import { UserNotification } from '@app/shared/users/user-notification.model'
7
8@Component({
9 selector: 'my-user-notifications',
10 templateUrl: 'user-notifications.component.html',
11 styleUrls: [ 'user-notifications.component.scss' ]
12})
13export class UserNotificationsComponent implements OnInit {
14 @Input() ignoreLoadingBar = false
15 @Input() infiniteScroll = true
16
17 notifications: UserNotification[] = []
18
19 // So we can access it in the template
20 UserNotificationType = UserNotificationType
21
22 componentPagination: ComponentPagination = {
23 currentPage: 1,
24 itemsPerPage: 10,
25 totalItems: null
26 }
27
28 constructor (
29 private userNotificationService: UserNotificationService,
30 private notifier: Notifier
31 ) { }
32
33 ngOnInit () {
34 this.loadMoreNotifications()
35 }
36
37 loadMoreNotifications () {
38 this.userNotificationService.listMyNotifications(this.componentPagination, undefined, this.ignoreLoadingBar)
39 .subscribe(
40 result => {
41 this.notifications = this.notifications.concat(result.data)
42 this.componentPagination.totalItems = result.total
43 },
44
45 err => this.notifier.error(err.message)
46 )
47 }
48
49 onNearOfBottom () {
50 if (this.infiniteScroll === false) return
51
52 this.componentPagination.currentPage++
53
54 if (hasMoreItems(this.componentPagination)) {
55 this.loadMoreNotifications()
56 }
57 }
58
59 markAsRead (notification: UserNotification) {
60 this.userNotificationService.markAsRead(notification)
61 .subscribe(
62 () => {
63 notification.read = true
64 },
65
66 err => this.notifier.error(err.message)
67 )
68 }
69
70 markAllAsRead () {
71 this.userNotificationService.markAllAsRead()
72 .subscribe(
73 () => {
74 for (const notification of this.notifications) {
75 notification.read = true
76 }
77 },
78
79 err => this.notifier.error(err.message)
80 )
81 }
82}
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 @@
1import { hasUserRight, User as UserServerModel, UserRight, UserRole, VideoChannel } from '../../../../../shared' 1import { hasUserRight, User as UserServerModel, UserNotificationSetting, UserRight, UserRole, VideoChannel } from '../../../../../shared'
2import { NSFWPolicyType } from '../../../../../shared/models/videos/nsfw-policy.type' 2import { NSFWPolicyType } from '../../../../../shared/models/videos/nsfw-policy.type'
3import { Account } from '@app/shared/account/account.model' 3import { Account } from '@app/shared/account/account.model'
4import { Avatar } from '../../../../../shared/models/avatars/avatar.model' 4import { Avatar } from '../../../../../shared/models/avatars/avatar.model'
@@ -24,6 +24,8 @@ export class User implements UserServerModel {
24 blocked: boolean 24 blocked: boolean
25 blockedReason?: string 25 blockedReason?: string
26 26
27 notificationSettings?: UserNotificationSetting
28
27 constructor (hash: Partial<UserServerModel>) { 29 constructor (hash: Partial<UserServerModel>) {
28 this.id = hash.id 30 this.id = hash.id
29 this.username = hash.username 31 this.username = hash.username
@@ -41,6 +43,8 @@ export class User implements UserServerModel {
41 this.blocked = hash.blocked 43 this.blocked = hash.blocked
42 this.blockedReason = hash.blockedReason 44 this.blockedReason = hash.blockedReason
43 45
46 this.notificationSettings = hash.notificationSettings
47
44 if (hash.account !== undefined) { 48 if (hash.account !== undefined) {
45 this.account = new Account(hash.account) 49 this.account = new Account(hash.account)
46 } 50 }