aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/+admin
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2019-02-11 11:52:34 +0100
committerChocobozzz <me@florianbigard.com>2019-02-11 11:52:34 +0100
commit88108880bbdba473cfe36ecbebc1c3c4f972e102 (patch)
treeb242efb3b4f0d7e49d88f2d1f2063b5b3b0489c0 /client/src/app/+admin
parent53a94c7cfa8368da4cd248d65df8346905938f0c (diff)
parent9b712a2017e4ab3cf12cd6bd58278905520159d0 (diff)
downloadPeerTube-88108880bbdba473cfe36ecbebc1c3c4f972e102.tar.gz
PeerTube-88108880bbdba473cfe36ecbebc1c3c4f972e102.tar.zst
PeerTube-88108880bbdba473cfe36ecbebc1c3c4f972e102.zip
Merge branch 'develop' into pr/1217
Diffstat (limited to 'client/src/app/+admin')
-rw-r--r--client/src/app/+admin/admin.module.ts6
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html480
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts234
-rw-r--r--client/src/app/+admin/follows/followers-list/followers-list.component.html9
-rw-r--r--client/src/app/+admin/follows/followers-list/followers-list.component.scss10
-rw-r--r--client/src/app/+admin/follows/followers-list/followers-list.component.ts10
-rw-r--r--client/src/app/+admin/follows/following-add/following-add.component.ts8
-rw-r--r--client/src/app/+admin/follows/following-list/following-list.component.html11
-rw-r--r--client/src/app/+admin/follows/following-list/following-list.component.scss11
-rw-r--r--client/src/app/+admin/follows/following-list/following-list.component.ts17
-rw-r--r--client/src/app/+admin/follows/shared/follow.service.ts8
-rw-r--r--client/src/app/+admin/follows/shared/redundancy-checkbox.component.ts21
-rw-r--r--client/src/app/+admin/jobs/jobs-list/jobs-list.component.ts8
-rw-r--r--client/src/app/+admin/moderation/instance-blocklist/index.ts2
-rw-r--r--client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.html22
-rw-r--r--client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.scss7
-rw-r--r--client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.ts58
-rw-r--r--client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html23
-rw-r--r--client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.scss7
-rw-r--r--client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.ts57
-rw-r--r--client/src/app/+admin/moderation/moderation.component.html4
-rw-r--r--client/src/app/+admin/moderation/moderation.component.scss1
-rw-r--r--client/src/app/+admin/moderation/moderation.component.ts8
-rw-r--r--client/src/app/+admin/moderation/moderation.routes.ts23
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.html9
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts27
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html10
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts29
-rw-r--r--client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html12
-rw-r--r--client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.ts27
-rw-r--r--client/src/app/+admin/users/user-edit/index.ts1
-rw-r--r--client/src/app/+admin/users/user-edit/user-create.component.ts10
-rw-r--r--client/src/app/+admin/users/user-edit/user-edit.component.html14
-rw-r--r--client/src/app/+admin/users/user-edit/user-edit.component.scss22
-rw-r--r--client/src/app/+admin/users/user-edit/user-edit.ts14
-rw-r--r--client/src/app/+admin/users/user-edit/user-password.component.html21
-rw-r--r--client/src/app/+admin/users/user-edit/user-password.component.scss22
-rw-r--r--client/src/app/+admin/users/user-edit/user-password.component.ts64
-rw-r--r--client/src/app/+admin/users/user-edit/user-update.component.ts23
-rw-r--r--client/src/app/+admin/users/user-list/user-list.component.html56
-rw-r--r--client/src/app/+admin/users/user-list/user-list.component.scss12
-rw-r--r--client/src/app/+admin/users/user-list/user-list.component.ts131
42 files changed, 1028 insertions, 521 deletions
diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts
index 8c6db98d9..f7f347105 100644
--- a/client/src/app/+admin/admin.module.ts
+++ b/client/src/app/+admin/admin.module.ts
@@ -10,11 +10,12 @@ import { FollowingListComponent } from './follows/following-list/following-list.
10import { JobsComponent } from './jobs/job.component' 10import { JobsComponent } from './jobs/job.component'
11import { JobsListComponent } from './jobs/jobs-list/jobs-list.component' 11import { JobsListComponent } from './jobs/jobs-list/jobs-list.component'
12import { JobService } from './jobs/shared/job.service' 12import { JobService } from './jobs/shared/job.service'
13import { UserCreateComponent, UserListComponent, UsersComponent, UserUpdateComponent } from './users' 13import { UserCreateComponent, UserListComponent, UsersComponent, UserUpdateComponent, UserPasswordComponent } from './users'
14import { ModerationCommentModalComponent, VideoAbuseListComponent, VideoBlacklistListComponent } from './moderation' 14import { ModerationCommentModalComponent, VideoAbuseListComponent, VideoBlacklistListComponent } from './moderation'
15import { ModerationComponent } from '@app/+admin/moderation/moderation.component' 15import { ModerationComponent } from '@app/+admin/moderation/moderation.component'
16import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component' 16import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component'
17import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service' 17import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service'
18import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from '@app/+admin/moderation/instance-blocklist'
18 19
19@NgModule({ 20@NgModule({
20 imports: [ 21 imports: [
@@ -35,12 +36,15 @@ import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service
35 UsersComponent, 36 UsersComponent,
36 UserCreateComponent, 37 UserCreateComponent,
37 UserUpdateComponent, 38 UserUpdateComponent,
39 UserPasswordComponent,
38 UserListComponent, 40 UserListComponent,
39 41
40 ModerationComponent, 42 ModerationComponent,
41 VideoBlacklistListComponent, 43 VideoBlacklistListComponent,
42 VideoAbuseListComponent, 44 VideoAbuseListComponent,
43 ModerationCommentModalComponent, 45 ModerationCommentModalComponent,
46 InstanceServerBlocklistComponent,
47 InstanceAccountBlocklistComponent,
44 48
45 JobsComponent, 49 JobsComponent,
46 JobsListComponent, 50 JobsListComponent,
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
index e2cbd35ca..52eb00d93 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
@@ -7,161 +7,169 @@
7 7
8 <div i18n class="inner-form-title">Instance</div> 8 <div i18n class="inner-form-title">Instance</div>
9 9
10 <div class="form-group"> 10 <ng-container formGroupName="instance">
11 <label i18n for="instanceName">Name</label> 11 <div class="form-group">
12 <input 12 <label i18n for="instanceName">Name</label>
13 type="text" id="instanceName" 13 <input
14 formControlName="instanceName" [ngClass]="{ 'input-error': formErrors['instanceName'] }" 14 type="text" id="instanceName"
15 > 15 formControlName="name" [ngClass]="{ 'input-error': formErrors.instance.name }"
16 <div *ngIf="formErrors.instanceName" class="form-error"> 16 >
17 {{ formErrors.instanceName }} 17 <div *ngIf="formErrors.instance.name" class="form-error">{{ formErrors.instance.name }}</div>
18 </div> 18 </div>
19 </div>
20 19
21 <div class="form-group"> 20 <div class="form-group">
22 <label i18n for="instanceShortDescription">Short description</label> 21 <label i18n for="instanceShortDescription">Short description</label>
23 <textarea 22 <textarea
24 id="instanceShortDescription" formControlName="instanceShortDescription" 23 id="instanceShortDescription" formControlName="shortDescription"
25 [ngClass]="{ 'input-error': formErrors['instanceShortDescription'] }" 24 [ngClass]="{ 'input-error': formErrors['instance.shortDescription'] }"
26 ></textarea> 25 ></textarea>
27 <div *ngIf="formErrors.instanceShortDescription" class="form-error"> 26 <div *ngIf="formErrors.instance.shortDescription" class="form-error">{{ formErrors.instance.shortDescription }}</div>
28 {{ formErrors.instanceShortDescription }}
29 </div> 27 </div>
30 </div>
31 28
32 <div class="form-group"> 29 <div class="form-group">
33 <label i18n for="instanceDescription">Description</label><my-help helpType="markdownText"></my-help> 30 <label i18n for="instanceDescription">Description</label><my-help helpType="markdownText"></my-help>
34 <my-markdown-textarea 31 <my-markdown-textarea
35 id="instanceDescription" formControlName="instanceDescription" textareaWidth="500px" [previewColumn]="true" 32 id="instanceDescription" formControlName="description" textareaWidth="500px" [previewColumn]="true"
36 [classes]="{ 'input-error': formErrors['instanceDescription'] }" 33 [classes]="{ 'input-error': formErrors['instance.description'] }"
37 ></my-markdown-textarea> 34 ></my-markdown-textarea>
38 <div *ngIf="formErrors.instanceDescription" class="form-error"> 35 <div *ngIf="formErrors.instance.description" class="form-error">{{ formErrors.instance.description }}</div>
39 {{ formErrors.instanceDescription }}
40 </div> 36 </div>
41 </div>
42 37
43 <div class="form-group"> 38 <div class="form-group">
44 <label i18n for="instanceTerms">Terms</label><my-help helpType="markdownText"></my-help> 39 <label i18n for="instanceTerms">Terms</label><my-help helpType="markdownText"></my-help>
45 <my-markdown-textarea 40 <my-markdown-textarea
46 id="instanceTerms" formControlName="instanceTerms" textareaWidth="500px" [previewColumn]="true" 41 id="instanceTerms" formControlName="terms" textareaWidth="500px" [previewColumn]="true"
47 [ngClass]="{ 'input-error': formErrors['instanceTerms'] }" 42 [ngClass]="{ 'input-error': formErrors['instance.terms'] }"
48 ></my-markdown-textarea> 43 ></my-markdown-textarea>
49 <div *ngIf="formErrors.instanceTerms" class="form-error"> 44 <div *ngIf="formErrors.instance.terms" class="form-error">{{ formErrors.instance.terms }}</div>
50 {{ formErrors.instanceTerms }}
51 </div> 45 </div>
52 </div>
53 46
54 <div class="form-group"> 47 <div class="form-group">
55 <label i18n for="instanceDefaultClientRoute">Default client route</label> 48 <label i18n for="instanceDefaultClientRoute">Default client route</label>
56 <div class="peertube-select-container"> 49 <div class="peertube-select-container">
57 <select id="instanceDefaultClientRoute" formControlName="instanceDefaultClientRoute"> 50 <select id="instanceDefaultClientRoute" formControlName="defaultClientRoute">
58 <option i18n value="/videos/overview">Videos Overview</option> 51 <option i18n value="/videos/overview">Videos Overview</option>
59 <option i18n value="/videos/trending">Videos Trending</option> 52 <option i18n value="/videos/trending">Videos Trending</option>
60 <option i18n value="/videos/recently-added">Videos Recently Added</option> 53 <option i18n value="/videos/recently-added">Videos Recently Added</option>
61 <option i18n value="/videos/local">Local videos</option> 54 <option i18n value="/videos/local">Local videos</option>
62 </select> 55 </select>
56 </div>
57 <div *ngIf="formErrors.instance.defaultClientRoute" class="form-error">{{ formErrors.instance.defaultClientRoute }}</div>
63 </div> 58 </div>
64 <div *ngIf="formErrors.instanceDefaultClientRoute" class="form-error"> 59
65 {{ formErrors.instanceDefaultClientRoute }} 60 <div class="form-group">
61 <label i18n for="instanceDefaultNSFWPolicy">Policy on videos containing sensitive content</label>
62 <my-help
63 helpType="custom" i18n-customHtml
64 customHtml="With <strong>Do not list</strong> or <strong>Blur thumbnails</strong>, a confirmation will be requested to watch the video."
65 ></my-help>
66
67 <div class="peertube-select-container">
68 <select id="instanceDefaultNSFWPolicy" formControlName="defaultNSFWPolicy">
69 <option i18n value="do_not_list">Do not list</option>
70 <option i18n value="blur">Blur thumbnails</option>
71 <option i18n value="display">Display</option>
72 </select>
73 </div>
74 <div *ngIf="formErrors.instance.defaultNSFWPolicy" class="form-error">{{ formErrors.instance.defaultNSFWPolicy }}</div>
66 </div> 75 </div>
67 </div> 76 </ng-container>
68 77
69 <div class="form-group"> 78 <div i18n class="inner-form-title">Signup</div>
70 <label i18n for="instanceDefaultNSFWPolicy">Policy on videos containing sensitive content</label>
71 <my-help
72 helpType="custom" i18n-customHtml
73 customHtml="With <strong>Do not list</strong> or <strong>Blur thumbnails</strong>, a confirmation will be requested to watch the video."
74 ></my-help>
75 79
76 <div class="peertube-select-container"> 80 <ng-container formGroupName="signup">
77 <select id="instanceDefaultNSFWPolicy" formControlName="instanceDefaultNSFWPolicy"> 81 <div class="form-group">
78 <option i18n value="do_not_list">Do not list</option> 82 <my-peertube-checkbox
79 <option i18n value="blur">Blur thumbnails</option> 83 inputName="signupEnabled" formControlName="enabled"
80 <option i18n value="display">Display</option> 84 i18n-labelText labelText="Signup enabled"
81 </select> 85 ></my-peertube-checkbox>
82 </div> 86 </div>
83 <div *ngIf="formErrors.instanceDefaultNSFWPolicy" class="form-error"> 87
84 {{ formErrors.instanceDefaultNSFWPolicy }} 88 <div class="form-group">
89 <my-peertube-checkbox *ngIf="isSignupEnabled()"
90 inputName="signupRequiresEmailVerification" formControlName="requiresEmailVerification"
91 i18n-labelText labelText="Signup requires email verification"
92 ></my-peertube-checkbox>
85 </div> 93 </div>
86 </div>
87 94
88 <div i18n class="inner-form-title">Signup</div> 95 <div *ngIf="isSignupEnabled()" class="form-group">
96 <label i18n for="signupLimit">Signup limit</label>
97 <input
98 type="text" id="signupLimit"
99 formControlName="limit" [ngClass]="{ 'input-error': formErrors['signup.limit'] }"
100 >
101 <div *ngIf="formErrors.signup.limit" class="form-error">{{ formErrors.signup.limit }}</div>
102 </div>
103 </ng-container>
89 104
90 <my-peertube-checkbox 105 <div i18n class="inner-form-title">Users</div>
91 inputName="signupEnabled" formControlName="signupEnabled"
92 i18n-labelText labelText="Signup enabled"
93 ></my-peertube-checkbox>
94 106
95 <my-peertube-checkbox *ngIf="isSignupEnabled()" 107 <ng-container formGroupName="user">
96 inputName="signupRequiresEmailVerification" formControlName="signupRequiresEmailVerification" 108 <div class="form-group">
97 i18n-labelText labelText="Signup requires email verification" 109 <label i18n for="userVideoQuota">User default video quota</label>
98 ></my-peertube-checkbox> 110 <div class="peertube-select-container">
111 <select id="userVideoQuota" formControlName="videoQuota">
112 <option *ngFor="let videoQuotaOption of videoQuotaOptions" [value]="videoQuotaOption.value">
113 {{ videoQuotaOption.label }}
114 </option>
115 </select>
116 </div>
117 <div *ngIf="formErrors.user.videoQuota" class="form-error">{{ formErrors.user.videoQuota }}</div>
118 </div>
99 119
100 <div *ngIf="isSignupEnabled()" class="form-group"> 120 <div class="form-group">
101 <label i18n for="signupLimit">Signup limit</label> 121 <label i18n for="userVideoQuotaDaily">User default daily upload limit</label>
102 <input 122 <div class="peertube-select-container">
103 type="text" id="signupLimit" 123 <select id="userVideoQuotaDaily" formControlName="videoQuotaDaily">
104 formControlName="signupLimit" [ngClass]="{ 'input-error': formErrors['signupLimit'] }" 124 <option *ngFor="let videoQuotaDailyOption of videoQuotaDailyOptions" [value]="videoQuotaDailyOption.value">
105 > 125 {{ videoQuotaDailyOption.label }}
106 <div *ngIf="formErrors.signupLimit" class="form-error"> 126 </option>
107 {{ formErrors.signupLimit }} 127 </select>
128 </div>
129 <div *ngIf="formErrors.user.videoQuotaDaily" class="form-error">{{ formErrors.user.videoQuotaDaily }}</div>
108 </div> 130 </div>
109 </div> 131 </ng-container>
110 132
111 <div i18n class="inner-form-title">Import</div> 133 <div i18n class="inner-form-title">Import</div>
112 134
113 <my-peertube-checkbox 135 <ng-container formGroupName="import">
114 inputName="importVideosHttpEnabled" formControlName="importVideosHttpEnabled" 136 <ng-container formGroupName="videos">
115 i18n-labelText labelText="Video import with HTTP enabled"
116 ></my-peertube-checkbox>
117 137
118 <my-peertube-checkbox 138 <div class="form-group" formGroupName="http">
119 inputName="importVideosTorrentEnabled" formControlName="importVideosTorrentEnabled" 139 <my-peertube-checkbox
120 i18n-labelText labelText="Video import with a torrent file or a magnet URI enabled" 140 inputName="importVideosHttpEnabled" formControlName="enabled"
121 ></my-peertube-checkbox> 141 i18n-labelText labelText="Video import with HTTP URL (i.e. YouTube) enabled"
142 ></my-peertube-checkbox>
143 </div>
144
145 <div class="form-group" formGroupName="torrent">
146 <my-peertube-checkbox
147 inputName="importVideosTorrentEnabled" formControlName="enabled"
148 i18n-labelText labelText="Video import with a torrent file or a magnet URI enabled"
149 ></my-peertube-checkbox>
150 </div>
151
152 </ng-container>
153 </ng-container>
122 154
123 <div i18n class="inner-form-title">Administrator</div> 155 <div i18n class="inner-form-title">Administrator</div>
124 156
125 <div class="form-group"> 157 <div class="form-group" formGroupName="admin">
126 <label i18n for="adminEmail">Admin email</label> 158 <label i18n for="adminEmail">Admin email</label>
127 <input 159 <input
128 type="text" id="adminEmail" 160 type="text" id="adminEmail"
129 formControlName="adminEmail" [ngClass]="{ 'input-error': formErrors['adminEmail'] }" 161 formControlName="email" [ngClass]="{ 'input-error': formErrors['admin.email'] }"
130 > 162 >
131 <div *ngIf="formErrors.adminEmail" class="form-error"> 163 <div *ngIf="formErrors.admin.email" class="form-error">{{ formErrors.admin.email }}</div>
132 {{ formErrors.adminEmail }}
133 </div>
134 </div> 164 </div>
135 165
136 <div i18n class="inner-form-title">Users</div> 166 <div class="form-group" formGroupName="contactForm">
137 167 <my-peertube-checkbox
138 <div class="form-group"> 168 inputName="enableContactForm" formControlName="enabled"
139 <label i18n for="userVideoQuota">User default video quota</label> 169 i18n-labelText labelText="Enable contact form"
140 <div class="peertube-select-container"> 170 ></my-peertube-checkbox>
141 <select id="userVideoQuota" formControlName="userVideoQuota">
142 <option *ngFor="let videoQuotaOption of videoQuotaOptions" [value]="videoQuotaOption.value">
143 {{ videoQuotaOption.label }}
144 </option>
145 </select>
146 </div>
147 <div *ngIf="formErrors.userVideoQuota" class="form-error">
148 {{ formErrors.userVideoQuota }}
149 </div>
150 </div> 171 </div>
151 172
152 <div class="form-group">
153 <label i18n for="userVideoQuotaDaily">User default daily upload limit</label>
154 <div class="peertube-select-container">
155 <select id="userVideoQuotaDaily" formControlName="userVideoQuotaDaily">
156 <option *ngFor="let videoQuotaDailyOption of videoQuotaDailyOptions" [value]="videoQuotaDailyOption.value">
157 {{ videoQuotaDailyOption.label }}
158 </option>
159 </select>
160 </div>
161 <div *ngIf="formErrors.userVideoQuotaDaily" class="form-error">
162 {{ formErrors.userVideoQuotaDaily }}
163 </div>
164 </div>
165 </ng-template> 173 </ng-template>
166 </ngb-tab> 174 </ngb-tab>
167 175
@@ -169,28 +177,35 @@
169 <ng-template ngbTabContent> 177 <ng-template ngbTabContent>
170 <div i18n class="inner-form-title">Twitter</div> 178 <div i18n class="inner-form-title">Twitter</div>
171 179
172 <div class="form-group"> 180 <ng-container formGroupName="services">
173 <label i18n for="signupLimit">Your Twitter username</label> 181 <ng-container formGroupName="twitter">
174 <my-help 182
175 helpType="custom" i18n-customHtml 183 <div class="form-group">
176 customHtml="Indicates the Twitter account for the website or platform on which the content was published." 184 <label i18n for="signupLimit">Your Twitter username</label>
177 ></my-help> 185 <my-help
178 <input 186 helpType="custom" i18n-customHtml
179 type="text" id="servicesTwitterUsername" 187 customHtml="Indicates the Twitter account for the website or platform on which the content was published."
180 formControlName="servicesTwitterUsername" [ngClass]="{ 'input-error': formErrors['servicesTwitterUsername'] }" 188 ></my-help>
181 > 189 <input
182 <div *ngIf="formErrors.servicesTwitterUsername" class="form-error"> 190 type="text" id="servicesTwitterUsername"
183 {{ formErrors.servicesTwitterUsername }} 191 formControlName="username" [ngClass]="{ 'input-error': formErrors['services.twitter.username'] }"
184 </div> 192 >
185 </div> 193 <div *ngIf="formErrors.services.twitter.username" class="form-error">{{ formErrors.services.twitter.username }}</div>
194 </div>
195
196 <div class="form-group">
197 <my-peertube-checkbox
198 inputName="servicesTwitterWhitelisted" formControlName="whitelisted"
199 i18n-labelText labelText="Instance whitelisted by Twitter"
200 i18n-helpHtml helpHtml="If your instance is whitelisted by Twitter, a video player will be embedded in the Twitter feed on PeerTube video share.<br />
201 If the instance is not whitelisted, we use an image link card that will redirect on your PeerTube instance.<br /><br />
202 Check this checkbox, save the configuration and test with a video URL of your instance (https://example.com/videos/watch/blabla) on <a target='_blank' rel='noopener noreferrer' href='https://cards-dev.twitter.com/validator'>https://cards-dev.twitter.com/validator</a> to see if you instance is whitelisted."
203 ></my-peertube-checkbox>
204 </div>
205
206 </ng-container>
207 </ng-container>
186 208
187 <my-peertube-checkbox
188 inputName="servicesTwitterWhitelisted" formControlName="servicesTwitterWhitelisted"
189 i18n-labelText labelText="Instance whitelisted by Twitter"
190 i18n-helpHtml helpHtml="If your instance is whitelisted by Twitter, a video player will be embedded in the Twitter feed on PeerTube video share.<br />
191 If the instance is not whitelisted, we use an image link card that will redirect on your PeerTube instance.<br /><br />
192 Check this checkbox, save the configuration and test with a video URL of your instance (https://example.com/videos/watch/blabla) on <a target='_blank' rel='noopener noreferrer' href='https://cards-dev.twitter.com/validator'>https://cards-dev.twitter.com/validator</a> to see if you instance is whitelisted."
193 ></my-peertube-checkbox>
194 </ng-template> 209 </ng-template>
195 </ngb-tab> 210 </ngb-tab>
196 211
@@ -199,36 +214,48 @@
199 214
200 <div i18n class="inner-form-title">Transcoding</div> 215 <div i18n class="inner-form-title">Transcoding</div>
201 216
202 <my-peertube-checkbox 217 <ng-container formGroupName="transcoding">
203 inputName="transcodingEnabled" formControlName="transcodingEnabled" 218 <div class="form-group">
204 i18n-labelText labelText="Transcoding enabled" 219 <my-peertube-checkbox
205 i18n-helpHtml helpHtml="If you disable transcoding, many videos from your users will not work!" 220 inputName="transcodingEnabled" formControlName="enabled"
206 ></my-peertube-checkbox> 221 i18n-labelText labelText="Transcoding enabled"
222 i18n-helpHtml helpHtml="If you disable transcoding, many videos from your users will not work!"
223 ></my-peertube-checkbox>
224 </div>
207 225
208 <ng-template [ngIf]="isTranscodingEnabled()"> 226 <ng-container *ngIf="isTranscodingEnabled()">
209 227
210 <div class="form-group"> 228 <div class="form-group">
211 <label i18n for="transcodingThreads">Transcoding threads</label> 229 <my-peertube-checkbox
212 <div class="peertube-select-container"> 230 inputName="transcodingAllowAdditionalExtensions" formControlName="allowAdditionalExtensions"
213 <select id="transcodingThreads" formControlName="transcodingThreads"> 231 i18n-labelText labelText="Allow additional extensions"
214 <option *ngFor="let transcodingThreadOption of transcodingThreadOptions" [value]="transcodingThreadOption.value"> 232 i18n-helpHtml helpHtml="Allow your users to upload .mkv, .mov, .avi, .flv videos"
215 {{ transcodingThreadOption.label }} 233 ></my-peertube-checkbox>
216 </option>
217 </select>
218 </div> 234 </div>
219 <div *ngIf="formErrors.transcodingThreads" class="form-error"> 235
220 {{ formErrors.transcodingThreads }} 236 <div class="form-group">
237 <label i18n for="transcodingThreads">Transcoding threads</label>
238 <div class="peertube-select-container">
239 <select id="transcodingThreads" formControlName="threads">
240 <option *ngFor="let transcodingThreadOption of transcodingThreadOptions" [value]="transcodingThreadOption.value">
241 {{ transcodingThreadOption.label }}
242 </option>
243 </select>
244 </div>
245 <div *ngIf="formErrors.transcoding.threads" class="form-error">{{ formErrors.transcoding.threads }}</div>
221 </div> 246 </div>
222 </div>
223 247
224 <div class="form-group" *ngFor="let resolution of resolutions"> 248 <ng-container formGroupName="resolutions">
225 <my-peertube-checkbox 249 <div class="form-group" *ngFor="let resolution of resolutions">
226 [inputName]="getResolutionKey(resolution)" [formControlName]="getResolutionKey(resolution)" 250 <my-peertube-checkbox
227 i18n-labelText labelText="Resolution {{resolution}} enabled" 251 [inputName]="getResolutionKey(resolution)" [formControlName]="resolution"
228 ></my-peertube-checkbox> 252 i18n-labelText labelText="Resolution {{resolution}} enabled"
253 ></my-peertube-checkbox>
254 </div>
255 </ng-container>
229 256
230 </div> 257 </ng-container>
231 </ng-template> 258 </ng-container>
232 259
233 <div i18n class="inner-form-title"> 260 <div i18n class="inner-form-title">
234 Cache 261 Cache
@@ -239,74 +266,73 @@
239 ></my-help> 266 ></my-help>
240 </div> 267 </div>
241 268
242 <div class="form-group"> 269 <ng-container formGroupName="cache">
243 <label i18n for="cachePreviewsSize">Previews cache size</label> 270 <div class="form-group" formGroupName="previews">
244 <input 271 <label i18n for="cachePreviewsSize">Previews cache size</label>
245 type="text" id="cachePreviewsSize" 272 <input
246 formControlName="cachePreviewsSize" [ngClass]="{ 'input-error': formErrors['cachePreviewsSize'] }" 273 type="text" id="cachePreviewsSize"
247 > 274 formControlName="size" [ngClass]="{ 'input-error': formErrors['cache.previews.size'] }"
248 <div *ngIf="formErrors.cachePreviewsSize" class="form-error"> 275 >
249 {{ formErrors.cachePreviewsSize }} 276 <div *ngIf="formErrors.cache.previews.size" class="form-error">{{ formErrors.cache.previews.size }}</div>
250 </div> 277 </div>
251 </div>
252 278
253 <div class="form-group"> 279 <div class="form-group" formGroupName="captions">
254 <label i18n for="cachePreviewsSize">Video captions cache size</label> 280 <label i18n for="cacheCaptionsSize">Video captions cache size</label>
255 <input 281 <input
256 type="text" id="cacheCaptionsSize" 282 type="text" id="cacheCaptionsSize"
257 formControlName="cacheCaptionsSize" [ngClass]="{ 'input-error': formErrors['cacheCaptionsSize'] }" 283 formControlName="size" [ngClass]="{ 'input-error': formErrors['cache.captions.size'] }"
258 > 284 >
259 <div *ngIf="formErrors.cacheCaptionsSize" class="form-error"> 285 <div *ngIf="formErrors.cache.captions.size" class="form-error">{{ formErrors.cache.captions.size }}</div>
260 {{ formErrors.cacheCaptionsSize }}
261 </div> 286 </div>
262 </div> 287 </ng-container>
263 288
264 <div i18n class="inner-form-title">Customizations</div> 289 <div i18n class="inner-form-title">Customizations</div>
265 290
266 <div class="form-group"> 291 <ng-container formGroupName="instance">
267 <label i18n for="customizationJavascript">JavaScript</label> 292 <ng-container formGroupName="customizations">
268 <my-help 293 <div class="form-group">
269 helpType="custom" i18n-customHtml 294 <label i18n for="customizationJavascript">JavaScript</label>
270 customHtml="Write directly JavaScript code.<br />Example: <pre>console.log('my instance is amazing');</pre>" 295 <my-help
271 ></my-help> 296 helpType="custom" i18n-customHtml
272 <textarea 297 customHtml="Write directly JavaScript code.<br />Example: <pre>console.log('my instance is amazing');</pre>"
273 id="customizationJavascript" formControlName="customizationJavascript" 298 ></my-help>
274 [ngClass]="{ 'input-error': formErrors['customizationJavascript'] }" 299 <textarea
275 ></textarea> 300 id="customizationJavascript" formControlName="javascript"
276 <div *ngIf="formErrors.customizationJavascript" class="form-error"> 301 [ngClass]="{ 'input-error': formErrors['instance.customizations.javascript'] }"
277 {{ formErrors.customizationJavascript }} 302 ></textarea>
278 </div> 303 <div *ngIf="formErrors.instance.customizations.javascript" class="form-error">{{ formErrors.instance.customizations.javascript }}</div>
279 </div> 304 </div>
305
306 <div class="form-group">
307 <label for="customizationCSS">CSS</label>
308 <my-help
309 helpType="custom"
310 i18n-customHtml
311 customHtml="
312 Write directly CSS code. Example:<br />
313 <pre>
314 body {{ '{' }}
315 background-color: red;
316 {{ '}' }}
317 </pre>
318
319 Prepend with <em>#custom-css</em> to override styles. Example:
320 <pre>
321 #custom-css .logged-in-email {{ '{' }}
322 color: red;
323 {{ '}' }}
324 </pre>
325 "
326 ></my-help>
327 <textarea
328 id="customizationCSS" formControlName="css"
329 [ngClass]="{ 'input-error': formErrors['instance.customizations.css'] }"
330 ></textarea>
331 <div *ngIf="formErrors.instance.customizations.css" class="form-error">{{ formErrors.instance.customizations.css }}</div>
332 </div>
333 </ng-container>
334 </ng-container>
280 335
281 <div class="form-group">
282 <label for="customizationCSS">CSS</label>
283 <my-help
284 helpType="custom"
285 i18n-customHtml
286 customHtml="
287 Write directly CSS code. Example:<br />
288 <pre>
289 body {{ '{' }}
290 background-color: red;
291 {{ '}' }}
292 </pre>
293
294 Prepend with <em>#custom-css</em> to override styles. Example:
295 <pre>
296 #custom-css .logged-in-email {{ '{' }}
297 color: red;
298 {{ '}' }}
299 </pre>
300 "
301 ></my-help>
302 <textarea
303 id="customizationCSS" formControlName="customizationCSS"
304 [ngClass]="{ 'input-error': formErrors['customizationCSS'] }"
305 ></textarea>
306 <div *ngIf="formErrors.customizationCSS" class="form-error">
307 {{ formErrors.customizationCSS }}
308 </div>
309 </div>
310 </ng-template> 336 </ng-template>
311 </ngb-tab> 337 </ngb-tab>
312 </ngb-tabset> 338 </ngb-tabset>
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
index 4983b0425..654a076b0 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
@@ -1,9 +1,8 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { ConfigService } from '@app/+admin/config/shared/config.service' 2import { ConfigService } from '@app/+admin/config/shared/config.service'
3import { ConfirmService } from '@app/core'
4import { ServerService } from '@app/core/server/server.service' 3import { ServerService } from '@app/core/server/server.service'
5import { CustomConfigValidatorsService, FormReactive, UserValidatorsService } from '@app/shared' 4import { CustomConfigValidatorsService, FormReactive, UserValidatorsService } from '@app/shared'
6import { NotificationsService } from 'angular2-notifications' 5import { Notifier } from '@app/core'
7import { CustomConfig } from '../../../../../../shared/models/server/custom-config.model' 6import { CustomConfig } from '../../../../../../shared/models/server/custom-config.model'
8import { I18n } from '@ngx-translate/i18n-polyfill' 7import { I18n } from '@ngx-translate/i18n-polyfill'
9import { BuildFormDefaultValues, FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 8import { BuildFormDefaultValues, FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
@@ -19,17 +18,13 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
19 resolutions: string[] = [] 18 resolutions: string[] = []
20 transcodingThreadOptions: { label: string, value: number }[] = [] 19 transcodingThreadOptions: { label: string, value: number }[] = []
21 20
22 private oldCustomJavascript: string
23 private oldCustomCSS: string
24
25 constructor ( 21 constructor (
26 protected formValidatorService: FormValidatorService, 22 protected formValidatorService: FormValidatorService,
27 private customConfigValidatorsService: CustomConfigValidatorsService, 23 private customConfigValidatorsService: CustomConfigValidatorsService,
28 private userValidatorsService: UserValidatorsService, 24 private userValidatorsService: UserValidatorsService,
29 private notificationsService: NotificationsService, 25 private notifier: Notifier,
30 private configService: ConfigService, 26 private configService: ConfigService,
31 private serverService: ServerService, 27 private serverService: ServerService,
32 private confirmService: ConfirmService,
33 private i18n: I18n 28 private i18n: I18n
34 ) { 29 ) {
35 super() 30 super()
@@ -60,40 +55,78 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
60 } 55 }
61 56
62 getResolutionKey (resolution: string) { 57 getResolutionKey (resolution: string) {
63 return 'transcodingResolution' + resolution 58 return 'transcoding.resolutions.' + resolution
64 } 59 }
65 60
66 ngOnInit () { 61 ngOnInit () {
67 const formGroupData = { 62 const formGroupData: { [key in keyof CustomConfig ]: any } = {
68 instanceName: this.customConfigValidatorsService.INSTANCE_NAME, 63 instance: {
69 instanceShortDescription: this.customConfigValidatorsService.INSTANCE_SHORT_DESCRIPTION, 64 name: this.customConfigValidatorsService.INSTANCE_NAME,
70 instanceDescription: null, 65 shortDescription: this.customConfigValidatorsService.INSTANCE_SHORT_DESCRIPTION,
71 instanceTerms: null, 66 description: null,
72 instanceDefaultClientRoute: null, 67 terms: null,
73 instanceDefaultNSFWPolicy: null, 68 defaultClientRoute: null,
74 servicesTwitterUsername: this.customConfigValidatorsService.SERVICES_TWITTER_USERNAME, 69 defaultNSFWPolicy: null,
75 servicesTwitterWhitelisted: null, 70 customizations: {
76 cachePreviewsSize: this.customConfigValidatorsService.CACHE_PREVIEWS_SIZE, 71 javascript: null,
77 cacheCaptionsSize: this.customConfigValidatorsService.CACHE_CAPTIONS_SIZE, 72 css: null
78 signupEnabled: null, 73 }
79 signupLimit: this.customConfigValidatorsService.SIGNUP_LIMIT, 74 },
80 signupRequiresEmailVerification: null, 75 services: {
81 importVideosHttpEnabled: null, 76 twitter: {
82 importVideosTorrentEnabled: null, 77 username: this.customConfigValidatorsService.SERVICES_TWITTER_USERNAME,
83 adminEmail: this.customConfigValidatorsService.ADMIN_EMAIL, 78 whitelisted: null
84 userVideoQuota: this.userValidatorsService.USER_VIDEO_QUOTA, 79 }
85 userVideoQuotaDaily: this.userValidatorsService.USER_VIDEO_QUOTA_DAILY, 80 },
86 transcodingThreads: this.customConfigValidatorsService.TRANSCODING_THREADS, 81 cache: {
87 transcodingEnabled: null, 82 previews: {
88 customizationJavascript: null, 83 size: this.customConfigValidatorsService.CACHE_PREVIEWS_SIZE
89 customizationCSS: null 84 },
85 captions: {
86 size: this.customConfigValidatorsService.CACHE_CAPTIONS_SIZE
87 }
88 },
89 signup: {
90 enabled: null,
91 limit: this.customConfigValidatorsService.SIGNUP_LIMIT,
92 requiresEmailVerification: null
93 },
94 import: {
95 videos: {
96 http: {
97 enabled: null
98 },
99 torrent: {
100 enabled: null
101 }
102 }
103 },
104 admin: {
105 email: this.customConfigValidatorsService.ADMIN_EMAIL
106 },
107 contactForm: {
108 enabled: null
109 },
110 user: {
111 videoQuota: this.userValidatorsService.USER_VIDEO_QUOTA,
112 videoQuotaDaily: this.userValidatorsService.USER_VIDEO_QUOTA_DAILY
113 },
114 transcoding: {
115 enabled: null,
116 threads: this.customConfigValidatorsService.TRANSCODING_THREADS,
117 allowAdditionalExtensions: null,
118 resolutions: {}
119 }
90 } 120 }
91 121
92 const defaultValues: BuildFormDefaultValues = {} 122 const defaultValues = {
123 transcoding: {
124 resolutions: {}
125 }
126 }
93 for (const resolution of this.resolutions) { 127 for (const resolution of this.resolutions) {
94 const key = this.getResolutionKey(resolution) 128 defaultValues.transcoding.resolutions[resolution] = 'false'
95 defaultValues[key] = 'false' 129 formGroupData.transcoding.resolutions[resolution] = null
96 formGroupData[key] = null
97 } 130 }
98 131
99 this.buildForm(formGroupData) 132 this.buildForm(formGroupData)
@@ -103,112 +136,25 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
103 res => { 136 res => {
104 this.customConfig = res 137 this.customConfig = res
105 138
106 this.oldCustomCSS = this.customConfig.instance.customizations.css
107 this.oldCustomJavascript = this.customConfig.instance.customizations.javascript
108
109 this.updateForm() 139 this.updateForm()
110 // Force form validation 140 // Force form validation
111 this.forceCheck() 141 this.forceCheck()
112 }, 142 },
113 143
114 err => this.notificationsService.error(this.i18n('Error'), err.message) 144 err => this.notifier.error(err.message)
115 ) 145 )
116 } 146 }
117 147
118 isTranscodingEnabled () { 148 isTranscodingEnabled () {
119 return this.form.value['transcodingEnabled'] === true 149 return this.form.value['transcoding']['enabled'] === true
120 } 150 }
121 151
122 isSignupEnabled () { 152 isSignupEnabled () {
123 return this.form.value['signupEnabled'] === true 153 return this.form.value['signup']['enabled'] === true
124 } 154 }
125 155
126 async formValidated () { 156 async formValidated () {
127 const newCustomizationJavascript = this.form.value['customizationJavascript'] 157 this.configService.updateCustomConfig(this.form.value)
128 const newCustomizationCSS = this.form.value['customizationCSS']
129
130 const customizations = []
131 if (newCustomizationJavascript && newCustomizationJavascript !== this.oldCustomJavascript) customizations.push('JavaScript')
132 if (newCustomizationCSS && newCustomizationCSS !== this.oldCustomCSS) customizations.push('CSS')
133
134 if (customizations.length !== 0) {
135 const customizationsText = customizations.join('/')
136
137 // FIXME: i18n service does not support string concatenation
138 const message = this.i18n('You set custom {{customizationsText}}. ', { customizationsText }) +
139 this.i18n('This could lead to security issues or bugs if you do not understand it. ') +
140 this.i18n('Are you sure you want to update the configuration?')
141
142 const label = this.i18n('Please type') + ` "I understand the ${customizationsText} I set" ` + this.i18n('to confirm.')
143 const expectedInputValue = `I understand the ${customizationsText} I set`
144
145 const confirmRes = await this.confirmService.confirmWithInput(message, label, expectedInputValue)
146 if (confirmRes === false) return
147 }
148
149 const data: CustomConfig = {
150 instance: {
151 name: this.form.value['instanceName'],
152 shortDescription: this.form.value['instanceShortDescription'],
153 description: this.form.value['instanceDescription'],
154 terms: this.form.value['instanceTerms'],
155 defaultClientRoute: this.form.value['instanceDefaultClientRoute'],
156 defaultNSFWPolicy: this.form.value['instanceDefaultNSFWPolicy'],
157 customizations: {
158 javascript: this.form.value['customizationJavascript'],
159 css: this.form.value['customizationCSS']
160 }
161 },
162 services: {
163 twitter: {
164 username: this.form.value['servicesTwitterUsername'],
165 whitelisted: this.form.value['servicesTwitterWhitelisted']
166 }
167 },
168 cache: {
169 previews: {
170 size: this.form.value['cachePreviewsSize']
171 },
172 captions: {
173 size: this.form.value['cacheCaptionsSize']
174 }
175 },
176 signup: {
177 enabled: this.form.value['signupEnabled'],
178 limit: this.form.value['signupLimit'],
179 requiresEmailVerification: this.form.value['signupRequiresEmailVerification']
180 },
181 admin: {
182 email: this.form.value['adminEmail']
183 },
184 user: {
185 videoQuota: this.form.value['userVideoQuota'],
186 videoQuotaDaily: this.form.value['userVideoQuotaDaily']
187 },
188 transcoding: {
189 enabled: this.form.value['transcodingEnabled'],
190 threads: this.form.value['transcodingThreads'],
191 resolutions: {
192 '240p': this.form.value[this.getResolutionKey('240p')],
193 '360p': this.form.value[this.getResolutionKey('360p')],
194 '480p': this.form.value[this.getResolutionKey('480p')],
195 '720p': this.form.value[this.getResolutionKey('720p')],
196 '1080p': this.form.value[this.getResolutionKey('1080p')]
197 }
198 },
199 import: {
200 videos: {
201 http: {
202 enabled: this.form.value['importVideosHttpEnabled']
203 },
204 torrent: {
205 enabled: this.form.value['importVideosTorrentEnabled']
206 }
207 }
208 }
209 }
210
211 this.configService.updateCustomConfig(data)
212 .subscribe( 158 .subscribe(
213 res => { 159 res => {
214 this.customConfig = res 160 this.customConfig = res
@@ -218,45 +164,15 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
218 164
219 this.updateForm() 165 this.updateForm()
220 166
221 this.notificationsService.success(this.i18n('Success'), this.i18n('Configuration updated.')) 167 this.notifier.success(this.i18n('Configuration updated.'))
222 }, 168 },
223 169
224 err => this.notificationsService.error(this.i18n('Error'), err.message) 170 err => this.notifier.error(err.message)
225 ) 171 )
226 } 172 }
227 173
228 private updateForm () { 174 private updateForm () {
229 const data = { 175 this.form.patchValue(this.customConfig)
230 instanceName: this.customConfig.instance.name,
231 instanceShortDescription: this.customConfig.instance.shortDescription,
232 instanceDescription: this.customConfig.instance.description,
233 instanceTerms: this.customConfig.instance.terms,
234 instanceDefaultClientRoute: this.customConfig.instance.defaultClientRoute,
235 instanceDefaultNSFWPolicy: this.customConfig.instance.defaultNSFWPolicy,
236 servicesTwitterUsername: this.customConfig.services.twitter.username,
237 servicesTwitterWhitelisted: this.customConfig.services.twitter.whitelisted,
238 cachePreviewsSize: this.customConfig.cache.previews.size,
239 cacheCaptionsSize: this.customConfig.cache.captions.size,
240 signupEnabled: this.customConfig.signup.enabled,
241 signupLimit: this.customConfig.signup.limit,
242 signupRequiresEmailVerification: this.customConfig.signup.requiresEmailVerification,
243 adminEmail: this.customConfig.admin.email,
244 userVideoQuota: this.customConfig.user.videoQuota,
245 userVideoQuotaDaily: this.customConfig.user.videoQuotaDaily,
246 transcodingThreads: this.customConfig.transcoding.threads,
247 transcodingEnabled: this.customConfig.transcoding.enabled,
248 customizationJavascript: this.customConfig.instance.customizations.javascript,
249 customizationCSS: this.customConfig.instance.customizations.css,
250 importVideosHttpEnabled: this.customConfig.import.videos.http.enabled,
251 importVideosTorrentEnabled: this.customConfig.import.videos.torrent.enabled
252 }
253
254 for (const resolution of this.resolutions) {
255 const key = this.getResolutionKey(resolution)
256 data[key] = this.customConfig.transcoding.resolutions[resolution]
257 }
258
259 this.form.patchValue(data)
260 } 176 }
261 177
262} 178}
diff --git a/client/src/app/+admin/follows/followers-list/followers-list.component.html b/client/src/app/+admin/follows/followers-list/followers-list.component.html
index 5645a60cc..fc022bdb4 100644
--- a/client/src/app/+admin/follows/followers-list/followers-list.component.html
+++ b/client/src/app/+admin/follows/followers-list/followers-list.component.html
@@ -2,6 +2,15 @@
2 [value]="followers" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage" 2 [value]="followers" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
3 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" 3 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)"
4> 4>
5 <ng-template pTemplate="caption">
6 <div class="caption">
7 <input
8 type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
9 (keyup)="onSearch($event.target.value)"
10 >
11 </div>
12 </ng-template>
13
5 <ng-template pTemplate="header"> 14 <ng-template pTemplate="header">
6 <tr> 15 <tr>
7 <th i18n style="width: 60px">ID</th> 16 <th i18n style="width: 60px">ID</th>
diff --git a/client/src/app/+admin/follows/followers-list/followers-list.component.scss b/client/src/app/+admin/follows/followers-list/followers-list.component.scss
index e69de29bb..a6f0656b8 100644
--- a/client/src/app/+admin/follows/followers-list/followers-list.component.scss
+++ b/client/src/app/+admin/follows/followers-list/followers-list.component.scss
@@ -0,0 +1,10 @@
1@import '_variables';
2@import '_mixins';
3
4.caption {
5 justify-content: flex-end;
6
7 input {
8 @include peertube-input-text(250px);
9 }
10} \ No newline at end of file
diff --git a/client/src/app/+admin/follows/followers-list/followers-list.component.ts b/client/src/app/+admin/follows/followers-list/followers-list.component.ts
index ca993dcd3..9a8848bfb 100644
--- a/client/src/app/+admin/follows/followers-list/followers-list.component.ts
+++ b/client/src/app/+admin/follows/followers-list/followers-list.component.ts
@@ -1,6 +1,6 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2 2
3import { NotificationsService } from 'angular2-notifications' 3import { Notifier } from '@app/core'
4import { SortMeta } from 'primeng/primeng' 4import { SortMeta } from 'primeng/primeng'
5import { ActorFollow } from '../../../../../../shared/models/actors/follow.model' 5import { ActorFollow } from '../../../../../../shared/models/actors/follow.model'
6import { RestPagination, RestTable } from '../../../shared' 6import { RestPagination, RestTable } from '../../../shared'
@@ -20,7 +20,7 @@ export class FollowersListComponent extends RestTable implements OnInit {
20 pagination: RestPagination = { count: this.rowsPerPage, start: 0 } 20 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
21 21
22 constructor ( 22 constructor (
23 private notificationsService: NotificationsService, 23 private notifier: Notifier,
24 private followService: FollowService, 24 private followService: FollowService,
25 private i18n: I18n 25 private i18n: I18n
26 ) { 26 ) {
@@ -28,18 +28,18 @@ export class FollowersListComponent extends RestTable implements OnInit {
28 } 28 }
29 29
30 ngOnInit () { 30 ngOnInit () {
31 this.loadSort() 31 this.initialize()
32 } 32 }
33 33
34 protected loadData () { 34 protected loadData () {
35 this.followService.getFollowers(this.pagination, this.sort) 35 this.followService.getFollowers(this.pagination, this.sort, this.search)
36 .subscribe( 36 .subscribe(
37 resultList => { 37 resultList => {
38 this.followers = resultList.data 38 this.followers = resultList.data
39 this.totalRecords = resultList.total 39 this.totalRecords = resultList.total
40 }, 40 },
41 41
42 err => this.notificationsService.error(this.i18n('Error'), err.message) 42 err => this.notifier.error(err.message)
43 ) 43 )
44 } 44 }
45} 45}
diff --git a/client/src/app/+admin/follows/following-add/following-add.component.ts b/client/src/app/+admin/follows/following-add/following-add.component.ts
index bd9cc022b..2bb249746 100644
--- a/client/src/app/+admin/follows/following-add/following-add.component.ts
+++ b/client/src/app/+admin/follows/following-add/following-add.component.ts
@@ -1,6 +1,6 @@
1import { Component } from '@angular/core' 1import { Component } from '@angular/core'
2import { Router } from '@angular/router' 2import { Router } from '@angular/router'
3import { NotificationsService } from 'angular2-notifications' 3import { Notifier } from '@app/core'
4import { ConfirmService } from '../../../core' 4import { ConfirmService } from '../../../core'
5import { validateHost } from '../../../shared' 5import { validateHost } from '../../../shared'
6import { FollowService } from '../shared' 6import { FollowService } from '../shared'
@@ -18,7 +18,7 @@ export class FollowingAddComponent {
18 18
19 constructor ( 19 constructor (
20 private router: Router, 20 private router: Router,
21 private notificationsService: NotificationsService, 21 private notifier: Notifier,
22 private confirmService: ConfirmService, 22 private confirmService: ConfirmService,
23 private followService: FollowService, 23 private followService: FollowService,
24 private i18n: I18n 24 private i18n: I18n
@@ -64,12 +64,12 @@ export class FollowingAddComponent {
64 64
65 this.followService.follow(hosts).subscribe( 65 this.followService.follow(hosts).subscribe(
66 () => { 66 () => {
67 this.notificationsService.success(this.i18n('Success'), this.i18n('Follow request(s) sent!')) 67 this.notifier.success(this.i18n('Follow request(s) sent!'))
68 68
69 setTimeout(() => this.router.navigate([ '/admin/follows/following-list' ]), 500) 69 setTimeout(() => this.router.navigate([ '/admin/follows/following-list' ]), 500)
70 }, 70 },
71 71
72 err => this.notificationsService.error(this.i18n('Error'), err.message) 72 err => this.notifier.error(err.message)
73 ) 73 )
74 } 74 }
75 75
diff --git a/client/src/app/+admin/follows/following-list/following-list.component.html b/client/src/app/+admin/follows/following-list/following-list.component.html
index 8af624ac5..5bc8fbc2d 100644
--- a/client/src/app/+admin/follows/following-list/following-list.component.html
+++ b/client/src/app/+admin/follows/following-list/following-list.component.html
@@ -2,6 +2,17 @@
2 [value]="following" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage" 2 [value]="following" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
3 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" 3 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)"
4> 4>
5 <ng-template pTemplate="caption">
6 <div class="caption">
7 <div>
8 <input
9 type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
10 (keyup)="onSearch($event.target.value)"
11 >
12 </div>
13 </div>
14 </ng-template>
15
5 <ng-template pTemplate="header"> 16 <ng-template pTemplate="header">
6 <tr> 17 <tr>
7 <th i18n style="width: 60px">ID</th> 18 <th i18n style="width: 60px">ID</th>
diff --git a/client/src/app/+admin/follows/following-list/following-list.component.scss b/client/src/app/+admin/follows/following-list/following-list.component.scss
index bfcdcaa49..a6f0656b8 100644
--- a/client/src/app/+admin/follows/following-list/following-list.component.scss
+++ b/client/src/app/+admin/follows/following-list/following-list.component.scss
@@ -1,13 +1,10 @@
1@import '_variables'; 1@import '_variables';
2@import '_mixins'; 2@import '_mixins';
3 3
4my-redundancy-checkbox /deep/ my-peertube-checkbox { 4.caption {
5 .form-group { 5 justify-content: flex-end;
6 margin-bottom: 0;
7 align-items: center;
8 }
9 6
10 label { 7 input {
11 margin: 0; 8 @include peertube-input-text(250px);
12 } 9 }
13} \ No newline at end of file 10} \ No newline at end of file
diff --git a/client/src/app/+admin/follows/following-list/following-list.component.ts b/client/src/app/+admin/follows/following-list/following-list.component.ts
index dd57884c6..4517a721e 100644
--- a/client/src/app/+admin/follows/following-list/following-list.component.ts
+++ b/client/src/app/+admin/follows/following-list/following-list.component.ts
@@ -1,5 +1,5 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications' 2import { Notifier } from '@app/core'
3import { SortMeta } from 'primeng/primeng' 3import { SortMeta } from 'primeng/primeng'
4import { ActorFollow } from '../../../../../../shared/models/actors/follow.model' 4import { ActorFollow } from '../../../../../../shared/models/actors/follow.model'
5import { ConfirmService } from '../../../core/confirm/confirm.service' 5import { ConfirmService } from '../../../core/confirm/confirm.service'
@@ -20,7 +20,7 @@ export class FollowingListComponent extends RestTable implements OnInit {
20 pagination: RestPagination = { count: this.rowsPerPage, start: 0 } 20 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
21 21
22 constructor ( 22 constructor (
23 private notificationsService: NotificationsService, 23 private notifier: Notifier,
24 private confirmService: ConfirmService, 24 private confirmService: ConfirmService,
25 private followService: FollowService, 25 private followService: FollowService,
26 private i18n: I18n 26 private i18n: I18n
@@ -29,7 +29,7 @@ export class FollowingListComponent extends RestTable implements OnInit {
29 } 29 }
30 30
31 ngOnInit () { 31 ngOnInit () {
32 this.loadSort() 32 this.initialize()
33 } 33 }
34 34
35 async removeFollowing (follow: ActorFollow) { 35 async removeFollowing (follow: ActorFollow) {
@@ -41,26 +41,23 @@ export class FollowingListComponent extends RestTable implements OnInit {
41 41
42 this.followService.unfollow(follow).subscribe( 42 this.followService.unfollow(follow).subscribe(
43 () => { 43 () => {
44 this.notificationsService.success( 44 this.notifier.success(this.i18n('You are not following {{host}} anymore.', { host: follow.following.host }))
45 this.i18n('Success'),
46 this.i18n('You are not following {{host}} anymore.', { host: follow.following.host })
47 )
48 this.loadData() 45 this.loadData()
49 }, 46 },
50 47
51 err => this.notificationsService.error(this.i18n('Error'), err.message) 48 err => this.notifier.error(err.message)
52 ) 49 )
53 } 50 }
54 51
55 protected loadData () { 52 protected loadData () {
56 this.followService.getFollowing(this.pagination, this.sort) 53 this.followService.getFollowing(this.pagination, this.sort, this.search)
57 .subscribe( 54 .subscribe(
58 resultList => { 55 resultList => {
59 this.following = resultList.data 56 this.following = resultList.data
60 this.totalRecords = resultList.total 57 this.totalRecords = resultList.total
61 }, 58 },
62 59
63 err => this.notificationsService.error(this.i18n('Error'), err.message) 60 err => this.notifier.error(err.message)
64 ) 61 )
65 } 62 }
66} 63}
diff --git a/client/src/app/+admin/follows/shared/follow.service.ts b/client/src/app/+admin/follows/shared/follow.service.ts
index 27169a9cd..a2904179e 100644
--- a/client/src/app/+admin/follows/shared/follow.service.ts
+++ b/client/src/app/+admin/follows/shared/follow.service.ts
@@ -18,10 +18,12 @@ export class FollowService {
18 ) { 18 ) {
19 } 19 }
20 20
21 getFollowing (pagination: RestPagination, sort: SortMeta): Observable<ResultList<ActorFollow>> { 21 getFollowing (pagination: RestPagination, sort: SortMeta, search?: string): Observable<ResultList<ActorFollow>> {
22 let params = new HttpParams() 22 let params = new HttpParams()
23 params = this.restService.addRestGetParams(params, pagination, sort) 23 params = this.restService.addRestGetParams(params, pagination, sort)
24 24
25 if (search) params = params.append('search', search)
26
25 return this.authHttp.get<ResultList<ActorFollow>>(FollowService.BASE_APPLICATION_URL + '/following', { params }) 27 return this.authHttp.get<ResultList<ActorFollow>>(FollowService.BASE_APPLICATION_URL + '/following', { params })
26 .pipe( 28 .pipe(
27 map(res => this.restExtractor.convertResultListDateToHuman(res)), 29 map(res => this.restExtractor.convertResultListDateToHuman(res)),
@@ -29,10 +31,12 @@ export class FollowService {
29 ) 31 )
30 } 32 }
31 33
32 getFollowers (pagination: RestPagination, sort: SortMeta): Observable<ResultList<ActorFollow>> { 34 getFollowers (pagination: RestPagination, sort: SortMeta, search?: string): Observable<ResultList<ActorFollow>> {
33 let params = new HttpParams() 35 let params = new HttpParams()
34 params = this.restService.addRestGetParams(params, pagination, sort) 36 params = this.restService.addRestGetParams(params, pagination, sort)
35 37
38 if (search) params = params.append('search', search)
39
36 return this.authHttp.get<ResultList<ActorFollow>>(FollowService.BASE_APPLICATION_URL + '/followers', { params }) 40 return this.authHttp.get<ResultList<ActorFollow>>(FollowService.BASE_APPLICATION_URL + '/followers', { params })
37 .pipe( 41 .pipe(
38 map(res => this.restExtractor.convertResultListDateToHuman(res)), 42 map(res => this.restExtractor.convertResultListDateToHuman(res)),
diff --git a/client/src/app/+admin/follows/shared/redundancy-checkbox.component.ts b/client/src/app/+admin/follows/shared/redundancy-checkbox.component.ts
index 6d77a0eb4..fa1da26bf 100644
--- a/client/src/app/+admin/follows/shared/redundancy-checkbox.component.ts
+++ b/client/src/app/+admin/follows/shared/redundancy-checkbox.component.ts
@@ -1,5 +1,5 @@
1import { Component, Input } from '@angular/core' 1import { Component, Input } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications' 2import { Notifier } from '@app/core'
3import { I18n } from '@ngx-translate/i18n-polyfill' 3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service' 4import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service'
5 5
@@ -13,24 +13,21 @@ export class RedundancyCheckboxComponent {
13 @Input() host: string 13 @Input() host: string
14 14
15 constructor ( 15 constructor (
16 private notificationsService: NotificationsService, 16 private notifier: Notifier,
17 private redundancyService: RedundancyService, 17 private redundancyService: RedundancyService,
18 private i18n: I18n 18 private i18n: I18n
19 ) { } 19 ) { }
20 20
21 updateRedundancyState () { 21 updateRedundancyState () {
22 this.redundancyService.updateRedundancy(this.host, this.redundancyAllowed) 22 this.redundancyService.updateRedundancy(this.host, this.redundancyAllowed)
23 .subscribe( 23 .subscribe(
24 () => { 24 () => {
25 const stateLabel = this.redundancyAllowed ? this.i18n('enabled') : this.i18n('disabled') 25 const stateLabel = this.redundancyAllowed ? this.i18n('enabled') : this.i18n('disabled')
26 26
27 this.notificationsService.success( 27 this.notifier.success(this.i18n('Redundancy for {{host}} is {{stateLabel}}', { host: this.host, stateLabel }))
28 this.i18n('Success'), 28 },
29 this.i18n('Redundancy for {{host}} is {{stateLabel}}', { host: this.host, stateLabel })
30 )
31 },
32 29
33 err => this.notificationsService.error(this.i18n('Error'), err.message) 30 err => this.notifier.error(err.message)
34 ) 31 )
35 } 32 }
36} 33}
diff --git a/client/src/app/+admin/jobs/jobs-list/jobs-list.component.ts b/client/src/app/+admin/jobs/jobs-list/jobs-list.component.ts
index 866ba1b23..b265e1dd6 100644
--- a/client/src/app/+admin/jobs/jobs-list/jobs-list.component.ts
+++ b/client/src/app/+admin/jobs/jobs-list/jobs-list.component.ts
@@ -1,6 +1,6 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage' 2import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage'
3import { NotificationsService } from 'angular2-notifications' 3import { Notifier } from '@app/core'
4import { SortMeta } from 'primeng/primeng' 4import { SortMeta } from 'primeng/primeng'
5import { Job } from '../../../../../../shared/index' 5import { Job } from '../../../../../../shared/index'
6import { JobState } from '../../../../../../shared/models' 6import { JobState } from '../../../../../../shared/models'
@@ -25,7 +25,7 @@ export class JobsListComponent extends RestTable implements OnInit {
25 pagination: RestPagination = { count: this.rowsPerPage, start: 0 } 25 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
26 26
27 constructor ( 27 constructor (
28 private notificationsService: NotificationsService, 28 private notifier: Notifier,
29 private jobsService: JobService, 29 private jobsService: JobService,
30 private i18n: I18n 30 private i18n: I18n
31 ) { 31 ) {
@@ -34,7 +34,7 @@ export class JobsListComponent extends RestTable implements OnInit {
34 34
35 ngOnInit () { 35 ngOnInit () {
36 this.loadJobState() 36 this.loadJobState()
37 this.loadSort() 37 this.initialize()
38 } 38 }
39 39
40 onJobStateChanged () { 40 onJobStateChanged () {
@@ -53,7 +53,7 @@ export class JobsListComponent extends RestTable implements OnInit {
53 this.totalRecords = resultList.total 53 this.totalRecords = resultList.total
54 }, 54 },
55 55
56 err => this.notificationsService.error(this.i18n('Error'), err.message) 56 err => this.notifier.error(err.message)
57 ) 57 )
58 } 58 }
59 59
diff --git a/client/src/app/+admin/moderation/instance-blocklist/index.ts b/client/src/app/+admin/moderation/instance-blocklist/index.ts
new file mode 100644
index 000000000..3e7a344bb
--- /dev/null
+++ b/client/src/app/+admin/moderation/instance-blocklist/index.ts
@@ -0,0 +1,2 @@
1export * from './instance-account-blocklist.component'
2export * from './instance-server-blocklist.component'
diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.html b/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.html
new file mode 100644
index 000000000..7797bc56e
--- /dev/null
+++ b/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.html
@@ -0,0 +1,22 @@
1<p-table
2 [value]="blockedAccounts" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
3 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)"
4>
5
6 <ng-template pTemplate="header">
7 <tr>
8 <th i18n>Account</th>
9 <th i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th>
10 </tr>
11 </ng-template>
12
13 <ng-template pTemplate="body" let-accountBlock>
14 <tr>
15 <td>{{ accountBlock.blockedAccount.nameWithHost }}</td>
16 <td>{{ accountBlock.createdAt }}</td>
17 <td class="action-cell">
18 <button class="unblock-button" (click)="unblockAccount(accountBlock)" i18n>Unmute</button>
19 </td>
20 </tr>
21 </ng-template>
22</p-table>
diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.scss b/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.scss
new file mode 100644
index 000000000..6028b75ea
--- /dev/null
+++ b/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.scss
@@ -0,0 +1,7 @@
1@import '_variables';
2@import '_mixins';
3
4.unblock-button {
5 @include peertube-button;
6 @include grey-button;
7} \ No newline at end of file
diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.ts b/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.ts
new file mode 100644
index 000000000..032bf745a
--- /dev/null
+++ b/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.ts
@@ -0,0 +1,58 @@
1import { Component, OnInit } from '@angular/core'
2import { Notifier } from '@app/core'
3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { RestPagination, RestTable } from '@app/shared'
5import { SortMeta } from 'primeng/components/common/sortmeta'
6import { AccountBlock, BlocklistService } from '@app/shared/blocklist'
7
8@Component({
9 selector: 'my-instance-account-blocklist',
10 styleUrls: [ './instance-account-blocklist.component.scss' ],
11 templateUrl: './instance-account-blocklist.component.html'
12})
13export class InstanceAccountBlocklistComponent extends RestTable implements OnInit {
14 blockedAccounts: AccountBlock[] = []
15 totalRecords = 0
16 rowsPerPage = 10
17 sort: SortMeta = { field: 'createdAt', order: -1 }
18 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
19
20 constructor (
21 private notifier: Notifier,
22 private blocklistService: BlocklistService,
23 private i18n: I18n
24 ) {
25 super()
26 }
27
28 ngOnInit () {
29 this.initialize()
30 }
31
32 unblockAccount (accountBlock: AccountBlock) {
33 const blockedAccount = accountBlock.blockedAccount
34
35 this.blocklistService.unblockAccountByInstance(blockedAccount)
36 .subscribe(
37 () => {
38 this.notifier.success(
39 this.i18n('Account {{nameWithHost}} unmuted by your instance.', { nameWithHost: blockedAccount.nameWithHost })
40 )
41
42 this.loadData()
43 }
44 )
45 }
46
47 protected loadData () {
48 return this.blocklistService.getInstanceAccountBlocklist(this.pagination, this.sort)
49 .subscribe(
50 resultList => {
51 this.blockedAccounts = resultList.data
52 this.totalRecords = resultList.total
53 },
54
55 err => this.notifier.error(err.message)
56 )
57 }
58}
diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html
new file mode 100644
index 000000000..f634ba834
--- /dev/null
+++ b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html
@@ -0,0 +1,23 @@
1<p-table
2 [value]="blockedServers" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
3 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)"
4>
5
6 <ng-template pTemplate="header">
7 <tr>
8 <th i18n>Instance</th>
9 <th i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th>
10 <th></th>
11 </tr>
12 </ng-template>
13
14 <ng-template pTemplate="body" let-serverBlock>
15 <tr>
16 <td>{{ serverBlock.blockedServer.host }}</td>
17 <td>{{ serverBlock.createdAt }}</td>
18 <td class="action-cell">
19 <button class="unblock-button" (click)="unblockServer(serverBlock)" i18n>Unmute</button>
20 </td>
21 </tr>
22 </ng-template>
23</p-table>
diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.scss b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.scss
new file mode 100644
index 000000000..6028b75ea
--- /dev/null
+++ b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.scss
@@ -0,0 +1,7 @@
1@import '_variables';
2@import '_mixins';
3
4.unblock-button {
5 @include peertube-button;
6 @include grey-button;
7} \ No newline at end of file
diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.ts b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.ts
new file mode 100644
index 000000000..db3dfcd1c
--- /dev/null
+++ b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.ts
@@ -0,0 +1,57 @@
1import { Component, OnInit } from '@angular/core'
2import { Notifier } from '@app/core'
3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { RestPagination, RestTable } from '@app/shared'
5import { SortMeta } from 'primeng/components/common/sortmeta'
6import { BlocklistService } from '@app/shared/blocklist'
7import { ServerBlock } from '../../../../../../shared'
8
9@Component({
10 selector: 'my-instance-server-blocklist',
11 styleUrls: [ './instance-server-blocklist.component.scss' ],
12 templateUrl: './instance-server-blocklist.component.html'
13})
14export class InstanceServerBlocklistComponent extends RestTable implements OnInit {
15 blockedServers: ServerBlock[] = []
16 totalRecords = 0
17 rowsPerPage = 10
18 sort: SortMeta = { field: 'createdAt', order: -1 }
19 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
20
21 constructor (
22 private notifier: Notifier,
23 private blocklistService: BlocklistService,
24 private i18n: I18n
25 ) {
26 super()
27 }
28
29 ngOnInit () {
30 this.initialize()
31 }
32
33 unblockServer (serverBlock: ServerBlock) {
34 const host = serverBlock.blockedServer.host
35
36 this.blocklistService.unblockServerByInstance(host)
37 .subscribe(
38 () => {
39 this.notifier.success(this.i18n('Instance {{host}} unmuted by your instance.', { host }))
40
41 this.loadData()
42 }
43 )
44 }
45
46 protected loadData () {
47 return this.blocklistService.getInstanceServerBlocklist(this.pagination, this.sort)
48 .subscribe(
49 resultList => {
50 this.blockedServers = resultList.data
51 this.totalRecords = resultList.total
52 },
53
54 err => this.notifier.error(err.message)
55 )
56 }
57}
diff --git a/client/src/app/+admin/moderation/moderation.component.html b/client/src/app/+admin/moderation/moderation.component.html
index 91e87fcd4..01457936c 100644
--- a/client/src/app/+admin/moderation/moderation.component.html
+++ b/client/src/app/+admin/moderation/moderation.component.html
@@ -5,6 +5,10 @@
5 <a *ngIf="hasVideoAbusesRight()" i18n routerLink="video-abuses/list" routerLinkActive="active">Video abuses</a> 5 <a *ngIf="hasVideoAbusesRight()" i18n routerLink="video-abuses/list" routerLinkActive="active">Video abuses</a>
6 6
7 <a *ngIf="hasVideoBlacklistRight()" i18n routerLink="video-blacklist/list" routerLinkActive="active">Blacklisted videos</a> 7 <a *ngIf="hasVideoBlacklistRight()" i18n routerLink="video-blacklist/list" routerLinkActive="active">Blacklisted videos</a>
8
9 <a *ngIf="hasAccountsBlocklistRight()" i18n routerLink="blocklist/accounts" routerLinkActive="active">Muted accounts</a>
10
11 <a *ngIf="hasServersBlocklistRight()" i18n routerLink="blocklist/servers" routerLinkActive="active">Muted servers</a>
8 </div> 12 </div>
9</div> 13</div>
10 14
diff --git a/client/src/app/+admin/moderation/moderation.component.scss b/client/src/app/+admin/moderation/moderation.component.scss
index 02ccfc8ca..13b019c5b 100644
--- a/client/src/app/+admin/moderation/moderation.component.scss
+++ b/client/src/app/+admin/moderation/moderation.component.scss
@@ -10,6 +10,7 @@
10 font-weight: $font-semibold; 10 font-weight: $font-semibold;
11 min-width: 200px; 11 min-width: 200px;
12 display: inline-block; 12 display: inline-block;
13 vertical-align: top;
13} 14}
14 15
15.moderation-expanded-text { 16.moderation-expanded-text {
diff --git a/client/src/app/+admin/moderation/moderation.component.ts b/client/src/app/+admin/moderation/moderation.component.ts
index 0f4efb970..2b2618933 100644
--- a/client/src/app/+admin/moderation/moderation.component.ts
+++ b/client/src/app/+admin/moderation/moderation.component.ts
@@ -16,4 +16,12 @@ export class ModerationComponent {
16 hasVideoBlacklistRight () { 16 hasVideoBlacklistRight () {
17 return this.auth.getUser().hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) 17 return this.auth.getUser().hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)
18 } 18 }
19
20 hasAccountsBlocklistRight () {
21 return this.auth.getUser().hasRight(UserRight.MANAGE_ACCOUNTS_BLOCKLIST)
22 }
23
24 hasServersBlocklistRight () {
25 return this.auth.getUser().hasRight(UserRight.MANAGE_SERVERS_BLOCKLIST)
26 }
19} 27}
diff --git a/client/src/app/+admin/moderation/moderation.routes.ts b/client/src/app/+admin/moderation/moderation.routes.ts
index 6d81b9b36..bc6dd49d5 100644
--- a/client/src/app/+admin/moderation/moderation.routes.ts
+++ b/client/src/app/+admin/moderation/moderation.routes.ts
@@ -4,6 +4,7 @@ import { UserRightGuard } from '@app/core'
4import { VideoAbuseListComponent } from '@app/+admin/moderation/video-abuse-list' 4import { VideoAbuseListComponent } from '@app/+admin/moderation/video-abuse-list'
5import { VideoBlacklistListComponent } from '@app/+admin/moderation/video-blacklist-list' 5import { VideoBlacklistListComponent } from '@app/+admin/moderation/video-blacklist-list'
6import { ModerationComponent } from '@app/+admin/moderation/moderation.component' 6import { ModerationComponent } from '@app/+admin/moderation/moderation.component'
7import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from '@app/+admin/moderation/instance-blocklist'
7 8
8export const ModerationRoutes: Routes = [ 9export const ModerationRoutes: Routes = [
9 { 10 {
@@ -46,6 +47,28 @@ export const ModerationRoutes: Routes = [
46 title: 'Blacklisted videos' 47 title: 'Blacklisted videos'
47 } 48 }
48 } 49 }
50 },
51 {
52 path: 'blocklist/accounts',
53 component: InstanceAccountBlocklistComponent,
54 canActivate: [ UserRightGuard ],
55 data: {
56 userRight: UserRight.MANAGE_ACCOUNTS_BLOCKLIST,
57 meta: {
58 title: 'Muted accounts'
59 }
60 }
61 },
62 {
63 path: 'blocklist/servers',
64 component: InstanceServerBlocklistComponent,
65 canActivate: [ UserRightGuard ],
66 data: {
67 userRight: UserRight.MANAGE_SERVER_REDUNDANCY,
68 meta: {
69 title: 'Muted instances'
70 }
71 }
49 } 72 }
50 ] 73 ]
51 } 74 }
diff --git a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.html b/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.html
index 3a8424f68..303a788d2 100644
--- a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.html
+++ b/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.html
@@ -1,7 +1,8 @@
1<ng-template #modal> 1<ng-template #modal>
2 <div class="modal-header"> 2 <div class="modal-header">
3 <h4 i18n class="modal-title">Moderation comment</h4> 3 <h4 i18n class="modal-title">Moderation comment</h4>
4 <span class="close" aria-hidden="true" (click)="hideModerationCommentModal()"></span> 4
5 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
5 </div> 6 </div>
6 7
7 <div class="modal-body"> 8 <div class="modal-body">
@@ -14,12 +15,12 @@
14 </div> 15 </div>
15 </div> 16 </div>
16 17
17 <div i18n> 18 <div class="form-group" i18n>
18 This comment can only be seen by you or the other moderators. 19 This comment can only be seen by you or the other moderators.
19 </div> 20 </div>
20 21
21 <div class="form-group inputs"> 22 <div class="form-group inputs">
22 <span i18n class="action-button action-button-cancel" (click)="hideModerationCommentModal()">Cancel</span> 23 <span i18n class="action-button action-button-cancel" (click)="hide()">Cancel</span>
23 24
24 <input 25 <input
25 type="submit" i18n-value value="Update this comment" class="action-button-submit" 26 type="submit" i18n-value value="Update this comment" class="action-button-submit"
@@ -29,4 +30,4 @@
29 </form> 30 </form>
30 </div> 31 </div>
31 32
32</ng-template> \ No newline at end of file 33</ng-template>
diff --git a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts b/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts
index 34ab384d1..f915978ee 100644
--- a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts
+++ b/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts
@@ -1,5 +1,5 @@
1import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' 1import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications' 2import { Notifier } from '@app/core'
3import { FormReactive, VideoAbuseService, VideoAbuseValidatorsService } from '../../../shared' 3import { FormReactive, VideoAbuseService, VideoAbuseValidatorsService } from '../../../shared'
4import { I18n } from '@ngx-translate/i18n-polyfill' 4import { I18n } from '@ngx-translate/i18n-polyfill'
5import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 5import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
@@ -22,7 +22,7 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI
22 constructor ( 22 constructor (
23 protected formValidatorService: FormValidatorService, 23 protected formValidatorService: FormValidatorService,
24 private modalService: NgbModal, 24 private modalService: NgbModal,
25 private notificationsService: NotificationsService, 25 private notifier: Notifier,
26 private videoAbuseService: VideoAbuseService, 26 private videoAbuseService: VideoAbuseService,
27 private videoAbuseValidatorsService: VideoAbuseValidatorsService, 27 private videoAbuseValidatorsService: VideoAbuseValidatorsService,
28 private i18n: I18n 28 private i18n: I18n
@@ -45,29 +45,26 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI
45 }) 45 })
46 } 46 }
47 47
48 hideModerationCommentModal () { 48 hide () {
49 this.abuseToComment = undefined 49 this.abuseToComment = undefined
50 this.openedModal.close() 50 this.openedModal.close()
51 this.form.reset() 51 this.form.reset()
52 } 52 }
53 53
54 async banUser () { 54 async banUser () {
55 const moderationComment: string = this.form.value['moderationComment'] 55 const moderationComment: string = this.form.value[ 'moderationComment' ]
56 56
57 this.videoAbuseService.updateVideoAbuse(this.abuseToComment, { moderationComment }) 57 this.videoAbuseService.updateVideoAbuse(this.abuseToComment, { moderationComment })
58 .subscribe( 58 .subscribe(
59 () => { 59 () => {
60 this.notificationsService.success( 60 this.notifier.success(this.i18n('Comment updated.'))
61 this.i18n('Success'),
62 this.i18n('Comment updated.')
63 )
64 61
65 this.commentUpdated.emit(moderationComment) 62 this.commentUpdated.emit(moderationComment)
66 this.hideModerationCommentModal() 63 this.hide()
67 }, 64 },
68 65
69 err => this.notificationsService.error(this.i18n('Error'), err.message) 66 err => this.notifier.error(err.message)
70 ) 67 )
71 } 68 }
72 69
73} 70}
diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html
index 287ab3e46..05b549de6 100644
--- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html
+++ b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html
@@ -9,7 +9,7 @@
9 <th i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th> 9 <th i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
10 <th i18n>Video</th> 10 <th i18n>Video</th>
11 <th i18n pSortableColumn="state" style="width: 80px;">State <p-sortIcon field="state"></p-sortIcon></th> 11 <th i18n pSortableColumn="state" style="width: 80px;">State <p-sortIcon field="state"></p-sortIcon></th>
12 <th style="width: 50px;"></th> 12 <th style="width: 120px;"></th>
13 </tr> 13 </tr>
14 </ng-template> 14 </ng-template>
15 15
@@ -41,7 +41,7 @@
41 </td> 41 </td>
42 42
43 <td class="action-cell"> 43 <td class="action-cell">
44 <my-action-dropdown i18n-label label="Actions" [actions]="videoAbuseActions" [entry]="videoAbuse"></my-action-dropdown> 44 <my-action-dropdown placement="bottom-right" i18n-label label="Actions" [actions]="videoAbuseActions" [entry]="videoAbuse"></my-action-dropdown>
45 </td> 45 </td>
46 </tr> 46 </tr>
47 </ng-template> 47 </ng-template>
@@ -51,15 +51,15 @@
51 <td class="moderation-expanded" colspan="6"> 51 <td class="moderation-expanded" colspan="6">
52 <div> 52 <div>
53 <span i18n class="moderation-expanded-label">Reason:</span> 53 <span i18n class="moderation-expanded-label">Reason:</span>
54 <span class="moderation-expanded-text">{{ videoAbuse.reason }}</span> 54 <span class="moderation-expanded-text" [innerHTML]="toHtml(videoAbuse.reason)"></span>
55 </div> 55 </div>
56 <div *ngIf="videoAbuse.moderationComment"> 56 <div *ngIf="videoAbuse.moderationComment">
57 <span i18n class="moderation-expanded-label">Moderation comment:</span> 57 <span i18n class="moderation-expanded-label">Moderation comment:</span>
58 <span class="moderation-expanded-text">{{ videoAbuse.moderationComment }}</span> 58 <span class="moderation-expanded-text" [innerHTML]="toHtml(videoAbuse.moderationComment)"></span>
59 </div> 59 </div>
60 </td> 60 </td>
61 </tr> 61 </tr>
62 </ng-template> 62 </ng-template>
63</p-table> 63</p-table>
64 64
65<my-moderation-comment-modal #moderationCommentModal (commentUpdated)="onModerationCommentUpdated()"></my-moderation-comment-modal> \ No newline at end of file 65<my-moderation-comment-modal #moderationCommentModal (commentUpdated)="onModerationCommentUpdated()"></my-moderation-comment-modal>
diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts
index 681db7434..00c871659 100644
--- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts
+++ b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts
@@ -1,6 +1,6 @@
1import { Component, OnInit, ViewChild } from '@angular/core' 1import { Component, OnInit, ViewChild } from '@angular/core'
2import { Account } from '../../../shared/account/account.model' 2import { Account } from '../../../shared/account/account.model'
3import { NotificationsService } from 'angular2-notifications' 3import { Notifier } from '@app/core'
4import { SortMeta } from 'primeng/components/common/sortmeta' 4import { SortMeta } from 'primeng/components/common/sortmeta'
5import { VideoAbuse, VideoAbuseState } from '../../../../../../shared' 5import { VideoAbuse, VideoAbuseState } from '../../../../../../shared'
6import { RestPagination, RestTable, VideoAbuseService } from '../../../shared' 6import { RestPagination, RestTable, VideoAbuseService } from '../../../shared'
@@ -9,6 +9,7 @@ import { DropdownAction } from '../../../shared/buttons/action-dropdown.componen
9import { ConfirmService } from '../../../core/index' 9import { ConfirmService } from '../../../core/index'
10import { ModerationCommentModalComponent } from './moderation-comment-modal.component' 10import { ModerationCommentModalComponent } from './moderation-comment-modal.component'
11import { Video } from '../../../shared/video/video.model' 11import { Video } from '../../../shared/video/video.model'
12import { MarkdownService } from '@app/shared/renderer'
12 13
13@Component({ 14@Component({
14 selector: 'my-video-abuse-list', 15 selector: 'my-video-abuse-list',
@@ -27,16 +28,17 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
27 videoAbuseActions: DropdownAction<VideoAbuse>[] = [] 28 videoAbuseActions: DropdownAction<VideoAbuse>[] = []
28 29
29 constructor ( 30 constructor (
30 private notificationsService: NotificationsService, 31 private notifier: Notifier,
31 private videoAbuseService: VideoAbuseService, 32 private videoAbuseService: VideoAbuseService,
32 private confirmService: ConfirmService, 33 private confirmService: ConfirmService,
33 private i18n: I18n 34 private i18n: I18n,
35 private markdownRenderer: MarkdownService
34 ) { 36 ) {
35 super() 37 super()
36 38
37 this.videoAbuseActions = [ 39 this.videoAbuseActions = [
38 { 40 {
39 label: this.i18n('Delete'), 41 label: this.i18n('Delete this report'),
40 handler: videoAbuse => this.removeVideoAbuse(videoAbuse) 42 handler: videoAbuse => this.removeVideoAbuse(videoAbuse)
41 }, 43 },
42 { 44 {
@@ -57,7 +59,7 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
57 } 59 }
58 60
59 ngOnInit () { 61 ngOnInit () {
60 this.loadSort() 62 this.initialize()
61 } 63 }
62 64
63 openModerationCommentModal (videoAbuse: VideoAbuse) { 65 openModerationCommentModal (videoAbuse: VideoAbuse) {
@@ -85,19 +87,16 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
85 } 87 }
86 88
87 async removeVideoAbuse (videoAbuse: VideoAbuse) { 89 async removeVideoAbuse (videoAbuse: VideoAbuse) {
88 const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this abuse?'), this.i18n('Delete')) 90 const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this abuse report?'), this.i18n('Delete'))
89 if (res === false) return 91 if (res === false) return
90 92
91 this.videoAbuseService.removeVideoAbuse(videoAbuse).subscribe( 93 this.videoAbuseService.removeVideoAbuse(videoAbuse).subscribe(
92 () => { 94 () => {
93 this.notificationsService.success( 95 this.notifier.success(this.i18n('Abuse deleted.'))
94 this.i18n('Success'),
95 this.i18n('Abuse deleted.')
96 )
97 this.loadData() 96 this.loadData()
98 }, 97 },
99 98
100 err => this.notificationsService.error(this.i18n('Error'), err.message) 99 err => this.notifier.error(err.message)
101 ) 100 )
102 } 101 }
103 102
@@ -106,11 +105,15 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
106 .subscribe( 105 .subscribe(
107 () => this.loadData(), 106 () => this.loadData(),
108 107
109 err => this.notificationsService.error(this.i18n('Error'), err.message) 108 err => this.notifier.error(err.message)
110 ) 109 )
111 110
112 } 111 }
113 112
113 toHtml (text: string) {
114 return this.markdownRenderer.textMarkdownToHTML(text)
115 }
116
114 protected loadData () { 117 protected loadData () {
115 return this.videoAbuseService.getVideoAbuses(this.pagination, this.sort) 118 return this.videoAbuseService.getVideoAbuses(this.pagination, this.sort)
116 .subscribe( 119 .subscribe(
@@ -119,7 +122,7 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
119 this.totalRecords = resultList.total 122 this.totalRecords = resultList.total
120 }, 123 },
121 124
122 err => this.notificationsService.error(this.i18n('Error'), err.message) 125 err => this.notifier.error(err.message)
123 ) 126 )
124 } 127 }
125} 128}
diff --git a/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html b/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html
index 0585e0490..247f441c1 100644
--- a/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html
+++ b/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html
@@ -7,8 +7,9 @@
7 <th style="width: 40px"></th> 7 <th style="width: 40px"></th>
8 <th i18n pSortableColumn="name">Video name <p-sortIcon field="name"></p-sortIcon></th> 8 <th i18n pSortableColumn="name">Video name <p-sortIcon field="name"></p-sortIcon></th>
9 <th i18n>Sensitive</th> 9 <th i18n>Sensitive</th>
10 <th i18n>Unfederated</th>
10 <th i18n pSortableColumn="createdAt">Date <p-sortIcon field="createdAt"></p-sortIcon></th> 11 <th i18n pSortableColumn="createdAt">Date <p-sortIcon field="createdAt"></p-sortIcon></th>
11 <th style="width: 50px;"></th> 12 <th style="width: 120px;"></th>
12 </tr> 13 </tr>
13 </ng-template> 14 </ng-template>
14 15
@@ -26,20 +27,21 @@
26 </a> 27 </a>
27 </td> 28 </td>
28 29
29 <td>{{ videoBlacklist.video.nsfw }}</td> 30 <td>{{ booleanToText(videoBlacklist.video.nsfw) }}</td>
31 <td>{{ booleanToText(videoBlacklist.unfederated) }}</td>
30 <td>{{ videoBlacklist.createdAt }}</td> 32 <td>{{ videoBlacklist.createdAt }}</td>
31 33
32 <td class="action-cell"> 34 <td class="action-cell">
33 <my-action-dropdown i18n-label label="Actions" [actions]="videoBlacklistActions" [entry]="videoBlacklist"></my-action-dropdown> 35 <my-action-dropdown i18n-label placement="bottom-right" label="Actions" [actions]="videoBlacklistActions" [entry]="videoBlacklist"></my-action-dropdown>
34 </td> 36 </td>
35 </tr> 37 </tr>
36 </ng-template> 38 </ng-template>
37 39
38 <ng-template pTemplate="rowexpansion" let-videoBlacklist> 40 <ng-template pTemplate="rowexpansion" let-videoBlacklist>
39 <tr> 41 <tr>
40 <td class="moderation-expanded" colspan="5"> 42 <td class="moderation-expanded" colspan="6">
41 <span i18n class="moderation-expanded-label">Blacklist reason:</span> 43 <span i18n class="moderation-expanded-label">Blacklist reason:</span>
42 <span class="moderation-expanded-text">{{ videoBlacklist.reason }}</span> 44 <span class="moderation-expanded-text" [innerHTML]="toHtml(videoBlacklist.reason)"></span>
43 </td> 45 </td>
44 </tr> 46 </tr>
45 </ng-template> 47 </ng-template>
diff --git a/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.ts b/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.ts
index bb051d00f..b27bbbfef 100644
--- a/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.ts
+++ b/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.ts
@@ -1,12 +1,13 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { SortMeta } from 'primeng/components/common/sortmeta' 2import { SortMeta } from 'primeng/components/common/sortmeta'
3import { NotificationsService } from 'angular2-notifications' 3import { Notifier } from '@app/core'
4import { ConfirmService } from '../../../core' 4import { ConfirmService } from '../../../core'
5import { RestPagination, RestTable, VideoBlacklistService } from '../../../shared' 5import { RestPagination, RestTable, VideoBlacklistService } from '../../../shared'
6import { VideoBlacklist } from '../../../../../../shared' 6import { VideoBlacklist } from '../../../../../../shared'
7import { I18n } from '@ngx-translate/i18n-polyfill' 7import { I18n } from '@ngx-translate/i18n-polyfill'
8import { DropdownAction } from '../../../shared/buttons/action-dropdown.component' 8import { DropdownAction } from '../../../shared/buttons/action-dropdown.component'
9import { Video } from '../../../shared/video/video.model' 9import { Video } from '../../../shared/video/video.model'
10import { MarkdownService } from '@app/shared/renderer'
10 11
11@Component({ 12@Component({
12 selector: 'my-video-blacklist-list', 13 selector: 'my-video-blacklist-list',
@@ -23,9 +24,10 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit {
23 videoBlacklistActions: DropdownAction<VideoBlacklist>[] = [] 24 videoBlacklistActions: DropdownAction<VideoBlacklist>[] = []
24 25
25 constructor ( 26 constructor (
26 private notificationsService: NotificationsService, 27 private notifier: Notifier,
27 private confirmService: ConfirmService, 28 private confirmService: ConfirmService,
28 private videoBlacklistService: VideoBlacklistService, 29 private videoBlacklistService: VideoBlacklistService,
30 private markdownRenderer: MarkdownService,
29 private i18n: I18n 31 private i18n: I18n
30 ) { 32 ) {
31 super() 33 super()
@@ -39,13 +41,23 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit {
39 } 41 }
40 42
41 ngOnInit () { 43 ngOnInit () {
42 this.loadSort() 44 this.initialize()
43 } 45 }
44 46
45 getVideoUrl (videoBlacklist: VideoBlacklist) { 47 getVideoUrl (videoBlacklist: VideoBlacklist) {
46 return Video.buildClientUrl(videoBlacklist.video.uuid) 48 return Video.buildClientUrl(videoBlacklist.video.uuid)
47 } 49 }
48 50
51 booleanToText (value: boolean) {
52 if (value === true) return this.i18n('yes')
53
54 return this.i18n('no')
55 }
56
57 toHtml (text: string) {
58 return this.markdownRenderer.textMarkdownToHTML(text)
59 }
60
49 async removeVideoFromBlacklist (entry: VideoBlacklist) { 61 async removeVideoFromBlacklist (entry: VideoBlacklist) {
50 const confirmMessage = this.i18n( 62 const confirmMessage = this.i18n(
51 'Do you really want to remove this video from the blacklist? It will be available again in the videos list.' 63 'Do you really want to remove this video from the blacklist? It will be available again in the videos list.'
@@ -56,14 +68,11 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit {
56 68
57 this.videoBlacklistService.removeVideoFromBlacklist(entry.video.id).subscribe( 69 this.videoBlacklistService.removeVideoFromBlacklist(entry.video.id).subscribe(
58 () => { 70 () => {
59 this.notificationsService.success( 71 this.notifier.success(this.i18n('Video {{name}} removed from the blacklist.', { name: entry.video.name }))
60 this.i18n('Success'),
61 this.i18n('Video {{name}} removed from the blacklist.', { name: entry.video.name })
62 )
63 this.loadData() 72 this.loadData()
64 }, 73 },
65 74
66 err => this.notificationsService.error(this.i18n('Error'), err.message) 75 err => this.notifier.error(err.message)
67 ) 76 )
68 } 77 }
69 78
@@ -75,7 +84,7 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit {
75 this.totalRecords = resultList.total 84 this.totalRecords = resultList.total
76 }, 85 },
77 86
78 err => this.notificationsService.error(this.i18n('Error'), err.message) 87 err => this.notifier.error(err.message)
79 ) 88 )
80 } 89 }
81} 90}
diff --git a/client/src/app/+admin/users/user-edit/index.ts b/client/src/app/+admin/users/user-edit/index.ts
index fd80a02e0..ec734ef92 100644
--- a/client/src/app/+admin/users/user-edit/index.ts
+++ b/client/src/app/+admin/users/user-edit/index.ts
@@ -1,2 +1,3 @@
1export * from './user-create.component' 1export * from './user-create.component'
2export * from './user-update.component' 2export * from './user-update.component'
3export * from './user-password.component'
diff --git a/client/src/app/+admin/users/user-edit/user-create.component.ts b/client/src/app/+admin/users/user-edit/user-create.component.ts
index dd8e4efd5..137ecfcbd 100644
--- a/client/src/app/+admin/users/user-edit/user-create.component.ts
+++ b/client/src/app/+admin/users/user-edit/user-create.component.ts
@@ -1,7 +1,6 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { Router } from '@angular/router' 2import { Router } from '@angular/router'
3import { NotificationsService } from 'angular2-notifications' 3import { Notifier, ServerService } from '@app/core'
4import { ServerService } from '../../../core'
5import { UserCreate, UserRole } from '../../../../../../shared' 4import { UserCreate, UserRole } from '../../../../../../shared'
6import { UserEdit } from './user-edit' 5import { UserEdit } from './user-edit'
7import { I18n } from '@ngx-translate/i18n-polyfill' 6import { I18n } from '@ngx-translate/i18n-polyfill'
@@ -24,7 +23,7 @@ export class UserCreateComponent extends UserEdit implements OnInit {
24 protected configService: ConfigService, 23 protected configService: ConfigService,
25 private userValidatorsService: UserValidatorsService, 24 private userValidatorsService: UserValidatorsService,
26 private router: Router, 25 private router: Router,
27 private notificationsService: NotificationsService, 26 private notifier: Notifier,
28 private userService: UserService, 27 private userService: UserService,
29 private i18n: I18n 28 private i18n: I18n
30 ) { 29 ) {
@@ -60,10 +59,7 @@ export class UserCreateComponent extends UserEdit implements OnInit {
60 59
61 this.userService.addUser(userCreate).subscribe( 60 this.userService.addUser(userCreate).subscribe(
62 () => { 61 () => {
63 this.notificationsService.success( 62 this.notifier.success(this.i18n('User {{username}} created.', { username: userCreate.username }))
64 this.i18n('Success'),
65 this.i18n('User {{username}} created.', { username: userCreate.username })
66 )
67 this.router.navigate([ '/admin/users/list' ]) 63 this.router.navigate([ '/admin/users/list' ])
68 }, 64 },
69 65
diff --git a/client/src/app/+admin/users/user-edit/user-edit.component.html b/client/src/app/+admin/users/user-edit/user-edit.component.html
index 56cf7d17d..c6566da24 100644
--- a/client/src/app/+admin/users/user-edit/user-edit.component.html
+++ b/client/src/app/+admin/users/user-edit/user-edit.component.html
@@ -81,3 +81,17 @@
81 81
82 <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid"> 82 <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
83</form> 83</form>
84
85<div *ngIf="!isCreation()" class="danger-zone">
86 <div class="account-title" i18n>Danger Zone</div>
87
88 <div class="form-group reset-password-email">
89 <label i18n>Send a link to reset the password by email to the user</label>
90 <button (click)="resetPassword()" i18n>Ask for new password</button>
91 </div>
92
93 <div class="form-group">
94 <label i18n>Manually set the user password</label>
95 <my-user-password [userId]="userId"></my-user-password>
96 </div>
97</div>
diff --git a/client/src/app/+admin/users/user-edit/user-edit.component.scss b/client/src/app/+admin/users/user-edit/user-edit.component.scss
index 6675f65cc..c1cc4ca45 100644
--- a/client/src/app/+admin/users/user-edit/user-edit.component.scss
+++ b/client/src/app/+admin/users/user-edit/user-edit.component.scss
@@ -14,7 +14,7 @@ input:not([type=submit]) {
14 @include peertube-select-container(340px); 14 @include peertube-select-container(340px);
15} 15}
16 16
17input[type=submit] { 17input[type=submit], button {
18 @include peertube-button; 18 @include peertube-button;
19 @include orange-button; 19 @include orange-button;
20 20
@@ -25,3 +25,23 @@ input[type=submit] {
25 margin-top: 5px; 25 margin-top: 5px;
26 font-size: 11px; 26 font-size: 11px;
27} 27}
28
29.account-title {
30 @include in-content-small-title;
31
32 margin-top: 55px;
33 margin-bottom: 30px;
34}
35
36.danger-zone {
37 .reset-password-email {
38 margin-bottom: 30px;
39 padding-bottom: 30px;
40 border-bottom: 1px solid rgba(0, 0, 0, 0.1);
41
42 button {
43 display: block;
44 margin-top: 0;
45 }
46 }
47}
diff --git a/client/src/app/+admin/users/user-edit/user-edit.ts b/client/src/app/+admin/users/user-edit/user-edit.ts
index 07b087b5b..649b35b0c 100644
--- a/client/src/app/+admin/users/user-edit/user-edit.ts
+++ b/client/src/app/+admin/users/user-edit/user-edit.ts
@@ -1,14 +1,14 @@
1import { ServerService } from '../../../core' 1import { ServerService } from '../../../core'
2import { FormReactive } from '../../../shared' 2import { FormReactive } from '../../../shared'
3import { USER_ROLE_LABELS, VideoResolution } from '../../../../../../shared' 3import { USER_ROLE_LABELS, VideoResolution } from '../../../../../../shared'
4import { EditCustomConfigComponent } from '../../../+admin/config/edit-custom-config/'
5import { ConfigService } from '@app/+admin/config/shared/config.service' 4import { ConfigService } from '@app/+admin/config/shared/config.service'
6 5
7export abstract class UserEdit extends FormReactive { 6export abstract class UserEdit extends FormReactive {
8
9 videoQuotaOptions: { value: string, label: string }[] = [] 7 videoQuotaOptions: { value: string, label: string }[] = []
10 videoQuotaDailyOptions: { value: string, label: string }[] = [] 8 videoQuotaDailyOptions: { value: string, label: string }[] = []
11 roles = Object.keys(USER_ROLE_LABELS).map(key => ({ value: key.toString(), label: USER_ROLE_LABELS[key] })) 9 roles = Object.keys(USER_ROLE_LABELS).map(key => ({ value: key.toString(), label: USER_ROLE_LABELS[key] }))
10 username: string
11 userId: number
12 12
13 protected abstract serverService: ServerService 13 protected abstract serverService: ServerService
14 protected abstract configService: ConfigService 14 protected abstract configService: ConfigService
@@ -23,7 +23,9 @@ export abstract class UserEdit extends FormReactive {
23 } 23 }
24 24
25 computeQuotaWithTranscoding () { 25 computeQuotaWithTranscoding () {
26 const resolutions = this.serverService.getConfig().transcoding.enabledResolutions 26 const transcodingConfig = this.serverService.getConfig().transcoding
27
28 const resolutions = transcodingConfig.enabledResolutions
27 const higherResolution = VideoResolution.H_1080P 29 const higherResolution = VideoResolution.H_1080P
28 let multiplier = 0 30 let multiplier = 0
29 31
@@ -31,9 +33,15 @@ export abstract class UserEdit extends FormReactive {
31 multiplier += resolution / higherResolution 33 multiplier += resolution / higherResolution
32 } 34 }
33 35
36 if (transcodingConfig.hls.enabled) multiplier *= 2
37
34 return multiplier * parseInt(this.form.value['videoQuota'], 10) 38 return multiplier * parseInt(this.form.value['videoQuota'], 10)
35 } 39 }
36 40
41 resetPassword () {
42 return
43 }
44
37 protected buildQuotaOptions () { 45 protected buildQuotaOptions () {
38 // These are used by a HTML select, so convert key into strings 46 // These are used by a HTML select, so convert key into strings
39 this.videoQuotaOptions = this.configService 47 this.videoQuotaOptions = this.configService
diff --git a/client/src/app/+admin/users/user-edit/user-password.component.html b/client/src/app/+admin/users/user-edit/user-password.component.html
new file mode 100644
index 000000000..a1e1f6216
--- /dev/null
+++ b/client/src/app/+admin/users/user-edit/user-password.component.html
@@ -0,0 +1,21 @@
1<form role="form" (ngSubmit)="formValidated()" [formGroup]="form">
2 <div class="form-group">
3
4 <div class="input-group">
5 <input id="password" [attr.type]="showPassword ? 'text' : 'password'"
6 formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }"
7 >
8 <div class="input-group-append">
9 <button class="btn btn-sm btn-outline-secondary" (click)="togglePasswordVisibility()" type="button">
10 <ng-container *ngIf="!showPassword" i18n>Show</ng-container>
11 <ng-container *ngIf="!!showPassword" i18n>Hide</ng-container>
12 </button>
13 </div>
14 </div>
15 <div *ngIf="formErrors.password" class="form-error">
16 {{ formErrors.password }}
17 </div>
18 </div>
19
20 <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
21</form>
diff --git a/client/src/app/+admin/users/user-edit/user-password.component.scss b/client/src/app/+admin/users/user-edit/user-password.component.scss
new file mode 100644
index 000000000..217d585af
--- /dev/null
+++ b/client/src/app/+admin/users/user-edit/user-password.component.scss
@@ -0,0 +1,22 @@
1@import '_variables';
2@import '_mixins';
3
4input:not([type=submit]):not([type=checkbox]) {
5 @include peertube-input-text(340px);
6
7 display: block;
8 border-top-right-radius: 0;
9 border-bottom-right-radius: 0;
10 border-right: none;
11}
12
13input[type=submit] {
14 @include peertube-button;
15 @include orange-button;
16
17 margin-top: 10px;
18}
19
20.input-group-append {
21 height: 30px;
22}
diff --git a/client/src/app/+admin/users/user-edit/user-password.component.ts b/client/src/app/+admin/users/user-edit/user-password.component.ts
new file mode 100644
index 000000000..5b3040440
--- /dev/null
+++ b/client/src/app/+admin/users/user-edit/user-password.component.ts
@@ -0,0 +1,64 @@
1import { Component, Input, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router'
3import { UserService } from '@app/shared/users/user.service'
4import { Notifier } from '../../../core'
5import { User, UserUpdate } from '../../../../../../shared'
6import { I18n } from '@ngx-translate/i18n-polyfill'
7import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
8import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service'
9import { FormReactive } from '../../../shared'
10
11@Component({
12 selector: 'my-user-password',
13 templateUrl: './user-password.component.html',
14 styleUrls: [ './user-password.component.scss' ]
15})
16export class UserPasswordComponent extends FormReactive implements OnInit {
17 error: string
18 username: string
19 showPassword = false
20
21 @Input() userId: number
22
23 constructor (
24 protected formValidatorService: FormValidatorService,
25 private userValidatorsService: UserValidatorsService,
26 private route: ActivatedRoute,
27 private router: Router,
28 private notifier: Notifier,
29 private userService: UserService,
30 private i18n: I18n
31 ) {
32 super()
33 }
34
35 ngOnInit () {
36 this.buildForm({
37 password: this.userValidatorsService.USER_PASSWORD
38 })
39 }
40
41 formValidated () {
42 this.error = undefined
43
44 const userUpdate: UserUpdate = this.form.value
45
46 this.userService.updateUser(this.userId, userUpdate).subscribe(
47 () => {
48 this.notifier.success(
49 this.i18n('Password changed for user {{username}}.', { username: this.username })
50 )
51 },
52
53 err => this.error = err.message
54 )
55 }
56
57 togglePasswordVisibility () {
58 this.showPassword = !this.showPassword
59 }
60
61 getFormButtonTitle () {
62 return this.i18n('Update user password')
63 }
64}
diff --git a/client/src/app/+admin/users/user-edit/user-update.component.ts b/client/src/app/+admin/users/user-edit/user-update.component.ts
index cd3885a99..94ef87b08 100644
--- a/client/src/app/+admin/users/user-edit/user-update.component.ts
+++ b/client/src/app/+admin/users/user-edit/user-update.component.ts
@@ -1,7 +1,7 @@
1import { Component, OnDestroy, OnInit } from '@angular/core' 1import { Component, OnDestroy, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router' 2import { ActivatedRoute, Router } from '@angular/router'
3import { Subscription } from 'rxjs' 3import { Subscription } from 'rxjs'
4import { NotificationsService } from 'angular2-notifications' 4import { Notifier } from '@app/core'
5import { ServerService } from '../../../core' 5import { ServerService } from '../../../core'
6import { UserEdit } from './user-edit' 6import { UserEdit } from './user-edit'
7import { User, UserUpdate } from '../../../../../../shared' 7import { User, UserUpdate } from '../../../../../../shared'
@@ -19,6 +19,7 @@ import { UserService } from '@app/shared'
19export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy { 19export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
20 error: string 20 error: string
21 userId: number 21 userId: number
22 userEmail: string
22 username: string 23 username: string
23 24
24 private paramsSub: Subscription 25 private paramsSub: Subscription
@@ -30,7 +31,7 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
30 private userValidatorsService: UserValidatorsService, 31 private userValidatorsService: UserValidatorsService,
31 private route: ActivatedRoute, 32 private route: ActivatedRoute,
32 private router: Router, 33 private router: Router,
33 private notificationsService: NotificationsService, 34 private notifier: Notifier,
34 private userService: UserService, 35 private userService: UserService,
35 private i18n: I18n 36 private i18n: I18n
36 ) { 37 ) {
@@ -73,10 +74,7 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
73 74
74 this.userService.updateUser(this.userId, userUpdate).subscribe( 75 this.userService.updateUser(this.userId, userUpdate).subscribe(
75 () => { 76 () => {
76 this.notificationsService.success( 77 this.notifier.success(this.i18n('User {{username}} updated.', { username: this.username }))
77 this.i18n('Success'),
78 this.i18n('User {{username}} updated.', { username: this.username })
79 )
80 this.router.navigate([ '/admin/users/list' ]) 78 this.router.navigate([ '/admin/users/list' ])
81 }, 79 },
82 80
@@ -92,9 +90,22 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
92 return this.i18n('Update user') 90 return this.i18n('Update user')
93 } 91 }
94 92
93 resetPassword () {
94 this.userService.askResetPassword(this.userEmail).subscribe(
95 () => {
96 this.notifier.success(
97 this.i18n('An email asking for password reset has been sent to {{username}}.', { username: this.username })
98 )
99 },
100
101 err => this.error = err.message
102 )
103 }
104
95 private onUserFetched (userJson: User) { 105 private onUserFetched (userJson: User) {
96 this.userId = userJson.id 106 this.userId = userJson.id
97 this.username = userJson.username 107 this.username = userJson.username
108 this.userEmail = userJson.email
98 109
99 this.form.patchValue({ 110 this.form.patchValue({
100 email: userJson.email, 111 email: userJson.email,
diff --git a/client/src/app/+admin/users/user-list/user-list.component.html b/client/src/app/+admin/users/user-list/user-list.component.html
index cca057ba1..69a4616a3 100644
--- a/client/src/app/+admin/users/user-list/user-list.component.html
+++ b/client/src/app/+admin/users/user-list/user-list.component.html
@@ -2,7 +2,7 @@
2 <div i18n class="form-sub-title">Users list</div> 2 <div i18n class="form-sub-title">Users list</div>
3 3
4 <a class="add-button" routerLink="/admin/users/create"> 4 <a class="add-button" routerLink="/admin/users/create">
5 <span class="icon icon-add"></span> 5 <my-global-icon iconName="add"></my-global-icon>
6 <ng-container i18n>Create user</ng-container> 6 <ng-container i18n>Create user</ng-container>
7 </a> 7 </a>
8</div> 8</div>
@@ -10,9 +10,32 @@
10<p-table 10<p-table
11 [value]="users" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage" 11 [value]="users" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
12 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id" 12 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id"
13 [(selection)]="selectedUsers"
13> 14>
15 <ng-template pTemplate="caption">
16 <div class="caption">
17 <div>
18 <my-action-dropdown
19 *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange"
20 [actions]="bulkUserActions" [entry]="selectedUsers"
21 >
22 </my-action-dropdown>
23 </div>
24
25 <div>
26 <input
27 type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
28 (keyup)="onSearch($event.target.value)"
29 >
30 </div>
31 </div>
32 </ng-template>
33
14 <ng-template pTemplate="header"> 34 <ng-template pTemplate="header">
15 <tr> 35 <tr>
36 <th style="width: 40px">
37 <p-tableHeaderCheckbox></p-tableHeaderCheckbox>
38 </th>
16 <th style="width: 40px"></th> 39 <th style="width: 40px"></th>
17 <th i18n pSortableColumn="username">Username <p-sortIcon field="username"></p-sortIcon></th> 40 <th i18n pSortableColumn="username">Username <p-sortIcon field="username"></p-sortIcon></th>
18 <th i18n>Email</th> 41 <th i18n>Email</th>
@@ -25,22 +48,42 @@
25 48
26 <ng-template pTemplate="body" let-expanded="expanded" let-user> 49 <ng-template pTemplate="body" let-expanded="expanded" let-user>
27 50
28 <tr [ngClass]="{ banned: user.blocked }"> 51 <tr [pSelectableRow]="user" [ngClass]="{ banned: user.blocked }">
52 <td>
53 <p-tableCheckbox [value]="user"></p-tableCheckbox>
54 </td>
55
29 <td> 56 <td>
30 <span *ngIf="user.blockedReason" class="expander" [pRowToggler]="user"> 57 <span *ngIf="user.blockedReason" class="expander" [pRowToggler]="user">
31 <i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i> 58 <i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i>
32 </span> 59 </span>
33 </td> 60 </td>
61
34 <td> 62 <td>
35 {{ user.username }} 63 <a i18n-title title="Go to the account page" target="_blank" rel="noopener noreferrer" [routerLink]="[ '/accounts/' + user.username ]">
36 <span *ngIf="user.blocked" class="banned-info">(banned)</span> 64 {{ user.username }}
65 <span i18n *ngIf="user.blocked" class="banned-info">(banned)</span>
66 </a>
37 </td> 67 </td>
38 <td>{{ user.email }}</td> 68
69 <td *ngIf="!requiresEmailVerification || user.blocked; else emailWithVerificationStatus">{{ user.email }}</td>
70
71 <ng-template #emailWithVerificationStatus>
72 <td *ngIf="user.emailVerified === false; else emailVerifiedNotFalse" i18n-title title="User's email must be verified to login">
73 <em>? {{ user.email }}</em>
74 </td>
75 <ng-template #emailVerifiedNotFalse>
76 <td i18n-title title="User's email is verified / User can login without email verification">
77 &#x2713; {{ user.email }}
78 </td>
79 </ng-template>
80 </ng-template>
81
39 <td>{{ user.videoQuotaUsed }} / {{ user.videoQuota }}</td> 82 <td>{{ user.videoQuotaUsed }} / {{ user.videoQuota }}</td>
40 <td>{{ user.roleLabel }}</td> 83 <td>{{ user.roleLabel }}</td>
41 <td>{{ user.createdAt }}</td> 84 <td>{{ user.createdAt }}</td>
42 <td class="action-cell"> 85 <td class="action-cell">
43 <my-user-moderation-dropdown [user]="user" (userChanged)="onUserChanged()" (userDeleted)="onUserChanged()"> 86 <my-user-moderation-dropdown *ngIf="!isInSelectionMode()" [user]="user" (userChanged)="onUserChanged()" (userDeleted)="onUserChanged()">
44 </my-user-moderation-dropdown> 87 </my-user-moderation-dropdown>
45 </td> 88 </td>
46 </tr> 89 </tr>
@@ -56,3 +99,4 @@
56 </ng-template> 99 </ng-template>
57</p-table> 100</p-table>
58 101
102<my-user-ban-modal #userBanModal (userBanned)="onUserChanged()"></my-user-ban-modal>
diff --git a/client/src/app/+admin/users/user-list/user-list.component.scss b/client/src/app/+admin/users/user-list/user-list.component.scss
index 47291918d..5274be01c 100644
--- a/client/src/app/+admin/users/user-list/user-list.component.scss
+++ b/client/src/app/+admin/users/user-list/user-list.component.scss
@@ -2,7 +2,7 @@
2@import '_mixins'; 2@import '_mixins';
3 3
4.add-button { 4.add-button {
5 @include create-button('../../../../assets/images/global/add.svg'); 5 @include create-button;
6} 6}
7 7
8tr.banned { 8tr.banned {
@@ -15,4 +15,12 @@ tr.banned {
15 15
16.ban-reason-label { 16.ban-reason-label {
17 font-weight: $font-semibold; 17 font-weight: $font-semibold;
18} \ No newline at end of file 18}
19
20.caption {
21 justify-content: space-between;
22
23 input {
24 @include peertube-input-text(250px);
25 }
26}
diff --git a/client/src/app/+admin/users/user-list/user-list.component.ts b/client/src/app/+admin/users/user-list/user-list.component.ts
index dee3ed643..66ab796f9 100644
--- a/client/src/app/+admin/users/user-list/user-list.component.ts
+++ b/client/src/app/+admin/users/user-list/user-list.component.ts
@@ -1,10 +1,12 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit, ViewChild } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications' 2import { Notifier } from '@app/core'
3import { SortMeta } from 'primeng/components/common/sortmeta' 3import { SortMeta } from 'primeng/components/common/sortmeta'
4import { ConfirmService } from '../../../core' 4import { ConfirmService, ServerService } from '../../../core'
5import { RestPagination, RestTable, UserService } from '../../../shared' 5import { RestPagination, RestTable, UserService } from '../../../shared'
6import { I18n } from '@ngx-translate/i18n-polyfill' 6import { I18n } from '@ngx-translate/i18n-polyfill'
7import { User } from '../../../../../../shared' 7import { User } from '../../../../../../shared'
8import { UserBanModalComponent } from '@app/shared/moderation'
9import { DropdownAction } from '@app/shared/buttons/action-dropdown.component'
8 10
9@Component({ 11@Component({
10 selector: 'my-user-list', 12 selector: 'my-user-list',
@@ -12,38 +14,139 @@ import { User } from '../../../../../../shared'
12 styleUrls: [ './user-list.component.scss' ] 14 styleUrls: [ './user-list.component.scss' ]
13}) 15})
14export class UserListComponent extends RestTable implements OnInit { 16export class UserListComponent extends RestTable implements OnInit {
17 @ViewChild('userBanModal') userBanModal: UserBanModalComponent
18
15 users: User[] = [] 19 users: User[] = []
16 totalRecords = 0 20 totalRecords = 0
17 rowsPerPage = 10 21 rowsPerPage = 10
18 sort: SortMeta = { field: 'createdAt', order: 1 } 22 sort: SortMeta = { field: 'createdAt', order: 1 }
19 pagination: RestPagination = { count: this.rowsPerPage, start: 0 } 23 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
20 24
25 selectedUsers: User[] = []
26 bulkUserActions: DropdownAction<User[]>[] = []
27
21 constructor ( 28 constructor (
22 private notificationsService: NotificationsService, 29 private notifier: Notifier,
23 private confirmService: ConfirmService, 30 private confirmService: ConfirmService,
31 private serverService: ServerService,
24 private userService: UserService, 32 private userService: UserService,
25 private i18n: I18n 33 private i18n: I18n
26 ) { 34 ) {
27 super() 35 super()
28 } 36 }
29 37
38 get requiresEmailVerification () {
39 return this.serverService.getConfig().signup.requiresEmailVerification
40 }
41
30 ngOnInit () { 42 ngOnInit () {
31 this.loadSort() 43 this.initialize()
44
45 this.bulkUserActions = [
46 {
47 label: this.i18n('Delete'),
48 handler: users => this.removeUsers(users)
49 },
50 {
51 label: this.i18n('Ban'),
52 handler: users => this.openBanUserModal(users),
53 isDisplayed: users => users.every(u => u.blocked === false)
54 },
55 {
56 label: this.i18n('Unban'),
57 handler: users => this.unbanUsers(users),
58 isDisplayed: users => users.every(u => u.blocked === true)
59 },
60 {
61 label: this.i18n('Set Email as Verified'),
62 handler: users => this.setEmailsAsVerified(users),
63 isDisplayed: users => this.requiresEmailVerification && users.every(u => !u.blocked && u.emailVerified === false)
64 }
65 ]
66 }
67
68 openBanUserModal (users: User[]) {
69 for (const user of users) {
70 if (user.username === 'root') {
71 this.notifier.error(this.i18n('You cannot ban root.'))
72 return
73 }
74 }
75
76 this.userBanModal.openModal(users)
32 } 77 }
33 78
34 onUserChanged () { 79 onUserChanged () {
35 this.loadData() 80 this.loadData()
36 } 81 }
37 82
83 async unbanUsers (users: User[]) {
84 const message = this.i18n('Do you really want to unban {{num}} users?', { num: users.length })
85
86 const res = await this.confirmService.confirm(message, this.i18n('Unban'))
87 if (res === false) return
88
89 this.userService.unbanUsers(users)
90 .subscribe(
91 () => {
92 const message = this.i18n('{{num}} users unbanned.', { num: users.length })
93
94 this.notifier.success(message)
95 this.loadData()
96 },
97
98 err => this.notifier.error(err.message)
99 )
100 }
101
102 async removeUsers (users: User[]) {
103 for (const user of users) {
104 if (user.username === 'root') {
105 this.notifier.error(this.i18n('You cannot delete root.'))
106 return
107 }
108 }
109
110 const message = this.i18n('If you remove these users, you will not be able to create others with the same username!')
111 const res = await this.confirmService.confirm(message, this.i18n('Delete'))
112 if (res === false) return
113
114 this.userService.removeUser(users).subscribe(
115 () => {
116 this.notifier.success(this.i18n('{{num}} users deleted.', { num: users.length }))
117 this.loadData()
118 },
119
120 err => this.notifier.error(err.message)
121 )
122 }
123
124 async setEmailsAsVerified (users: User[]) {
125 this.userService.updateUsers(users, { emailVerified: true }).subscribe(
126 () => {
127 this.notifier.success(this.i18n('{{num}} users email set as verified.', { num: users.length }))
128 this.loadData()
129 },
130
131 err => this.notifier.error(err.message)
132 )
133 }
134
135 isInSelectionMode () {
136 return this.selectedUsers.length !== 0
137 }
138
38 protected loadData () { 139 protected loadData () {
39 this.userService.getUsers(this.pagination, this.sort) 140 this.selectedUsers = []
40 .subscribe( 141
41 resultList => { 142 this.userService.getUsers(this.pagination, this.sort, this.search)
42 this.users = resultList.data 143 .subscribe(
43 this.totalRecords = resultList.total 144 resultList => {
44 }, 145 this.users = resultList.data
45 146 this.totalRecords = resultList.total
46 err => this.notificationsService.error(this.i18n('Error'), err.message) 147 },
47 ) 148
149 err => this.notifier.error(err.message)
150 )
48 } 151 }
49} 152}