aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/core
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app/core')
-rw-r--r--client/src/app/core/auth/auth-user.model.ts18
-rw-r--r--client/src/app/core/auth/auth.service.ts13
-rw-r--r--client/src/app/core/core.module.ts48
-rw-r--r--client/src/app/core/index.ts9
-rw-r--r--client/src/app/core/menu/menu.service.ts4
-rw-r--r--client/src/app/core/notification/user-notification-socket.service.ts4
-rw-r--r--client/src/app/core/plugins/hooks.service.ts3
-rw-r--r--client/src/app/core/plugins/index.ts2
-rw-r--r--client/src/app/core/plugins/plugin.service.ts44
-rw-r--r--client/src/app/core/renderer/html-renderer.service.ts40
-rw-r--r--client/src/app/core/renderer/index.ts3
-rw-r--r--client/src/app/core/renderer/linkifier.service.ts114
-rw-r--r--client/src/app/core/renderer/markdown.service.ts145
-rw-r--r--client/src/app/core/rest/component-pagination.model.ts18
-rw-r--r--client/src/app/core/rest/index.ts5
-rw-r--r--client/src/app/core/rest/rest-extractor.service.ts109
-rw-r--r--client/src/app/core/rest/rest-pagination.ts4
-rw-r--r--client/src/app/core/rest/rest-table.ts105
-rw-r--r--client/src/app/core/rest/rest.service.ts111
-rw-r--r--client/src/app/core/routing/can-deactivate-guard.service.ts30
-rw-r--r--client/src/app/core/routing/index.ts9
-rw-r--r--client/src/app/core/routing/login-guard.service.ts1
-rw-r--r--client/src/app/core/routing/menu-guard.service.ts4
-rw-r--r--client/src/app/core/routing/preload-selected-modules-list.ts1
-rw-r--r--client/src/app/core/routing/server-config-resolver.service.ts2
-rw-r--r--client/src/app/core/routing/unlogged-guard.service.ts3
-rw-r--r--client/src/app/core/routing/user-right-guard.service.ts9
-rw-r--r--client/src/app/core/server/server.service.ts18
-rw-r--r--client/src/app/core/theme/theme.service.ts16
-rw-r--r--client/src/app/core/users/index.ts2
-rw-r--r--client/src/app/core/users/user.model.ts150
-rw-r--r--client/src/app/core/users/user.service.ts375
-rw-r--r--client/src/app/core/wrappers/index.ts2
-rw-r--r--client/src/app/core/wrappers/screen.service.ts66
-rw-r--r--client/src/app/core/wrappers/storage.service.ts37
35 files changed, 1433 insertions, 91 deletions
diff --git a/client/src/app/core/auth/auth-user.model.ts b/client/src/app/core/auth/auth-user.model.ts
index 4ad904beb..4e7801550 100644
--- a/client/src/app/core/auth/auth-user.model.ts
+++ b/client/src/app/core/auth/auth-user.model.ts
@@ -1,10 +1,14 @@
1import { peertubeLocalStorage } from '@app/shared/misc/peertube-web-storage' 1import { User } from '@app/core/users/user.model'
2import { UserRight } from '../../../../../shared/models/users/user-right.enum' 2import { peertubeLocalStorage } from '@app/helpers/peertube-web-storage'
3import { MyUser as ServerMyUserModel, User as ServerUserModel, MyUserSpecialPlaylist } from '../../../../../shared/models/users/user.model' 3import {
4// Do not use the barrel (dependency loop) 4 hasUserRight,
5import { hasUserRight, UserRole } from '../../../../../shared/models/users/user-role' 5 MyUser as ServerMyUserModel,
6import { User } from '../../shared/users/user.model' 6 MyUserSpecialPlaylist,
7import { NSFWPolicyType } from '../../../../../shared/models/videos/nsfw-policy.type' 7 NSFWPolicyType,
8 User as ServerUserModel,
9 UserRight,
10 UserRole
11} from '@shared/models'
8 12
9export type TokenOptions = { 13export type TokenOptions = {
10 accessToken: string 14 accessToken: string
diff --git a/client/src/app/core/auth/auth.service.ts b/client/src/app/core/auth/auth.service.ts
index de8c509d1..94262b9aa 100644
--- a/client/src/app/core/auth/auth.service.ts
+++ b/client/src/app/core/auth/auth.service.ts
@@ -1,20 +1,17 @@
1import { Hotkey, HotkeysService } from 'angular2-hotkeys'
1import { Observable, ReplaySubject, Subject, throwError as observableThrowError } from 'rxjs' 2import { Observable, ReplaySubject, Subject, throwError as observableThrowError } from 'rxjs'
2import { catchError, map, mergeMap, share, tap } from 'rxjs/operators' 3import { catchError, map, mergeMap, share, tap } from 'rxjs/operators'
3import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http' 4import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'
4import { Injectable } from '@angular/core' 5import { Injectable } from '@angular/core'
5import { Router } from '@angular/router' 6import { Router } from '@angular/router'
6import { Notifier } from '@app/core/notification/notifier.service' 7import { Notifier } from '@app/core/notification/notifier.service'
7import { OAuthClientLocal, MyUser as UserServerModel, UserRefreshToken } from '../../../../../shared' 8import { objectToUrlEncoded, peertubeLocalStorage } from '@app/helpers'
8import { User } from '../../../../../shared/models/users' 9import { I18n } from '@ngx-translate/i18n-polyfill'
9import { UserLogin } from '../../../../../shared/models/users/user-login.model' 10import { MyUser as UserServerModel, OAuthClientLocal, User, UserLogin, UserRefreshToken } from '@shared/models'
10import { environment } from '../../../environments/environment' 11import { environment } from '../../../environments/environment'
11import { RestExtractor } from '../../shared/rest/rest-extractor.service' 12import { RestExtractor } from '../rest/rest-extractor.service'
12import { AuthStatus } from './auth-status.model' 13import { AuthStatus } from './auth-status.model'
13import { AuthUser } from './auth-user.model' 14import { AuthUser } from './auth-user.model'
14import { objectToUrlEncoded } from '@app/shared/misc/utils'
15import { peertubeLocalStorage } from '@app/shared/misc/peertube-web-storage'
16import { I18n } from '@ngx-translate/i18n-polyfill'
17import { Hotkey, HotkeysService } from 'angular2-hotkeys'
18 15
19interface UserLoginWithUsername extends UserLogin { 16interface UserLoginWithUsername extends UserLogin {
20 access_token: string 17 access_token: string
diff --git a/client/src/app/core/core.module.ts b/client/src/app/core/core.module.ts
index a1734ad80..22896e2e9 100644
--- a/client/src/app/core/core.module.ts
+++ b/client/src/app/core/core.module.ts
@@ -1,35 +1,35 @@
1import { HotkeyModule } from 'angular2-hotkeys'
2import { MessageService } from 'primeng/api'
3import { ToastModule } from 'primeng/toast'
1import { CommonModule } from '@angular/common' 4import { CommonModule } from '@angular/common'
2import { NgModule, Optional, SkipSelf } from '@angular/core' 5import { NgModule, Optional, SkipSelf } from '@angular/core'
3import { FormsModule } from '@angular/forms'
4import { BrowserAnimationsModule } from '@angular/platform-browser/animations' 6import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
5import { RouterModule } from '@angular/router' 7import { UserNotificationSocket } from '@app/core/notification/user-notification-socket.service'
8import { HooksService } from '@app/core/plugins/hooks.service'
9import { PluginService } from '@app/core/plugins/plugin.service'
10import { UnloggedGuard } from '@app/core/routing/unlogged-guard.service'
6import { LoadingBarModule } from '@ngx-loading-bar/core' 11import { LoadingBarModule } from '@ngx-loading-bar/core'
7import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client' 12import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client'
8import { LoadingBarRouterModule } from '@ngx-loading-bar/router' 13import { LoadingBarRouterModule } from '@ngx-loading-bar/router'
9
10import { AuthService } from './auth' 14import { AuthService } from './auth'
11import { ConfirmService } from './confirm' 15import { ConfirmService } from './confirm'
16import { CheatSheetComponent } from './hotkeys'
17import { MenuService } from './menu'
12import { throwIfAlreadyLoaded } from './module-import-guard' 18import { throwIfAlreadyLoaded } from './module-import-guard'
19import { Notifier } from './notification'
20import { HtmlRendererService, LinkifierService, MarkdownService } from './renderer'
21import { RestExtractor, RestService } from './rest'
13import { LoginGuard, RedirectService, UserRightGuard } from './routing' 22import { LoginGuard, RedirectService, UserRightGuard } from './routing'
23import { CanDeactivateGuard } from './routing/can-deactivate-guard.service'
24import { ServerConfigResolver } from './routing/server-config-resolver.service'
14import { ServerService } from './server' 25import { ServerService } from './server'
15import { ThemeService } from './theme' 26import { ThemeService } from './theme'
16import { MenuService } from './menu' 27import { UserService } from './users'
17import { HotkeyModule } from 'angular2-hotkeys' 28import { LocalStorageService, ScreenService, SessionStorageService } from './wrappers'
18import { CheatSheetComponent } from './hotkeys'
19import { ToastModule } from 'primeng/toast'
20import { Notifier } from './notification'
21import { MessageService } from 'primeng/api'
22import { UserNotificationSocket } from '@app/core/notification/user-notification-socket.service'
23import { ServerConfigResolver } from './routing/server-config-resolver.service'
24import { UnloggedGuard } from '@app/core/routing/unlogged-guard.service'
25import { PluginService } from '@app/core/plugins/plugin.service'
26import { HooksService } from '@app/core/plugins/hooks.service'
27 29
28@NgModule({ 30@NgModule({
29 imports: [ 31 imports: [
30 CommonModule, 32 CommonModule,
31 RouterModule,
32 FormsModule,
33 BrowserAnimationsModule, 33 BrowserAnimationsModule,
34 34
35 LoadingBarHttpClientModule, 35 LoadingBarHttpClientModule,
@@ -68,11 +68,25 @@ import { HooksService } from '@app/core/plugins/hooks.service'
68 PluginService, 68 PluginService,
69 HooksService, 69 HooksService,
70 70
71 HtmlRendererService,
72 LinkifierService,
73 MarkdownService,
74
75 RestExtractor,
76 RestService,
77
78 UserService,
79
80 ScreenService,
81 LocalStorageService,
82 SessionStorageService,
83
71 RedirectService, 84 RedirectService,
72 Notifier, 85 Notifier,
73 MessageService, 86 MessageService,
74 UserNotificationSocket, 87 UserNotificationSocket,
75 ServerConfigResolver 88 ServerConfigResolver,
89 CanDeactivateGuard
76 ] 90 ]
77}) 91})
78export class CoreModule { 92export class CoreModule {
diff --git a/client/src/app/core/index.ts b/client/src/app/core/index.ts
index f664aff41..a0c34543d 100644
--- a/client/src/app/core/index.ts
+++ b/client/src/app/core/index.ts
@@ -1,8 +1,15 @@
1export * from './auth' 1export * from './auth'
2export * from './confirm' 2export * from './confirm'
3export * from './hotkeys'
4export * from './menu'
5export * from './notification'
6export * from './plugins'
7export * from './renderer'
8export * from './rest'
3export * from './routing' 9export * from './routing'
4export * from './server' 10export * from './server'
5export * from './notification'
6export * from './theme' 11export * from './theme'
12export * from './users'
13export * from './wrappers'
7 14
8export * from './core.module' 15export * from './core.module'
diff --git a/client/src/app/core/menu/menu.service.ts b/client/src/app/core/menu/menu.service.ts
index 81093c666..ef5271f97 100644
--- a/client/src/app/core/menu/menu.service.ts
+++ b/client/src/app/core/menu/menu.service.ts
@@ -1,7 +1,7 @@
1import { Injectable } from '@angular/core'
2import { ScreenService } from '@app/shared/misc/screen.service'
3import { fromEvent } from 'rxjs' 1import { fromEvent } from 'rxjs'
4import { debounceTime } from 'rxjs/operators' 2import { debounceTime } from 'rxjs/operators'
3import { Injectable } from '@angular/core'
4import { ScreenService } from '../wrappers'
5 5
6@Injectable() 6@Injectable()
7export class MenuService { 7export class MenuService {
diff --git a/client/src/app/core/notification/user-notification-socket.service.ts b/client/src/app/core/notification/user-notification-socket.service.ts
index 3f22da476..37f0bc32c 100644
--- a/client/src/app/core/notification/user-notification-socket.service.ts
+++ b/client/src/app/core/notification/user-notification-socket.service.ts
@@ -1,7 +1,7 @@
1import { Subject } from 'rxjs'
1import { Injectable, NgZone } from '@angular/core' 2import { Injectable, NgZone } from '@angular/core'
3import { UserNotification as UserNotificationServer } from '@shared/models'
2import { environment } from '../../../environments/environment' 4import { environment } from '../../../environments/environment'
3import { UserNotification as UserNotificationServer } from '../../../../../shared'
4import { Subject } from 'rxjs'
5import { AuthService } from '../auth' 5import { AuthService } from '../auth'
6 6
7export type NotificationEvent = 'new' | 'read' | 'read-all' 7export type NotificationEvent = 'new' | 'read' | 'read-all'
diff --git a/client/src/app/core/plugins/hooks.service.ts b/client/src/app/core/plugins/hooks.service.ts
index 2fbf406d1..ec47aa48c 100644
--- a/client/src/app/core/plugins/hooks.service.ts
+++ b/client/src/app/core/plugins/hooks.service.ts
@@ -2,8 +2,7 @@ import { from, Observable } from 'rxjs'
2import { mergeMap, switchMap } from 'rxjs/operators' 2import { mergeMap, switchMap } from 'rxjs/operators'
3import { Injectable } from '@angular/core' 3import { Injectable } from '@angular/core'
4import { PluginService } from '@app/core/plugins/plugin.service' 4import { PluginService } from '@app/core/plugins/plugin.service'
5import { ClientActionHookName, ClientFilterHookName } from '@shared/models/plugins/client-hook.model' 5import { ClientActionHookName, ClientFilterHookName, PluginClientScope } from '@shared/models'
6import { PluginClientScope } from '@shared/models/plugins/plugin-client-scope.type'
7 6
8type RawFunction<U, T> = (params: U) => T 7type RawFunction<U, T> = (params: U) => T
9type ObservableFunction<U, T> = RawFunction<U, Observable<T>> 8type ObservableFunction<U, T> = RawFunction<U, Observable<T>>
diff --git a/client/src/app/core/plugins/index.ts b/client/src/app/core/plugins/index.ts
new file mode 100644
index 000000000..b3239f1f3
--- /dev/null
+++ b/client/src/app/core/plugins/index.ts
@@ -0,0 +1,2 @@
1export * from './hooks.service'
2export * from './plugin.service'
diff --git a/client/src/app/core/plugins/plugin.service.ts b/client/src/app/core/plugins/plugin.service.ts
index c6efcac6d..3cab64142 100644
--- a/client/src/app/core/plugins/plugin.service.ts
+++ b/client/src/app/core/plugins/plugin.service.ts
@@ -1,28 +1,33 @@
1import { Inject, Injectable, LOCALE_ID, NgZone } from '@angular/core'
2import { Router } from '@angular/router'
3import { getCompleteLocale, isDefaultLocale, peertubeTranslate, ServerConfigPlugin } from '@shared/models'
4import { ServerService } from '@app/core/server/server.service'
5import { ClientScript } from '@shared/models/plugins/plugin-package-json.model'
6import { ClientScript as ClientScriptModule } from '../../../types/client-script.model'
7import { environment } from '../../../environments/environment'
8import { Observable, of, ReplaySubject } from 'rxjs' 1import { Observable, of, ReplaySubject } from 'rxjs'
9import { catchError, first, map, shareReplay } from 'rxjs/operators' 2import { catchError, first, map, shareReplay } from 'rxjs/operators'
10import { getHookType, internalRunHook } from '@shared/core-utils/plugins/hooks'
11import { ClientHook, ClientHookName, clientHookObject } from '@shared/models/plugins/client-hook.model'
12import { PluginClientScope } from '@shared/models/plugins/plugin-client-scope.type'
13import { RegisterClientHookOptions } from '@shared/models/plugins/register-client-hook.model'
14import { HttpClient } from '@angular/common/http' 3import { HttpClient } from '@angular/common/http'
4import { Inject, Injectable, LOCALE_ID, NgZone } from '@angular/core'
15import { AuthService } from '@app/core/auth' 5import { AuthService } from '@app/core/auth'
16import { Notifier } from '@app/core/notification' 6import { Notifier } from '@app/core/notification'
17import { RestExtractor } from '@app/shared/rest' 7import { MarkdownService } from '@app/core/renderer'
18import { MarkdownService } from '@app/shared/renderer' 8import { RestExtractor } from '@app/core/rest'
19import { PluginType } from '@shared/models/plugins/plugin.type' 9import { ServerService } from '@app/core/server/server.service'
20import { PublicServerSetting } from '@shared/models/plugins/public-server.setting' 10import { getDevLocale, importModule, isOnDevLocale } from '@app/helpers'
21import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils'
22import { RegisterClientHelpers } from '../../../types/register-client-option.model'
23import { PluginTranslation } from '@shared/models/plugins/plugin-translation.model'
24import { importModule } from '@app/shared/misc/utils'
25import { CustomModalComponent } from '@app/modal/custom-modal.component' 11import { CustomModalComponent } from '@app/modal/custom-modal.component'
12import { getHookType, internalRunHook } from '@shared/core-utils/plugins/hooks'
13import {
14 ClientHook,
15 ClientHookName,
16 clientHookObject,
17 ClientScript,
18 getCompleteLocale,
19 isDefaultLocale,
20 peertubeTranslate,
21 PluginClientScope,
22 PluginTranslation,
23 PluginType,
24 PublicServerSetting,
25 RegisterClientHookOptions,
26 ServerConfigPlugin
27} from '@shared/models'
28import { environment } from '../../../environments/environment'
29import { ClientScript as ClientScriptModule } from '../../../types/client-script.model'
30import { RegisterClientHelpers } from '../../../types/register-client-option.model'
26 31
27interface HookStructValue extends RegisterClientHookOptions { 32interface HookStructValue extends RegisterClientHookOptions {
28 plugin: ServerConfigPlugin 33 plugin: ServerConfigPlugin
@@ -64,7 +69,6 @@ export class PluginService implements ClientHook {
64 private hooks: { [ name: string ]: HookStructValue[] } = {} 69 private hooks: { [ name: string ]: HookStructValue[] } = {}
65 70
66 constructor ( 71 constructor (
67 private router: Router,
68 private authService: AuthService, 72 private authService: AuthService,
69 private notifier: Notifier, 73 private notifier: Notifier,
70 private markdownRenderer: MarkdownService, 74 private markdownRenderer: MarkdownService,
diff --git a/client/src/app/core/renderer/html-renderer.service.ts b/client/src/app/core/renderer/html-renderer.service.ts
new file mode 100644
index 000000000..f0527c759
--- /dev/null
+++ b/client/src/app/core/renderer/html-renderer.service.ts
@@ -0,0 +1,40 @@
1import { Injectable } from '@angular/core'
2import { LinkifierService } from './linkifier.service'
3
4@Injectable()
5export class HtmlRendererService {
6
7 constructor (private linkifier: LinkifierService) {
8
9 }
10
11 async toSafeHtml (text: string) {
12 // FIXME: import('..') returns a struct module, containing a "default" field corresponding to our sanitizeHtml function
13 const sanitizeHtml: typeof import ('sanitize-html') = (await import('sanitize-html') as any).default
14
15 // Convert possible markdown to html
16 const html = this.linkifier.linkify(text)
17
18 return sanitizeHtml(html, {
19 allowedTags: [ 'a', 'p', 'span', 'br', 'strong', 'em', 'ul', 'ol', 'li' ],
20 allowedSchemes: [ 'http', 'https' ],
21 allowedAttributes: {
22 'a': [ 'href', 'class', 'target', 'rel' ]
23 },
24 transformTags: {
25 a: (tagName, attribs) => {
26 let rel = 'noopener noreferrer'
27 if (attribs.rel === 'me') rel += ' me'
28
29 return {
30 tagName,
31 attribs: Object.assign(attribs, {
32 target: '_blank',
33 rel
34 })
35 }
36 }
37 }
38 })
39 }
40}
diff --git a/client/src/app/core/renderer/index.ts b/client/src/app/core/renderer/index.ts
new file mode 100644
index 000000000..39202b385
--- /dev/null
+++ b/client/src/app/core/renderer/index.ts
@@ -0,0 +1,3 @@
1export * from './html-renderer.service'
2export * from './linkifier.service'
3export * from './markdown.service'
diff --git a/client/src/app/core/renderer/linkifier.service.ts b/client/src/app/core/renderer/linkifier.service.ts
new file mode 100644
index 000000000..46d5b0089
--- /dev/null
+++ b/client/src/app/core/renderer/linkifier.service.ts
@@ -0,0 +1,114 @@
1import { Injectable } from '@angular/core'
2import { getAbsoluteAPIUrl } from '@app/helpers/utils'
3import * as linkify from 'linkifyjs'
4import linkifyHtml from 'linkifyjs/html'
5
6@Injectable()
7export class LinkifierService {
8
9 static CLASSNAME = 'linkified'
10
11 private linkifyOptions = {
12 className: {
13 mention: LinkifierService.CLASSNAME + '-mention',
14 url: LinkifierService.CLASSNAME + '-url'
15 }
16 }
17
18 constructor () {
19 // Apply plugin
20 this.mentionWithDomainPlugin(linkify)
21 }
22
23 linkify (text: string) {
24 return linkifyHtml(text, this.linkifyOptions)
25 }
26
27 private mentionWithDomainPlugin (linkify: any) {
28 const TT = linkify.scanner.TOKENS // Text tokens
29 const { TOKENS: MT, State } = linkify.parser // Multi tokens, state
30 const MultiToken = MT.Base
31 const S_START = linkify.parser.start
32
33 const TT_AT = TT.AT
34 const TT_DOMAIN = TT.DOMAIN
35 const TT_LOCALHOST = TT.LOCALHOST
36 const TT_NUM = TT.NUM
37 const TT_COLON = TT.COLON
38 const TT_SLASH = TT.SLASH
39 const TT_TLD = TT.TLD
40 const TT_UNDERSCORE = TT.UNDERSCORE
41 const TT_DOT = TT.DOT
42
43 function MENTION (this: any, value: any) {
44 this.v = value
45 }
46
47 linkify.inherits(MultiToken, MENTION, {
48 type: 'mentionWithDomain',
49 isLink: true,
50 toHref () {
51 return getAbsoluteAPIUrl() + '/services/redirect/accounts/' + this.toString().substr(1)
52 }
53 })
54
55 const S_AT = S_START.jump(TT_AT) // @
56 const S_AT_SYMS = new State()
57 const S_MENTION = new State(MENTION)
58 const S_MENTION_DIVIDER = new State()
59 const S_MENTION_DIVIDER_SYMS = new State()
60
61 // @_,
62 S_AT.on(TT_UNDERSCORE, S_AT_SYMS)
63
64 // @_*
65 S_AT_SYMS
66 .on(TT_UNDERSCORE, S_AT_SYMS)
67 .on(TT_DOT, S_AT_SYMS)
68
69 // Valid mention (not made up entirely of symbols)
70 S_AT
71 .on(TT_DOMAIN, S_MENTION)
72 .on(TT_LOCALHOST, S_MENTION)
73 .on(TT_TLD, S_MENTION)
74 .on(TT_NUM, S_MENTION)
75
76 S_AT_SYMS
77 .on(TT_DOMAIN, S_MENTION)
78 .on(TT_LOCALHOST, S_MENTION)
79 .on(TT_TLD, S_MENTION)
80 .on(TT_NUM, S_MENTION)
81
82 // More valid mentions
83 S_MENTION
84 .on(TT_DOMAIN, S_MENTION)
85 .on(TT_LOCALHOST, S_MENTION)
86 .on(TT_TLD, S_MENTION)
87 .on(TT_COLON, S_MENTION)
88 .on(TT_NUM, S_MENTION)
89 .on(TT_UNDERSCORE, S_MENTION)
90
91 // Mention with a divider
92 S_MENTION
93 .on(TT_AT, S_MENTION_DIVIDER)
94 .on(TT_SLASH, S_MENTION_DIVIDER)
95 .on(TT_DOT, S_MENTION_DIVIDER)
96
97 // Mention _ trailing stash plus syms
98 S_MENTION_DIVIDER.on(TT_UNDERSCORE, S_MENTION_DIVIDER_SYMS)
99 S_MENTION_DIVIDER_SYMS.on(TT_UNDERSCORE, S_MENTION_DIVIDER_SYMS)
100
101 // Once we get a word token, mentions can start up again
102 S_MENTION_DIVIDER
103 .on(TT_DOMAIN, S_MENTION)
104 .on(TT_LOCALHOST, S_MENTION)
105 .on(TT_TLD, S_MENTION)
106 .on(TT_NUM, S_MENTION)
107
108 S_MENTION_DIVIDER_SYMS
109 .on(TT_DOMAIN, S_MENTION)
110 .on(TT_LOCALHOST, S_MENTION)
111 .on(TT_TLD, S_MENTION)
112 .on(TT_NUM, S_MENTION)
113 }
114}
diff --git a/client/src/app/core/renderer/markdown.service.ts b/client/src/app/core/renderer/markdown.service.ts
new file mode 100644
index 000000000..0c43bebab
--- /dev/null
+++ b/client/src/app/core/renderer/markdown.service.ts
@@ -0,0 +1,145 @@
1import * as MarkdownIt from 'markdown-it'
2import { buildVideoLink } from 'src/assets/player/utils'
3import { Injectable } from '@angular/core'
4import { HtmlRendererService } from './html-renderer.service'
5
6type MarkdownParsers = {
7 textMarkdownIt: MarkdownIt
8 textWithHTMLMarkdownIt: MarkdownIt
9
10 enhancedMarkdownIt: MarkdownIt
11 enhancedWithHTMLMarkdownIt: MarkdownIt
12
13 completeMarkdownIt: MarkdownIt
14}
15
16type MarkdownConfig = {
17 rules: string[]
18 html: boolean
19 escape?: boolean
20}
21
22type MarkdownParserConfigs = {
23 [id in keyof MarkdownParsers]: MarkdownConfig
24}
25
26@Injectable()
27export class MarkdownService {
28 static TEXT_RULES = [
29 'linkify',
30 'autolink',
31 'emphasis',
32 'link',
33 'newline',
34 'list'
35 ]
36 static TEXT_WITH_HTML_RULES = MarkdownService.TEXT_RULES.concat([ 'html_inline', 'html_block' ])
37
38 static ENHANCED_RULES = MarkdownService.TEXT_RULES.concat([ 'image' ])
39 static ENHANCED_WITH_HTML_RULES = MarkdownService.TEXT_WITH_HTML_RULES.concat([ 'image' ])
40
41 static COMPLETE_RULES = MarkdownService.ENHANCED_WITH_HTML_RULES.concat([ 'block', 'inline', 'heading', 'paragraph' ])
42
43 private markdownParsers: MarkdownParsers = {
44 textMarkdownIt: null,
45 textWithHTMLMarkdownIt: null,
46 enhancedMarkdownIt: null,
47 enhancedWithHTMLMarkdownIt: null,
48 completeMarkdownIt: null
49 }
50 private parsersConfig: MarkdownParserConfigs = {
51 textMarkdownIt: { rules: MarkdownService.TEXT_RULES, html: false },
52 textWithHTMLMarkdownIt: { rules: MarkdownService.TEXT_WITH_HTML_RULES, html: true, escape: true },
53
54 enhancedMarkdownIt: { rules: MarkdownService.ENHANCED_RULES, html: false },
55 enhancedWithHTMLMarkdownIt: { rules: MarkdownService.ENHANCED_WITH_HTML_RULES, html: true, escape: true },
56
57 completeMarkdownIt: { rules: MarkdownService.COMPLETE_RULES, html: true }
58 }
59
60 constructor (private htmlRenderer: HtmlRendererService) {}
61
62 textMarkdownToHTML (markdown: string, withHtml = false) {
63 if (withHtml) return this.render('textWithHTMLMarkdownIt', markdown)
64
65 return this.render('textMarkdownIt', markdown)
66 }
67
68 enhancedMarkdownToHTML (markdown: string, withHtml = false) {
69 if (withHtml) return this.render('enhancedWithHTMLMarkdownIt', markdown)
70
71 return this.render('enhancedMarkdownIt', markdown)
72 }
73
74 completeMarkdownToHTML (markdown: string) {
75 return this.render('completeMarkdownIt', markdown)
76 }
77
78 async processVideoTimestamps (html: string) {
79 return html.replace(/((\d{1,2}):)?(\d{1,2}):(\d{1,2})/g, function (str, _, h, m, s) {
80 const t = (3600 * +(h || 0)) + (60 * +(m || 0)) + (+(s || 0))
81 const url = buildVideoLink({ startTime: t })
82 return `<a class="video-timestamp" href="${url}">${str}</a>`
83 })
84 }
85
86 private async render (name: keyof MarkdownParsers, markdown: string) {
87 if (!markdown) return ''
88
89 const config = this.parsersConfig[ name ]
90 if (!this.markdownParsers[ name ]) {
91 this.markdownParsers[ name ] = await this.createMarkdownIt(config)
92 }
93
94 let html = this.markdownParsers[ name ].render(markdown)
95 html = this.avoidTruncatedTags(html)
96
97 if (config.escape) return this.htmlRenderer.toSafeHtml(html)
98
99 return html
100 }
101
102 private async createMarkdownIt (config: MarkdownConfig) {
103 // FIXME: import('...') returns a struct module, containing a "default" field
104 const MarkdownItClass: typeof import ('markdown-it') = (await import('markdown-it') as any).default
105
106 const markdownIt = new MarkdownItClass('zero', { linkify: true, breaks: true, html: config.html })
107
108 for (const rule of config.rules) {
109 markdownIt.enable(rule)
110 }
111
112 this.setTargetToLinks(markdownIt)
113
114 return markdownIt
115 }
116
117 private setTargetToLinks (markdownIt: MarkdownIt) {
118 // Snippet from markdown-it documentation: https://github.com/markdown-it/markdown-it/blob/master/docs/architecture.md#renderer
119 const defaultRender = markdownIt.renderer.rules.link_open || function (tokens, idx, options, env, self) {
120 return self.renderToken(tokens, idx, options)
121 }
122
123 markdownIt.renderer.rules.link_open = function (tokens, index, options, env, self) {
124 const token = tokens[index]
125
126 const targetIndex = token.attrIndex('target')
127 if (targetIndex < 0) token.attrPush([ 'target', '_blank' ])
128 else token.attrs[targetIndex][1] = '_blank'
129
130 const relIndex = token.attrIndex('rel')
131 if (relIndex < 0) token.attrPush([ 'rel', 'noopener noreferrer' ])
132 else token.attrs[relIndex][1] = 'noopener noreferrer'
133
134 // pass token to default renderer.
135 return defaultRender(tokens, index, options, env, self)
136 }
137 }
138
139 private avoidTruncatedTags (html: string) {
140 return html.replace(/\*\*?([^*]+)$/, '$1')
141 .replace(/<a[^>]+>([^<]+)<\/a>\s*...((<\/p>)|(<\/li>)|(<\/strong>))?$/mi, '$1...')
142 .replace(/\[[^\]]+\]\(([^\)]+)$/m, '$1')
143 .replace(/\s?\[[^\]]+\]?[.]{3}<\/p>$/m, '...</p>')
144 }
145}
diff --git a/client/src/app/core/rest/component-pagination.model.ts b/client/src/app/core/rest/component-pagination.model.ts
new file mode 100644
index 000000000..bcb73ed0f
--- /dev/null
+++ b/client/src/app/core/rest/component-pagination.model.ts
@@ -0,0 +1,18 @@
1export interface ComponentPagination {
2 currentPage: number
3 itemsPerPage: number
4 totalItems: number
5}
6
7export type ComponentPaginationLight = Omit<ComponentPagination, 'totalItems'>
8
9export function hasMoreItems (componentPagination: ComponentPagination) {
10 // No results
11 if (componentPagination.totalItems === 0) return false
12
13 // Not loaded yet
14 if (!componentPagination.totalItems) return true
15
16 const maxPage = componentPagination.totalItems / componentPagination.itemsPerPage
17 return maxPage > componentPagination.currentPage
18}
diff --git a/client/src/app/core/rest/index.ts b/client/src/app/core/rest/index.ts
new file mode 100644
index 000000000..93899beaf
--- /dev/null
+++ b/client/src/app/core/rest/index.ts
@@ -0,0 +1,5 @@
1export * from './component-pagination.model'
2export * from './rest-extractor.service'
3export * from './rest-pagination'
4export * from './rest-table'
5export * from './rest.service'
diff --git a/client/src/app/core/rest/rest-extractor.service.ts b/client/src/app/core/rest/rest-extractor.service.ts
new file mode 100644
index 000000000..9de964f79
--- /dev/null
+++ b/client/src/app/core/rest/rest-extractor.service.ts
@@ -0,0 +1,109 @@
1import { throwError as observableThrowError } from 'rxjs'
2import { Injectable } from '@angular/core'
3import { Router } from '@angular/router'
4import { dateToHuman } from '@app/helpers'
5import { I18n } from '@ngx-translate/i18n-polyfill'
6import { ResultList } from '@shared/models'
7
8@Injectable()
9export class RestExtractor {
10
11 constructor (
12 private router: Router,
13 private i18n: I18n
14 ) { }
15
16 extractDataBool () {
17 return true
18 }
19
20 applyToResultListData <T> (result: ResultList<T>, fun: Function, additionalArgs?: any[]): ResultList<T> {
21 const data: T[] = result.data
22 const newData: T[] = []
23
24 data.forEach(d => newData.push(fun.apply(this, [ d ].concat(additionalArgs))))
25
26 return {
27 total: result.total,
28 data: newData
29 }
30 }
31
32 convertResultListDateToHuman <T> (result: ResultList<T>, fieldsToConvert: string[] = [ 'createdAt' ]): ResultList<T> {
33 return this.applyToResultListData(result, this.convertDateToHuman, [ fieldsToConvert ])
34 }
35
36 convertDateToHuman (target: { [ id: string ]: string }, fieldsToConvert: string[]) {
37 fieldsToConvert.forEach(field => target[field] = dateToHuman(target[field]))
38
39 return target
40 }
41
42 handleError (err: any) {
43 let errorMessage
44
45 if (err.error instanceof Error) {
46 // A client-side or network error occurred. Handle it accordingly.
47 errorMessage = err.error.message
48 console.error('An error occurred:', errorMessage)
49 } else if (typeof err.error === 'string') {
50 errorMessage = err.error
51 } else if (err.status !== undefined) {
52 // A server-side error occurred.
53 if (err.error && err.error.errors) {
54 const errors = err.error.errors
55 const errorsArray: string[] = []
56
57 Object.keys(errors).forEach(key => {
58 errorsArray.push(errors[key].msg)
59 })
60
61 errorMessage = errorsArray.join('. ')
62 } else if (err.error && err.error.error) {
63 errorMessage = err.error.error
64 } else if (err.status === 413) {
65 errorMessage = this.i18n(
66 'Request is too large for the server. Please contact you administrator if you want to increase the limit size.'
67 )
68 } else if (err.status === 429) {
69 const secondsLeft = err.headers.get('retry-after')
70 if (secondsLeft) {
71 const minutesLeft = Math.floor(parseInt(secondsLeft, 10) / 60)
72 errorMessage = this.i18n('Too many attempts, please try again after {{minutesLeft}} minutes.', { minutesLeft })
73 } else {
74 errorMessage = this.i18n('Too many attempts, please try again later.')
75 }
76 } else if (err.status === 500) {
77 errorMessage = this.i18n('Server error. Please retry later.')
78 }
79
80 errorMessage = errorMessage ? errorMessage : 'Unknown error.'
81 console.error(`Backend returned code ${err.status}, errorMessage is: ${errorMessage}`)
82 } else {
83 console.error(err)
84 errorMessage = err
85 }
86
87 const errorObj: { message: string, status: string, body: string } = {
88 message: errorMessage,
89 status: undefined,
90 body: undefined
91 }
92
93 if (err.status) {
94 errorObj.status = err.status
95 errorObj.body = err.error
96 }
97
98 return observableThrowError(errorObj)
99 }
100
101 redirectTo404IfNotFound (obj: { status: number }, status = [ 404 ]) {
102 if (obj && obj.status && status.indexOf(obj.status) !== -1) {
103 // Do not use redirectService to avoid circular dependencies
104 this.router.navigate([ '/404' ], { skipLocationChange: true })
105 }
106
107 return observableThrowError(obj)
108 }
109}
diff --git a/client/src/app/core/rest/rest-pagination.ts b/client/src/app/core/rest/rest-pagination.ts
new file mode 100644
index 000000000..0faa59303
--- /dev/null
+++ b/client/src/app/core/rest/rest-pagination.ts
@@ -0,0 +1,4 @@
1export interface RestPagination {
2 start: number
3 count: number
4}
diff --git a/client/src/app/core/rest/rest-table.ts b/client/src/app/core/rest/rest-table.ts
new file mode 100644
index 000000000..1b35ad47d
--- /dev/null
+++ b/client/src/app/core/rest/rest-table.ts
@@ -0,0 +1,105 @@
1import { peertubeLocalStorage } from '@app/helpers/peertube-web-storage'
2import { LazyLoadEvent, SortMeta } from 'primeng/api'
3import { RestPagination } from './rest-pagination'
4import { Subject } from 'rxjs'
5import { debounceTime, distinctUntilChanged } from 'rxjs/operators'
6
7export abstract class RestTable {
8
9 abstract totalRecords: number
10 abstract sort: SortMeta
11 abstract pagination: RestPagination
12
13 search: string
14 rowsPerPageOptions = [ 10, 20, 50, 100 ]
15 rowsPerPage = this.rowsPerPageOptions[0]
16 expandedRows = {}
17
18 private searchStream: Subject<string>
19
20 abstract getIdentifier (): string
21
22 initialize () {
23 this.loadSort()
24 this.initSearch()
25 }
26
27 loadSort () {
28 const result = peertubeLocalStorage.getItem(this.getSortLocalStorageKey())
29
30 if (result) {
31 try {
32 this.sort = JSON.parse(result)
33 } catch (err) {
34 console.error('Cannot load sort of local storage key ' + this.getSortLocalStorageKey(), err)
35 }
36 }
37 }
38
39 loadLazy (event: LazyLoadEvent) {
40 this.sort = {
41 order: event.sortOrder,
42 field: event.sortField
43 }
44
45 this.pagination = {
46 start: event.first,
47 count: this.rowsPerPage
48 }
49
50 this.loadData()
51 this.saveSort()
52 }
53
54 saveSort () {
55 peertubeLocalStorage.setItem(this.getSortLocalStorageKey(), JSON.stringify(this.sort))
56 }
57
58 initSearch () {
59 this.searchStream = new Subject()
60
61 this.searchStream
62 .pipe(
63 debounceTime(400),
64 distinctUntilChanged()
65 )
66 .subscribe(search => {
67 this.search = search
68 this.loadData()
69 })
70 }
71
72 onSearch (event: Event) {
73 const target = event.target as HTMLInputElement
74 this.searchStream.next(target.value)
75 }
76
77 onPage (event: { first: number, rows: number }) {
78 if (this.rowsPerPage !== event.rows) {
79 this.rowsPerPage = event.rows
80 this.pagination = {
81 start: event.first,
82 count: this.rowsPerPage
83 }
84 this.loadData()
85 }
86 this.expandedRows = {}
87 }
88
89 setTableFilter (filter: string) {
90 // FIXME: cannot use ViewChild, so create a component for the filter input
91 const filterInput = document.getElementById('table-filter') as HTMLInputElement
92 if (filterInput) filterInput.value = filter
93 }
94
95 resetSearch () {
96 this.searchStream.next('')
97 this.setTableFilter('')
98 }
99
100 protected abstract loadData (): void
101
102 private getSortLocalStorageKey () {
103 return 'rest-table-sort-' + this.getIdentifier()
104 }
105}
diff --git a/client/src/app/core/rest/rest.service.ts b/client/src/app/core/rest/rest.service.ts
new file mode 100644
index 000000000..78558851a
--- /dev/null
+++ b/client/src/app/core/rest/rest.service.ts
@@ -0,0 +1,111 @@
1import { SortMeta } from 'primeng/api'
2import { HttpParams } from '@angular/common/http'
3import { Injectable } from '@angular/core'
4import { ComponentPaginationLight } from './component-pagination.model'
5import { RestPagination } from './rest-pagination'
6
7interface QueryStringFilterPrefixes {
8 [key: string]: {
9 prefix: string
10 handler?: (v: string) => string | number
11 multiple?: boolean
12 }
13}
14
15type ParseQueryStringFilterResult = {
16 [key: string]: string | number | (string | number)[]
17}
18
19@Injectable()
20export class RestService {
21
22 addRestGetParams (params: HttpParams, pagination?: RestPagination, sort?: SortMeta | string) {
23 let newParams = params
24
25 if (pagination !== undefined) {
26 newParams = newParams.set('start', pagination.start.toString())
27 .set('count', pagination.count.toString())
28 }
29
30 if (sort !== undefined) {
31 let sortString = ''
32
33 if (typeof sort === 'string') {
34 sortString = sort
35 } else {
36 const sortPrefix = sort.order === 1 ? '' : '-'
37 sortString = sortPrefix + sort.field
38 }
39
40 newParams = newParams.set('sort', sortString)
41 }
42
43 return newParams
44 }
45
46 addObjectParams (params: HttpParams, object: { [ name: string ]: any }) {
47 for (const name of Object.keys(object)) {
48 const value = object[name]
49 if (value === undefined || value === null) continue
50
51 if (Array.isArray(value) && value.length !== 0) {
52 for (const v of value) params = params.append(name, v)
53 } else {
54 params = params.append(name, value)
55 }
56 }
57
58 return params
59 }
60
61 componentPaginationToRestPagination (componentPagination: ComponentPaginationLight): RestPagination {
62 const start: number = (componentPagination.currentPage - 1) * componentPagination.itemsPerPage
63 const count: number = componentPagination.itemsPerPage
64
65 return { start, count }
66 }
67
68 parseQueryStringFilter (q: string, prefixes: QueryStringFilterPrefixes): ParseQueryStringFilterResult {
69 if (!q) return {}
70
71 // Tokenize the strings using spaces
72 const tokens = q.split(' ').filter(token => !!token)
73
74 // Build prefix array
75 const prefixeStrings = Object.values(prefixes)
76 .map(p => p.prefix)
77
78 // Search is the querystring minus defined filters
79 const searchTokens = tokens.filter(t => {
80 return prefixeStrings.every(prefixString => t.startsWith(prefixString) === false)
81 })
82
83 const additionalFilters: ParseQueryStringFilterResult = {}
84
85 for (const prefixKey of Object.keys(prefixes)) {
86 const prefixObj = prefixes[prefixKey]
87 const prefix = prefixObj.prefix
88
89 const matchedTokens = tokens.filter(t => t.startsWith(prefix))
90 .map(t => t.slice(prefix.length)) // Keep the value filter
91 .map(t => {
92 if (prefixObj.handler) return prefixObj.handler(t)
93
94 return t
95 })
96 .filter(t => !!t || t === 0)
97
98 if (matchedTokens.length === 0) continue
99
100 additionalFilters[prefixKey] = prefixObj.multiple === true
101 ? matchedTokens
102 : matchedTokens[0]
103 }
104
105 return {
106 search: searchTokens.join(' ') || undefined,
107
108 ...additionalFilters
109 }
110 }
111}
diff --git a/client/src/app/core/routing/can-deactivate-guard.service.ts b/client/src/app/core/routing/can-deactivate-guard.service.ts
new file mode 100644
index 000000000..e0405293a
--- /dev/null
+++ b/client/src/app/core/routing/can-deactivate-guard.service.ts
@@ -0,0 +1,30 @@
1import { Observable } from 'rxjs'
2import { Injectable } from '@angular/core'
3import { CanDeactivate } from '@angular/router'
4import { ConfirmService } from '@app/core/confirm'
5import { I18n } from '@ngx-translate/i18n-polyfill'
6
7export type CanComponentDeactivateResult = { text?: string, canDeactivate: Observable<boolean> | boolean }
8
9export interface CanComponentDeactivate {
10 canDeactivate: () => CanComponentDeactivateResult
11}
12
13@Injectable()
14export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate> {
15 constructor (
16 private confirmService: ConfirmService,
17 private i18n: I18n
18 ) { }
19
20 canDeactivate (component: CanComponentDeactivate) {
21 const result = component.canDeactivate()
22 const text = result.text || this.i18n('All unsaved data will be lost, are you sure you want to leave this page?')
23
24 return result.canDeactivate || this.confirmService.confirm(
25 text,
26 this.i18n('Warning')
27 )
28 }
29
30}
diff --git a/client/src/app/core/routing/index.ts b/client/src/app/core/routing/index.ts
index 58b83bb2a..239c27caf 100644
--- a/client/src/app/core/routing/index.ts
+++ b/client/src/app/core/routing/index.ts
@@ -1,5 +1,10 @@
1export * from './can-deactivate-guard.service'
2export * from './custom-reuse-strategy'
3export * from './disable-for-reuse-hook'
1export * from './login-guard.service' 4export * from './login-guard.service'
2export * from './user-right-guard.service' 5export * from './menu-guard.service'
3export * from './preload-selected-modules-list' 6export * from './preload-selected-modules-list'
4export * from './redirect.service' 7export * from './redirect.service'
5export * from './menu-guard.service' 8export * from './server-config-resolver.service'
9export * from './unlogged-guard.service'
10export * from './user-right-guard.service'
diff --git a/client/src/app/core/routing/login-guard.service.ts b/client/src/app/core/routing/login-guard.service.ts
index 7b1c37ee8..a949be14c 100644
--- a/client/src/app/core/routing/login-guard.service.ts
+++ b/client/src/app/core/routing/login-guard.service.ts
@@ -1,6 +1,5 @@
1import { Injectable } from '@angular/core' 1import { Injectable } from '@angular/core'
2import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, Router, RouterStateSnapshot } from '@angular/router' 2import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, Router, RouterStateSnapshot } from '@angular/router'
3
4import { AuthService } from '../auth/auth.service' 3import { AuthService } from '../auth/auth.service'
5 4
6@Injectable() 5@Injectable()
diff --git a/client/src/app/core/routing/menu-guard.service.ts b/client/src/app/core/routing/menu-guard.service.ts
index 907d145fd..9df285635 100644
--- a/client/src/app/core/routing/menu-guard.service.ts
+++ b/client/src/app/core/routing/menu-guard.service.ts
@@ -1,7 +1,7 @@
1import { Injectable } from '@angular/core' 1import { Injectable } from '@angular/core'
2import { CanActivate, CanDeactivate } from '@angular/router' 2import { CanActivate, CanDeactivate } from '@angular/router'
3import { MenuService } from '@app/core/menu' 3import { MenuService } from '../menu'
4import { ScreenService } from '@app/shared/misc/screen.service' 4import { ScreenService } from '../wrappers'
5 5
6abstract class MenuGuard implements CanActivate, CanDeactivate<any> { 6abstract class MenuGuard implements CanActivate, CanDeactivate<any> {
7 display = true 7 display = true
diff --git a/client/src/app/core/routing/preload-selected-modules-list.ts b/client/src/app/core/routing/preload-selected-modules-list.ts
index 64af68225..b494a40bc 100644
--- a/client/src/app/core/routing/preload-selected-modules-list.ts
+++ b/client/src/app/core/routing/preload-selected-modules-list.ts
@@ -5,6 +5,7 @@ import { Injectable } from '@angular/core'
5 5
6@Injectable() 6@Injectable()
7export class PreloadSelectedModulesList implements PreloadingStrategy { 7export class PreloadSelectedModulesList implements PreloadingStrategy {
8
8 preload (route: Route, load: Function): Observable<any> { 9 preload (route: Route, load: Function): Observable<any> {
9 if (!route.data || !route.data.preload) return ofObservable(null) 10 if (!route.data || !route.data.preload) return ofObservable(null)
10 11
diff --git a/client/src/app/core/routing/server-config-resolver.service.ts b/client/src/app/core/routing/server-config-resolver.service.ts
index 3b7ed99bf..0ce2023a0 100644
--- a/client/src/app/core/routing/server-config-resolver.service.ts
+++ b/client/src/app/core/routing/server-config-resolver.service.ts
@@ -1,6 +1,6 @@
1import { Injectable } from '@angular/core' 1import { Injectable } from '@angular/core'
2import { Resolve } from '@angular/router' 2import { Resolve } from '@angular/router'
3import { ServerService } from '@app/core/server' 3import { ServerService } from '../server'
4import { ServerConfig } from '@shared/models' 4import { ServerConfig } from '@shared/models'
5 5
6@Injectable() 6@Injectable()
diff --git a/client/src/app/core/routing/unlogged-guard.service.ts b/client/src/app/core/routing/unlogged-guard.service.ts
index 3132a1a77..0be7911a0 100644
--- a/client/src/app/core/routing/unlogged-guard.service.ts
+++ b/client/src/app/core/routing/unlogged-guard.service.ts
@@ -1,5 +1,5 @@
1import { Injectable } from '@angular/core' 1import { Injectable } from '@angular/core'
2import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, Router, RouterStateSnapshot } from '@angular/router' 2import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, RouterStateSnapshot } from '@angular/router'
3import { AuthService } from '../auth/auth.service' 3import { AuthService } from '../auth/auth.service'
4import { RedirectService } from './redirect.service' 4import { RedirectService } from './redirect.service'
5 5
@@ -7,7 +7,6 @@ import { RedirectService } from './redirect.service'
7export class UnloggedGuard implements CanActivate, CanActivateChild { 7export class UnloggedGuard implements CanActivate, CanActivateChild {
8 8
9 constructor ( 9 constructor (
10 private router: Router,
11 private auth: AuthService, 10 private auth: AuthService,
12 private redirectService: RedirectService 11 private redirectService: RedirectService
13 ) {} 12 ) {}
diff --git a/client/src/app/core/routing/user-right-guard.service.ts b/client/src/app/core/routing/user-right-guard.service.ts
index 50c3d8c19..a2ce772db 100644
--- a/client/src/app/core/routing/user-right-guard.service.ts
+++ b/client/src/app/core/routing/user-right-guard.service.ts
@@ -1,12 +1,5 @@
1import { Injectable } from '@angular/core' 1import { Injectable } from '@angular/core'
2import { 2import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, Router, RouterStateSnapshot } from '@angular/router'
3 ActivatedRouteSnapshot,
4 CanActivateChild,
5 RouterStateSnapshot,
6 CanActivate,
7 Router
8} from '@angular/router'
9
10import { AuthService } from '../auth/auth.service' 3import { AuthService } from '../auth/auth.service'
11 4
12@Injectable() 5@Injectable()
diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts
index a804efd28..32a135203 100644
--- a/client/src/app/core/server/server.service.ts
+++ b/client/src/app/core/server/server.service.ts
@@ -2,14 +2,16 @@ import { Observable, of, Subject } from 'rxjs'
2import { first, map, share, shareReplay, switchMap, tap } from 'rxjs/operators' 2import { first, map, share, shareReplay, switchMap, tap } from 'rxjs/operators'
3import { HttpClient } from '@angular/common/http' 3import { HttpClient } from '@angular/common/http'
4import { Inject, Injectable, LOCALE_ID } from '@angular/core' 4import { Inject, Injectable, LOCALE_ID } from '@angular/core'
5import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils' 5import { getDevLocale, isOnDevLocale, peertubeLocalStorage, sortBy } from '@app/helpers'
6import { peertubeLocalStorage } from '@app/shared/misc/peertube-web-storage' 6import {
7import { sortBy } from '@app/shared/misc/utils' 7 getCompleteLocale,
8import { SearchTargetType } from '@shared/models/search/search-target-query.model' 8 isDefaultLocale,
9import { ServerStats } from '@shared/models/server' 9 peertubeTranslate,
10import { getCompleteLocale, ServerConfig } from '../../../../../shared' 10 SearchTargetType,
11import { isDefaultLocale, peertubeTranslate } from '../../../../../shared/models/i18n' 11 ServerConfig,
12import { VideoConstant } from '../../../../../shared/models/videos' 12 ServerStats,
13 VideoConstant
14} from '@shared/models'
13import { environment } from '../../../environments/environment' 15import { environment } from '../../../environments/environment'
14 16
15@Injectable() 17@Injectable()
diff --git a/client/src/app/core/theme/theme.service.ts b/client/src/app/core/theme/theme.service.ts
index c0189ad32..9dbf22e20 100644
--- a/client/src/app/core/theme/theme.service.ts
+++ b/client/src/app/core/theme/theme.service.ts
@@ -1,13 +1,13 @@
1import { first } from 'rxjs/operators'
1import { Injectable } from '@angular/core' 2import { Injectable } from '@angular/core'
2import { AuthService } from '@app/core/auth' 3import { AuthService } from '../auth'
3import { ServerService } from '@app/core/server' 4import { PluginService } from '../plugins/plugin.service'
4import { environment } from '../../../environments/environment' 5import { ServerService } from '../server'
5import { PluginService } from '@app/core/plugins/plugin.service' 6import { LocalStorageService } from '../wrappers/storage.service'
7import { User } from '../users/user.model'
8import { UserService } from '../users/user.service'
6import { ServerConfig, ServerConfigTheme } from '@shared/models' 9import { ServerConfig, ServerConfigTheme } from '@shared/models'
7import { first } from 'rxjs/operators' 10import { environment } from '../../../environments/environment'
8import { User } from '@app/shared/users/user.model'
9import { UserService } from '@app/shared/users/user.service'
10import { LocalStorageService } from '@app/shared/misc/storage.service'
11 11
12@Injectable() 12@Injectable()
13export class ThemeService { 13export class ThemeService {
diff --git a/client/src/app/core/users/index.ts b/client/src/app/core/users/index.ts
new file mode 100644
index 000000000..7b5a67bc7
--- /dev/null
+++ b/client/src/app/core/users/index.ts
@@ -0,0 +1,2 @@
1export * from './user.model'
2export * from './user.service'
diff --git a/client/src/app/core/users/user.model.ts b/client/src/app/core/users/user.model.ts
new file mode 100644
index 000000000..8ecdf9fcd
--- /dev/null
+++ b/client/src/app/core/users/user.model.ts
@@ -0,0 +1,150 @@
1import { Account } from '@app/shared/shared-main/account/account.model'
2import {
3 Avatar,
4 hasUserRight,
5 NSFWPolicyType,
6 User as UserServerModel,
7 UserAdminFlag,
8 UserNotificationSetting,
9 UserRight,
10 UserRole,
11 VideoChannel
12} from '@shared/models'
13
14export class User implements UserServerModel {
15 static KEYS = {
16 ID: 'id',
17 ROLE: 'role',
18 EMAIL: 'email',
19 VIDEOS_HISTORY_ENABLED: 'videos-history-enabled',
20 USERNAME: 'username',
21 NSFW_POLICY: 'nsfw_policy',
22 WEBTORRENT_ENABLED: 'peertube-videojs-' + 'webtorrent_enabled',
23 AUTO_PLAY_VIDEO: 'auto_play_video',
24 SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO: 'auto_play_next_video',
25 AUTO_PLAY_VIDEO_PLAYLIST: 'auto_play_video_playlist',
26 THEME: 'last_active_theme',
27 VIDEO_LANGUAGES: 'video_languages'
28 }
29
30 id: number
31 username: string
32 email: string
33 pendingEmail: string | null
34
35 emailVerified: boolean
36 nsfwPolicy: NSFWPolicyType
37
38 adminFlags?: UserAdminFlag
39
40 autoPlayVideo: boolean
41 autoPlayNextVideo: boolean
42 autoPlayNextVideoPlaylist: boolean
43 webTorrentEnabled: boolean
44 videosHistoryEnabled: boolean
45 videoLanguages: string[]
46
47 role: UserRole
48 roleLabel: string
49
50 videoQuota: number
51 videoQuotaDaily: number
52 videoQuotaUsed?: number
53 videoQuotaUsedDaily?: number
54 videosCount?: number
55 videoAbusesCount?: number
56 videoAbusesAcceptedCount?: number
57 videoAbusesCreatedCount?: number
58 videoCommentsCount?: number
59
60 theme: string
61
62 account: Account
63 notificationSettings?: UserNotificationSetting
64 videoChannels?: VideoChannel[]
65
66 blocked: boolean
67 blockedReason?: string
68
69 noInstanceConfigWarningModal: boolean
70 noWelcomeModal: boolean
71
72 pluginAuth: string | null
73
74 lastLoginDate: Date | null
75
76 createdAt: Date
77
78 constructor (hash: Partial<UserServerModel>) {
79 this.id = hash.id
80 this.username = hash.username
81 this.email = hash.email
82
83 this.role = hash.role
84
85 this.videoChannels = hash.videoChannels
86
87 this.videoQuota = hash.videoQuota
88 this.videoQuotaDaily = hash.videoQuotaDaily
89 this.videoQuotaUsed = hash.videoQuotaUsed
90 this.videoQuotaUsedDaily = hash.videoQuotaUsedDaily
91 this.videosCount = hash.videosCount
92 this.videoAbusesCount = hash.videoAbusesCount
93 this.videoAbusesAcceptedCount = hash.videoAbusesAcceptedCount
94 this.videoAbusesCreatedCount = hash.videoAbusesCreatedCount
95 this.videoCommentsCount = hash.videoCommentsCount
96
97 this.nsfwPolicy = hash.nsfwPolicy
98 this.webTorrentEnabled = hash.webTorrentEnabled
99 this.autoPlayVideo = hash.autoPlayVideo
100 this.autoPlayNextVideo = hash.autoPlayNextVideo
101 this.autoPlayNextVideoPlaylist = hash.autoPlayNextVideoPlaylist
102 this.videosHistoryEnabled = hash.videosHistoryEnabled
103 this.videoLanguages = hash.videoLanguages
104
105 this.theme = hash.theme
106
107 this.adminFlags = hash.adminFlags
108
109 this.blocked = hash.blocked
110 this.blockedReason = hash.blockedReason
111
112 this.noInstanceConfigWarningModal = hash.noInstanceConfigWarningModal
113 this.noWelcomeModal = hash.noWelcomeModal
114
115 this.notificationSettings = hash.notificationSettings
116
117 this.createdAt = hash.createdAt
118
119 this.pluginAuth = hash.pluginAuth
120 this.lastLoginDate = hash.lastLoginDate
121
122 if (hash.account !== undefined) {
123 this.account = new Account(hash.account)
124 }
125 }
126
127 get accountAvatarUrl () {
128 if (!this.account) return ''
129
130 return this.account.avatarUrl
131 }
132
133 hasRight (right: UserRight) {
134 return hasUserRight(this.role, right)
135 }
136
137 patch (obj: UserServerModel) {
138 for (const key of Object.keys(obj)) {
139 this[key] = obj[key]
140 }
141
142 if (obj.account !== undefined) {
143 this.account = new Account(obj.account)
144 }
145 }
146
147 updateAccountAvatar (newAccountAvatar: Avatar) {
148 this.account.updateAvatar(newAccountAvatar)
149 }
150}
diff --git a/client/src/app/core/users/user.service.ts b/client/src/app/core/users/user.service.ts
new file mode 100644
index 000000000..ab395b1f9
--- /dev/null
+++ b/client/src/app/core/users/user.service.ts
@@ -0,0 +1,375 @@
1import { has } from 'lodash-es'
2import { BytesPipe } from 'ngx-pipes'
3import { SortMeta } from 'primeng/api'
4import { from, Observable, of } from 'rxjs'
5import { catchError, concatMap, filter, first, map, shareReplay, throttleTime, toArray } from 'rxjs/operators'
6import { HttpClient, HttpParams } from '@angular/common/http'
7import { Injectable } from '@angular/core'
8import { AuthService } from '@app/core/auth'
9import { I18n } from '@ngx-translate/i18n-polyfill'
10import {
11 Avatar,
12 NSFWPolicyType,
13 ResultList,
14 User as UserServerModel,
15 UserCreate,
16 UserRegister,
17 UserRole,
18 UserUpdate,
19 UserUpdateMe,
20 UserVideoQuota
21} from '@shared/models'
22import { environment } from '../../../environments/environment'
23import { RestExtractor, RestPagination, RestService } from '../rest'
24import { LocalStorageService, SessionStorageService } from '../wrappers/storage.service'
25import { User } from './user.model'
26
27@Injectable()
28export class UserService {
29 static BASE_USERS_URL = environment.apiUrl + '/api/v1/users/'
30
31 private bytesPipe = new BytesPipe()
32
33 private userCache: { [ id: number ]: Observable<UserServerModel> } = {}
34
35 constructor (
36 private authHttp: HttpClient,
37 private authService: AuthService,
38 private restExtractor: RestExtractor,
39 private restService: RestService,
40 private localStorageService: LocalStorageService,
41 private sessionStorageService: SessionStorageService,
42 private i18n: I18n
43 ) { }
44
45 changePassword (currentPassword: string, newPassword: string) {
46 const url = UserService.BASE_USERS_URL + 'me'
47 const body: UserUpdateMe = {
48 currentPassword,
49 password: newPassword
50 }
51
52 return this.authHttp.put(url, body)
53 .pipe(
54 map(this.restExtractor.extractDataBool),
55 catchError(err => this.restExtractor.handleError(err))
56 )
57 }
58
59 changeEmail (password: string, newEmail: string) {
60 const url = UserService.BASE_USERS_URL + 'me'
61 const body: UserUpdateMe = {
62 currentPassword: password,
63 email: newEmail
64 }
65
66 return this.authHttp.put(url, body)
67 .pipe(
68 map(this.restExtractor.extractDataBool),
69 catchError(err => this.restExtractor.handleError(err))
70 )
71 }
72
73 updateMyProfile (profile: UserUpdateMe) {
74 const url = UserService.BASE_USERS_URL + 'me'
75
76 return this.authHttp.put(url, profile)
77 .pipe(
78 map(this.restExtractor.extractDataBool),
79 catchError(err => this.restExtractor.handleError(err))
80 )
81 }
82
83 updateMyAnonymousProfile (profile: UserUpdateMe) {
84 const supportedKeys = {
85 // local storage keys
86 nsfwPolicy: (val: NSFWPolicyType) => this.localStorageService.setItem(User.KEYS.NSFW_POLICY, val),
87 webTorrentEnabled: (val: boolean) => this.localStorageService.setItem(User.KEYS.WEBTORRENT_ENABLED, String(val)),
88 autoPlayVideo: (val: boolean) => this.localStorageService.setItem(User.KEYS.AUTO_PLAY_VIDEO, String(val)),
89 autoPlayNextVideoPlaylist: (val: boolean) => this.localStorageService.setItem(User.KEYS.AUTO_PLAY_VIDEO_PLAYLIST, String(val)),
90 theme: (val: string) => this.localStorageService.setItem(User.KEYS.THEME, val),
91 videoLanguages: (val: string[]) => this.localStorageService.setItem(User.KEYS.VIDEO_LANGUAGES, JSON.stringify(val)),
92
93 // session storage keys
94 autoPlayNextVideo: (val: boolean) =>
95 this.sessionStorageService.setItem(User.KEYS.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO, String(val))
96 }
97
98 for (const key of Object.keys(profile)) {
99 try {
100 if (has(supportedKeys, key)) supportedKeys[key](profile[key])
101 } catch (err) {
102 console.error(`Cannot set item ${key} in localStorage. Likely due to a value impossible to stringify.`, err)
103 }
104 }
105 }
106
107 listenAnonymousUpdate () {
108 return this.localStorageService.watch([
109 User.KEYS.NSFW_POLICY,
110 User.KEYS.WEBTORRENT_ENABLED,
111 User.KEYS.AUTO_PLAY_VIDEO,
112 User.KEYS.AUTO_PLAY_VIDEO_PLAYLIST,
113 User.KEYS.THEME,
114 User.KEYS.VIDEO_LANGUAGES
115 ]).pipe(
116 throttleTime(200),
117 filter(() => this.authService.isLoggedIn() !== true),
118 map(() => this.getAnonymousUser())
119 )
120 }
121
122 deleteMe () {
123 const url = UserService.BASE_USERS_URL + 'me'
124
125 return this.authHttp.delete(url)
126 .pipe(
127 map(this.restExtractor.extractDataBool),
128 catchError(err => this.restExtractor.handleError(err))
129 )
130 }
131
132 changeAvatar (avatarForm: FormData) {
133 const url = UserService.BASE_USERS_URL + 'me/avatar/pick'
134
135 return this.authHttp.post<{ avatar: Avatar }>(url, avatarForm)
136 .pipe(catchError(err => this.restExtractor.handleError(err)))
137 }
138
139 signup (userCreate: UserRegister) {
140 return this.authHttp.post(UserService.BASE_USERS_URL + 'register', userCreate)
141 .pipe(
142 map(this.restExtractor.extractDataBool),
143 catchError(err => this.restExtractor.handleError(err))
144 )
145 }
146
147 getMyVideoQuotaUsed () {
148 const url = UserService.BASE_USERS_URL + 'me/video-quota-used'
149
150 return this.authHttp.get<UserVideoQuota>(url)
151 .pipe(catchError(err => this.restExtractor.handleError(err)))
152 }
153
154 askResetPassword (email: string) {
155 const url = UserService.BASE_USERS_URL + '/ask-reset-password'
156
157 return this.authHttp.post(url, { email })
158 .pipe(
159 map(this.restExtractor.extractDataBool),
160 catchError(err => this.restExtractor.handleError(err))
161 )
162 }
163
164 resetPassword (userId: number, verificationString: string, password: string) {
165 const url = `${UserService.BASE_USERS_URL}/${userId}/reset-password`
166 const body = {
167 verificationString,
168 password
169 }
170
171 return this.authHttp.post(url, body)
172 .pipe(
173 map(this.restExtractor.extractDataBool),
174 catchError(res => this.restExtractor.handleError(res))
175 )
176 }
177
178 verifyEmail (userId: number, verificationString: string, isPendingEmail: boolean) {
179 const url = `${UserService.BASE_USERS_URL}/${userId}/verify-email`
180 const body = {
181 verificationString,
182 isPendingEmail
183 }
184
185 return this.authHttp.post(url, body)
186 .pipe(
187 map(this.restExtractor.extractDataBool),
188 catchError(res => this.restExtractor.handleError(res))
189 )
190 }
191
192 askSendVerifyEmail (email: string) {
193 const url = UserService.BASE_USERS_URL + '/ask-send-verify-email'
194
195 return this.authHttp.post(url, { email })
196 .pipe(
197 map(this.restExtractor.extractDataBool),
198 catchError(err => this.restExtractor.handleError(err))
199 )
200 }
201
202 autocomplete (search: string): Observable<string[]> {
203 const url = UserService.BASE_USERS_URL + 'autocomplete'
204 const params = new HttpParams().append('search', search)
205
206 return this.authHttp
207 .get<string[]>(url, { params })
208 .pipe(catchError(res => this.restExtractor.handleError(res)))
209 }
210
211 getNewUsername (oldDisplayName: string, newDisplayName: string, currentUsername: string) {
212 // Don't update display name, the user seems to have changed it
213 if (this.displayNameToUsername(oldDisplayName) !== currentUsername) return currentUsername
214
215 return this.displayNameToUsername(newDisplayName)
216 }
217
218 displayNameToUsername (displayName: string) {
219 if (!displayName) return ''
220
221 return displayName
222 .toLowerCase()
223 .replace(/\s/g, '_')
224 .replace(/[^a-z0-9_.]/g, '')
225 }
226
227 /* ###### Admin methods ###### */
228
229 addUser (userCreate: UserCreate) {
230 return this.authHttp.post(UserService.BASE_USERS_URL, userCreate)
231 .pipe(
232 map(this.restExtractor.extractDataBool),
233 catchError(err => this.restExtractor.handleError(err))
234 )
235 }
236
237 updateUser (userId: number, userUpdate: UserUpdate) {
238 return this.authHttp.put(UserService.BASE_USERS_URL + userId, userUpdate)
239 .pipe(
240 map(this.restExtractor.extractDataBool),
241 catchError(err => this.restExtractor.handleError(err))
242 )
243 }
244
245 updateUsers (users: UserServerModel[], userUpdate: UserUpdate) {
246 return from(users)
247 .pipe(
248 concatMap(u => this.authHttp.put(UserService.BASE_USERS_URL + u.id, userUpdate)),
249 toArray(),
250 catchError(err => this.restExtractor.handleError(err))
251 )
252 }
253
254 getUserWithCache (userId: number) {
255 if (!this.userCache[userId]) {
256 this.userCache[ userId ] = this.getUser(userId).pipe(shareReplay())
257 }
258
259 return this.userCache[userId]
260 }
261
262 getUser (userId: number, withStats = false) {
263 const params = new HttpParams().append('withStats', withStats + '')
264 return this.authHttp.get<UserServerModel>(UserService.BASE_USERS_URL + userId, { params })
265 .pipe(catchError(err => this.restExtractor.handleError(err)))
266 }
267
268 getAnonymousUser () {
269 let videoLanguages: string[]
270
271 try {
272 videoLanguages = JSON.parse(this.localStorageService.getItem(User.KEYS.VIDEO_LANGUAGES))
273 } catch (err) {
274 videoLanguages = null
275 console.error('Cannot parse desired video languages from localStorage.', err)
276 }
277
278 return new User({
279 // local storage keys
280 nsfwPolicy: this.localStorageService.getItem(User.KEYS.NSFW_POLICY) as NSFWPolicyType,
281 webTorrentEnabled: this.localStorageService.getItem(User.KEYS.WEBTORRENT_ENABLED) !== 'false',
282 theme: this.localStorageService.getItem(User.KEYS.THEME) || 'instance-default',
283 videoLanguages,
284
285 autoPlayNextVideoPlaylist: this.localStorageService.getItem(User.KEYS.AUTO_PLAY_VIDEO_PLAYLIST) !== 'false',
286 autoPlayVideo: this.localStorageService.getItem(User.KEYS.AUTO_PLAY_VIDEO) === 'true',
287
288 // session storage keys
289 autoPlayNextVideo: this.sessionStorageService.getItem(User.KEYS.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO) === 'true'
290 })
291 }
292
293 getUsers (pagination: RestPagination, sort: SortMeta, search?: string): Observable<ResultList<UserServerModel>> {
294 let params = new HttpParams()
295 params = this.restService.addRestGetParams(params, pagination, sort)
296
297 if (search) params = params.append('search', search)
298
299 return this.authHttp.get<ResultList<UserServerModel>>(UserService.BASE_USERS_URL, { params })
300 .pipe(
301 map(res => this.restExtractor.convertResultListDateToHuman(res)),
302 map(res => this.restExtractor.applyToResultListData(res, this.formatUser.bind(this))),
303 catchError(err => this.restExtractor.handleError(err))
304 )
305 }
306
307 removeUser (usersArg: UserServerModel | UserServerModel[]) {
308 const users = Array.isArray(usersArg) ? usersArg : [ usersArg ]
309
310 return from(users)
311 .pipe(
312 concatMap(u => this.authHttp.delete(UserService.BASE_USERS_URL + u.id)),
313 toArray(),
314 catchError(err => this.restExtractor.handleError(err))
315 )
316 }
317
318 banUsers (usersArg: UserServerModel | UserServerModel[], reason?: string) {
319 const body = reason ? { reason } : {}
320 const users = Array.isArray(usersArg) ? usersArg : [ usersArg ]
321
322 return from(users)
323 .pipe(
324 concatMap(u => this.authHttp.post(UserService.BASE_USERS_URL + u.id + '/block', body)),
325 toArray(),
326 catchError(err => this.restExtractor.handleError(err))
327 )
328 }
329
330 unbanUsers (usersArg: UserServerModel | UserServerModel[]) {
331 const users = Array.isArray(usersArg) ? usersArg : [ usersArg ]
332
333 return from(users)
334 .pipe(
335 concatMap(u => this.authHttp.post(UserService.BASE_USERS_URL + u.id + '/unblock', {})),
336 toArray(),
337 catchError(err => this.restExtractor.handleError(err))
338 )
339 }
340
341 getAnonymousOrLoggedUser () {
342 if (!this.authService.isLoggedIn()) {
343 return of(this.getAnonymousUser())
344 }
345
346 return this.authService.userInformationLoaded
347 .pipe(
348 first(),
349 map(() => this.authService.getUser())
350 )
351 }
352
353 private formatUser (user: UserServerModel) {
354 let videoQuota
355 if (user.videoQuota === -1) {
356 videoQuota = this.i18n('Unlimited')
357 } else {
358 videoQuota = this.bytesPipe.transform(user.videoQuota, 0)
359 }
360
361 const videoQuotaUsed = this.bytesPipe.transform(user.videoQuotaUsed, 0)
362
363 const roleLabels: { [ id in UserRole ]: string } = {
364 [UserRole.USER]: this.i18n('User'),
365 [UserRole.ADMINISTRATOR]: this.i18n('Administrator'),
366 [UserRole.MODERATOR]: this.i18n('Moderator')
367 }
368
369 return Object.assign(user, {
370 roleLabel: roleLabels[user.role],
371 videoQuota,
372 videoQuotaUsed
373 })
374 }
375}
diff --git a/client/src/app/core/wrappers/index.ts b/client/src/app/core/wrappers/index.ts
new file mode 100644
index 000000000..d82b70070
--- /dev/null
+++ b/client/src/app/core/wrappers/index.ts
@@ -0,0 +1,2 @@
1export * from './screen.service'
2export * from './storage.service'
diff --git a/client/src/app/core/wrappers/screen.service.ts b/client/src/app/core/wrappers/screen.service.ts
new file mode 100644
index 000000000..a69fad31d
--- /dev/null
+++ b/client/src/app/core/wrappers/screen.service.ts
@@ -0,0 +1,66 @@
1import { Injectable } from '@angular/core'
2
3@Injectable()
4export class ScreenService {
5 private windowInnerWidth: number
6 private lastFunctionCallTime: number
7 private cacheForMs = 500
8
9 constructor () {
10 this.refreshWindowInnerWidth()
11 }
12
13 isInSmallView (marginLeft = 0) {
14 if (marginLeft > 0) {
15 const contentWidth = this.getWindowInnerWidth() - marginLeft
16 return contentWidth < 800
17 }
18
19 return this.getWindowInnerWidth() < 800
20 }
21
22 isInMediumView () {
23 return this.getWindowInnerWidth() < 1100
24 }
25
26 isInMobileView () {
27 return this.getWindowInnerWidth() < 500
28 }
29
30 isInTouchScreen () {
31 return 'ontouchstart' in window || navigator.msMaxTouchPoints
32 }
33
34 getNumberOfAvailableMiniatures () {
35 const screenWidth = this.getWindowInnerWidth()
36
37 let numberOfVideos = 1
38
39 if (screenWidth > 1850) numberOfVideos = 7
40 else if (screenWidth > 1600) numberOfVideos = 6
41 else if (screenWidth > 1370) numberOfVideos = 5
42 else if (screenWidth > 1100) numberOfVideos = 4
43 else if (screenWidth > 850) numberOfVideos = 3
44
45 return numberOfVideos
46 }
47
48 // Cache window inner width, because it's an expensive call
49 getWindowInnerWidth () {
50 if (this.cacheWindowInnerWidthExpired()) this.refreshWindowInnerWidth()
51
52 return this.windowInnerWidth
53 }
54
55 private refreshWindowInnerWidth () {
56 this.lastFunctionCallTime = new Date().getTime()
57
58 this.windowInnerWidth = window.innerWidth
59 }
60
61 private cacheWindowInnerWidthExpired () {
62 if (!this.lastFunctionCallTime) return true
63
64 return new Date().getTime() > (this.lastFunctionCallTime + this.cacheForMs)
65 }
66}
diff --git a/client/src/app/core/wrappers/storage.service.ts b/client/src/app/core/wrappers/storage.service.ts
new file mode 100644
index 000000000..9a60b9785
--- /dev/null
+++ b/client/src/app/core/wrappers/storage.service.ts
@@ -0,0 +1,37 @@
1import { Observable, Subject } from 'rxjs'
2import { filter } from 'rxjs/operators'
3import { Injectable } from '@angular/core'
4import { peertubeLocalStorage, peertubeSessionStorage } from '@app/helpers'
5
6abstract class StorageService {
7 protected instance: Storage
8 static storageSub = new Subject<string>()
9
10 watch (keys?: string[]): Observable<string> {
11 return StorageService.storageSub.asObservable().pipe(filter(val => keys ? keys.includes(val) : true))
12 }
13
14 getItem (key: string) {
15 return this.instance.getItem(key)
16 }
17
18 setItem (key: string, data: any, notifyOfUpdate = true) {
19 this.instance.setItem(key, data)
20 if (notifyOfUpdate) StorageService.storageSub.next(key)
21 }
22
23 removeItem (key: string, notifyOfUpdate = true) {
24 this.instance.removeItem(key)
25 if (notifyOfUpdate) StorageService.storageSub.next(key)
26 }
27}
28
29@Injectable()
30export class LocalStorageService extends StorageService {
31 protected instance: Storage = peertubeLocalStorage
32}
33
34@Injectable()
35export class SessionStorageService extends StorageService {
36 protected instance: Storage = peertubeSessionStorage
37}