aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/+admin/moderation/moderation.component.scss1
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html40
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.scss8
-rw-r--r--client/src/app/+admin/users/user-edit/user-password.component.scss4
-rw-r--r--client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.component.scss4
-rw-r--r--client/src/app/+signup/+register/register.component.scss4
-rw-r--r--client/src/app/shared/user-subscription/subscribe-button.component.scss4
-rw-r--r--client/src/sass/bootstrap.scss20
-rw-r--r--client/src/sass/primeng-custom.scss4
-rw-r--r--server/models/utils.ts51
-rw-r--r--server/models/video/video-abuse.ts66
11 files changed, 175 insertions, 31 deletions
diff --git a/client/src/app/+admin/moderation/moderation.component.scss b/client/src/app/+admin/moderation/moderation.component.scss
index cf06401cf..26c2a30d4 100644
--- a/client/src/app/+admin/moderation/moderation.component.scss
+++ b/client/src/app/+admin/moderation/moderation.component.scss
@@ -12,6 +12,7 @@
12 12
13 input { 13 input {
14 @include peertube-input-text(250px); 14 @include peertube-input-text(250px);
15 flex-grow: 1;
15 } 16 }
16} 17}
17 18
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 2f6e12d1c..b55b18333 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
@@ -7,10 +7,32 @@
7 <ng-template pTemplate="caption"> 7 <ng-template pTemplate="caption">
8 <div class="caption"> 8 <div class="caption">
9 <div class="ml-auto"> 9 <div class="ml-auto">
10 <input 10 <div class="input-group">
11 type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..." 11 <div class="input-group-prepend c-hand" ngbDropdown placement="bottom-left auto" container="body">
12 (keyup)="onSearch($event)" 12 <div class="input-group-text" ngbDropdownToggle>
13 > 13 <span class="caret" aria-haspopup="menu" role="button"></span>
14 </div>
15
16 <div role="menu" ngbDropdownMenu>
17 <h6 class="dropdown-header" i18n>Filter reports</h6>
18
19 <!-- TODO:
20 <div class="dropdown-item" i18n>Reports opened by admins</div>
21 <div class="dropdown-item" i18n>Reports on videos with multiple reports</div>
22 <div class="dropdown-item" i18n>Unassigned reports</div>
23 -->
24 <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'state:pending' }" class="dropdown-item" i18n>Unsolved reports</a>
25 <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'state:accepted' }" class="dropdown-item" i18n>Accepted reports</a>
26 <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'state:rejected' }" class="dropdown-item" i18n>Refused reports</a>
27 <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'is:blocked' }" class="dropdown-item" i18n>Reports with blocked videos</a>
28 <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'is:deleted' }" class="dropdown-item" i18n>Reports with deleted videos</a>
29 </div>
30 </div>
31 <input
32 type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
33 (keyup)="onSearch($event)"
34 >
35 </div>
14 </div> 36 </div>
15 </div> 37 </div>
16 </ng-template> 38 </ng-template>
@@ -100,7 +122,7 @@
100 122
101 <td class="action-cell"> 123 <td class="action-cell">
102 <my-action-dropdown 124 <my-action-dropdown
103 [ngClass]="{ 'show': expanded }" placement="bottom-right auto" container="body" 125 [ngClass]="{ 'show': expanded }" placement="bottom-right top-right left auto" container="body"
104 i18n-label label="Actions" [actions]="videoAbuseActions" [entry]="videoAbuse" 126 i18n-label label="Actions" [actions]="videoAbuseActions" [entry]="videoAbuse"
105 ></my-action-dropdown> 127 ></my-action-dropdown>
106 </td> 128 </td>
@@ -118,7 +140,7 @@
118 <div class="d-flex"> 140 <div class="d-flex">
119 <span class="col-3 moderation-expanded-label" i18n>Reporter</span> 141 <span class="col-3 moderation-expanded-label" i18n>Reporter</span>
120 <span class="col-9 moderation-expanded-text"> 142 <span class="col-9 moderation-expanded-text">
121 <div class="chip"> 143 <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'reporter:&quot;' + videoAbuse.reporterAccount.displayName + '&quot;' }" class="chip">
122 <img 144 <img
123 class="avatar" 145 class="avatar"
124 [src]="videoAbuse.reporterAccount.avatar.path" 146 [src]="videoAbuse.reporterAccount.avatar.path"
@@ -128,8 +150,8 @@
128 <div> 150 <div>
129 <span class="text-muted">{{ createByString(videoAbuse.reporterAccount) }}</span> 151 <span class="text-muted">{{ createByString(videoAbuse.reporterAccount) }}</span>
130 </div> 152 </div>
131 </div> 153 </a>
132 <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': videoAbuse.reporterAccount.displayName }" class="ml-auto text-muted video-details-links" i18n> 154 <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'reportee:&quot;' + videoAbuse.reporterAccount.displayName + '&quot;' }" class="ml-auto text-muted video-details-links" i18n>
133 {videoAbuse.countReportsForReporter, plural, =1 {1 report} other {{{ videoAbuse.countReportsForReporter }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span> 155 {videoAbuse.countReportsForReporter, plural, =1 {1 report} other {{{ videoAbuse.countReportsForReporter }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span>
134 </a> 156 </a>
135 </span> 157 </span>
@@ -149,7 +171,7 @@
149 <span class="text-muted">{{ videoAbuse.video.channel.ownerAccount ? createByString(videoAbuse.video.channel.ownerAccount) : '' }}</span> 171 <span class="text-muted">{{ videoAbuse.video.channel.ownerAccount ? createByString(videoAbuse.video.channel.ownerAccount) : '' }}</span>
150 </div> 172 </div>
151 </div> 173 </div>
152 <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': videoAbuse.video.channel.ownerAccount.displayName }" class="ml-auto text-muted video-details-links" i18n> 174 <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'reportee:&quot;' +videoAbuse.video.channel.ownerAccount.displayName + '&quot;' }" class="ml-auto text-muted video-details-links" i18n>
153 {videoAbuse.countReportsForReportee, plural, =1 {1 report} other {{{ videoAbuse.countReportsForReportee }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span> 175 {videoAbuse.countReportsForReportee, plural, =1 {1 report} other {{{ videoAbuse.countReportsForReportee }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span>
154 </a> 176 </a>
155 </span> 177 </span>
diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.scss b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.scss
index d6bc34935..8eee15b64 100644
--- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.scss
+++ b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.scss
@@ -13,3 +13,11 @@
13.video-abuse-states .glyphicon-comment { 13.video-abuse-states .glyphicon-comment {
14 margin-left: 0.5rem; 14 margin-left: 0.5rem;
15} 15}
16
17.input-group {
18 @include peertube-input-group(300px);
19
20 .dropdown-toggle::after {
21 margin-left: 0;
22 }
23}
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
index 217d585af..5cd93f6af 100644
--- a/client/src/app/+admin/users/user-edit/user-password.component.scss
+++ b/client/src/app/+admin/users/user-edit/user-password.component.scss
@@ -16,7 +16,3 @@ input[type=submit] {
16 16
17 margin-top: 10px; 17 margin-top: 10px;
18} 18}
19
20.input-group-append {
21 height: 30px;
22}
diff --git a/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.component.scss b/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.component.scss
index 8f8af655c..ba27ee7ff 100644
--- a/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.component.scss
+++ b/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.component.scss
@@ -19,10 +19,6 @@ my-actor-avatar-info {
19 @include peertube-input-group(fit-content); 19 @include peertube-input-group(fit-content);
20} 20}
21 21
22.input-group-append {
23 height: 30px;
24}
25
26input { 22input {
27 &[type=text] { 23 &[type=text] {
28 @include peertube-input-text(340px); 24 @include peertube-input-text(340px);
diff --git a/client/src/app/+signup/+register/register.component.scss b/client/src/app/+signup/+register/register.component.scss
index e135b5cb4..cc60ef524 100644
--- a/client/src/app/+signup/+register/register.component.scss
+++ b/client/src/app/+signup/+register/register.component.scss
@@ -58,10 +58,6 @@
58 @include peertube-input-group(400px); 58 @include peertube-input-group(400px);
59} 59}
60 60
61.input-group-append {
62 height: 30px;
63}
64
65input:not([type=submit]) { 61input:not([type=submit]) {
66 @include peertube-input-text(400px); 62 @include peertube-input-text(400px);
67 63
diff --git a/client/src/app/shared/user-subscription/subscribe-button.component.scss b/client/src/app/shared/user-subscription/subscribe-button.component.scss
index 5283a6cc3..b739c5ae2 100644
--- a/client/src/app/shared/user-subscription/subscribe-button.component.scss
+++ b/client/src/app/shared/user-subscription/subscribe-button.component.scss
@@ -88,10 +88,6 @@
88 } 88 }
89 } 89 }
90 90
91 .dropdown-header {
92 padding-left: 1rem;
93 }
94
95 ::ng-deep form { 91 ::ng-deep form {
96 padding: 0.25rem 1rem; 92 padding: 0.25rem 1rem;
97 } 93 }
diff --git a/client/src/sass/bootstrap.scss b/client/src/sass/bootstrap.scss
index 377c85070..50f1dafed 100644
--- a/client/src/sass/bootstrap.scss
+++ b/client/src/sass/bootstrap.scss
@@ -41,6 +41,10 @@ $icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/';
41 box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2); 41 box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2);
42 font-size: 15px; 42 font-size: 15px;
43 43
44 .dropdown-header {
45 padding-left: 1rem;
46 }
47
44 .dropdown-item { 48 .dropdown-item {
45 padding: 3px 15px; 49 padding: 3px 15px;
46 50
@@ -262,6 +266,18 @@ ngb-tooltip-window {
262 } 266 }
263} 267}
264 268
265.input-group > .form-control { 269.input-group {
266 flex: initial; 270 & > .form-control {
271 flex: initial;
272 }
273
274 .input-group-prepend,
275 .input-group-append {
276 height: 30px;
277 }
278
279 .input-group-prepend + input {
280 border-top-left-radius: 0 !important;
281 border-bottom-left-radius: 0 !important;
282 }
267} 283}
diff --git a/client/src/sass/primeng-custom.scss b/client/src/sass/primeng-custom.scss
index b3cd7cf51..eab2b2dfd 100644
--- a/client/src/sass/primeng-custom.scss
+++ b/client/src/sass/primeng-custom.scss
@@ -32,6 +32,10 @@ p-table {
32 height: 40px; 32 height: 40px;
33 display: flex; 33 display: flex;
34 align-items: center; 34 align-items: center;
35
36 .input-group-text {
37 background-color: transparent;
38 }
35 } 39 }
36 } 40 }
37 41
diff --git a/server/models/utils.ts b/server/models/utils.ts
index bdf2291f0..3e3825b32 100644
--- a/server/models/utils.ts
+++ b/server/models/utils.ts
@@ -219,6 +219,54 @@ function searchAttribute (sourceField, targetField) {
219 } 219 }
220} 220}
221 221
222interface QueryStringFilterPrefixes {
223 [key: string]: string | { prefix: string, handler: Function, multiple?: boolean }
224}
225
226function parseQueryStringFilter (q: string, prefixes: QueryStringFilterPrefixes) {
227 const tokens = q // tokenize only if we have a querystring
228 ? [].concat.apply([], q.split('"').map((v, i) => i % 2 ? v : v.split(' '))).filter(Boolean)
229 : []
230
231 // TODO: when Typescript supports Object.fromEntries, replace with the Object method
232 function fromEntries<T> (entries: [keyof T, T[keyof T]][]): T {
233 return entries.reduce(
234 (acc, [ key, value ]) => ({ ...acc, [key]: value }),
235 {} as T
236 )
237 }
238
239 const objectMap = (obj, fn) => fromEntries(
240 Object.entries(obj).map(
241 ([ k, v ], i) => [ k, fn(v, k, i) ]
242 )
243 )
244
245 return {
246 // search is the querystring minus defined filters
247 search: tokens.filter(e => !Object.values(prefixes).some(p => {
248 if (typeof p === "string") {
249 return e.startsWith(p)
250 } else {
251 return e.startsWith(p.prefix)
252 }
253 })).join(' '),
254 // filters defined in prefixes are added under their own name
255 ...objectMap(prefixes, v => {
256 if (typeof v === "string") {
257 return tokens.filter(e => e.startsWith(v)).map(e => e.slice(v.length))
258 } else {
259 const _tokens = tokens.filter(e => e.startsWith(v.prefix)).map(e => e.slice(v.prefix.length)).map(v.handler)
260 return !v.multiple
261 ? _tokens.length > 0
262 ? _tokens[0]
263 : ''
264 : _tokens
265 }
266 })
267 }
268}
269
222// --------------------------------------------------------------------------- 270// ---------------------------------------------------------------------------
223 271
224export { 272export {
@@ -241,7 +289,8 @@ export {
241 getFollowsSort, 289 getFollowsSort,
242 buildDirectionAndField, 290 buildDirectionAndField,
243 createSafeIn, 291 createSafeIn,
244 searchAttribute 292 searchAttribute,
293 parseQueryStringFilter
245} 294}
246 295
247// --------------------------------------------------------------------------- 296// ---------------------------------------------------------------------------
diff --git a/server/models/video/video-abuse.ts b/server/models/video/video-abuse.ts
index 628f1caa6..b1f8fed90 100644
--- a/server/models/video/video-abuse.ts
+++ b/server/models/video/video-abuse.ts
@@ -9,7 +9,7 @@ import {
9 isVideoAbuseStateValid 9 isVideoAbuseStateValid
10} from '../../helpers/custom-validators/video-abuses' 10} from '../../helpers/custom-validators/video-abuses'
11import { AccountModel } from '../account/account' 11import { AccountModel } from '../account/account'
12import { buildBlockedAccountSQL, getSort, throwIfNotValid, searchAttribute } from '../utils' 12import { buildBlockedAccountSQL, getSort, throwIfNotValid, searchAttribute, parseQueryStringFilter } from '../utils'
13import { VideoModel } from './video' 13import { VideoModel } from './video'
14import { VideoAbuseState, VideoDetails } from '../../../shared' 14import { VideoAbuseState, VideoDetails } from '../../../shared'
15import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants' 15import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants'
@@ -26,10 +26,17 @@ export enum ScopeNames {
26 26
27@Scopes(() => ({ 27@Scopes(() => ({
28 [ScopeNames.FOR_API]: (options: { 28 [ScopeNames.FOR_API]: (options: {
29 // search
29 search?: string 30 search?: string
30 searchReporter?: string 31 searchReporter?: string
32 searchReportee?: string
31 searchVideo?: string 33 searchVideo?: string
32 searchVideoChannel?: string 34 searchVideoChannel?: string
35 // filters
36 id?: number
37 state?: VideoAbuseState
38 is?: any
39 // accountIds
33 serverAccountId: number 40 serverAccountId: number
34 userAccountId: number 41 userAccountId: number
35 }) => { 42 }) => {
@@ -71,6 +78,24 @@ export enum ScopeNames {
71 }) 78 })
72 } 79 }
73 80
81 if (options.id) {
82 where = Object.assign(where, {
83 id: options.id
84 })
85 }
86
87 if (options.state) {
88 where = Object.assign(where, {
89 state: options.state
90 })
91 }
92
93 if (options.is) {
94 where = Object.assign(where, {
95 ...options.is
96 })
97 }
98
74 return { 99 return {
75 attributes: { 100 attributes: {
76 include: [ 101 include: [
@@ -167,7 +192,13 @@ export enum ScopeNames {
167 }, 192 },
168 { 193 {
169 model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, { withAccount: true } as SummaryOptions ] }), 194 model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, { withAccount: true } as SummaryOptions ] }),
170 where: searchAttribute(options.searchVideoChannel, 'name') 195 where: searchAttribute(options.searchVideoChannel, 'name'),
196 include: [
197 {
198 model: AccountModel,
199 where: searchAttribute(options.searchReportee, 'name')
200 }
201 ]
171 }, 202 },
172 { 203 {
173 attributes: [ 'id', 'reason', 'unfederated' ], 204 attributes: [ 'id', 'reason', 'unfederated' ],
@@ -280,7 +311,36 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
280 } 311 }
281 312
282 const filters = { 313 const filters = {
283 search, 314 ...parseQueryStringFilter(search, {
315 id: {
316 prefix: '#',
317 handler: v => v
318 },
319 state: {
320 prefix: 'state:',
321 handler: v => {
322 if (v === "accepted") return VideoAbuseState.ACCEPTED
323 if (v === "pending") return VideoAbuseState.PENDING
324 if (v === "rejected") return VideoAbuseState.REJECTED
325 return undefined
326 }
327 },
328 is: {
329 prefix: 'is:',
330 handler: v => {
331 if (v === "deleted") return { deletedVideo: { [Op.not]: null } }
332 return undefined
333 }
334 },
335 searchReporter: {
336 prefix: 'reporter:',
337 handler: v => v
338 },
339 searchReportee: {
340 prefix: 'reportee:',
341 handler: v => v
342 }
343 }),
284 serverAccountId, 344 serverAccountId,
285 userAccountId 345 userAccountId
286 } 346 }