aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/shared/users
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2020-06-23 14:10:17 +0200
committerChocobozzz <chocobozzz@cpy.re>2020-06-23 16:00:49 +0200
commit67ed6552b831df66713bac9e672738796128d33f (patch)
tree59c97d41e0b49d75a90aa3de987968ab9b1ff447 /client/src/app/shared/users
parent0c4bacbff53bc732f5a2677d62a6ead7752e2405 (diff)
downloadPeerTube-67ed6552b831df66713bac9e672738796128d33f.tar.gz
PeerTube-67ed6552b831df66713bac9e672738796128d33f.tar.zst
PeerTube-67ed6552b831df66713bac9e672738796128d33f.zip
Reorganize client shared modules
Diffstat (limited to 'client/src/app/shared/users')
-rw-r--r--client/src/app/shared/users/index.ts3
-rw-r--r--client/src/app/shared/users/user-history.service.ts45
-rw-r--r--client/src/app/shared/users/user-notification.model.ts184
-rw-r--r--client/src/app/shared/users/user-notification.service.ts86
-rw-r--r--client/src/app/shared/users/user-notifications.component.html166
-rw-r--r--client/src/app/shared/users/user-notifications.component.scss53
-rw-r--r--client/src/app/shared/users/user-notifications.component.ts101
-rw-r--r--client/src/app/shared/users/user.model.ts150
-rw-r--r--client/src/app/shared/users/user.service.ts367
9 files changed, 0 insertions, 1155 deletions
diff --git a/client/src/app/shared/users/index.ts b/client/src/app/shared/users/index.ts
deleted file mode 100644
index ebd715fb1..000000000
--- a/client/src/app/shared/users/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
1export * from './user.model'
2export * from './user.service'
3export * from './user-notifications.component'
diff --git a/client/src/app/shared/users/user-history.service.ts b/client/src/app/shared/users/user-history.service.ts
deleted file mode 100644
index b358cdf20..000000000
--- a/client/src/app/shared/users/user-history.service.ts
+++ /dev/null
@@ -1,45 +0,0 @@
1import { HttpClient, HttpParams } from '@angular/common/http'
2import { Injectable } from '@angular/core'
3import { environment } from '../../../environments/environment'
4import { RestExtractor } from '../rest/rest-extractor.service'
5import { RestService } from '../rest/rest.service'
6import { Video } from '../video/video.model'
7import { catchError, map, switchMap } from 'rxjs/operators'
8import { ComponentPaginationLight } from '@app/shared/rest/component-pagination.model'
9import { VideoService } from '@app/shared/video/video.service'
10import { ResultList } from '../../../../../shared'
11
12@Injectable()
13export class UserHistoryService {
14 static BASE_USER_VIDEOS_HISTORY_URL = environment.apiUrl + '/api/v1/users/me/history/videos'
15
16 constructor (
17 private authHttp: HttpClient,
18 private restExtractor: RestExtractor,
19 private restService: RestService,
20 private videoService: VideoService
21 ) {}
22
23 getUserVideosHistory (historyPagination: ComponentPaginationLight) {
24 const pagination = this.restService.componentPaginationToRestPagination(historyPagination)
25
26 let params = new HttpParams()
27 params = this.restService.addRestGetParams(params, pagination)
28
29 return this.authHttp
30 .get<ResultList<Video>>(UserHistoryService.BASE_USER_VIDEOS_HISTORY_URL, { params })
31 .pipe(
32 switchMap(res => this.videoService.extractVideos(res)),
33 catchError(err => this.restExtractor.handleError(err))
34 )
35 }
36
37 deleteUserVideosHistory () {
38 return this.authHttp
39 .post(UserHistoryService.BASE_USER_VIDEOS_HISTORY_URL + '/remove', {})
40 .pipe(
41 map(() => this.restExtractor.extractDataBool()),
42 catchError(err => this.restExtractor.handleError(err))
43 )
44 }
45}
diff --git a/client/src/app/shared/users/user-notification.model.ts b/client/src/app/shared/users/user-notification.model.ts
deleted file mode 100644
index 7b8368d87..000000000
--- a/client/src/app/shared/users/user-notification.model.ts
+++ /dev/null
@@ -1,184 +0,0 @@
1import { ActorInfo, FollowState, UserNotification as UserNotificationServer, UserNotificationType, VideoInfo, Avatar } from '../../../../../shared'
2import { Actor } from '@app/shared/actor/actor.model'
3
4export class UserNotification implements UserNotificationServer {
5 id: number
6 type: UserNotificationType
7 read: boolean
8
9 video?: VideoInfo & {
10 channel: ActorInfo & { avatarUrl?: string }
11 }
12
13 videoImport?: {
14 id: number
15 video?: VideoInfo
16 torrentName?: string
17 magnetUri?: string
18 targetUrl?: string
19 }
20
21 comment?: {
22 id: number
23 threadId: number
24 account: ActorInfo & { avatarUrl?: string }
25 video: VideoInfo
26 }
27
28 videoAbuse?: {
29 id: number
30 video: VideoInfo
31 }
32
33 videoBlacklist?: {
34 id: number
35 video: VideoInfo
36 }
37
38 account?: ActorInfo & { avatarUrl?: string }
39
40 actorFollow?: {
41 id: number
42 state: FollowState
43 follower: ActorInfo & { avatarUrl?: string }
44 following: {
45 type: 'account' | 'channel' | 'instance'
46 name: string
47 displayName: string
48 host: string
49 }
50 }
51
52 createdAt: string
53 updatedAt: string
54
55 // Additional fields
56 videoUrl?: string
57 commentUrl?: any[]
58 videoAbuseUrl?: string
59 videoAutoBlacklistUrl?: string
60 accountUrl?: string
61 videoImportIdentifier?: string
62 videoImportUrl?: string
63 instanceFollowUrl?: string
64
65 constructor (hash: UserNotificationServer) {
66 this.id = hash.id
67 this.type = hash.type
68 this.read = hash.read
69
70 // We assume that some fields exist
71 // To prevent a notification popup crash in case of bug, wrap it inside a try/catch
72 try {
73 this.video = hash.video
74 if (this.video) this.setAvatarUrl(this.video.channel)
75
76 this.videoImport = hash.videoImport
77
78 this.comment = hash.comment
79 if (this.comment) this.setAvatarUrl(this.comment.account)
80
81 this.videoAbuse = hash.videoAbuse
82
83 this.videoBlacklist = hash.videoBlacklist
84
85 this.account = hash.account
86 if (this.account) this.setAvatarUrl(this.account)
87
88 this.actorFollow = hash.actorFollow
89 if (this.actorFollow) this.setAvatarUrl(this.actorFollow.follower)
90
91 this.createdAt = hash.createdAt
92 this.updatedAt = hash.updatedAt
93
94 switch (this.type) {
95 case UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION:
96 this.videoUrl = this.buildVideoUrl(this.video)
97 break
98
99 case UserNotificationType.UNBLACKLIST_ON_MY_VIDEO:
100 this.videoUrl = this.buildVideoUrl(this.video)
101 break
102
103 case UserNotificationType.NEW_COMMENT_ON_MY_VIDEO:
104 case UserNotificationType.COMMENT_MENTION:
105 if (!this.comment) break
106 this.accountUrl = this.buildAccountUrl(this.comment.account)
107 this.commentUrl = [ this.buildVideoUrl(this.comment.video), { threadId: this.comment.threadId } ]
108 break
109
110 case UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS:
111 this.videoAbuseUrl = '/admin/moderation/video-abuses/list'
112 this.videoUrl = this.buildVideoUrl(this.videoAbuse.video)
113 break
114
115 case UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS:
116 this.videoAutoBlacklistUrl = '/admin/moderation/video-auto-blacklist/list'
117 // Backward compatibility where we did not assign videoBlacklist to this type of notification before
118 if (!this.videoBlacklist) this.videoBlacklist = { id: null, video: this.video }
119
120 this.videoUrl = this.buildVideoUrl(this.videoBlacklist.video)
121 break
122
123 case UserNotificationType.BLACKLIST_ON_MY_VIDEO:
124 this.videoUrl = this.buildVideoUrl(this.videoBlacklist.video)
125 break
126
127 case UserNotificationType.MY_VIDEO_PUBLISHED:
128 this.videoUrl = this.buildVideoUrl(this.video)
129 break
130
131 case UserNotificationType.MY_VIDEO_IMPORT_SUCCESS:
132 this.videoImportUrl = this.buildVideoImportUrl()
133 this.videoImportIdentifier = this.buildVideoImportIdentifier(this.videoImport)
134
135 if (this.videoImport.video) this.videoUrl = this.buildVideoUrl(this.videoImport.video)
136 break
137
138 case UserNotificationType.MY_VIDEO_IMPORT_ERROR:
139 this.videoImportUrl = this.buildVideoImportUrl()
140 this.videoImportIdentifier = this.buildVideoImportIdentifier(this.videoImport)
141 break
142
143 case UserNotificationType.NEW_USER_REGISTRATION:
144 this.accountUrl = this.buildAccountUrl(this.account)
145 break
146
147 case UserNotificationType.NEW_FOLLOW:
148 this.accountUrl = this.buildAccountUrl(this.actorFollow.follower)
149 break
150
151 case UserNotificationType.NEW_INSTANCE_FOLLOWER:
152 this.instanceFollowUrl = '/admin/follows/followers-list'
153 break
154
155 case UserNotificationType.AUTO_INSTANCE_FOLLOWING:
156 this.instanceFollowUrl = '/admin/follows/following-list'
157 break
158 }
159 } catch (err) {
160 this.type = null
161 console.error(err)
162 }
163 }
164
165 private buildVideoUrl (video: { uuid: string }) {
166 return '/videos/watch/' + video.uuid
167 }
168
169 private buildAccountUrl (account: { name: string, host: string }) {
170 return '/accounts/' + Actor.CREATE_BY_STRING(account.name, account.host)
171 }
172
173 private buildVideoImportUrl () {
174 return '/my-account/video-imports'
175 }
176
177 private buildVideoImportIdentifier (videoImport: { targetUrl?: string, magnetUri?: string, torrentName?: string }) {
178 return videoImport.targetUrl || videoImport.magnetUri || videoImport.torrentName
179 }
180
181 private setAvatarUrl (actor: { avatarUrl?: string, avatar?: Avatar }) {
182 actor.avatarUrl = Actor.GET_ACTOR_AVATAR_URL(actor)
183 }
184}
diff --git a/client/src/app/shared/users/user-notification.service.ts b/client/src/app/shared/users/user-notification.service.ts
deleted file mode 100644
index e525a1d58..000000000
--- a/client/src/app/shared/users/user-notification.service.ts
+++ /dev/null
@@ -1,86 +0,0 @@
1import { Injectable } from '@angular/core'
2import { HttpClient, HttpParams } from '@angular/common/http'
3import { RestExtractor, RestService } from '../rest'
4import { catchError, map, tap } from 'rxjs/operators'
5import { environment } from '../../../environments/environment'
6import { ResultList, UserNotification as UserNotificationServer, UserNotificationSetting } from '../../../../../shared'
7import { UserNotification } from './user-notification.model'
8import { AuthService } from '../../core'
9import { ComponentPaginationLight } from '../rest/component-pagination.model'
10import { User } from '../users/user.model'
11import { UserNotificationSocket } from '@app/core/notification/user-notification-socket.service'
12
13@Injectable()
14export class UserNotificationService {
15 static BASE_NOTIFICATIONS_URL = environment.apiUrl + '/api/v1/users/me/notifications'
16 static BASE_NOTIFICATION_SETTINGS = environment.apiUrl + '/api/v1/users/me/notification-settings'
17
18 constructor (
19 private auth: AuthService,
20 private authHttp: HttpClient,
21 private restExtractor: RestExtractor,
22 private restService: RestService,
23 private userNotificationSocket: UserNotificationSocket
24 ) {}
25
26 listMyNotifications (pagination: ComponentPaginationLight, unread?: boolean, ignoreLoadingBar = false) {
27 let params = new HttpParams()
28 params = this.restService.addRestGetParams(params, this.restService.componentPaginationToRestPagination(pagination))
29
30 if (unread) params = params.append('unread', `${unread}`)
31
32 const headers = ignoreLoadingBar ? { ignoreLoadingBar: '' } : undefined
33
34 return this.authHttp.get<ResultList<UserNotification>>(UserNotificationService.BASE_NOTIFICATIONS_URL, { params, headers })
35 .pipe(
36 map(res => this.restExtractor.convertResultListDateToHuman(res)),
37 map(res => this.restExtractor.applyToResultListData(res, this.formatNotification.bind(this))),
38 catchError(err => this.restExtractor.handleError(err))
39 )
40 }
41
42 countUnreadNotifications () {
43 return this.listMyNotifications({ currentPage: 1, itemsPerPage: 0 }, true)
44 .pipe(map(n => n.total))
45 }
46
47 markAsRead (notification: UserNotification) {
48 const url = UserNotificationService.BASE_NOTIFICATIONS_URL + '/read'
49
50 const body = { ids: [ notification.id ] }
51 const headers = { ignoreLoadingBar: '' }
52
53 return this.authHttp.post(url, body, { headers })
54 .pipe(
55 map(this.restExtractor.extractDataBool),
56 tap(() => this.userNotificationSocket.dispatch('read')),
57 catchError(res => this.restExtractor.handleError(res))
58 )
59 }
60
61 markAllAsRead () {
62 const url = UserNotificationService.BASE_NOTIFICATIONS_URL + '/read-all'
63 const headers = { ignoreLoadingBar: '' }
64
65 return this.authHttp.post(url, {}, { headers })
66 .pipe(
67 map(this.restExtractor.extractDataBool),
68 tap(() => this.userNotificationSocket.dispatch('read-all')),
69 catchError(res => this.restExtractor.handleError(res))
70 )
71 }
72
73 updateNotificationSettings (user: User, settings: UserNotificationSetting) {
74 const url = UserNotificationService.BASE_NOTIFICATION_SETTINGS
75
76 return this.authHttp.put(url, settings)
77 .pipe(
78 map(this.restExtractor.extractDataBool),
79 catchError(res => this.restExtractor.handleError(res))
80 )
81 }
82
83 private formatNotification (notification: UserNotificationServer) {
84 return new UserNotification(notification)
85 }
86}
diff --git a/client/src/app/shared/users/user-notifications.component.html b/client/src/app/shared/users/user-notifications.component.html
deleted file mode 100644
index 08771110d..000000000
--- a/client/src/app/shared/users/user-notifications.component.html
+++ /dev/null
@@ -1,166 +0,0 @@
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()" [dataObservable]="onDataSubject.asObservable()">
4 <div *ngFor="let notification of notifications" class="notification" [ngClass]="{ unread: !notification.read }" (click)="markAsRead(notification)">
5
6 <ng-container [ngSwitch]="notification.type">
7 <ng-container *ngSwitchCase="UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION">
8 <ng-container *ngIf="notification.video; then hasVideo; else noVideo"></ng-container>
9
10 <ng-template #hasVideo>
11 <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">
12 <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.video.channel.avatarUrl" />
13 </a>
14
15 <div class="message" i18n>
16 {{ notification.video.channel.displayName }} published a new video: <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.video.name }}</a>
17 </div>
18 </ng-template>
19
20 <ng-template #noVideo>
21 <my-global-icon iconName="alert" aria-hidden="true"></my-global-icon>
22
23 <div class="message" i18n>
24 The notification concerns a video now unavailable
25 </div>
26 </ng-template>
27 </ng-container>
28
29 <ng-container *ngSwitchCase="UserNotificationType.UNBLACKLIST_ON_MY_VIDEO">
30 <my-global-icon iconName="undo" aria-hidden="true"></my-global-icon>
31
32 <div class="message" i18n>
33 Your video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.video.name }}</a> has been unblocked
34 </div>
35 </ng-container>
36
37 <ng-container *ngSwitchCase="UserNotificationType.BLACKLIST_ON_MY_VIDEO">
38 <my-global-icon iconName="no" aria-hidden="true"></my-global-icon>
39
40 <div class="message" i18n>
41 Your video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.videoBlacklist.video.name }}</a> has been blocked
42 </div>
43 </ng-container>
44
45 <ng-container *ngSwitchCase="UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS">
46 <my-global-icon iconName="alert" aria-hidden="true"></my-global-icon>
47
48 <div class="message" i18n>
49 <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>
50 </div>
51 </ng-container>
52
53 <ng-container *ngSwitchCase="UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS">
54 <my-global-icon iconName="no" aria-hidden="true"></my-global-icon>
55
56 <div class="message" i18n>
57 The recently added video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.videoBlacklist.video.name }}</a> has been <a (click)="markAsRead(notification)" [routerLink]="notification.videoAutoBlacklistUrl">automatically blocked</a>
58 </div>
59 </ng-container>
60
61 <ng-container *ngSwitchCase="UserNotificationType.NEW_COMMENT_ON_MY_VIDEO">
62 <ng-container *ngIf="notification.comment; then hasComment; else noComment"></ng-container>
63
64 <ng-template #hasComment>
65 <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">
66 <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" />
67 </a>
68
69 <div class="message" i18n>
70 <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.comment.account.displayName }}</a> commented your video <a (click)="markAsRead(notification)" [routerLink]="notification.commentUrl">{{ notification.comment.video.name }}</a>
71 </div>
72 </ng-template>
73
74 <ng-template #noComment>
75 <my-global-icon iconName="alert" aria-hidden="true"></my-global-icon>
76
77 <div class="message" i18n>
78 The notification concerns a comment now unavailable
79 </div>
80 </ng-template>
81 </ng-container>
82
83 <ng-container *ngSwitchCase="UserNotificationType.MY_VIDEO_PUBLISHED">
84 <my-global-icon iconName="sparkle" aria-hidden="true"></my-global-icon>
85
86 <div class="message" i18n>
87 Your video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.video.name }}</a> has been published
88 </div>
89 </ng-container>
90
91 <ng-container *ngSwitchCase="UserNotificationType.MY_VIDEO_IMPORT_SUCCESS">
92 <my-global-icon iconName="cloud-download" aria-hidden="true"></my-global-icon>
93
94 <div class="message" i18n>
95 <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl || notification.videoImportUrl">Your video import</a> {{ notification.videoImportIdentifier }} succeeded
96 </div>
97 </ng-container>
98
99 <ng-container *ngSwitchCase="UserNotificationType.MY_VIDEO_IMPORT_ERROR">
100 <my-global-icon iconName="cloud-error" aria-hidden="true"></my-global-icon>
101
102 <div class="message" i18n>
103 <a (click)="markAsRead(notification)" [routerLink]="notification.videoImportUrl">Your video import</a> {{ notification.videoImportIdentifier }} failed
104 </div>
105 </ng-container>
106
107 <ng-container *ngSwitchCase="UserNotificationType.NEW_USER_REGISTRATION">
108 <my-global-icon iconName="user-add" aria-hidden="true"></my-global-icon>
109
110 <div class="message" i18n>
111 User <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.account.name }}</a> registered on your instance
112 </div>
113 </ng-container>
114
115 <ng-container *ngSwitchCase="UserNotificationType.NEW_FOLLOW">
116 <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">
117 <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.actorFollow.follower.avatarUrl" />
118 </a>
119
120 <div class="message" i18n>
121 <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.actorFollow.follower.displayName }}</a> is following
122
123 <ng-container *ngIf="notification.actorFollow.following.type === 'channel'">your channel {{ notification.actorFollow.following.displayName }}</ng-container>
124 <ng-container *ngIf="notification.actorFollow.following.type === 'account'">your account</ng-container>
125 </div>
126 </ng-container>
127
128 <ng-container *ngSwitchCase="UserNotificationType.COMMENT_MENTION">
129 <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">
130 <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" />
131 </a>
132
133 <div class="message" i18n>
134 <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.comment.account.displayName }}</a> mentioned you on <a (click)="markAsRead(notification)" [routerLink]="notification.commentUrl">video {{ notification.comment.video.name }}</a>
135 </div>
136 </ng-container>
137
138 <ng-container *ngSwitchCase="UserNotificationType.NEW_INSTANCE_FOLLOWER">
139 <my-global-icon iconName="users" aria-hidden="true"></my-global-icon>
140
141 <div class="message" i18n>
142 Your instance has <a (click)="markAsRead(notification)" [routerLink]="notification.instanceFollowUrl">a new follower</a> ({{ notification.actorFollow?.follower.host }})
143 <ng-container *ngIf="notification.actorFollow?.state === 'pending'"> awaiting your approval</ng-container>
144 </div>
145 </ng-container>
146
147 <ng-container *ngSwitchCase="UserNotificationType.AUTO_INSTANCE_FOLLOWING">
148 <my-global-icon iconName="users" aria-hidden="true"></my-global-icon>
149
150 <div class="message" i18n>
151 Your instance automatically followed <a (click)="markAsRead(notification)" [routerLink]="notification.instanceFollowUrl">{{ notification.actorFollow.following.host }}</a>
152 </div>
153 </ng-container>
154
155 <ng-container *ngSwitchDefault>
156 <my-global-icon iconName="alert" aria-hidden="true"></my-global-icon>
157
158 <div class="message" i18n>
159 The notification points to a content now unavailable
160 </div>
161 </ng-container>
162 </ng-container>
163
164 <div [title]="notification.createdAt" class="from-date">{{ notification.createdAt | myFromNow }}</div>
165 </div>
166</div>
diff --git a/client/src/app/shared/users/user-notifications.component.scss b/client/src/app/shared/users/user-notifications.component.scss
deleted file mode 100644
index 5166bd559..000000000
--- a/client/src/app/shared/users/user-notifications.component.scss
+++ /dev/null
@@ -1,53 +0,0 @@
1@import '_variables';
2@import '_mixins';
3
4.no-notification {
5 display: flex;
6 justify-content: center;
7 align-items: center;
8 padding: 20px 0;
9}
10
11.notification {
12 display: flex;
13 align-items: center;
14 font-size: inherit;
15 padding: 15px 5px 15px 10px;
16 border-bottom: 1px solid $separator-border-color;
17 word-break: break-word;
18
19 &.unread {
20 background-color: rgba(0, 0, 0, 0.05);
21 }
22
23 my-global-icon {
24 width: 24px;
25 margin-right: 11px;
26 margin-left: 3px;
27
28 @include apply-svg-color(#333);
29 }
30
31 .avatar {
32 @include avatar(30px);
33
34 margin-right: 10px;
35 }
36
37 .message {
38 flex-grow: 1;
39
40 a {
41 font-weight: $font-semibold;
42 }
43 }
44
45 .from-date {
46 font-size: 0.85em;
47 color: pvar(--greyForegroundColor);
48 padding-left: 5px;
49 min-width: 70px;
50 text-align: right;
51 margin-left: auto;
52 }
53}
diff --git a/client/src/app/shared/users/user-notifications.component.ts b/client/src/app/shared/users/user-notifications.component.ts
deleted file mode 100644
index 977dd8925..000000000
--- a/client/src/app/shared/users/user-notifications.component.ts
+++ /dev/null
@@ -1,101 +0,0 @@
1import { Component, EventEmitter, Input, OnInit, Output } 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'
7import { Subject } from 'rxjs'
8
9@Component({
10 selector: 'my-user-notifications',
11 templateUrl: 'user-notifications.component.html',
12 styleUrls: [ 'user-notifications.component.scss' ]
13})
14export class UserNotificationsComponent implements OnInit {
15 @Input() ignoreLoadingBar = false
16 @Input() infiniteScroll = true
17 @Input() itemsPerPage = 20
18 @Input() markAllAsReadSubject: Subject<boolean>
19
20 @Output() notificationsLoaded = new EventEmitter()
21
22 notifications: UserNotification[] = []
23
24 // So we can access it in the template
25 UserNotificationType = UserNotificationType
26
27 componentPagination: ComponentPagination
28
29 onDataSubject = new Subject<any[]>()
30
31 constructor (
32 private userNotificationService: UserNotificationService,
33 private notifier: Notifier
34 ) { }
35
36 ngOnInit () {
37 this.componentPagination = {
38 currentPage: 1,
39 itemsPerPage: this.itemsPerPage, // Reset items per page, because of the @Input() variable
40 totalItems: null
41 }
42
43 this.loadMoreNotifications()
44
45 if (this.markAllAsReadSubject) {
46 this.markAllAsReadSubject.subscribe(() => this.markAllAsRead())
47 }
48 }
49
50 loadMoreNotifications () {
51 this.userNotificationService.listMyNotifications(this.componentPagination, undefined, this.ignoreLoadingBar)
52 .subscribe(
53 result => {
54 this.notifications = this.notifications.concat(result.data)
55 this.componentPagination.totalItems = result.total
56
57 this.notificationsLoaded.emit()
58
59 this.onDataSubject.next(result.data)
60 },
61
62 err => this.notifier.error(err.message)
63 )
64 }
65
66 onNearOfBottom () {
67 if (this.infiniteScroll === false) return
68
69 this.componentPagination.currentPage++
70
71 if (hasMoreItems(this.componentPagination)) {
72 this.loadMoreNotifications()
73 }
74 }
75
76 markAsRead (notification: UserNotification) {
77 if (notification.read) return
78
79 this.userNotificationService.markAsRead(notification)
80 .subscribe(
81 () => {
82 notification.read = true
83 },
84
85 err => this.notifier.error(err.message)
86 )
87 }
88
89 markAllAsRead () {
90 this.userNotificationService.markAllAsRead()
91 .subscribe(
92 () => {
93 for (const notification of this.notifications) {
94 notification.read = true
95 }
96 },
97
98 err => this.notifier.error(err.message)
99 )
100 }
101}
diff --git a/client/src/app/shared/users/user.model.ts b/client/src/app/shared/users/user.model.ts
deleted file mode 100644
index 3348fe75f..000000000
--- a/client/src/app/shared/users/user.model.ts
+++ /dev/null
@@ -1,150 +0,0 @@
1import {
2 hasUserRight,
3 User as UserServerModel,
4 UserNotificationSetting,
5 UserRight,
6 UserRole
7} from '../../../../../shared/models/users'
8import { VideoChannel } from '../../../../../shared/models/videos'
9import { NSFWPolicyType } from '../../../../../shared/models/videos/nsfw-policy.type'
10import { Account } from '@app/shared/account/account.model'
11import { Avatar } from '../../../../../shared/models/avatars/avatar.model'
12import { UserAdminFlag } from '@shared/models/users/user-flag.model'
13
14export class User implements UserServerModel {
15 static KEYS = {
16 ID: 'id',
17 ROLE: 'role',
18 EMAIL: 'email',
19 VIDEOS_HISTORY_ENABLED: 'videos-history-enabled',
20 USERNAME: 'username',
21 NSFW_POLICY: 'nsfw_policy',
22 WEBTORRENT_ENABLED: 'peertube-videojs-' + 'webtorrent_enabled',
23 AUTO_PLAY_VIDEO: 'auto_play_video',
24 SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO: 'auto_play_next_video',
25 AUTO_PLAY_VIDEO_PLAYLIST: 'auto_play_video_playlist',
26 THEME: 'last_active_theme',
27 VIDEO_LANGUAGES: 'video_languages'
28 }
29
30 id: number
31 username: string
32 email: string
33 pendingEmail: string | null
34
35 emailVerified: boolean
36 nsfwPolicy: NSFWPolicyType
37
38 adminFlags?: UserAdminFlag
39
40 autoPlayVideo: boolean
41 autoPlayNextVideo: boolean
42 autoPlayNextVideoPlaylist: boolean
43 webTorrentEnabled: boolean
44 videosHistoryEnabled: boolean
45 videoLanguages: string[]
46
47 role: UserRole
48 roleLabel: string
49
50 videoQuota: number
51 videoQuotaDaily: number
52 videoQuotaUsed?: number
53 videoQuotaUsedDaily?: number
54 videosCount?: number
55 videoAbusesCount?: number
56 videoAbusesAcceptedCount?: number
57 videoAbusesCreatedCount?: number
58 videoCommentsCount?: number
59
60 theme: string
61
62 account: Account
63 notificationSettings?: UserNotificationSetting
64 videoChannels?: VideoChannel[]
65
66 blocked: boolean
67 blockedReason?: string
68
69 noInstanceConfigWarningModal: boolean
70 noWelcomeModal: boolean
71
72 pluginAuth: string | null
73
74 lastLoginDate: Date | null
75
76 createdAt: Date
77
78 constructor (hash: Partial<UserServerModel>) {
79 this.id = hash.id
80 this.username = hash.username
81 this.email = hash.email
82
83 this.role = hash.role
84
85 this.videoChannels = hash.videoChannels
86
87 this.videoQuota = hash.videoQuota
88 this.videoQuotaDaily = hash.videoQuotaDaily
89 this.videoQuotaUsed = hash.videoQuotaUsed
90 this.videoQuotaUsedDaily = hash.videoQuotaUsedDaily
91 this.videosCount = hash.videosCount
92 this.videoAbusesCount = hash.videoAbusesCount
93 this.videoAbusesAcceptedCount = hash.videoAbusesAcceptedCount
94 this.videoAbusesCreatedCount = hash.videoAbusesCreatedCount
95 this.videoCommentsCount = hash.videoCommentsCount
96
97 this.nsfwPolicy = hash.nsfwPolicy
98 this.webTorrentEnabled = hash.webTorrentEnabled
99 this.autoPlayVideo = hash.autoPlayVideo
100 this.autoPlayNextVideo = hash.autoPlayNextVideo
101 this.autoPlayNextVideoPlaylist = hash.autoPlayNextVideoPlaylist
102 this.videosHistoryEnabled = hash.videosHistoryEnabled
103 this.videoLanguages = hash.videoLanguages
104
105 this.theme = hash.theme
106
107 this.adminFlags = hash.adminFlags
108
109 this.blocked = hash.blocked
110 this.blockedReason = hash.blockedReason
111
112 this.noInstanceConfigWarningModal = hash.noInstanceConfigWarningModal
113 this.noWelcomeModal = hash.noWelcomeModal
114
115 this.notificationSettings = hash.notificationSettings
116
117 this.createdAt = hash.createdAt
118
119 this.pluginAuth = hash.pluginAuth
120 this.lastLoginDate = hash.lastLoginDate
121
122 if (hash.account !== undefined) {
123 this.account = new Account(hash.account)
124 }
125 }
126
127 get accountAvatarUrl () {
128 if (!this.account) return ''
129
130 return this.account.avatarUrl
131 }
132
133 hasRight (right: UserRight) {
134 return hasUserRight(this.role, right)
135 }
136
137 patch (obj: UserServerModel) {
138 for (const key of Object.keys(obj)) {
139 this[key] = obj[key]
140 }
141
142 if (obj.account !== undefined) {
143 this.account = new Account(obj.account)
144 }
145 }
146
147 updateAccountAvatar (newAccountAvatar: Avatar) {
148 this.account.updateAvatar(newAccountAvatar)
149 }
150}
diff --git a/client/src/app/shared/users/user.service.ts b/client/src/app/shared/users/user.service.ts
deleted file mode 100644
index de1c8ec94..000000000
--- a/client/src/app/shared/users/user.service.ts
+++ /dev/null
@@ -1,367 +0,0 @@
1import { has } from 'lodash-es'
2import { BytesPipe } from 'ngx-pipes'
3import { SortMeta } from 'primeng/api'
4import { from, Observable, of } from 'rxjs'
5import { catchError, concatMap, first, map, shareReplay, toArray, throttleTime, filter } from 'rxjs/operators'
6import { HttpClient, HttpParams } from '@angular/common/http'
7import { Injectable } from '@angular/core'
8import { AuthService } from '@app/core/auth'
9import { I18n } from '@ngx-translate/i18n-polyfill'
10import { UserRegister } from '@shared/models/users/user-register.model'
11import { NSFWPolicyType } from '@shared/models/videos/nsfw-policy.type'
12import { ResultList, User as UserServerModel, UserCreate, UserRole, UserUpdate, UserUpdateMe, UserVideoQuota } from '../../../../../shared'
13import { Avatar } from '../../../../../shared/models/avatars/avatar.model'
14import { environment } from '../../../environments/environment'
15import { LocalStorageService, SessionStorageService } from '../misc/storage.service'
16import { RestExtractor, RestPagination, RestService } from '../rest'
17import { User } from './user.model'
18
19@Injectable()
20export class UserService {
21 static BASE_USERS_URL = environment.apiUrl + '/api/v1/users/'
22
23 private bytesPipe = new BytesPipe()
24
25 private userCache: { [ id: number ]: Observable<UserServerModel> } = {}
26
27 constructor (
28 private authHttp: HttpClient,
29 private authService: AuthService,
30 private restExtractor: RestExtractor,
31 private restService: RestService,
32 private localStorageService: LocalStorageService,
33 private sessionStorageService: SessionStorageService,
34 private i18n: I18n
35 ) { }
36
37 changePassword (currentPassword: string, newPassword: string) {
38 const url = UserService.BASE_USERS_URL + 'me'
39 const body: UserUpdateMe = {
40 currentPassword,
41 password: newPassword
42 }
43
44 return this.authHttp.put(url, body)
45 .pipe(
46 map(this.restExtractor.extractDataBool),
47 catchError(err => this.restExtractor.handleError(err))
48 )
49 }
50
51 changeEmail (password: string, newEmail: string) {
52 const url = UserService.BASE_USERS_URL + 'me'
53 const body: UserUpdateMe = {
54 currentPassword: password,
55 email: newEmail
56 }
57
58 return this.authHttp.put(url, body)
59 .pipe(
60 map(this.restExtractor.extractDataBool),
61 catchError(err => this.restExtractor.handleError(err))
62 )
63 }
64
65 updateMyProfile (profile: UserUpdateMe) {
66 const url = UserService.BASE_USERS_URL + 'me'
67
68 return this.authHttp.put(url, profile)
69 .pipe(
70 map(this.restExtractor.extractDataBool),
71 catchError(err => this.restExtractor.handleError(err))
72 )
73 }
74
75 updateMyAnonymousProfile (profile: UserUpdateMe) {
76 const supportedKeys = {
77 // local storage keys
78 nsfwPolicy: (val: NSFWPolicyType) => this.localStorageService.setItem(User.KEYS.NSFW_POLICY, val),
79 webTorrentEnabled: (val: boolean) => this.localStorageService.setItem(User.KEYS.WEBTORRENT_ENABLED, String(val)),
80 autoPlayVideo: (val: boolean) => this.localStorageService.setItem(User.KEYS.AUTO_PLAY_VIDEO, String(val)),
81 autoPlayNextVideoPlaylist: (val: boolean) => this.localStorageService.setItem(User.KEYS.AUTO_PLAY_VIDEO_PLAYLIST, String(val)),
82 theme: (val: string) => this.localStorageService.setItem(User.KEYS.THEME, val),
83 videoLanguages: (val: string[]) => this.localStorageService.setItem(User.KEYS.VIDEO_LANGUAGES, JSON.stringify(val)),
84
85 // session storage keys
86 autoPlayNextVideo: (val: boolean) =>
87 this.sessionStorageService.setItem(User.KEYS.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO, String(val))
88 }
89
90 for (const key of Object.keys(profile)) {
91 try {
92 if (has(supportedKeys, key)) supportedKeys[key](profile[key])
93 } catch (err) {
94 console.error(`Cannot set item ${key} in localStorage. Likely due to a value impossible to stringify.`, err)
95 }
96 }
97 }
98
99 listenAnonymousUpdate () {
100 return this.localStorageService.watch([
101 User.KEYS.NSFW_POLICY,
102 User.KEYS.WEBTORRENT_ENABLED,
103 User.KEYS.AUTO_PLAY_VIDEO,
104 User.KEYS.AUTO_PLAY_VIDEO_PLAYLIST,
105 User.KEYS.THEME,
106 User.KEYS.VIDEO_LANGUAGES
107 ]).pipe(
108 throttleTime(200),
109 filter(() => this.authService.isLoggedIn() !== true),
110 map(() => this.getAnonymousUser())
111 )
112 }
113
114 deleteMe () {
115 const url = UserService.BASE_USERS_URL + 'me'
116
117 return this.authHttp.delete(url)
118 .pipe(
119 map(this.restExtractor.extractDataBool),
120 catchError(err => this.restExtractor.handleError(err))
121 )
122 }
123
124 changeAvatar (avatarForm: FormData) {
125 const url = UserService.BASE_USERS_URL + 'me/avatar/pick'
126
127 return this.authHttp.post<{ avatar: Avatar }>(url, avatarForm)
128 .pipe(catchError(err => this.restExtractor.handleError(err)))
129 }
130
131 signup (userCreate: UserRegister) {
132 return this.authHttp.post(UserService.BASE_USERS_URL + 'register', userCreate)
133 .pipe(
134 map(this.restExtractor.extractDataBool),
135 catchError(err => this.restExtractor.handleError(err))
136 )
137 }
138
139 getMyVideoQuotaUsed () {
140 const url = UserService.BASE_USERS_URL + 'me/video-quota-used'
141
142 return this.authHttp.get<UserVideoQuota>(url)
143 .pipe(catchError(err => this.restExtractor.handleError(err)))
144 }
145
146 askResetPassword (email: string) {
147 const url = UserService.BASE_USERS_URL + '/ask-reset-password'
148
149 return this.authHttp.post(url, { email })
150 .pipe(
151 map(this.restExtractor.extractDataBool),
152 catchError(err => this.restExtractor.handleError(err))
153 )
154 }
155
156 resetPassword (userId: number, verificationString: string, password: string) {
157 const url = `${UserService.BASE_USERS_URL}/${userId}/reset-password`
158 const body = {
159 verificationString,
160 password
161 }
162
163 return this.authHttp.post(url, body)
164 .pipe(
165 map(this.restExtractor.extractDataBool),
166 catchError(res => this.restExtractor.handleError(res))
167 )
168 }
169
170 verifyEmail (userId: number, verificationString: string, isPendingEmail: boolean) {
171 const url = `${UserService.BASE_USERS_URL}/${userId}/verify-email`
172 const body = {
173 verificationString,
174 isPendingEmail
175 }
176
177 return this.authHttp.post(url, body)
178 .pipe(
179 map(this.restExtractor.extractDataBool),
180 catchError(res => this.restExtractor.handleError(res))
181 )
182 }
183
184 askSendVerifyEmail (email: string) {
185 const url = UserService.BASE_USERS_URL + '/ask-send-verify-email'
186
187 return this.authHttp.post(url, { email })
188 .pipe(
189 map(this.restExtractor.extractDataBool),
190 catchError(err => this.restExtractor.handleError(err))
191 )
192 }
193
194 autocomplete (search: string): Observable<string[]> {
195 const url = UserService.BASE_USERS_URL + 'autocomplete'
196 const params = new HttpParams().append('search', search)
197
198 return this.authHttp
199 .get<string[]>(url, { params })
200 .pipe(catchError(res => this.restExtractor.handleError(res)))
201 }
202
203 getNewUsername (oldDisplayName: string, newDisplayName: string, currentUsername: string) {
204 // Don't update display name, the user seems to have changed it
205 if (this.displayNameToUsername(oldDisplayName) !== currentUsername) return currentUsername
206
207 return this.displayNameToUsername(newDisplayName)
208 }
209
210 displayNameToUsername (displayName: string) {
211 if (!displayName) return ''
212
213 return displayName
214 .toLowerCase()
215 .replace(/\s/g, '_')
216 .replace(/[^a-z0-9_.]/g, '')
217 }
218
219 /* ###### Admin methods ###### */
220
221 addUser (userCreate: UserCreate) {
222 return this.authHttp.post(UserService.BASE_USERS_URL, userCreate)
223 .pipe(
224 map(this.restExtractor.extractDataBool),
225 catchError(err => this.restExtractor.handleError(err))
226 )
227 }
228
229 updateUser (userId: number, userUpdate: UserUpdate) {
230 return this.authHttp.put(UserService.BASE_USERS_URL + userId, userUpdate)
231 .pipe(
232 map(this.restExtractor.extractDataBool),
233 catchError(err => this.restExtractor.handleError(err))
234 )
235 }
236
237 updateUsers (users: UserServerModel[], userUpdate: UserUpdate) {
238 return from(users)
239 .pipe(
240 concatMap(u => this.authHttp.put(UserService.BASE_USERS_URL + u.id, userUpdate)),
241 toArray(),
242 catchError(err => this.restExtractor.handleError(err))
243 )
244 }
245
246 getUserWithCache (userId: number) {
247 if (!this.userCache[userId]) {
248 this.userCache[ userId ] = this.getUser(userId).pipe(shareReplay())
249 }
250
251 return this.userCache[userId]
252 }
253
254 getUser (userId: number, withStats = false) {
255 const params = new HttpParams().append('withStats', withStats + '')
256 return this.authHttp.get<UserServerModel>(UserService.BASE_USERS_URL + userId, { params })
257 .pipe(catchError(err => this.restExtractor.handleError(err)))
258 }
259
260 getAnonymousUser () {
261 let videoLanguages: string[]
262
263 try {
264 videoLanguages = JSON.parse(this.localStorageService.getItem(User.KEYS.VIDEO_LANGUAGES))
265 } catch (err) {
266 videoLanguages = null
267 console.error('Cannot parse desired video languages from localStorage.', err)
268 }
269
270 return new User({
271 // local storage keys
272 nsfwPolicy: this.localStorageService.getItem(User.KEYS.NSFW_POLICY) as NSFWPolicyType,
273 webTorrentEnabled: this.localStorageService.getItem(User.KEYS.WEBTORRENT_ENABLED) !== 'false',
274 theme: this.localStorageService.getItem(User.KEYS.THEME) || 'instance-default',
275 videoLanguages,
276
277 autoPlayNextVideoPlaylist: this.localStorageService.getItem(User.KEYS.AUTO_PLAY_VIDEO_PLAYLIST) !== 'false',
278 autoPlayVideo: this.localStorageService.getItem(User.KEYS.AUTO_PLAY_VIDEO) === 'true',
279
280 // session storage keys
281 autoPlayNextVideo: this.sessionStorageService.getItem(User.KEYS.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO) === 'true'
282 })
283 }
284
285 getUsers (pagination: RestPagination, sort: SortMeta, search?: string): Observable<ResultList<UserServerModel>> {
286 let params = new HttpParams()
287 params = this.restService.addRestGetParams(params, pagination, sort)
288
289 if (search) params = params.append('search', search)
290
291 return this.authHttp.get<ResultList<UserServerModel>>(UserService.BASE_USERS_URL, { params })
292 .pipe(
293 map(res => this.restExtractor.convertResultListDateToHuman(res)),
294 map(res => this.restExtractor.applyToResultListData(res, this.formatUser.bind(this))),
295 catchError(err => this.restExtractor.handleError(err))
296 )
297 }
298
299 removeUser (usersArg: UserServerModel | UserServerModel[]) {
300 const users = Array.isArray(usersArg) ? usersArg : [ usersArg ]
301
302 return from(users)
303 .pipe(
304 concatMap(u => this.authHttp.delete(UserService.BASE_USERS_URL + u.id)),
305 toArray(),
306 catchError(err => this.restExtractor.handleError(err))
307 )
308 }
309
310 banUsers (usersArg: UserServerModel | UserServerModel[], reason?: string) {
311 const body = reason ? { reason } : {}
312 const users = Array.isArray(usersArg) ? usersArg : [ usersArg ]
313
314 return from(users)
315 .pipe(
316 concatMap(u => this.authHttp.post(UserService.BASE_USERS_URL + u.id + '/block', body)),
317 toArray(),
318 catchError(err => this.restExtractor.handleError(err))
319 )
320 }
321
322 unbanUsers (usersArg: UserServerModel | UserServerModel[]) {
323 const users = Array.isArray(usersArg) ? usersArg : [ usersArg ]
324
325 return from(users)
326 .pipe(
327 concatMap(u => this.authHttp.post(UserService.BASE_USERS_URL + u.id + '/unblock', {})),
328 toArray(),
329 catchError(err => this.restExtractor.handleError(err))
330 )
331 }
332
333 getAnonymousOrLoggedUser () {
334 if (!this.authService.isLoggedIn()) {
335 return of(this.getAnonymousUser())
336 }
337
338 return this.authService.userInformationLoaded
339 .pipe(
340 first(),
341 map(() => this.authService.getUser())
342 )
343 }
344
345 private formatUser (user: UserServerModel) {
346 let videoQuota
347 if (user.videoQuota === -1) {
348 videoQuota = this.i18n('Unlimited')
349 } else {
350 videoQuota = this.bytesPipe.transform(user.videoQuota, 0)
351 }
352
353 const videoQuotaUsed = this.bytesPipe.transform(user.videoQuotaUsed, 0)
354
355 const roleLabels: { [ id in UserRole ]: string } = {
356 [UserRole.USER]: this.i18n('User'),
357 [UserRole.ADMINISTRATOR]: this.i18n('Administrator'),
358 [UserRole.MODERATOR]: this.i18n('Moderator')
359 }
360
361 return Object.assign(user, {
362 roleLabel: roleLabels[user.role],
363 videoQuota,
364 videoQuotaUsed
365 })
366 }
367}