diff options
Diffstat (limited to 'client')
143 files changed, 2621 insertions, 728 deletions
diff --git a/client/.gitignore b/client/.gitignore index 3241b09ed..ca68413c8 100644 --- a/client/.gitignore +++ b/client/.gitignore | |||
@@ -11,5 +11,6 @@ | |||
11 | /src/locale/target/server_*.xml | 11 | /src/locale/target/server_*.xml |
12 | /e2e/local.log | 12 | /e2e/local.log |
13 | /e2e/browserstack.err | 13 | /e2e/browserstack.err |
14 | /e2e/screenshots | ||
14 | /src/standalone/player/build | 15 | /src/standalone/player/build |
15 | /src/standalone/player/dist | 16 | /src/standalone/player/dist |
diff --git a/client/e2e/src/po/admin-config.po.ts b/client/e2e/src/po/admin-config.po.ts index 27957a71f..510037ddd 100644 --- a/client/e2e/src/po/admin-config.po.ts +++ b/client/e2e/src/po/admin-config.po.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { getCheckbox, go } from '../utils' | 1 | import { browserSleep, getCheckbox, go, isCheckboxSelected } from '../utils' |
2 | 2 | ||
3 | export class AdminConfigPage { | 3 | export class AdminConfigPage { |
4 | 4 | ||
@@ -8,7 +8,6 @@ export class AdminConfigPage { | |||
8 | 'basic-configuration': 'APPEARANCE', | 8 | 'basic-configuration': 'APPEARANCE', |
9 | 'instance-information': 'INSTANCE' | 9 | 'instance-information': 'INSTANCE' |
10 | } | 10 | } |
11 | |||
12 | await go('/admin/config/edit-custom#' + tab) | 11 | await go('/admin/config/edit-custom#' + tab) |
13 | 12 | ||
14 | await $('.inner-form-title=' + waitTitles[tab]).waitForDisplayed() | 13 | await $('.inner-form-title=' + waitTitles[tab]).waitForDisplayed() |
@@ -28,17 +27,39 @@ export class AdminConfigPage { | |||
28 | return $('#instanceCustomHomepageContent').setValue(newValue) | 27 | return $('#instanceCustomHomepageContent').setValue(newValue) |
29 | } | 28 | } |
30 | 29 | ||
31 | async toggleSignup () { | 30 | async toggleSignup (enabled: boolean) { |
31 | if (await isCheckboxSelected('signupEnabled') === enabled) return | ||
32 | |||
32 | const checkbox = await getCheckbox('signupEnabled') | 33 | const checkbox = await getCheckbox('signupEnabled') |
33 | 34 | ||
34 | await checkbox.waitForClickable() | 35 | await checkbox.waitForClickable() |
35 | await checkbox.click() | 36 | await checkbox.click() |
36 | } | 37 | } |
37 | 38 | ||
39 | async toggleSignupApproval (required: boolean) { | ||
40 | if (await isCheckboxSelected('signupRequiresApproval') === required) return | ||
41 | |||
42 | const checkbox = await getCheckbox('signupRequiresApproval') | ||
43 | |||
44 | await checkbox.waitForClickable() | ||
45 | await checkbox.click() | ||
46 | } | ||
47 | |||
48 | async toggleSignupEmailVerification (required: boolean) { | ||
49 | if (await isCheckboxSelected('signupRequiresEmailVerification') === required) return | ||
50 | |||
51 | const checkbox = await getCheckbox('signupRequiresEmailVerification') | ||
52 | |||
53 | await checkbox.waitForClickable() | ||
54 | await checkbox.click() | ||
55 | } | ||
56 | |||
38 | async save () { | 57 | async save () { |
39 | const button = $('input[type=submit]') | 58 | const button = $('input[type=submit]') |
40 | 59 | ||
41 | await button.waitForClickable() | 60 | await button.waitForClickable() |
42 | await button.click() | 61 | await button.click() |
62 | |||
63 | await browserSleep(1000) | ||
43 | } | 64 | } |
44 | } | 65 | } |
diff --git a/client/e2e/src/po/admin-registration.po.ts b/client/e2e/src/po/admin-registration.po.ts new file mode 100644 index 000000000..85234654d --- /dev/null +++ b/client/e2e/src/po/admin-registration.po.ts | |||
@@ -0,0 +1,35 @@ | |||
1 | import { browserSleep, findParentElement, go } from '../utils' | ||
2 | |||
3 | export class AdminRegistrationPage { | ||
4 | |||
5 | async navigateToRegistratonsList () { | ||
6 | await go('/admin/moderation/registrations/list') | ||
7 | |||
8 | await $('my-registration-list').waitForDisplayed() | ||
9 | } | ||
10 | |||
11 | async accept (username: string, moderationResponse: string) { | ||
12 | const usernameEl = await $('*=' + username) | ||
13 | await usernameEl.waitForDisplayed() | ||
14 | |||
15 | const tr = await findParentElement(usernameEl, async el => await el.getTagName() === 'tr') | ||
16 | |||
17 | await tr.$('.action-cell .dropdown-root').click() | ||
18 | |||
19 | const accept = await $('span*=Accept this registration') | ||
20 | await accept.waitForClickable() | ||
21 | await accept.click() | ||
22 | |||
23 | const moderationResponseTextarea = await $('#moderationResponse') | ||
24 | await moderationResponseTextarea.waitForDisplayed() | ||
25 | |||
26 | await moderationResponseTextarea.setValue(moderationResponse) | ||
27 | |||
28 | const submitButton = $('.modal-footer input[type=submit]') | ||
29 | await submitButton.waitForClickable() | ||
30 | await submitButton.click() | ||
31 | |||
32 | await browserSleep(1000) | ||
33 | } | ||
34 | |||
35 | } | ||
diff --git a/client/e2e/src/po/login.po.ts b/client/e2e/src/po/login.po.ts index bc1854dbc..f1d13a2b0 100644 --- a/client/e2e/src/po/login.po.ts +++ b/client/e2e/src/po/login.po.ts | |||
@@ -6,7 +6,14 @@ export class LoginPage { | |||
6 | 6 | ||
7 | } | 7 | } |
8 | 8 | ||
9 | async login (username: string, password: string, url = '/login') { | 9 | async login (options: { |
10 | username: string | ||
11 | password: string | ||
12 | displayName?: string | ||
13 | url?: string | ||
14 | }) { | ||
15 | const { username, password, url = '/login', displayName = username } = options | ||
16 | |||
10 | await go(url) | 17 | await go(url) |
11 | 18 | ||
12 | await browser.execute(`window.localStorage.setItem('no_account_setup_warning_modal', 'true')`) | 19 | await browser.execute(`window.localStorage.setItem('no_account_setup_warning_modal', 'true')`) |
@@ -27,27 +34,40 @@ export class LoginPage { | |||
27 | 34 | ||
28 | await menuToggle.click() | 35 | await menuToggle.click() |
29 | 36 | ||
30 | await this.ensureIsLoggedInAs(username) | 37 | await this.ensureIsLoggedInAs(displayName) |
31 | 38 | ||
32 | await menuToggle.click() | 39 | await menuToggle.click() |
33 | } else { | 40 | } else { |
34 | await this.ensureIsLoggedInAs(username) | 41 | await this.ensureIsLoggedInAs(displayName) |
35 | } | 42 | } |
36 | } | 43 | } |
37 | 44 | ||
45 | async getLoginError (username: string, password: string) { | ||
46 | await go('/login') | ||
47 | |||
48 | await $('input#username').setValue(username) | ||
49 | await $('input#password').setValue(password) | ||
50 | |||
51 | await browser.pause(1000) | ||
52 | |||
53 | await $('form input[type=submit]').click() | ||
54 | |||
55 | return $('.alert-danger').getText() | ||
56 | } | ||
57 | |||
38 | async loginAsRootUser () { | 58 | async loginAsRootUser () { |
39 | return this.login('root', 'test' + this.getSuffix()) | 59 | return this.login({ username: 'root', password: 'test' + this.getSuffix() }) |
40 | } | 60 | } |
41 | 61 | ||
42 | loginOnPeerTube2 () { | 62 | loginOnPeerTube2 () { |
43 | return this.login('e2e', process.env.PEERTUBE2_E2E_PASSWORD, 'https://peertube2.cpy.re/login') | 63 | return this.login({ username: 'e2e', password: process.env.PEERTUBE2_E2E_PASSWORD, url: 'https://peertube2.cpy.re/login' }) |
44 | } | 64 | } |
45 | 65 | ||
46 | async logout () { | 66 | async logout () { |
47 | const loggedInMore = $('.logged-in-more') | 67 | const loggedInDropdown = $('.logged-in-more .logged-in-info') |
48 | 68 | ||
49 | await loggedInMore.waitForClickable() | 69 | await loggedInDropdown.waitForClickable() |
50 | await loggedInMore.click() | 70 | await loggedInDropdown.click() |
51 | 71 | ||
52 | const logout = $('.dropdown-item*=Log out') | 72 | const logout = $('.dropdown-item*=Log out') |
53 | 73 | ||
diff --git a/client/e2e/src/po/signup.po.ts b/client/e2e/src/po/signup.po.ts index cc2ed7c5f..7917cdda7 100644 --- a/client/e2e/src/po/signup.po.ts +++ b/client/e2e/src/po/signup.po.ts | |||
@@ -27,42 +27,39 @@ export class SignupPage { | |||
27 | return terms.click() | 27 | return terms.click() |
28 | } | 28 | } |
29 | 29 | ||
30 | async getEndMessage () { | ||
31 | const alert = $('.pt-alert-primary') | ||
32 | await alert.waitForDisplayed() | ||
33 | |||
34 | return alert.getText() | ||
35 | } | ||
36 | |||
37 | async fillRegistrationReason (reason: string) { | ||
38 | await $('#registrationReason').setValue(reason) | ||
39 | } | ||
40 | |||
30 | async fillAccountStep (options: { | 41 | async fillAccountStep (options: { |
31 | displayName: string | ||
32 | username: string | 42 | username: string |
33 | email: string | 43 | password?: string |
34 | password: string | 44 | displayName?: string |
45 | email?: string | ||
35 | }) { | 46 | }) { |
36 | if (options.displayName) { | 47 | await $('#displayName').setValue(options.displayName || `${options.username} display name`) |
37 | await $('#displayName').setValue(options.displayName) | ||
38 | } | ||
39 | |||
40 | if (options.username) { | ||
41 | await $('#username').setValue(options.username) | ||
42 | } | ||
43 | 48 | ||
44 | if (options.email) { | 49 | await $('#username').setValue(options.username) |
45 | // Fix weird bug on firefox that "cannot scroll into view" when using just `setValue` | 50 | await $('#password').setValue(options.password || 'password') |
46 | await $('#email').scrollIntoView(false) | ||
47 | await $('#email').waitForClickable() | ||
48 | await $('#email').setValue(options.email) | ||
49 | } | ||
50 | 51 | ||
51 | if (options.password) { | 52 | // Fix weird bug on firefox that "cannot scroll into view" when using just `setValue` |
52 | await $('#password').setValue(options.password) | 53 | await $('#email').scrollIntoView(false) |
53 | } | 54 | await $('#email').waitForClickable() |
55 | await $('#email').setValue(options.email || `${options.username}@example.com`) | ||
54 | } | 56 | } |
55 | 57 | ||
56 | async fillChannelStep (options: { | 58 | async fillChannelStep (options: { |
57 | displayName: string | ||
58 | name: string | 59 | name: string |
60 | displayName?: string | ||
59 | }) { | 61 | }) { |
60 | if (options.displayName) { | 62 | await $('#displayName').setValue(options.displayName || `${options.name} channel display name`) |
61 | await $('#displayName').setValue(options.displayName) | 63 | await $('#name').setValue(options.name) |
62 | } | ||
63 | |||
64 | if (options.name) { | ||
65 | await $('#name').setValue(options.name) | ||
66 | } | ||
67 | } | 64 | } |
68 | } | 65 | } |
diff --git a/client/e2e/src/suites-local/signup.e2e-spec.ts b/client/e2e/src/suites-local/signup.e2e-spec.ts index 4eed3eefe..b6f7ad1a7 100644 --- a/client/e2e/src/suites-local/signup.e2e-spec.ts +++ b/client/e2e/src/suites-local/signup.e2e-spec.ts | |||
@@ -1,12 +1,89 @@ | |||
1 | import { AdminConfigPage } from '../po/admin-config.po' | 1 | import { AdminConfigPage } from '../po/admin-config.po' |
2 | import { AdminRegistrationPage } from '../po/admin-registration.po' | ||
2 | import { LoginPage } from '../po/login.po' | 3 | import { LoginPage } from '../po/login.po' |
3 | import { SignupPage } from '../po/signup.po' | 4 | import { SignupPage } from '../po/signup.po' |
4 | import { isMobileDevice, waitServerUp } from '../utils' | 5 | import { browserSleep, getVerificationLink, go, findEmailTo, isMobileDevice, MockSMTPServer, waitServerUp } from '../utils' |
6 | |||
7 | function checkEndMessage (options: { | ||
8 | message: string | ||
9 | requiresEmailVerification: boolean | ||
10 | requiresApproval: boolean | ||
11 | afterEmailVerification: boolean | ||
12 | }) { | ||
13 | const { message, requiresApproval, requiresEmailVerification, afterEmailVerification } = options | ||
14 | |||
15 | { | ||
16 | const created = 'account has been created' | ||
17 | const request = 'account request has been sent' | ||
18 | |||
19 | if (requiresApproval) { | ||
20 | expect(message).toContain(request) | ||
21 | expect(message).not.toContain(created) | ||
22 | } else { | ||
23 | expect(message).not.toContain(request) | ||
24 | expect(message).toContain(created) | ||
25 | } | ||
26 | } | ||
27 | |||
28 | { | ||
29 | const checkEmail = 'Check your emails' | ||
30 | |||
31 | if (requiresEmailVerification) { | ||
32 | expect(message).toContain(checkEmail) | ||
33 | } else { | ||
34 | expect(message).not.toContain(checkEmail) | ||
35 | |||
36 | const moderatorsApproval = 'moderator will check your registration request' | ||
37 | if (requiresApproval) { | ||
38 | expect(message).toContain(moderatorsApproval) | ||
39 | } else { | ||
40 | expect(message).not.toContain(moderatorsApproval) | ||
41 | } | ||
42 | } | ||
43 | } | ||
44 | |||
45 | { | ||
46 | const emailVerified = 'email has been verified' | ||
47 | |||
48 | if (afterEmailVerification) { | ||
49 | expect(message).toContain(emailVerified) | ||
50 | } else { | ||
51 | expect(message).not.toContain(emailVerified) | ||
52 | } | ||
53 | } | ||
54 | } | ||
5 | 55 | ||
6 | describe('Signup', () => { | 56 | describe('Signup', () => { |
7 | let loginPage: LoginPage | 57 | let loginPage: LoginPage |
8 | let adminConfigPage: AdminConfigPage | 58 | let adminConfigPage: AdminConfigPage |
9 | let signupPage: SignupPage | 59 | let signupPage: SignupPage |
60 | let adminRegistrationPage: AdminRegistrationPage | ||
61 | |||
62 | async function prepareSignup (options: { | ||
63 | enabled: boolean | ||
64 | requiresApproval?: boolean | ||
65 | requiresEmailVerification?: boolean | ||
66 | }) { | ||
67 | await loginPage.loginAsRootUser() | ||
68 | |||
69 | await adminConfigPage.navigateTo('basic-configuration') | ||
70 | await adminConfigPage.toggleSignup(options.enabled) | ||
71 | |||
72 | if (options.enabled) { | ||
73 | if (options.requiresApproval !== undefined) { | ||
74 | await adminConfigPage.toggleSignupApproval(options.requiresApproval) | ||
75 | } | ||
76 | |||
77 | if (options.requiresEmailVerification !== undefined) { | ||
78 | await adminConfigPage.toggleSignupEmailVerification(options.requiresEmailVerification) | ||
79 | } | ||
80 | } | ||
81 | |||
82 | await adminConfigPage.save() | ||
83 | |||
84 | await loginPage.logout() | ||
85 | await browser.refresh() | ||
86 | } | ||
10 | 87 | ||
11 | before(async () => { | 88 | before(async () => { |
12 | await waitServerUp() | 89 | await waitServerUp() |
@@ -16,72 +93,310 @@ describe('Signup', () => { | |||
16 | loginPage = new LoginPage(isMobileDevice()) | 93 | loginPage = new LoginPage(isMobileDevice()) |
17 | adminConfigPage = new AdminConfigPage() | 94 | adminConfigPage = new AdminConfigPage() |
18 | signupPage = new SignupPage() | 95 | signupPage = new SignupPage() |
96 | adminRegistrationPage = new AdminRegistrationPage() | ||
19 | 97 | ||
20 | await browser.maximizeWindow() | 98 | await browser.maximizeWindow() |
21 | }) | 99 | }) |
22 | 100 | ||
23 | it('Should disable signup', async () => { | 101 | describe('Signup disabled', function () { |
24 | await loginPage.loginAsRootUser() | 102 | it('Should disable signup', async () => { |
103 | await prepareSignup({ enabled: false }) | ||
25 | 104 | ||
26 | await adminConfigPage.navigateTo('basic-configuration') | 105 | await expect(signupPage.getRegisterMenuButton()).not.toBeDisplayed() |
27 | await adminConfigPage.toggleSignup() | 106 | }) |
107 | }) | ||
28 | 108 | ||
29 | await adminConfigPage.save() | 109 | describe('Email verification disabled', function () { |
30 | 110 | ||
31 | await loginPage.logout() | 111 | describe('Direct registration', function () { |
32 | await browser.refresh() | ||
33 | 112 | ||
34 | expect(signupPage.getRegisterMenuButton()).not.toBeDisplayed() | 113 | it('Should enable signup without approval', async () => { |
35 | }) | 114 | await prepareSignup({ enabled: true, requiresApproval: false, requiresEmailVerification: false }) |
36 | 115 | ||
37 | it('Should enable signup', async () => { | 116 | await signupPage.getRegisterMenuButton().waitForDisplayed() |
38 | await loginPage.loginAsRootUser() | 117 | }) |
39 | 118 | ||
40 | await adminConfigPage.navigateTo('basic-configuration') | 119 | it('Should go on signup page', async function () { |
41 | await adminConfigPage.toggleSignup() | 120 | await signupPage.clickOnRegisterInMenu() |
121 | }) | ||
42 | 122 | ||
43 | await adminConfigPage.save() | 123 | it('Should validate the first step (about page)', async function () { |
124 | await signupPage.validateStep() | ||
125 | }) | ||
44 | 126 | ||
45 | await loginPage.logout() | 127 | it('Should validate the second step (terms)', async function () { |
46 | await browser.refresh() | 128 | await signupPage.checkTerms() |
129 | await signupPage.validateStep() | ||
130 | }) | ||
47 | 131 | ||
48 | expect(signupPage.getRegisterMenuButton()).toBeDisplayed() | 132 | it('Should validate the third step (account)', async function () { |
49 | }) | 133 | await signupPage.fillAccountStep({ username: 'user_1', displayName: 'user_1_dn' }) |
50 | 134 | ||
51 | it('Should go on signup page', async function () { | 135 | await signupPage.validateStep() |
52 | await signupPage.clickOnRegisterInMenu() | 136 | }) |
53 | }) | ||
54 | 137 | ||
55 | it('Should validate the first step (about page)', async function () { | 138 | it('Should validate the third step (channel)', async function () { |
56 | await signupPage.validateStep() | 139 | await signupPage.fillChannelStep({ name: 'user_1_channel' }) |
57 | }) | ||
58 | 140 | ||
59 | it('Should validate the second step (terms)', async function () { | 141 | await signupPage.validateStep() |
60 | await signupPage.checkTerms() | 142 | }) |
61 | await signupPage.validateStep() | 143 | |
62 | }) | 144 | it('Should be logged in', async function () { |
145 | await loginPage.ensureIsLoggedInAs('user_1_dn') | ||
146 | }) | ||
147 | |||
148 | it('Should have a valid end message', async function () { | ||
149 | const message = await signupPage.getEndMessage() | ||
150 | |||
151 | checkEndMessage({ | ||
152 | message, | ||
153 | requiresEmailVerification: false, | ||
154 | requiresApproval: false, | ||
155 | afterEmailVerification: false | ||
156 | }) | ||
63 | 157 | ||
64 | it('Should validate the third step (account)', async function () { | 158 | await browser.saveScreenshot('./screenshots/direct-without-email.png') |
65 | await signupPage.fillAccountStep({ | 159 | |
66 | displayName: 'user 1', | 160 | await loginPage.logout() |
67 | username: 'user_1', | 161 | }) |
68 | email: 'user_1@example.com', | ||
69 | password: 'my_super_password' | ||
70 | }) | 162 | }) |
71 | 163 | ||
72 | await signupPage.validateStep() | 164 | describe('Registration with approval', function () { |
165 | |||
166 | it('Should enable signup with approval', async () => { | ||
167 | await prepareSignup({ enabled: true, requiresApproval: true, requiresEmailVerification: false }) | ||
168 | |||
169 | await signupPage.getRegisterMenuButton().waitForDisplayed() | ||
170 | }) | ||
171 | |||
172 | it('Should go on signup page', async function () { | ||
173 | await signupPage.clickOnRegisterInMenu() | ||
174 | }) | ||
175 | |||
176 | it('Should validate the first step (about page)', async function () { | ||
177 | await signupPage.validateStep() | ||
178 | }) | ||
179 | |||
180 | it('Should validate the second step (terms)', async function () { | ||
181 | await signupPage.checkTerms() | ||
182 | await signupPage.fillRegistrationReason('my super reason') | ||
183 | await signupPage.validateStep() | ||
184 | }) | ||
185 | |||
186 | it('Should validate the third step (account)', async function () { | ||
187 | await signupPage.fillAccountStep({ username: 'user_2', displayName: 'user_2 display name', password: 'password' }) | ||
188 | await signupPage.validateStep() | ||
189 | }) | ||
190 | |||
191 | it('Should validate the third step (channel)', async function () { | ||
192 | await signupPage.fillChannelStep({ name: 'user_2_channel' }) | ||
193 | await signupPage.validateStep() | ||
194 | }) | ||
195 | |||
196 | it('Should have a valid end message', async function () { | ||
197 | const message = await signupPage.getEndMessage() | ||
198 | |||
199 | checkEndMessage({ | ||
200 | message, | ||
201 | requiresEmailVerification: false, | ||
202 | requiresApproval: true, | ||
203 | afterEmailVerification: false | ||
204 | }) | ||
205 | |||
206 | await browser.saveScreenshot('./screenshots/request-without-email.png') | ||
207 | }) | ||
208 | |||
209 | it('Should display a message when trying to login with this account', async function () { | ||
210 | const error = await loginPage.getLoginError('user_2', 'password') | ||
211 | |||
212 | expect(error).toContain('awaiting approval') | ||
213 | }) | ||
214 | |||
215 | it('Should accept the registration', async function () { | ||
216 | await loginPage.loginAsRootUser() | ||
217 | |||
218 | await adminRegistrationPage.navigateToRegistratonsList() | ||
219 | await adminRegistrationPage.accept('user_2', 'moderation response') | ||
220 | |||
221 | await loginPage.logout() | ||
222 | }) | ||
223 | |||
224 | it('Should be able to login with this new account', async function () { | ||
225 | await loginPage.login({ username: 'user_2', password: 'password', displayName: 'user_2 display name' }) | ||
226 | |||
227 | await loginPage.logout() | ||
228 | }) | ||
229 | }) | ||
73 | }) | 230 | }) |
74 | 231 | ||
75 | it('Should validate the third step (channel)', async function () { | 232 | describe('Email verification enabled', function () { |
76 | await signupPage.fillChannelStep({ | 233 | const emails: any[] = [] |
77 | displayName: 'user 1 channel', | 234 | let emailPort: number |
78 | name: 'user_1_channel' | 235 | |
236 | before(async () => { | ||
237 | // FIXME: typings are wrong, get returns a promise | ||
238 | emailPort = await browser.sharedStore.get('emailPort') as unknown as number | ||
239 | |||
240 | MockSMTPServer.Instance.collectEmails(emailPort, emails) | ||
79 | }) | 241 | }) |
80 | 242 | ||
81 | await signupPage.validateStep() | 243 | describe('Direct registration', function () { |
82 | }) | 244 | |
245 | it('Should enable signup without approval', async () => { | ||
246 | await prepareSignup({ enabled: true, requiresApproval: false, requiresEmailVerification: true }) | ||
247 | |||
248 | await signupPage.getRegisterMenuButton().waitForDisplayed() | ||
249 | }) | ||
250 | |||
251 | it('Should go on signup page', async function () { | ||
252 | await signupPage.clickOnRegisterInMenu() | ||
253 | }) | ||
254 | |||
255 | it('Should validate the first step (about page)', async function () { | ||
256 | await signupPage.validateStep() | ||
257 | }) | ||
258 | |||
259 | it('Should validate the second step (terms)', async function () { | ||
260 | await signupPage.checkTerms() | ||
261 | await signupPage.validateStep() | ||
262 | }) | ||
263 | |||
264 | it('Should validate the third step (account)', async function () { | ||
265 | await signupPage.fillAccountStep({ username: 'user_3', displayName: 'user_3 display name', email: 'user_3@example.com' }) | ||
266 | |||
267 | await signupPage.validateStep() | ||
268 | }) | ||
269 | |||
270 | it('Should validate the third step (channel)', async function () { | ||
271 | await signupPage.fillChannelStep({ name: 'user_3_channel' }) | ||
272 | |||
273 | await signupPage.validateStep() | ||
274 | }) | ||
275 | |||
276 | it('Should have a valid end message', async function () { | ||
277 | const message = await signupPage.getEndMessage() | ||
278 | |||
279 | checkEndMessage({ | ||
280 | message, | ||
281 | requiresEmailVerification: true, | ||
282 | requiresApproval: false, | ||
283 | afterEmailVerification: false | ||
284 | }) | ||
285 | |||
286 | await browser.saveScreenshot('./screenshots/direct-with-email.png') | ||
287 | }) | ||
288 | |||
289 | it('Should validate the email', async function () { | ||
290 | let email: { text: string } | ||
291 | |||
292 | while (!(email = findEmailTo(emails, 'user_3@example.com'))) { | ||
293 | await browserSleep(100) | ||
294 | } | ||
295 | |||
296 | await go(getVerificationLink(email)) | ||
297 | |||
298 | const message = await signupPage.getEndMessage() | ||
299 | |||
300 | checkEndMessage({ | ||
301 | message, | ||
302 | requiresEmailVerification: false, | ||
303 | requiresApproval: false, | ||
304 | afterEmailVerification: true | ||
305 | }) | ||
83 | 306 | ||
84 | it('Should be logged in', async function () { | 307 | await browser.saveScreenshot('./screenshots/direct-after-email.png') |
85 | await loginPage.ensureIsLoggedInAs('user 1') | 308 | }) |
309 | }) | ||
310 | |||
311 | describe('Registration with approval', function () { | ||
312 | |||
313 | it('Should enable signup without approval', async () => { | ||
314 | await prepareSignup({ enabled: true, requiresApproval: true, requiresEmailVerification: true }) | ||
315 | |||
316 | await signupPage.getRegisterMenuButton().waitForDisplayed() | ||
317 | }) | ||
318 | |||
319 | it('Should go on signup page', async function () { | ||
320 | await signupPage.clickOnRegisterInMenu() | ||
321 | }) | ||
322 | |||
323 | it('Should validate the first step (about page)', async function () { | ||
324 | await signupPage.validateStep() | ||
325 | }) | ||
326 | |||
327 | it('Should validate the second step (terms)', async function () { | ||
328 | await signupPage.checkTerms() | ||
329 | await signupPage.fillRegistrationReason('my super reason 2') | ||
330 | await signupPage.validateStep() | ||
331 | }) | ||
332 | |||
333 | it('Should validate the third step (account)', async function () { | ||
334 | await signupPage.fillAccountStep({ | ||
335 | username: 'user_4', | ||
336 | displayName: 'user_4 display name', | ||
337 | email: 'user_4@example.com', | ||
338 | password: 'password' | ||
339 | }) | ||
340 | await signupPage.validateStep() | ||
341 | }) | ||
342 | |||
343 | it('Should validate the third step (channel)', async function () { | ||
344 | await signupPage.fillChannelStep({ name: 'user_4_channel' }) | ||
345 | await signupPage.validateStep() | ||
346 | }) | ||
347 | |||
348 | it('Should have a valid end message', async function () { | ||
349 | const message = await signupPage.getEndMessage() | ||
350 | |||
351 | checkEndMessage({ | ||
352 | message, | ||
353 | requiresEmailVerification: true, | ||
354 | requiresApproval: true, | ||
355 | afterEmailVerification: false | ||
356 | }) | ||
357 | |||
358 | await browser.saveScreenshot('./screenshots/request-with-email.png') | ||
359 | }) | ||
360 | |||
361 | it('Should display a message when trying to login with this account', async function () { | ||
362 | const error = await loginPage.getLoginError('user_4', 'password') | ||
363 | |||
364 | expect(error).toContain('awaiting approval') | ||
365 | }) | ||
366 | |||
367 | it('Should accept the registration', async function () { | ||
368 | await loginPage.loginAsRootUser() | ||
369 | |||
370 | await adminRegistrationPage.navigateToRegistratonsList() | ||
371 | await adminRegistrationPage.accept('user_4', 'moderation response 2') | ||
372 | |||
373 | await loginPage.logout() | ||
374 | }) | ||
375 | |||
376 | it('Should validate the email', async function () { | ||
377 | let email: { text: string } | ||
378 | |||
379 | while (!(email = findEmailTo(emails, 'user_4@example.com'))) { | ||
380 | await browserSleep(100) | ||
381 | } | ||
382 | |||
383 | await go(getVerificationLink(email)) | ||
384 | |||
385 | const message = await signupPage.getEndMessage() | ||
386 | |||
387 | checkEndMessage({ | ||
388 | message, | ||
389 | requiresEmailVerification: false, | ||
390 | requiresApproval: true, | ||
391 | afterEmailVerification: true | ||
392 | }) | ||
393 | |||
394 | await browser.saveScreenshot('./screenshots/request-after-email.png') | ||
395 | }) | ||
396 | }) | ||
397 | |||
398 | before(() => { | ||
399 | MockSMTPServer.Instance.kill() | ||
400 | }) | ||
86 | }) | 401 | }) |
87 | }) | 402 | }) |
diff --git a/client/e2e/src/utils/elements.ts b/client/e2e/src/utils/elements.ts index b0ddd5a65..d9435e520 100644 --- a/client/e2e/src/utils/elements.ts +++ b/client/e2e/src/utils/elements.ts | |||
@@ -5,6 +5,10 @@ async function getCheckbox (name: string) { | |||
5 | return input.parentElement() | 5 | return input.parentElement() |
6 | } | 6 | } |
7 | 7 | ||
8 | function isCheckboxSelected (name: string) { | ||
9 | return $(`input[id=${name}]`).isSelected() | ||
10 | } | ||
11 | |||
8 | async function selectCustomSelect (id: string, valueLabel: string) { | 12 | async function selectCustomSelect (id: string, valueLabel: string) { |
9 | const wrapper = $(`[formcontrolname=${id}] .ng-arrow-wrapper`) | 13 | const wrapper = $(`[formcontrolname=${id}] .ng-arrow-wrapper`) |
10 | 14 | ||
@@ -22,7 +26,18 @@ async function selectCustomSelect (id: string, valueLabel: string) { | |||
22 | return option.click() | 26 | return option.click() |
23 | } | 27 | } |
24 | 28 | ||
29 | async function findParentElement ( | ||
30 | el: WebdriverIO.Element, | ||
31 | finder: (el: WebdriverIO.Element) => Promise<boolean> | ||
32 | ) { | ||
33 | if (await finder(el) === true) return el | ||
34 | |||
35 | return findParentElement(await el.parentElement(), finder) | ||
36 | } | ||
37 | |||
25 | export { | 38 | export { |
26 | getCheckbox, | 39 | getCheckbox, |
27 | selectCustomSelect | 40 | isCheckboxSelected, |
41 | selectCustomSelect, | ||
42 | findParentElement | ||
28 | } | 43 | } |
diff --git a/client/e2e/src/utils/email.ts b/client/e2e/src/utils/email.ts new file mode 100644 index 000000000..2ad120333 --- /dev/null +++ b/client/e2e/src/utils/email.ts | |||
@@ -0,0 +1,31 @@ | |||
1 | function getVerificationLink (email: { text: string }) { | ||
2 | const { text } = email | ||
3 | |||
4 | const regexp = /\[(?<link>http:\/\/[^\]]+)\]/g | ||
5 | const matched = text.matchAll(regexp) | ||
6 | |||
7 | if (!matched) throw new Error('Could not find verification link in email') | ||
8 | |||
9 | for (const match of matched) { | ||
10 | const link = match.groups.link | ||
11 | |||
12 | if (link.includes('/verify-account/')) return link | ||
13 | } | ||
14 | |||
15 | throw new Error('Could not find /verify-account/ link') | ||
16 | } | ||
17 | |||
18 | function findEmailTo (emails: { text: string, to: { address: string }[] }[], to: string) { | ||
19 | for (const email of emails) { | ||
20 | for (const { address } of email.to) { | ||
21 | if (address === to) return email | ||
22 | } | ||
23 | } | ||
24 | |||
25 | return undefined | ||
26 | } | ||
27 | |||
28 | export { | ||
29 | getVerificationLink, | ||
30 | findEmailTo | ||
31 | } | ||
diff --git a/client/e2e/src/utils/hooks.ts b/client/e2e/src/utils/hooks.ts index 889cf1d86..7fe247681 100644 --- a/client/e2e/src/utils/hooks.ts +++ b/client/e2e/src/utils/hooks.ts | |||
@@ -1,10 +1,13 @@ | |||
1 | import { ChildProcessWithoutNullStreams } from 'child_process' | 1 | import { ChildProcessWithoutNullStreams } from 'child_process' |
2 | import { basename } from 'path' | 2 | import { basename } from 'path' |
3 | import { runCommand, runServer } from './server' | 3 | import { runCommand, runServer } from './server' |
4 | import { setValue } from '@wdio/shared-store-service' | ||
4 | 5 | ||
5 | let appInstance: string | 6 | let appInstance: number |
6 | let app: ChildProcessWithoutNullStreams | 7 | let app: ChildProcessWithoutNullStreams |
7 | 8 | ||
9 | let emailPort: number | ||
10 | |||
8 | async function beforeLocalSuite (suite: any) { | 11 | async function beforeLocalSuite (suite: any) { |
9 | const config = buildConfig(suite.file) | 12 | const config = buildConfig(suite.file) |
10 | 13 | ||
@@ -17,13 +20,20 @@ function afterLocalSuite () { | |||
17 | app = undefined | 20 | app = undefined |
18 | } | 21 | } |
19 | 22 | ||
20 | function beforeLocalSession (config: { baseUrl: string }, capabilities: { browserName: string }) { | 23 | async function beforeLocalSession (config: { baseUrl: string }, capabilities: { browserName: string }) { |
21 | appInstance = capabilities['browserName'] === 'chrome' ? '1' : '2' | 24 | appInstance = capabilities['browserName'] === 'chrome' |
25 | ? 1 | ||
26 | : 2 | ||
27 | |||
28 | emailPort = 1025 + appInstance | ||
29 | |||
22 | config.baseUrl = 'http://localhost:900' + appInstance | 30 | config.baseUrl = 'http://localhost:900' + appInstance |
31 | |||
32 | await setValue('emailPort', emailPort) | ||
23 | } | 33 | } |
24 | 34 | ||
25 | async function onBrowserStackPrepare () { | 35 | async function onBrowserStackPrepare () { |
26 | const appInstance = '1' | 36 | const appInstance = 1 |
27 | 37 | ||
28 | await runCommand('npm run clean:server:test -- ' + appInstance) | 38 | await runCommand('npm run clean:server:test -- ' + appInstance) |
29 | app = runServer(appInstance) | 39 | app = runServer(appInstance) |
@@ -71,7 +81,11 @@ function buildConfig (suiteFile: string = undefined) { | |||
71 | if (filename === 'signup.e2e-spec.ts') { | 81 | if (filename === 'signup.e2e-spec.ts') { |
72 | return { | 82 | return { |
73 | signup: { | 83 | signup: { |
74 | enabled: true | 84 | limit: -1 |
85 | }, | ||
86 | smtp: { | ||
87 | hostname: '127.0.0.1', | ||
88 | port: emailPort | ||
75 | } | 89 | } |
76 | } | 90 | } |
77 | } | 91 | } |
diff --git a/client/e2e/src/utils/index.ts b/client/e2e/src/utils/index.ts index 354352ee2..420fd239e 100644 --- a/client/e2e/src/utils/index.ts +++ b/client/e2e/src/utils/index.ts | |||
@@ -1,5 +1,7 @@ | |||
1 | export * from './common' | 1 | export * from './common' |
2 | export * from './elements' | 2 | export * from './elements' |
3 | export * from './email' | ||
3 | export * from './hooks' | 4 | export * from './hooks' |
5 | export * from './mock-smtp' | ||
4 | export * from './server' | 6 | export * from './server' |
5 | export * from './urls' | 7 | export * from './urls' |
diff --git a/client/e2e/src/utils/mock-smtp.ts b/client/e2e/src/utils/mock-smtp.ts new file mode 100644 index 000000000..614477d7d --- /dev/null +++ b/client/e2e/src/utils/mock-smtp.ts | |||
@@ -0,0 +1,58 @@ | |||
1 | import { ChildProcess } from 'child_process' | ||
2 | import MailDev from '@peertube/maildev' | ||
3 | |||
4 | class MockSMTPServer { | ||
5 | |||
6 | private static instance: MockSMTPServer | ||
7 | private started = false | ||
8 | private emailChildProcess: ChildProcess | ||
9 | private emails: object[] | ||
10 | |||
11 | collectEmails (port: number, emailsCollection: object[]) { | ||
12 | return new Promise<number>((res, rej) => { | ||
13 | this.emails = emailsCollection | ||
14 | |||
15 | if (this.started) { | ||
16 | return res(undefined) | ||
17 | } | ||
18 | |||
19 | const maildev = new MailDev({ | ||
20 | ip: '127.0.0.1', | ||
21 | smtp: port, | ||
22 | disableWeb: true, | ||
23 | silent: true | ||
24 | }) | ||
25 | |||
26 | maildev.on('new', email => { | ||
27 | this.emails.push(email) | ||
28 | }) | ||
29 | |||
30 | maildev.listen(err => { | ||
31 | if (err) return rej(err) | ||
32 | |||
33 | this.started = true | ||
34 | |||
35 | return res(port) | ||
36 | }) | ||
37 | }) | ||
38 | } | ||
39 | |||
40 | kill () { | ||
41 | if (!this.emailChildProcess) return | ||
42 | |||
43 | process.kill(this.emailChildProcess.pid) | ||
44 | |||
45 | this.emailChildProcess = null | ||
46 | MockSMTPServer.instance = null | ||
47 | } | ||
48 | |||
49 | static get Instance () { | ||
50 | return this.instance || (this.instance = new this()) | ||
51 | } | ||
52 | } | ||
53 | |||
54 | // --------------------------------------------------------------------------- | ||
55 | |||
56 | export { | ||
57 | MockSMTPServer | ||
58 | } | ||
diff --git a/client/e2e/src/utils/server.ts b/client/e2e/src/utils/server.ts index 140054794..227f4aea6 100644 --- a/client/e2e/src/utils/server.ts +++ b/client/e2e/src/utils/server.ts | |||
@@ -1,10 +1,10 @@ | |||
1 | import { exec, spawn } from 'child_process' | 1 | import { exec, spawn } from 'child_process' |
2 | import { join, resolve } from 'path' | 2 | import { join, resolve } from 'path' |
3 | 3 | ||
4 | function runServer (appInstance: string, config: any = {}) { | 4 | function runServer (appInstance: number, config: any = {}) { |
5 | const env = Object.create(process.env) | 5 | const env = Object.create(process.env) |
6 | env['NODE_ENV'] = 'test' | 6 | env['NODE_ENV'] = 'test' |
7 | env['NODE_APP_INSTANCE'] = appInstance | 7 | env['NODE_APP_INSTANCE'] = appInstance + '' |
8 | 8 | ||
9 | env['NODE_CONFIG'] = JSON.stringify({ | 9 | env['NODE_CONFIG'] = JSON.stringify({ |
10 | rates_limit: { | 10 | rates_limit: { |
diff --git a/client/e2e/wdio.local-test.conf.ts b/client/e2e/wdio.local-test.conf.ts index ca0bb5bfe..bc15123a0 100644 --- a/client/e2e/wdio.local-test.conf.ts +++ b/client/e2e/wdio.local-test.conf.ts | |||
@@ -37,7 +37,7 @@ module.exports = { | |||
37 | // } | 37 | // } |
38 | ], | 38 | ], |
39 | 39 | ||
40 | services: [ 'chromedriver', 'geckodriver' ], | 40 | services: [ 'chromedriver', 'geckodriver', 'shared-store' ], |
41 | 41 | ||
42 | beforeSession: beforeLocalSession, | 42 | beforeSession: beforeLocalSession, |
43 | beforeSuite: beforeLocalSuite, | 43 | beforeSuite: beforeLocalSuite, |
diff --git a/client/e2e/wdio.local.conf.ts b/client/e2e/wdio.local.conf.ts index d02679e06..27c6e867b 100644 --- a/client/e2e/wdio.local.conf.ts +++ b/client/e2e/wdio.local.conf.ts | |||
@@ -33,7 +33,7 @@ module.exports = { | |||
33 | } | 33 | } |
34 | ], | 34 | ], |
35 | 35 | ||
36 | services: [ 'chromedriver', 'geckodriver' ], | 36 | services: [ 'chromedriver', 'geckodriver', 'shared-store' ], |
37 | 37 | ||
38 | beforeSession: beforeLocalSession, | 38 | beforeSession: beforeLocalSession, |
39 | beforeSuite: beforeLocalSuite, | 39 | beforeSuite: beforeLocalSuite, |
diff --git a/client/package.json b/client/package.json index 115a4a199..31d9b1e7c 100644 --- a/client/package.json +++ b/client/package.json | |||
@@ -52,8 +52,9 @@ | |||
52 | "@ngx-loading-bar/core": "^6.0.0", | 52 | "@ngx-loading-bar/core": "^6.0.0", |
53 | "@ngx-loading-bar/http-client": "^6.0.0", | 53 | "@ngx-loading-bar/http-client": "^6.0.0", |
54 | "@ngx-loading-bar/router": "^6.0.0", | 54 | "@ngx-loading-bar/router": "^6.0.0", |
55 | "@peertube/p2p-media-loader-core": "^1.0.13", | 55 | "@peertube/maildev": "^1.2.0", |
56 | "@peertube/p2p-media-loader-hlsjs": "^1.0.13", | 56 | "@peertube/p2p-media-loader-core": "^1.0.14", |
57 | "@peertube/p2p-media-loader-hlsjs": "^1.0.14", | ||
57 | "@peertube/videojs-contextmenu": "^5.5.0", | 58 | "@peertube/videojs-contextmenu": "^5.5.0", |
58 | "@peertube/xliffmerge": "^2.0.3", | 59 | "@peertube/xliffmerge": "^2.0.3", |
59 | "@popperjs/core": "^2.11.5", | 60 | "@popperjs/core": "^2.11.5", |
@@ -75,6 +76,7 @@ | |||
75 | "@wdio/cli": "^7.25.2", | 76 | "@wdio/cli": "^7.25.2", |
76 | "@wdio/local-runner": "^7.25.2", | 77 | "@wdio/local-runner": "^7.25.2", |
77 | "@wdio/mocha-framework": "^7.25.2", | 78 | "@wdio/mocha-framework": "^7.25.2", |
79 | "@wdio/shared-store-service": "^7.25.2", | ||
78 | "@wdio/spec-reporter": "^7.25.1", | 80 | "@wdio/spec-reporter": "^7.25.1", |
79 | "angular2-hotkeys": "^13.1.0", | 81 | "angular2-hotkeys": "^13.1.0", |
80 | "angularx-qrcode": "14.0.0", | 82 | "angularx-qrcode": "14.0.0", |
diff --git a/client/src/app/+about/about-instance/about-instance.component.html b/client/src/app/+about/about-instance/about-instance.component.html index b113df82f..fdd6157e5 100644 --- a/client/src/app/+about/about-instance/about-instance.component.html +++ b/client/src/app/+about/about-instance/about-instance.component.html | |||
@@ -21,7 +21,7 @@ | |||
21 | 21 | ||
22 | <div class="anchor" id="administrators-and-sustainability"></div> | 22 | <div class="anchor" id="administrators-and-sustainability"></div> |
23 | <a | 23 | <a |
24 | *ngIf="html.administrator || html.maintenanceLifetime || html.businessModel" | 24 | *ngIf="aboutHTML.administrator || aboutHTML.maintenanceLifetime || aboutHTML.businessModel" |
25 | class="anchor-link" | 25 | class="anchor-link" |
26 | routerLink="/about/instance" | 26 | routerLink="/about/instance" |
27 | fragment="administrators-and-sustainability" | 27 | fragment="administrators-and-sustainability" |
@@ -33,7 +33,7 @@ | |||
33 | </h2> | 33 | </h2> |
34 | </a> | 34 | </a> |
35 | 35 | ||
36 | <div class="block administrator" *ngIf="html.administrator"> | 36 | <div class="block administrator" *ngIf="aboutHTML.administrator"> |
37 | <div class="anchor" id="administrators"></div> | 37 | <div class="anchor" id="administrators"></div> |
38 | <a | 38 | <a |
39 | class="anchor-link" | 39 | class="anchor-link" |
@@ -44,10 +44,10 @@ | |||
44 | <h3 i18n class="section-title">Who we are</h3> | 44 | <h3 i18n class="section-title">Who we are</h3> |
45 | </a> | 45 | </a> |
46 | 46 | ||
47 | <div [innerHTML]="html.administrator"></div> | 47 | <div [innerHTML]="aboutHTML.administrator"></div> |
48 | </div> | 48 | </div> |
49 | 49 | ||
50 | <div class="block creation-reason" *ngIf="html.creationReason"> | 50 | <div class="block creation-reason" *ngIf="aboutHTML.creationReason"> |
51 | <div class="anchor" id="creation-reason"></div> | 51 | <div class="anchor" id="creation-reason"></div> |
52 | <a | 52 | <a |
53 | class="anchor-link" | 53 | class="anchor-link" |
@@ -58,10 +58,10 @@ | |||
58 | <h3 i18n class="section-title">Why we created this instance</h3> | 58 | <h3 i18n class="section-title">Why we created this instance</h3> |
59 | </a> | 59 | </a> |
60 | 60 | ||
61 | <div [innerHTML]="html.creationReason"></div> | 61 | <div [innerHTML]="aboutHTML.creationReason"></div> |
62 | </div> | 62 | </div> |
63 | 63 | ||
64 | <div class="block maintenance-lifetime" *ngIf="html.maintenanceLifetime"> | 64 | <div class="block maintenance-lifetime" *ngIf="aboutHTML.maintenanceLifetime"> |
65 | <div class="anchor" id="maintenance-lifetime"></div> | 65 | <div class="anchor" id="maintenance-lifetime"></div> |
66 | <a | 66 | <a |
67 | class="anchor-link" | 67 | class="anchor-link" |
@@ -72,10 +72,10 @@ | |||
72 | <h3 i18n class="section-title">How long we plan to maintain this instance</h3> | 72 | <h3 i18n class="section-title">How long we plan to maintain this instance</h3> |
73 | </a> | 73 | </a> |
74 | 74 | ||
75 | <div [innerHTML]="html.maintenanceLifetime"></div> | 75 | <div [innerHTML]="aboutHTML.maintenanceLifetime"></div> |
76 | </div> | 76 | </div> |
77 | 77 | ||
78 | <div class="block business-model" *ngIf="html.businessModel"> | 78 | <div class="block business-model" *ngIf="aboutHTML.businessModel"> |
79 | <div class="anchor" id="business-model"></div> | 79 | <div class="anchor" id="business-model"></div> |
80 | <a | 80 | <a |
81 | class="anchor-link" | 81 | class="anchor-link" |
@@ -86,12 +86,12 @@ | |||
86 | <h3 i18n class="section-title">How we will pay for keeping our instance running</h3> | 86 | <h3 i18n class="section-title">How we will pay for keeping our instance running</h3> |
87 | </a> | 87 | </a> |
88 | 88 | ||
89 | <div [innerHTML]="html.businessModel"></div> | 89 | <div [innerHTML]="aboutHTML.businessModel"></div> |
90 | </div> | 90 | </div> |
91 | 91 | ||
92 | <div class="anchor" id="information"></div> | 92 | <div class="anchor" id="information"></div> |
93 | <a | 93 | <a |
94 | *ngIf="descriptionContent" | 94 | *ngIf="descriptionElement" |
95 | class="anchor-link" | 95 | class="anchor-link" |
96 | routerLink="/about/instance" | 96 | routerLink="/about/instance" |
97 | fragment="information" | 97 | fragment="information" |
@@ -113,13 +113,13 @@ | |||
113 | <h3 i18n class="section-title">Description</h3> | 113 | <h3 i18n class="section-title">Description</h3> |
114 | </a> | 114 | </a> |
115 | 115 | ||
116 | <my-custom-markup-container [content]="descriptionContent"></my-custom-markup-container> | 116 | <my-custom-markup-container [content]="descriptionElement"></my-custom-markup-container> |
117 | </div> | 117 | </div> |
118 | 118 | ||
119 | <div myPluginSelector pluginSelectorId="about-instance-moderation"> | 119 | <div myPluginSelector pluginSelectorId="about-instance-moderation"> |
120 | <div class="anchor" id="moderation"></div> | 120 | <div class="anchor" id="moderation"></div> |
121 | <a | 121 | <a |
122 | *ngIf="html.moderationInformation || html.codeOfConduct || html.terms" | 122 | *ngIf="aboutHTML.moderationInformation || aboutHTML.codeOfConduct || aboutHTML.terms" |
123 | class="anchor-link" | 123 | class="anchor-link" |
124 | routerLink="/about/instance" | 124 | routerLink="/about/instance" |
125 | fragment="moderation" | 125 | fragment="moderation" |
@@ -130,7 +130,7 @@ | |||
130 | </h2> | 130 | </h2> |
131 | </a> | 131 | </a> |
132 | 132 | ||
133 | <div class="block moderation-information" *ngIf="html.moderationInformation"> | 133 | <div class="block moderation-information" *ngIf="aboutHTML.moderationInformation"> |
134 | <div class="anchor" id="moderation-information"></div> | 134 | <div class="anchor" id="moderation-information"></div> |
135 | <a | 135 | <a |
136 | class="anchor-link" | 136 | class="anchor-link" |
@@ -141,10 +141,10 @@ | |||
141 | <h3 i18n class="section-title">Moderation information</h3> | 141 | <h3 i18n class="section-title">Moderation information</h3> |
142 | </a> | 142 | </a> |
143 | 143 | ||
144 | <div [innerHTML]="html.moderationInformation"></div> | 144 | <div [innerHTML]="aboutHTML.moderationInformation"></div> |
145 | </div> | 145 | </div> |
146 | 146 | ||
147 | <div class="block code-of-conduct" *ngIf="html.codeOfConduct"> | 147 | <div class="block code-of-conduct" *ngIf="aboutHTML.codeOfConduct"> |
148 | <div class="anchor" id="code-of-conduct"></div> | 148 | <div class="anchor" id="code-of-conduct"></div> |
149 | <a | 149 | <a |
150 | class="anchor-link" | 150 | class="anchor-link" |
@@ -155,7 +155,7 @@ | |||
155 | <h3 i18n class="section-title">Code of conduct</h3> | 155 | <h3 i18n class="section-title">Code of conduct</h3> |
156 | </a> | 156 | </a> |
157 | 157 | ||
158 | <div [innerHTML]="html.codeOfConduct"></div> | 158 | <div [innerHTML]="aboutHTML.codeOfConduct"></div> |
159 | </div> | 159 | </div> |
160 | 160 | ||
161 | <div class="block terms"> | 161 | <div class="block terms"> |
@@ -169,14 +169,14 @@ | |||
169 | <h3 i18n class="section-title">Terms</h3> | 169 | <h3 i18n class="section-title">Terms</h3> |
170 | </a> | 170 | </a> |
171 | 171 | ||
172 | <div [innerHTML]="html.terms"></div> | 172 | <div [innerHTML]="aboutHTML.terms"></div> |
173 | </div> | 173 | </div> |
174 | </div> | 174 | </div> |
175 | 175 | ||
176 | <div myPluginSelector pluginSelectorId="about-instance-other-information"> | 176 | <div myPluginSelector pluginSelectorId="about-instance-other-information"> |
177 | <div class="anchor" id="other-information"></div> | 177 | <div class="anchor" id="other-information"></div> |
178 | <a | 178 | <a |
179 | *ngIf="html.hardwareInformation" | 179 | *ngIf="aboutHTML.hardwareInformation" |
180 | class="anchor-link" | 180 | class="anchor-link" |
181 | routerLink="/about/instance" | 181 | routerLink="/about/instance" |
182 | fragment="other-information" | 182 | fragment="other-information" |
@@ -187,7 +187,7 @@ | |||
187 | </h2> | 187 | </h2> |
188 | </a> | 188 | </a> |
189 | 189 | ||
190 | <div class="block hardware-information" *ngIf="html.hardwareInformation"> | 190 | <div class="block hardware-information" *ngIf="aboutHTML.hardwareInformation"> |
191 | <div class="anchor" id="hardware-information"></div> | 191 | <div class="anchor" id="hardware-information"></div> |
192 | <a | 192 | <a |
193 | class="anchor-link" | 193 | class="anchor-link" |
@@ -198,7 +198,7 @@ | |||
198 | <h3 i18n class="section-title">Hardware information</h3> | 198 | <h3 i18n class="section-title">Hardware information</h3> |
199 | </a> | 199 | </a> |
200 | 200 | ||
201 | <div [innerHTML]="html.hardwareInformation"></div> | 201 | <div [innerHTML]="aboutHTML.hardwareInformation"></div> |
202 | </div> | 202 | </div> |
203 | </div> | 203 | </div> |
204 | </div> | 204 | </div> |
diff --git a/client/src/app/+about/about-instance/about-instance.component.ts b/client/src/app/+about/about-instance/about-instance.component.ts index 0826bbc5a..e1501d7de 100644 --- a/client/src/app/+about/about-instance/about-instance.component.ts +++ b/client/src/app/+about/about-instance/about-instance.component.ts | |||
@@ -2,7 +2,7 @@ import { ViewportScroller } from '@angular/common' | |||
2 | import { AfterViewChecked, Component, ElementRef, OnInit, ViewChild } from '@angular/core' | 2 | import { AfterViewChecked, Component, ElementRef, OnInit, ViewChild } from '@angular/core' |
3 | import { ActivatedRoute } from '@angular/router' | 3 | import { ActivatedRoute } from '@angular/router' |
4 | import { Notifier, ServerService } from '@app/core' | 4 | import { Notifier, ServerService } from '@app/core' |
5 | import { InstanceService } from '@app/shared/shared-instance' | 5 | import { AboutHTML } from '@app/shared/shared-instance' |
6 | import { copyToClipboard } from '@root-helpers/utils' | 6 | import { copyToClipboard } from '@root-helpers/utils' |
7 | import { HTMLServerConfig } from '@shared/models/server' | 7 | import { HTMLServerConfig } from '@shared/models/server' |
8 | import { ResolverData } from './about-instance.resolver' | 8 | import { ResolverData } from './about-instance.resolver' |
@@ -17,22 +17,12 @@ export class AboutInstanceComponent implements OnInit, AfterViewChecked { | |||
17 | @ViewChild('descriptionWrapper') descriptionWrapper: ElementRef<HTMLInputElement> | 17 | @ViewChild('descriptionWrapper') descriptionWrapper: ElementRef<HTMLInputElement> |
18 | @ViewChild('contactAdminModal', { static: true }) contactAdminModal: ContactAdminModalComponent | 18 | @ViewChild('contactAdminModal', { static: true }) contactAdminModal: ContactAdminModalComponent |
19 | 19 | ||
20 | shortDescription = '' | 20 | aboutHTML: AboutHTML |
21 | descriptionContent: string | 21 | descriptionElement: HTMLDivElement |
22 | |||
23 | html = { | ||
24 | terms: '', | ||
25 | codeOfConduct: '', | ||
26 | moderationInformation: '', | ||
27 | administrator: '', | ||
28 | creationReason: '', | ||
29 | maintenanceLifetime: '', | ||
30 | businessModel: '', | ||
31 | hardwareInformation: '' | ||
32 | } | ||
33 | 22 | ||
34 | languages: string[] = [] | 23 | languages: string[] = [] |
35 | categories: string[] = [] | 24 | categories: string[] = [] |
25 | shortDescription = '' | ||
36 | 26 | ||
37 | initialized = false | 27 | initialized = false |
38 | 28 | ||
@@ -44,8 +34,7 @@ export class AboutInstanceComponent implements OnInit, AfterViewChecked { | |||
44 | private viewportScroller: ViewportScroller, | 34 | private viewportScroller: ViewportScroller, |
45 | private route: ActivatedRoute, | 35 | private route: ActivatedRoute, |
46 | private notifier: Notifier, | 36 | private notifier: Notifier, |
47 | private serverService: ServerService, | 37 | private serverService: ServerService |
48 | private instanceService: InstanceService | ||
49 | ) {} | 38 | ) {} |
50 | 39 | ||
51 | get instanceName () { | 40 | get instanceName () { |
@@ -60,8 +49,16 @@ export class AboutInstanceComponent implements OnInit, AfterViewChecked { | |||
60 | return this.serverConfig.instance.isNSFW | 49 | return this.serverConfig.instance.isNSFW |
61 | } | 50 | } |
62 | 51 | ||
63 | async ngOnInit () { | 52 | ngOnInit () { |
64 | const { about, languages, categories }: ResolverData = this.route.snapshot.data.instanceData | 53 | const { about, languages, categories, aboutHTML, descriptionElement }: ResolverData = this.route.snapshot.data.instanceData |
54 | |||
55 | this.aboutHTML = aboutHTML | ||
56 | this.descriptionElement = descriptionElement | ||
57 | |||
58 | this.languages = languages | ||
59 | this.categories = categories | ||
60 | |||
61 | this.shortDescription = about.instance.shortDescription | ||
65 | 62 | ||
66 | this.serverConfig = this.serverService.getHTMLConfig() | 63 | this.serverConfig = this.serverService.getHTMLConfig() |
67 | 64 | ||
@@ -73,14 +70,6 @@ export class AboutInstanceComponent implements OnInit, AfterViewChecked { | |||
73 | this.contactAdminModal.show(prefill) | 70 | this.contactAdminModal.show(prefill) |
74 | }) | 71 | }) |
75 | 72 | ||
76 | this.languages = languages | ||
77 | this.categories = categories | ||
78 | |||
79 | this.shortDescription = about.instance.shortDescription | ||
80 | this.descriptionContent = about.instance.description | ||
81 | |||
82 | this.html = await this.instanceService.buildHtml(about) | ||
83 | |||
84 | this.initialized = true | 73 | this.initialized = true |
85 | } | 74 | } |
86 | 75 | ||
diff --git a/client/src/app/+about/about-instance/about-instance.resolver.ts b/client/src/app/+about/about-instance/about-instance.resolver.ts index ee0219df0..8818fc582 100644 --- a/client/src/app/+about/about-instance/about-instance.resolver.ts +++ b/client/src/app/+about/about-instance/about-instance.resolver.ts | |||
@@ -2,16 +2,25 @@ import { forkJoin } from 'rxjs' | |||
2 | import { map, switchMap } from 'rxjs/operators' | 2 | import { map, switchMap } from 'rxjs/operators' |
3 | import { Injectable } from '@angular/core' | 3 | import { Injectable } from '@angular/core' |
4 | import { Resolve } from '@angular/router' | 4 | import { Resolve } from '@angular/router' |
5 | import { InstanceService } from '@app/shared/shared-instance' | 5 | import { CustomMarkupService } from '@app/shared/shared-custom-markup' |
6 | import { AboutHTML, InstanceService } from '@app/shared/shared-instance' | ||
6 | import { About } from '@shared/models/server' | 7 | import { About } from '@shared/models/server' |
7 | 8 | ||
8 | export type ResolverData = { about: About, languages: string[], categories: string[] } | 9 | export type ResolverData = { |
10 | about: About | ||
11 | languages: string[] | ||
12 | categories: string[] | ||
13 | aboutHTML: AboutHTML | ||
14 | descriptionElement: HTMLDivElement | ||
15 | } | ||
9 | 16 | ||
10 | @Injectable() | 17 | @Injectable() |
11 | export class AboutInstanceResolver implements Resolve<any> { | 18 | export class AboutInstanceResolver implements Resolve<any> { |
12 | 19 | ||
13 | constructor ( | 20 | constructor ( |
14 | private instanceService: InstanceService | 21 | private instanceService: InstanceService, |
22 | private customMarkupService: CustomMarkupService | ||
23 | |||
15 | ) {} | 24 | ) {} |
16 | 25 | ||
17 | resolve () { | 26 | resolve () { |
@@ -19,9 +28,15 @@ export class AboutInstanceResolver implements Resolve<any> { | |||
19 | .pipe( | 28 | .pipe( |
20 | switchMap(about => { | 29 | switchMap(about => { |
21 | return forkJoin([ | 30 | return forkJoin([ |
31 | Promise.resolve(about), | ||
22 | this.instanceService.buildTranslatedLanguages(about), | 32 | this.instanceService.buildTranslatedLanguages(about), |
23 | this.instanceService.buildTranslatedCategories(about) | 33 | this.instanceService.buildTranslatedCategories(about), |
24 | ]).pipe(map(([ languages, categories ]) => ({ about, languages, categories }) as ResolverData)) | 34 | this.instanceService.buildHtml(about), |
35 | this.customMarkupService.buildElement(about.instance.description) | ||
36 | ]) | ||
37 | }), | ||
38 | map(([ about, languages, categories, aboutHTML, { rootElement } ]) => { | ||
39 | return { about, languages, categories, aboutHTML, descriptionElement: rootElement } as ResolverData | ||
25 | }) | 40 | }) |
26 | ) | 41 | ) |
27 | } | 42 | } |
diff --git a/client/src/app/+admin/admin.component.ts b/client/src/app/+admin/admin.component.ts index 746549555..630bfe253 100644 --- a/client/src/app/+admin/admin.component.ts +++ b/client/src/app/+admin/admin.component.ts | |||
@@ -96,6 +96,14 @@ export class AdminComponent implements OnInit { | |||
96 | children: [] | 96 | children: [] |
97 | } | 97 | } |
98 | 98 | ||
99 | if (this.hasRegistrationsRight()) { | ||
100 | moderationItems.children.push({ | ||
101 | label: $localize`Registrations`, | ||
102 | routerLink: '/admin/moderation/registrations/list', | ||
103 | iconName: 'user' | ||
104 | }) | ||
105 | } | ||
106 | |||
99 | if (this.hasAbusesRight()) { | 107 | if (this.hasAbusesRight()) { |
100 | moderationItems.children.push({ | 108 | moderationItems.children.push({ |
101 | label: $localize`Reports`, | 109 | label: $localize`Reports`, |
@@ -229,4 +237,8 @@ export class AdminComponent implements OnInit { | |||
229 | private hasVideosRight () { | 237 | private hasVideosRight () { |
230 | return this.auth.getUser().hasRight(UserRight.SEE_ALL_VIDEOS) | 238 | return this.auth.getUser().hasRight(UserRight.SEE_ALL_VIDEOS) |
231 | } | 239 | } |
240 | |||
241 | private hasRegistrationsRight () { | ||
242 | return this.auth.getUser().hasRight(UserRight.MANAGE_REGISTRATIONS) | ||
243 | } | ||
232 | } | 244 | } |
diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts index f01967ea6..891ff4ed1 100644 --- a/client/src/app/+admin/admin.module.ts +++ b/client/src/app/+admin/admin.module.ts | |||
@@ -30,7 +30,13 @@ import { FollowersListComponent, FollowModalComponent, VideoRedundanciesListComp | |||
30 | import { FollowingListComponent } from './follows/following-list/following-list.component' | 30 | import { FollowingListComponent } from './follows/following-list/following-list.component' |
31 | import { RedundancyCheckboxComponent } from './follows/shared/redundancy-checkbox.component' | 31 | import { RedundancyCheckboxComponent } from './follows/shared/redundancy-checkbox.component' |
32 | import { VideoRedundancyInformationComponent } from './follows/video-redundancies-list/video-redundancy-information.component' | 32 | import { VideoRedundancyInformationComponent } from './follows/video-redundancies-list/video-redundancy-information.component' |
33 | import { AbuseListComponent, VideoBlockListComponent } from './moderation' | 33 | import { |
34 | AbuseListComponent, | ||
35 | AdminRegistrationService, | ||
36 | ProcessRegistrationModalComponent, | ||
37 | RegistrationListComponent, | ||
38 | VideoBlockListComponent | ||
39 | } from './moderation' | ||
34 | import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from './moderation/instance-blocklist' | 40 | import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from './moderation/instance-blocklist' |
35 | import { | 41 | import { |
36 | UserCreateComponent, | 42 | UserCreateComponent, |
@@ -116,7 +122,10 @@ import { JobsComponent } from './system/jobs/jobs.component' | |||
116 | EditLiveConfigurationComponent, | 122 | EditLiveConfigurationComponent, |
117 | EditAdvancedConfigurationComponent, | 123 | EditAdvancedConfigurationComponent, |
118 | EditInstanceInformationComponent, | 124 | EditInstanceInformationComponent, |
119 | EditHomepageComponent | 125 | EditHomepageComponent, |
126 | |||
127 | RegistrationListComponent, | ||
128 | ProcessRegistrationModalComponent | ||
120 | ], | 129 | ], |
121 | 130 | ||
122 | exports: [ | 131 | exports: [ |
@@ -130,7 +139,8 @@ import { JobsComponent } from './system/jobs/jobs.component' | |||
130 | ConfigService, | 139 | ConfigService, |
131 | PluginApiService, | 140 | PluginApiService, |
132 | EditConfigurationService, | 141 | EditConfigurationService, |
133 | VideoAdminService | 142 | VideoAdminService, |
143 | AdminRegistrationService | ||
134 | ] | 144 | ] |
135 | }) | 145 | }) |
136 | export class AdminModule { } | 146 | export class AdminModule { } |
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html index 43f1438e0..0f3803f97 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html +++ b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html | |||
@@ -44,9 +44,13 @@ | |||
44 | 44 | ||
45 | <div class="peertube-select-container"> | 45 | <div class="peertube-select-container"> |
46 | <select id="trendingVideosAlgorithmsDefault" formControlName="default" class="form-control"> | 46 | <select id="trendingVideosAlgorithmsDefault" formControlName="default" class="form-control"> |
47 | <option i18n value="publishedAt">Recently added videos</option> | ||
48 | <option i18n value="originallyPublishedAt">Original publication date</option> | ||
49 | <option i18n value="name">Name</option> | ||
47 | <option i18n value="hot">Hot videos</option> | 50 | <option i18n value="hot">Hot videos</option> |
48 | <option i18n value="most-viewed">Most viewed videos</option> | 51 | <option i18n value="most-viewed">Recent views</option> |
49 | <option i18n value="most-liked">Most liked videos</option> | 52 | <option i18n value="most-liked">Most liked videos</option> |
53 | <option i18n value="views">Global views</option> | ||
50 | </select> | 54 | </select> |
51 | </div> | 55 | </div> |
52 | 56 | ||
@@ -167,12 +171,21 @@ | |||
167 | </ng-container> | 171 | </ng-container> |
168 | 172 | ||
169 | <ng-container ngProjectAs="extra"> | 173 | <ng-container ngProjectAs="extra"> |
170 | <my-peertube-checkbox [ngClass]="getDisabledSignupClass()" | 174 | <div class="form-group"> |
171 | inputName="signupRequiresEmailVerification" formControlName="requiresEmailVerification" | 175 | <my-peertube-checkbox [ngClass]="getDisabledSignupClass()" |
172 | i18n-labelText labelText="Signup requires email verification" | 176 | inputName="signupRequiresApproval" formControlName="requiresApproval" |
173 | ></my-peertube-checkbox> | 177 | i18n-labelText labelText="Signup requires approval by moderators" |
178 | ></my-peertube-checkbox> | ||
179 | </div> | ||
174 | 180 | ||
175 | <div [ngClass]="getDisabledSignupClass()" class="mt-3"> | 181 | <div class="form-group"> |
182 | <my-peertube-checkbox [ngClass]="getDisabledSignupClass()" | ||
183 | inputName="signupRequiresEmailVerification" formControlName="requiresEmailVerification" | ||
184 | i18n-labelText labelText="Signup requires email verification" | ||
185 | ></my-peertube-checkbox> | ||
186 | </div> | ||
187 | |||
188 | <div [ngClass]="getDisabledSignupClass()"> | ||
176 | <label i18n for="signupLimit">Signup limit</label> | 189 | <label i18n for="signupLimit">Signup limit</label> |
177 | 190 | ||
178 | <div class="number-with-unit"> | 191 | <div class="number-with-unit"> |
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 168f4702c..2afe80a03 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 | |||
@@ -132,6 +132,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit { | |||
132 | signup: { | 132 | signup: { |
133 | enabled: null, | 133 | enabled: null, |
134 | limit: SIGNUP_LIMIT_VALIDATOR, | 134 | limit: SIGNUP_LIMIT_VALIDATOR, |
135 | requiresApproval: null, | ||
135 | requiresEmailVerification: null, | 136 | requiresEmailVerification: null, |
136 | minimumAge: SIGNUP_MINIMUM_AGE_VALIDATOR | 137 | minimumAge: SIGNUP_MINIMUM_AGE_VALIDATOR |
137 | }, | 138 | }, |
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-homepage.component.html b/client/src/app/+admin/config/edit-custom-config/edit-homepage.component.html index 5339240bb..3d8414f5c 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-homepage.component.html +++ b/client/src/app/+admin/config/edit-custom-config/edit-homepage.component.html | |||
@@ -17,7 +17,7 @@ | |||
17 | 17 | ||
18 | <my-markdown-textarea | 18 | <my-markdown-textarea |
19 | name="instanceCustomHomepageContent" formControlName="content" | 19 | name="instanceCustomHomepageContent" formControlName="content" |
20 | [customMarkdownRenderer]="getCustomMarkdownRenderer()" | 20 | [customMarkdownRenderer]="getCustomMarkdownRenderer()" [debounceTime]="500" |
21 | [formError]="formErrors['instanceCustomHomepage.content']" | 21 | [formError]="formErrors['instanceCustomHomepage.content']" |
22 | ></my-markdown-textarea> | 22 | ></my-markdown-textarea> |
23 | 23 | ||
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-instance-information.component.html b/client/src/app/+admin/config/edit-custom-config/edit-instance-information.component.html index b54733327..504afa189 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-instance-information.component.html +++ b/client/src/app/+admin/config/edit-custom-config/edit-instance-information.component.html | |||
@@ -38,7 +38,7 @@ | |||
38 | 38 | ||
39 | <my-markdown-textarea | 39 | <my-markdown-textarea |
40 | name="instanceDescription" formControlName="description" | 40 | name="instanceDescription" formControlName="description" |
41 | [customMarkdownRenderer]="getCustomMarkdownRenderer()" | 41 | [customMarkdownRenderer]="getCustomMarkdownRenderer()" [debounceTime]="500" |
42 | [formError]="formErrors['instance.description']" | 42 | [formError]="formErrors['instance.description']" |
43 | ></my-markdown-textarea> | 43 | ></my-markdown-textarea> |
44 | </div> | 44 | </div> |
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 8fe0d2348..14c62f1af 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 | |||
@@ -9,14 +9,14 @@ | |||
9 | [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" | 9 | [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" |
10 | [showCurrentPageReport]="true" i18n-currentPageReportTemplate | 10 | [showCurrentPageReport]="true" i18n-currentPageReportTemplate |
11 | currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} followers" | 11 | currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} followers" |
12 | [(selection)]="selectedFollows" | 12 | [(selection)]="selectedRows" |
13 | > | 13 | > |
14 | <ng-template pTemplate="caption"> | 14 | <ng-template pTemplate="caption"> |
15 | <div class="caption"> | 15 | <div class="caption"> |
16 | <div class="left-buttons"> | 16 | <div class="left-buttons"> |
17 | <my-action-dropdown | 17 | <my-action-dropdown |
18 | *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange" | 18 | *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange" |
19 | [actions]="bulkFollowsActions" [entry]="selectedFollows" | 19 | [actions]="bulkActions" [entry]="selectedRows" |
20 | > | 20 | > |
21 | </my-action-dropdown> | 21 | </my-action-dropdown> |
22 | </div> | 22 | </div> |
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 b2d333e83..cebb2e1a2 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 | |||
@@ -12,7 +12,7 @@ import { ActorFollow } from '@shared/models' | |||
12 | templateUrl: './followers-list.component.html', | 12 | templateUrl: './followers-list.component.html', |
13 | styleUrls: [ './followers-list.component.scss' ] | 13 | styleUrls: [ './followers-list.component.scss' ] |
14 | }) | 14 | }) |
15 | export class FollowersListComponent extends RestTable implements OnInit { | 15 | export class FollowersListComponent extends RestTable <ActorFollow> implements OnInit { |
16 | followers: ActorFollow[] = [] | 16 | followers: ActorFollow[] = [] |
17 | totalRecords = 0 | 17 | totalRecords = 0 |
18 | sort: SortMeta = { field: 'createdAt', order: -1 } | 18 | sort: SortMeta = { field: 'createdAt', order: -1 } |
@@ -20,8 +20,7 @@ export class FollowersListComponent extends RestTable implements OnInit { | |||
20 | 20 | ||
21 | searchFilters: AdvancedInputFilter[] = [] | 21 | searchFilters: AdvancedInputFilter[] = [] |
22 | 22 | ||
23 | selectedFollows: ActorFollow[] = [] | 23 | bulkActions: DropdownAction<ActorFollow[]>[] = [] |
24 | bulkFollowsActions: DropdownAction<ActorFollow[]>[] = [] | ||
25 | 24 | ||
26 | constructor ( | 25 | constructor ( |
27 | private confirmService: ConfirmService, | 26 | private confirmService: ConfirmService, |
@@ -36,7 +35,7 @@ export class FollowersListComponent extends RestTable implements OnInit { | |||
36 | 35 | ||
37 | this.searchFilters = this.followService.buildFollowsListFilters() | 36 | this.searchFilters = this.followService.buildFollowsListFilters() |
38 | 37 | ||
39 | this.bulkFollowsActions = [ | 38 | this.bulkActions = [ |
40 | { | 39 | { |
41 | label: $localize`Reject`, | 40 | label: $localize`Reject`, |
42 | handler: follows => this.rejectFollower(follows), | 41 | handler: follows => this.rejectFollower(follows), |
@@ -105,12 +104,14 @@ export class FollowersListComponent extends RestTable implements OnInit { | |||
105 | } | 104 | } |
106 | 105 | ||
107 | async deleteFollowers (follows: ActorFollow[]) { | 106 | async deleteFollowers (follows: ActorFollow[]) { |
107 | const icuParams = { count: follows.length, followerName: this.buildFollowerName(follows[0]) } | ||
108 | |||
108 | let message = $localize`Deleted followers will be able to send again a follow request.` | 109 | let message = $localize`Deleted followers will be able to send again a follow request.` |
109 | message += '<br /><br />' | 110 | message += '<br /><br />' |
110 | 111 | ||
111 | // eslint-disable-next-line max-len | 112 | // eslint-disable-next-line max-len |
112 | message += prepareIcu($localize`Do you really want to delete {count, plural, =1 {{followerName} follow request?} other {{count} follow requests?}}`)( | 113 | message += prepareIcu($localize`Do you really want to delete {count, plural, =1 {{followerName} follow request?} other {{count} follow requests?}}`)( |
113 | { count: follows.length, followerName: this.buildFollowerName(follows[0]) }, | 114 | icuParams, |
114 | $localize`Do you really want to delete these follow requests?` | 115 | $localize`Do you really want to delete these follow requests?` |
115 | ) | 116 | ) |
116 | 117 | ||
@@ -122,7 +123,7 @@ export class FollowersListComponent extends RestTable implements OnInit { | |||
122 | next: () => { | 123 | next: () => { |
123 | // eslint-disable-next-line max-len | 124 | // eslint-disable-next-line max-len |
124 | const message = prepareIcu($localize`Removed {count, plural, =1 {{followerName} follow request} other {{count} follow requests}}`)( | 125 | const message = prepareIcu($localize`Removed {count, plural, =1 {{followerName} follow request} other {{count} follow requests}}`)( |
125 | { count: follows.length, followerName: this.buildFollowerName(follows[0]) }, | 126 | icuParams, |
126 | $localize`Follow requests removed` | 127 | $localize`Follow requests removed` |
127 | ) | 128 | ) |
128 | 129 | ||
@@ -139,11 +140,7 @@ export class FollowersListComponent extends RestTable implements OnInit { | |||
139 | return follow.follower.name + '@' + follow.follower.host | 140 | return follow.follower.name + '@' + follow.follower.host |
140 | } | 141 | } |
141 | 142 | ||
142 | isInSelectionMode () { | 143 | protected reloadDataInternal () { |
143 | return this.selectedFollows.length !== 0 | ||
144 | } | ||
145 | |||
146 | protected reloadData () { | ||
147 | this.followService.getFollowers({ pagination: this.pagination, sort: this.sort, search: this.search }) | 144 | this.followService.getFollowers({ pagination: this.pagination, sort: this.sort, search: this.search }) |
148 | .subscribe({ | 145 | .subscribe({ |
149 | next: resultList => { | 146 | next: resultList => { |
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 f7abb7ede..eca79be71 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 | |||
@@ -9,14 +9,14 @@ | |||
9 | [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" | 9 | [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" |
10 | [showCurrentPageReport]="true" i18n-currentPageReportTemplate | 10 | [showCurrentPageReport]="true" i18n-currentPageReportTemplate |
11 | currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} hosts" | 11 | currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} hosts" |
12 | [(selection)]="selectedFollows" | 12 | [(selection)]="selectedRows" |
13 | > | 13 | > |
14 | <ng-template pTemplate="caption"> | 14 | <ng-template pTemplate="caption"> |
15 | <div class="caption"> | 15 | <div class="caption"> |
16 | <div class="left-buttons"> | 16 | <div class="left-buttons"> |
17 | <my-action-dropdown | 17 | <my-action-dropdown |
18 | *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange" | 18 | *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange" |
19 | [actions]="bulkFollowsActions" [entry]="selectedFollows" | 19 | [actions]="bulkActions" [entry]="selectedRows" |
20 | > | 20 | > |
21 | </my-action-dropdown> | 21 | </my-action-dropdown> |
22 | 22 | ||
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 e3a56651a..71f2fbe66 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 | |||
@@ -12,7 +12,7 @@ import { prepareIcu } from '@app/helpers' | |||
12 | templateUrl: './following-list.component.html', | 12 | templateUrl: './following-list.component.html', |
13 | styleUrls: [ './following-list.component.scss' ] | 13 | styleUrls: [ './following-list.component.scss' ] |
14 | }) | 14 | }) |
15 | export class FollowingListComponent extends RestTable implements OnInit { | 15 | export class FollowingListComponent extends RestTable <ActorFollow> implements OnInit { |
16 | @ViewChild('followModal') followModal: FollowModalComponent | 16 | @ViewChild('followModal') followModal: FollowModalComponent |
17 | 17 | ||
18 | following: ActorFollow[] = [] | 18 | following: ActorFollow[] = [] |
@@ -22,8 +22,7 @@ export class FollowingListComponent extends RestTable implements OnInit { | |||
22 | 22 | ||
23 | searchFilters: AdvancedInputFilter[] = [] | 23 | searchFilters: AdvancedInputFilter[] = [] |
24 | 24 | ||
25 | selectedFollows: ActorFollow[] = [] | 25 | bulkActions: DropdownAction<ActorFollow[]>[] = [] |
26 | bulkFollowsActions: DropdownAction<ActorFollow[]>[] = [] | ||
27 | 26 | ||
28 | constructor ( | 27 | constructor ( |
29 | private notifier: Notifier, | 28 | private notifier: Notifier, |
@@ -38,7 +37,7 @@ export class FollowingListComponent extends RestTable implements OnInit { | |||
38 | 37 | ||
39 | this.searchFilters = this.followService.buildFollowsListFilters() | 38 | this.searchFilters = this.followService.buildFollowsListFilters() |
40 | 39 | ||
41 | this.bulkFollowsActions = [ | 40 | this.bulkActions = [ |
42 | { | 41 | { |
43 | label: $localize`Delete`, | 42 | label: $localize`Delete`, |
44 | handler: follows => this.removeFollowing(follows) | 43 | handler: follows => this.removeFollowing(follows) |
@@ -58,17 +57,15 @@ export class FollowingListComponent extends RestTable implements OnInit { | |||
58 | return follow.following.name === 'peertube' | 57 | return follow.following.name === 'peertube' |
59 | } | 58 | } |
60 | 59 | ||
61 | isInSelectionMode () { | ||
62 | return this.selectedFollows.length !== 0 | ||
63 | } | ||
64 | |||
65 | buildFollowingName (follow: ActorFollow) { | 60 | buildFollowingName (follow: ActorFollow) { |
66 | return follow.following.name + '@' + follow.following.host | 61 | return follow.following.name + '@' + follow.following.host |
67 | } | 62 | } |
68 | 63 | ||
69 | async removeFollowing (follows: ActorFollow[]) { | 64 | async removeFollowing (follows: ActorFollow[]) { |
65 | const icuParams = { count: follows.length, entryName: this.buildFollowingName(follows[0]) } | ||
66 | |||
70 | const message = prepareIcu($localize`Do you really want to unfollow {count, plural, =1 {{entryName}?} other {{count} entries?}}`)( | 67 | const message = prepareIcu($localize`Do you really want to unfollow {count, plural, =1 {{entryName}?} other {{count} entries?}}`)( |
71 | { count: follows.length, entryName: this.buildFollowingName(follows[0]) }, | 68 | icuParams, |
72 | $localize`Do you really want to unfollow these entries?` | 69 | $localize`Do you really want to unfollow these entries?` |
73 | ) | 70 | ) |
74 | 71 | ||
@@ -80,7 +77,7 @@ export class FollowingListComponent extends RestTable implements OnInit { | |||
80 | next: () => { | 77 | next: () => { |
81 | // eslint-disable-next-line max-len | 78 | // eslint-disable-next-line max-len |
82 | const message = prepareIcu($localize`You are not following {count, plural, =1 {{entryName} anymore.} other {these {count} entries anymore.}}`)( | 79 | const message = prepareIcu($localize`You are not following {count, plural, =1 {{entryName} anymore.} other {these {count} entries anymore.}}`)( |
83 | { count: follows.length, entryName: this.buildFollowingName(follows[0]) }, | 80 | icuParams, |
84 | $localize`You are not following them anymore.` | 81 | $localize`You are not following them anymore.` |
85 | ) | 82 | ) |
86 | 83 | ||
@@ -92,7 +89,7 @@ export class FollowingListComponent extends RestTable implements OnInit { | |||
92 | }) | 89 | }) |
93 | } | 90 | } |
94 | 91 | ||
95 | protected reloadData () { | 92 | protected reloadDataInternal () { |
96 | this.followService.getFollowing({ pagination: this.pagination, sort: this.sort, search: this.search }) | 93 | this.followService.getFollowing({ pagination: this.pagination, sort: this.sort, search: this.search }) |
97 | .subscribe({ | 94 | .subscribe({ |
98 | next: resultList => { | 95 | next: resultList => { |
diff --git a/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.ts b/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.ts index a89603048..b31c5b35e 100644 --- a/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.ts +++ b/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.ts | |||
@@ -162,7 +162,7 @@ export class VideoRedundanciesListComponent extends RestTable implements OnInit | |||
162 | 162 | ||
163 | } | 163 | } |
164 | 164 | ||
165 | protected reloadData () { | 165 | protected reloadDataInternal () { |
166 | const options = { | 166 | const options = { |
167 | pagination: this.pagination, | 167 | pagination: this.pagination, |
168 | sort: this.sort, | 168 | sort: this.sort, |
diff --git a/client/src/app/+admin/moderation/index.ts b/client/src/app/+admin/moderation/index.ts index 9dab270cc..135b4b408 100644 --- a/client/src/app/+admin/moderation/index.ts +++ b/client/src/app/+admin/moderation/index.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | export * from './abuse-list' | 1 | export * from './abuse-list' |
2 | export * from './instance-blocklist' | 2 | export * from './instance-blocklist' |
3 | export * from './video-block-list' | 3 | export * from './video-block-list' |
4 | export * from './registration-list' | ||
4 | export * from './moderation.routes' | 5 | export * from './moderation.routes' |
diff --git a/client/src/app/+admin/moderation/moderation.routes.ts b/client/src/app/+admin/moderation/moderation.routes.ts index 1ad301039..378d2bed7 100644 --- a/client/src/app/+admin/moderation/moderation.routes.ts +++ b/client/src/app/+admin/moderation/moderation.routes.ts | |||
@@ -4,6 +4,7 @@ import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } f | |||
4 | import { VideoBlockListComponent } from '@app/+admin/moderation/video-block-list' | 4 | import { VideoBlockListComponent } from '@app/+admin/moderation/video-block-list' |
5 | import { UserRightGuard } from '@app/core' | 5 | import { UserRightGuard } from '@app/core' |
6 | import { UserRight } from '@shared/models' | 6 | import { UserRight } from '@shared/models' |
7 | import { RegistrationListComponent } from './registration-list' | ||
7 | 8 | ||
8 | export const ModerationRoutes: Routes = [ | 9 | export const ModerationRoutes: Routes = [ |
9 | { | 10 | { |
@@ -68,7 +69,19 @@ export const ModerationRoutes: Routes = [ | |||
68 | } | 69 | } |
69 | }, | 70 | }, |
70 | 71 | ||
71 | // We move this component in admin overview pages | 72 | { |
73 | path: 'registrations/list', | ||
74 | component: RegistrationListComponent, | ||
75 | canActivate: [ UserRightGuard ], | ||
76 | data: { | ||
77 | userRight: UserRight.MANAGE_REGISTRATIONS, | ||
78 | meta: { | ||
79 | title: $localize`User registrations` | ||
80 | } | ||
81 | } | ||
82 | }, | ||
83 | |||
84 | // We moved this component in admin overview pages | ||
72 | { | 85 | { |
73 | path: 'video-comments', | 86 | path: 'video-comments', |
74 | redirectTo: 'video-comments/list', | 87 | redirectTo: 'video-comments/list', |
diff --git a/client/src/app/+admin/moderation/registration-list/admin-registration.service.ts b/client/src/app/+admin/moderation/registration-list/admin-registration.service.ts new file mode 100644 index 000000000..a9f13cf2f --- /dev/null +++ b/client/src/app/+admin/moderation/registration-list/admin-registration.service.ts | |||
@@ -0,0 +1,81 @@ | |||
1 | import { SortMeta } from 'primeng/api' | ||
2 | import { from } from 'rxjs' | ||
3 | import { catchError, concatMap, toArray } from 'rxjs/operators' | ||
4 | import { HttpClient, HttpParams } from '@angular/common/http' | ||
5 | import { Injectable } from '@angular/core' | ||
6 | import { RestExtractor, RestPagination, RestService } from '@app/core' | ||
7 | import { arrayify } from '@shared/core-utils' | ||
8 | import { ResultList, UserRegistration, UserRegistrationUpdateState } from '@shared/models' | ||
9 | import { environment } from '../../../../environments/environment' | ||
10 | |||
11 | @Injectable() | ||
12 | export class AdminRegistrationService { | ||
13 | private static BASE_REGISTRATION_URL = environment.apiUrl + '/api/v1/users/registrations' | ||
14 | |||
15 | constructor ( | ||
16 | private authHttp: HttpClient, | ||
17 | private restExtractor: RestExtractor, | ||
18 | private restService: RestService | ||
19 | ) { } | ||
20 | |||
21 | listRegistrations (options: { | ||
22 | pagination: RestPagination | ||
23 | sort: SortMeta | ||
24 | search?: string | ||
25 | }) { | ||
26 | const { pagination, sort, search } = options | ||
27 | |||
28 | const url = AdminRegistrationService.BASE_REGISTRATION_URL | ||
29 | |||
30 | let params = new HttpParams() | ||
31 | params = this.restService.addRestGetParams(params, pagination, sort) | ||
32 | |||
33 | if (search) { | ||
34 | params = params.append('search', search) | ||
35 | } | ||
36 | |||
37 | return this.authHttp.get<ResultList<UserRegistration>>(url, { params }) | ||
38 | .pipe( | ||
39 | catchError(res => this.restExtractor.handleError(res)) | ||
40 | ) | ||
41 | } | ||
42 | |||
43 | acceptRegistration (options: { | ||
44 | registration: UserRegistration | ||
45 | moderationResponse: string | ||
46 | preventEmailDelivery: boolean | ||
47 | }) { | ||
48 | const { registration, moderationResponse, preventEmailDelivery } = options | ||
49 | |||
50 | const url = AdminRegistrationService.BASE_REGISTRATION_URL + '/' + registration.id + '/accept' | ||
51 | const body: UserRegistrationUpdateState = { moderationResponse, preventEmailDelivery } | ||
52 | |||
53 | return this.authHttp.post(url, body) | ||
54 | .pipe(catchError(res => this.restExtractor.handleError(res))) | ||
55 | } | ||
56 | |||
57 | rejectRegistration (options: { | ||
58 | registration: UserRegistration | ||
59 | moderationResponse: string | ||
60 | preventEmailDelivery: boolean | ||
61 | }) { | ||
62 | const { registration, moderationResponse, preventEmailDelivery } = options | ||
63 | |||
64 | const url = AdminRegistrationService.BASE_REGISTRATION_URL + '/' + registration.id + '/reject' | ||
65 | const body: UserRegistrationUpdateState = { moderationResponse, preventEmailDelivery } | ||
66 | |||
67 | return this.authHttp.post(url, body) | ||
68 | .pipe(catchError(res => this.restExtractor.handleError(res))) | ||
69 | } | ||
70 | |||
71 | removeRegistrations (registrationsArg: UserRegistration | UserRegistration[]) { | ||
72 | const registrations = arrayify(registrationsArg) | ||
73 | |||
74 | return from(registrations) | ||
75 | .pipe( | ||
76 | concatMap(r => this.authHttp.delete(AdminRegistrationService.BASE_REGISTRATION_URL + '/' + r.id)), | ||
77 | toArray(), | ||
78 | catchError(err => this.restExtractor.handleError(err)) | ||
79 | ) | ||
80 | } | ||
81 | } | ||
diff --git a/client/src/app/+admin/moderation/registration-list/index.ts b/client/src/app/+admin/moderation/registration-list/index.ts new file mode 100644 index 000000000..060b676a4 --- /dev/null +++ b/client/src/app/+admin/moderation/registration-list/index.ts | |||
@@ -0,0 +1,4 @@ | |||
1 | export * from './admin-registration.service' | ||
2 | export * from './process-registration-modal.component' | ||
3 | export * from './process-registration-validators' | ||
4 | export * from './registration-list.component' | ||
diff --git a/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.html b/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.html new file mode 100644 index 000000000..8e46b0cf9 --- /dev/null +++ b/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.html | |||
@@ -0,0 +1,74 @@ | |||
1 | <ng-template #modal> | ||
2 | <div class="modal-header"> | ||
3 | <h4 i18n class="modal-title"> | ||
4 | <ng-container *ngIf="isAccept()">Accept {{ registration.username }} registration</ng-container> | ||
5 | <ng-container *ngIf="isReject()">Reject {{ registration.username }} registration</ng-container> | ||
6 | </h4> | ||
7 | |||
8 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> | ||
9 | </div> | ||
10 | |||
11 | <form novalidate [formGroup]="form" (ngSubmit)="processRegistration()"> | ||
12 | <div class="modal-body mb-3"> | ||
13 | |||
14 | <div i18n *ngIf="!registration.emailVerified" class="alert alert-warning"> | ||
15 | Registration email has not been verified. Email delivery has been disabled by default. | ||
16 | </div> | ||
17 | |||
18 | <div class="description"> | ||
19 | <ng-container *ngIf="isAccept()"> | ||
20 | <p i18n> | ||
21 | <strong>Accepting</strong> <em>{{ registration.username }}</em> registration will create the account and channel. | ||
22 | </p> | ||
23 | |||
24 | <p *ngIf="isEmailEnabled()" i18n [ngClass]="{ 'text-decoration-line-through': isPreventEmailDeliveryChecked() }"> | ||
25 | An email will be sent to <em>{{ registration.email }}</em> explaining its account has been created with the moderation response you'll write below. | ||
26 | </p> | ||
27 | |||
28 | <div *ngIf="!isEmailEnabled()" class="alert alert-warning" i18n> | ||
29 | Emails are not enabled on this instance so PeerTube won't be able to send an email to <em>{{ registration.email }}</em> explaining its account has been created. | ||
30 | </div> | ||
31 | </ng-container> | ||
32 | |||
33 | <ng-container *ngIf="isReject()"> | ||
34 | <p i18n [ngClass]="{ 'text-decoration-line-through': isPreventEmailDeliveryChecked() }"> | ||
35 | An email will be sent to <em>{{ registration.email }}</em> explaining its registration request has been <strong>rejected</strong> with the moderation response you'll write below. | ||
36 | </p> | ||
37 | |||
38 | <div *ngIf="!isEmailEnabled()" class="alert alert-warning" i18n> | ||
39 | Emails are not enabled on this instance so PeerTube won't be able to send an email to <em>{{ registration.email }}</em> explaining its registration request has been rejected. | ||
40 | </div> | ||
41 | </ng-container> | ||
42 | </div> | ||
43 | |||
44 | <div class="form-group"> | ||
45 | <label for="moderationResponse" i18n>Send a message to the user</label> | ||
46 | |||
47 | <textarea | ||
48 | formControlName="moderationResponse" ngbAutofocus name="moderationResponse" id="moderationResponse" | ||
49 | [ngClass]="{ 'input-error': formErrors['moderationResponse'] }" class="form-control" | ||
50 | ></textarea> | ||
51 | |||
52 | <div *ngIf="formErrors.moderationResponse" class="form-error"> | ||
53 | {{ formErrors.moderationResponse }} | ||
54 | </div> | ||
55 | </div> | ||
56 | |||
57 | <div class="form-group"> | ||
58 | <my-peertube-checkbox | ||
59 | inputName="preventEmailDelivery" formControlName="preventEmailDelivery" [disabled]="!isEmailEnabled()" | ||
60 | i18n-labelText labelText="Prevent email from being sent to the user" | ||
61 | ></my-peertube-checkbox> | ||
62 | </div> | ||
63 | </div> | ||
64 | |||
65 | <div class="modal-footer inputs"> | ||
66 | <input | ||
67 | type="button" role="button" i18n-value value="Cancel" class="peertube-button grey-button" | ||
68 | (click)="hide()" (key.enter)="hide()" | ||
69 | > | ||
70 | |||
71 | <input type="submit" [value]="getSubmitValue()" class="peertube-button orange-button" [disabled]="!form.valid"> | ||
72 | </div> | ||
73 | </form> | ||
74 | </ng-template> | ||
diff --git a/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.scss b/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.scss new file mode 100644 index 000000000..3e03bed89 --- /dev/null +++ b/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.scss | |||
@@ -0,0 +1,3 @@ | |||
1 | @use '_variables' as *; | ||
2 | @use '_mixins' as *; | ||
3 | |||
diff --git a/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.ts b/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.ts new file mode 100644 index 000000000..3a7e5dea1 --- /dev/null +++ b/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.ts | |||
@@ -0,0 +1,122 @@ | |||
1 | import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' | ||
2 | import { Notifier, ServerService } from '@app/core' | ||
3 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' | ||
4 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | ||
5 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' | ||
6 | import { UserRegistration } from '@shared/models' | ||
7 | import { AdminRegistrationService } from './admin-registration.service' | ||
8 | import { REGISTRATION_MODERATION_RESPONSE_VALIDATOR } from './process-registration-validators' | ||
9 | |||
10 | @Component({ | ||
11 | selector: 'my-process-registration-modal', | ||
12 | templateUrl: './process-registration-modal.component.html', | ||
13 | styleUrls: [ './process-registration-modal.component.scss' ] | ||
14 | }) | ||
15 | export class ProcessRegistrationModalComponent extends FormReactive implements OnInit { | ||
16 | @ViewChild('modal', { static: true }) modal: NgbModal | ||
17 | |||
18 | @Output() registrationProcessed = new EventEmitter() | ||
19 | |||
20 | registration: UserRegistration | ||
21 | |||
22 | private openedModal: NgbModalRef | ||
23 | private processMode: 'accept' | 'reject' | ||
24 | |||
25 | constructor ( | ||
26 | protected formReactiveService: FormReactiveService, | ||
27 | private server: ServerService, | ||
28 | private modalService: NgbModal, | ||
29 | private notifier: Notifier, | ||
30 | private registrationService: AdminRegistrationService | ||
31 | ) { | ||
32 | super() | ||
33 | } | ||
34 | |||
35 | ngOnInit () { | ||
36 | this.buildForm({ | ||
37 | moderationResponse: REGISTRATION_MODERATION_RESPONSE_VALIDATOR, | ||
38 | preventEmailDelivery: null | ||
39 | }) | ||
40 | } | ||
41 | |||
42 | isAccept () { | ||
43 | return this.processMode === 'accept' | ||
44 | } | ||
45 | |||
46 | isReject () { | ||
47 | return this.processMode === 'reject' | ||
48 | } | ||
49 | |||
50 | openModal (registration: UserRegistration, mode: 'accept' | 'reject') { | ||
51 | this.processMode = mode | ||
52 | this.registration = registration | ||
53 | |||
54 | this.form.patchValue({ | ||
55 | preventEmailDelivery: !this.isEmailEnabled() || registration.emailVerified !== true | ||
56 | }) | ||
57 | |||
58 | this.openedModal = this.modalService.open(this.modal, { centered: true }) | ||
59 | } | ||
60 | |||
61 | hide () { | ||
62 | this.form.reset() | ||
63 | |||
64 | this.openedModal.close() | ||
65 | } | ||
66 | |||
67 | getSubmitValue () { | ||
68 | if (this.isAccept()) { | ||
69 | return $localize`Accept registration` | ||
70 | } | ||
71 | |||
72 | return $localize`Reject registration` | ||
73 | } | ||
74 | |||
75 | processRegistration () { | ||
76 | if (this.isAccept()) return this.acceptRegistration() | ||
77 | |||
78 | return this.rejectRegistration() | ||
79 | } | ||
80 | |||
81 | isEmailEnabled () { | ||
82 | return this.server.getHTMLConfig().email.enabled | ||
83 | } | ||
84 | |||
85 | isPreventEmailDeliveryChecked () { | ||
86 | return this.form.value.preventEmailDelivery | ||
87 | } | ||
88 | |||
89 | private acceptRegistration () { | ||
90 | this.registrationService.acceptRegistration({ | ||
91 | registration: this.registration, | ||
92 | moderationResponse: this.form.value.moderationResponse, | ||
93 | preventEmailDelivery: this.form.value.preventEmailDelivery | ||
94 | }).subscribe({ | ||
95 | next: () => { | ||
96 | this.notifier.success($localize`${this.registration.username} account created`) | ||
97 | |||
98 | this.registrationProcessed.emit() | ||
99 | this.hide() | ||
100 | }, | ||
101 | |||
102 | error: err => this.notifier.error(err.message) | ||
103 | }) | ||
104 | } | ||
105 | |||
106 | private rejectRegistration () { | ||
107 | this.registrationService.rejectRegistration({ | ||
108 | registration: this.registration, | ||
109 | moderationResponse: this.form.value.moderationResponse, | ||
110 | preventEmailDelivery: this.form.value.preventEmailDelivery | ||
111 | }).subscribe({ | ||
112 | next: () => { | ||
113 | this.notifier.success($localize`${this.registration.username} registration rejected`) | ||
114 | |||
115 | this.registrationProcessed.emit() | ||
116 | this.hide() | ||
117 | }, | ||
118 | |||
119 | error: err => this.notifier.error(err.message) | ||
120 | }) | ||
121 | } | ||
122 | } | ||
diff --git a/client/src/app/+admin/moderation/registration-list/process-registration-validators.ts b/client/src/app/+admin/moderation/registration-list/process-registration-validators.ts new file mode 100644 index 000000000..e01a07d9d --- /dev/null +++ b/client/src/app/+admin/moderation/registration-list/process-registration-validators.ts | |||
@@ -0,0 +1,11 @@ | |||
1 | import { Validators } from '@angular/forms' | ||
2 | import { BuildFormValidator } from '@app/shared/form-validators' | ||
3 | |||
4 | export const REGISTRATION_MODERATION_RESPONSE_VALIDATOR: BuildFormValidator = { | ||
5 | VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ], | ||
6 | MESSAGES: { | ||
7 | required: $localize`Moderation response is required.`, | ||
8 | minlength: $localize`Moderation response must be at least 2 characters long.`, | ||
9 | maxlength: $localize`Moderation response cannot be more than 3000 characters long.` | ||
10 | } | ||
11 | } | ||
diff --git a/client/src/app/+admin/moderation/registration-list/registration-list.component.html b/client/src/app/+admin/moderation/registration-list/registration-list.component.html new file mode 100644 index 000000000..a2b888101 --- /dev/null +++ b/client/src/app/+admin/moderation/registration-list/registration-list.component.html | |||
@@ -0,0 +1,135 @@ | |||
1 | <h1> | ||
2 | <my-global-icon iconName="user" aria-hidden="true"></my-global-icon> | ||
3 | <ng-container i18n>Registration requests</ng-container> | ||
4 | </h1> | ||
5 | |||
6 | <p-table | ||
7 | [value]="registrations" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start" | ||
8 | [rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order" dataKey="id" | ||
9 | [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" | ||
10 | [(selection)]="selectedRows" [showCurrentPageReport]="true" i18n-currentPageReportTemplate | ||
11 | currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} registrations" | ||
12 | [expandedRowKeys]="expandedRows" | ||
13 | > | ||
14 | <ng-template pTemplate="caption"> | ||
15 | <div class="caption"> | ||
16 | <div class="left-buttons"> | ||
17 | <my-action-dropdown | ||
18 | *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange" | ||
19 | [actions]="bulkActions" [entry]="selectedRows" | ||
20 | > | ||
21 | </my-action-dropdown> | ||
22 | </div> | ||
23 | |||
24 | <div class="ms-auto"> | ||
25 | <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter> | ||
26 | </div> | ||
27 | </div> | ||
28 | </ng-template> | ||
29 | |||
30 | <ng-template pTemplate="header"> | ||
31 | <tr> <!-- header --> | ||
32 | <th style="width: 40px"> | ||
33 | <p-tableHeaderCheckbox ariaLabel="Select all rows" i18n-ariaLabel></p-tableHeaderCheckbox> | ||
34 | </th> | ||
35 | <th style="width: 40px;"></th> | ||
36 | <th style="width: 150px;"></th> | ||
37 | <th i18n>Account</th> | ||
38 | <th i18n>Email</th> | ||
39 | <th i18n>Channel</th> | ||
40 | <th i18n>Registration reason</th> | ||
41 | <th i18n pSortableColumn="state" style="width: 80px;">State <p-sortIcon field="state"></p-sortIcon></th> | ||
42 | <th i18n>Moderation response</th> | ||
43 | <th style="width: 150px;" i18n pSortableColumn="createdAt">Requested on <p-sortIcon field="createdAt"></p-sortIcon></th> | ||
44 | </tr> | ||
45 | </ng-template> | ||
46 | |||
47 | <ng-template pTemplate="body" let-expanded="expanded" let-registration> | ||
48 | <tr [pSelectableRow]="registration"> | ||
49 | <td class="checkbox-cell"> | ||
50 | <p-tableCheckbox [value]="registration" ariaLabel="Select this row" i18n-ariaLabel></p-tableCheckbox> | ||
51 | </td> | ||
52 | |||
53 | <td class="expand-cell" [pRowToggler]="registration"> | ||
54 | <my-table-expander-icon [expanded]="expanded"></my-table-expander-icon> | ||
55 | </td> | ||
56 | |||
57 | <td class="action-cell"> | ||
58 | <my-action-dropdown | ||
59 | [ngClass]="{ 'show': expanded }" placement="bottom-right top-right left auto" container="body" | ||
60 | i18n-label label="Actions" [actions]="registrationActions" [entry]="registration" | ||
61 | ></my-action-dropdown> | ||
62 | </td> | ||
63 | |||
64 | <td> | ||
65 | <div class="chip two-lines"> | ||
66 | <div> | ||
67 | <span>{{ registration.username }}</span> | ||
68 | <span class="muted">{{ registration.accountDisplayName }}</span> | ||
69 | </div> | ||
70 | </div> | ||
71 | </td> | ||
72 | |||
73 | <td> | ||
74 | <my-user-email-info [entry]="registration" [requiresEmailVerification]="requiresEmailVerification"></my-user-email-info> | ||
75 | </td> | ||
76 | |||
77 | <td> | ||
78 | <div class="chip two-lines"> | ||
79 | <div> | ||
80 | <span>{{ registration.channelHandle }}</span> | ||
81 | <span class="muted">{{ registration.channelDisplayName }}</span> | ||
82 | </div> | ||
83 | </div> | ||
84 | </td> | ||
85 | |||
86 | <td container="body" placement="left auto" [ngbTooltip]="registration.registrationReason"> | ||
87 | {{ registration.registrationReason }} | ||
88 | </td> | ||
89 | |||
90 | <td class="c-hand abuse-states" [pRowToggler]="registration"> | ||
91 | <my-global-icon *ngIf="isRegistrationAccepted(registration)" [title]="registration.state.label" iconName="tick"></my-global-icon> | ||
92 | <my-global-icon *ngIf="isRegistrationRejected(registration)" [title]="registration.state.label" iconName="cross"></my-global-icon> | ||
93 | </td> | ||
94 | |||
95 | <td container="body" placement="left auto" [ngbTooltip]="registration.moderationResponse"> | ||
96 | {{ registration.moderationResponse }} | ||
97 | </td> | ||
98 | |||
99 | <td class="c-hand" [pRowToggler]="registration">{{ registration.createdAt | date: 'short' }}</td> | ||
100 | </tr> | ||
101 | </ng-template> | ||
102 | |||
103 | <ng-template pTemplate="rowexpansion" let-registration> | ||
104 | <tr> | ||
105 | <td colspan="9"> | ||
106 | <div class="moderation-expanded"> | ||
107 | <div class="left"> | ||
108 | <div class="d-flex"> | ||
109 | <span class="moderation-expanded-label" i18n>Registration reason:</span> | ||
110 | <span class="moderation-expanded-text" [innerHTML]="registration.registrationReasonHTML"></span> | ||
111 | </div> | ||
112 | |||
113 | <div *ngIf="registration.moderationResponse"> | ||
114 | <span class="moderation-expanded-label" i18n>Moderation response:</span> | ||
115 | <span class="moderation-expanded-text" [innerHTML]="registration.moderationResponseHTML"></span> | ||
116 | </div> | ||
117 | </div> | ||
118 | </div> | ||
119 | </td> | ||
120 | </tr> | ||
121 | </ng-template> | ||
122 | |||
123 | <ng-template pTemplate="emptymessage"> | ||
124 | <tr> | ||
125 | <td colspan="9"> | ||
126 | <div class="no-results"> | ||
127 | <ng-container *ngIf="search" i18n>No registrations found matching current filters.</ng-container> | ||
128 | <ng-container *ngIf="!search" i18n>No registrations found.</ng-container> | ||
129 | </div> | ||
130 | </td> | ||
131 | </tr> | ||
132 | </ng-template> | ||
133 | </p-table> | ||
134 | |||
135 | <my-process-registration-modal #processRegistrationModal (registrationProcessed)="onRegistrationProcessed()"></my-process-registration-modal> | ||
diff --git a/client/src/app/+admin/moderation/registration-list/registration-list.component.scss b/client/src/app/+admin/moderation/registration-list/registration-list.component.scss new file mode 100644 index 000000000..9cae08e85 --- /dev/null +++ b/client/src/app/+admin/moderation/registration-list/registration-list.component.scss | |||
@@ -0,0 +1,7 @@ | |||
1 | @use '_mixins' as *; | ||
2 | @use '_variables' as *; | ||
3 | |||
4 | my-global-icon { | ||
5 | width: 24px; | ||
6 | height: 24px; | ||
7 | } | ||
diff --git a/client/src/app/+admin/moderation/registration-list/registration-list.component.ts b/client/src/app/+admin/moderation/registration-list/registration-list.component.ts new file mode 100644 index 000000000..ed8fbec51 --- /dev/null +++ b/client/src/app/+admin/moderation/registration-list/registration-list.component.ts | |||
@@ -0,0 +1,151 @@ | |||
1 | import { SortMeta } from 'primeng/api' | ||
2 | import { Component, OnInit, ViewChild } from '@angular/core' | ||
3 | import { ActivatedRoute, Router } from '@angular/router' | ||
4 | import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable, ServerService } from '@app/core' | ||
5 | import { prepareIcu } from '@app/helpers' | ||
6 | import { AdvancedInputFilter } from '@app/shared/shared-forms' | ||
7 | import { DropdownAction } from '@app/shared/shared-main' | ||
8 | import { UserRegistration, UserRegistrationState } from '@shared/models' | ||
9 | import { AdminRegistrationService } from './admin-registration.service' | ||
10 | import { ProcessRegistrationModalComponent } from './process-registration-modal.component' | ||
11 | |||
12 | @Component({ | ||
13 | selector: 'my-registration-list', | ||
14 | templateUrl: './registration-list.component.html', | ||
15 | styleUrls: [ '../../../shared/shared-moderation/moderation.scss', './registration-list.component.scss' ] | ||
16 | }) | ||
17 | export class RegistrationListComponent extends RestTable <UserRegistration> implements OnInit { | ||
18 | @ViewChild('processRegistrationModal', { static: true }) processRegistrationModal: ProcessRegistrationModalComponent | ||
19 | |||
20 | registrations: (UserRegistration & { registrationReasonHTML?: string, moderationResponseHTML?: string })[] = [] | ||
21 | totalRecords = 0 | ||
22 | sort: SortMeta = { field: 'createdAt', order: -1 } | ||
23 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } | ||
24 | |||
25 | registrationActions: DropdownAction<UserRegistration>[][] = [] | ||
26 | bulkActions: DropdownAction<UserRegistration[]>[] = [] | ||
27 | |||
28 | inputFilters: AdvancedInputFilter[] = [] | ||
29 | |||
30 | requiresEmailVerification: boolean | ||
31 | |||
32 | constructor ( | ||
33 | protected route: ActivatedRoute, | ||
34 | protected router: Router, | ||
35 | private server: ServerService, | ||
36 | private notifier: Notifier, | ||
37 | private markdownRenderer: MarkdownService, | ||
38 | private confirmService: ConfirmService, | ||
39 | private adminRegistrationService: AdminRegistrationService | ||
40 | ) { | ||
41 | super() | ||
42 | |||
43 | this.registrationActions = [ | ||
44 | [ | ||
45 | { | ||
46 | label: $localize`Accept this request`, | ||
47 | handler: registration => this.openRegistrationRequestProcessModal(registration, 'accept'), | ||
48 | isDisplayed: registration => registration.state.id === UserRegistrationState.PENDING | ||
49 | }, | ||
50 | { | ||
51 | label: $localize`Reject this request`, | ||
52 | handler: registration => this.openRegistrationRequestProcessModal(registration, 'reject'), | ||
53 | isDisplayed: registration => registration.state.id === UserRegistrationState.PENDING | ||
54 | }, | ||
55 | { | ||
56 | label: $localize`Remove this request`, | ||
57 | handler: registration => this.removeRegistrations([ registration ]) | ||
58 | } | ||
59 | ] | ||
60 | ] | ||
61 | |||
62 | this.bulkActions = [ | ||
63 | { | ||
64 | label: $localize`Delete`, | ||
65 | handler: registrations => this.removeRegistrations(registrations) | ||
66 | } | ||
67 | ] | ||
68 | } | ||
69 | |||
70 | ngOnInit () { | ||
71 | this.initialize() | ||
72 | |||
73 | this.server.getConfig() | ||
74 | .subscribe(config => { | ||
75 | this.requiresEmailVerification = config.signup.requiresEmailVerification | ||
76 | }) | ||
77 | } | ||
78 | |||
79 | getIdentifier () { | ||
80 | return 'RegistrationListComponent' | ||
81 | } | ||
82 | |||
83 | isRegistrationAccepted (registration: UserRegistration) { | ||
84 | return registration.state.id === UserRegistrationState.ACCEPTED | ||
85 | } | ||
86 | |||
87 | isRegistrationRejected (registration: UserRegistration) { | ||
88 | return registration.state.id === UserRegistrationState.REJECTED | ||
89 | } | ||
90 | |||
91 | onRegistrationProcessed () { | ||
92 | this.reloadData() | ||
93 | } | ||
94 | |||
95 | protected reloadDataInternal () { | ||
96 | this.adminRegistrationService.listRegistrations({ | ||
97 | pagination: this.pagination, | ||
98 | sort: this.sort, | ||
99 | search: this.search | ||
100 | }).subscribe({ | ||
101 | next: async resultList => { | ||
102 | this.totalRecords = resultList.total | ||
103 | this.registrations = resultList.data | ||
104 | |||
105 | for (const registration of this.registrations) { | ||
106 | registration.registrationReasonHTML = await this.toHtml(registration.registrationReason) | ||
107 | registration.moderationResponseHTML = await this.toHtml(registration.moderationResponse) | ||
108 | } | ||
109 | }, | ||
110 | |||
111 | error: err => this.notifier.error(err.message) | ||
112 | }) | ||
113 | } | ||
114 | |||
115 | private openRegistrationRequestProcessModal (registration: UserRegistration, mode: 'accept' | 'reject') { | ||
116 | this.processRegistrationModal.openModal(registration, mode) | ||
117 | } | ||
118 | |||
119 | private async removeRegistrations (registrations: UserRegistration[]) { | ||
120 | const icuParams = { count: registrations.length, username: registrations[0].username } | ||
121 | |||
122 | // eslint-disable-next-line max-len | ||
123 | const message = prepareIcu($localize`Do you really want to delete {count, plural, =1 {{username} registration request?} other {{count} registration requests?}}`)( | ||
124 | icuParams, | ||
125 | $localize`Do you really want to delete these registration requests?` | ||
126 | ) | ||
127 | |||
128 | const res = await this.confirmService.confirm(message, $localize`Delete`) | ||
129 | if (res === false) return | ||
130 | |||
131 | this.adminRegistrationService.removeRegistrations(registrations) | ||
132 | .subscribe({ | ||
133 | next: () => { | ||
134 | // eslint-disable-next-line max-len | ||
135 | const message = prepareIcu($localize`Removed {count, plural, =1 {{username} registration request} other {{count} registration requests}}`)( | ||
136 | icuParams, | ||
137 | $localize`Registration requests removed` | ||
138 | ) | ||
139 | |||
140 | this.notifier.success(message) | ||
141 | this.reloadData() | ||
142 | }, | ||
143 | |||
144 | error: err => this.notifier.error(err.message) | ||
145 | }) | ||
146 | } | ||
147 | |||
148 | private toHtml (text: string) { | ||
149 | return this.markdownRenderer.textMarkdownToHTML({ markdown: text }) | ||
150 | } | ||
151 | } | ||
diff --git a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts index efd99e52b..f365a2500 100644 --- a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts +++ b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts | |||
@@ -159,26 +159,25 @@ export class VideoBlockListComponent extends RestTable implements OnInit { | |||
159 | }) | 159 | }) |
160 | } | 160 | } |
161 | 161 | ||
162 | protected reloadData () { | 162 | protected reloadDataInternal () { |
163 | this.videoBlocklistService.listBlocks({ | 163 | this.videoBlocklistService.listBlocks({ |
164 | pagination: this.pagination, | 164 | pagination: this.pagination, |
165 | sort: this.sort, | 165 | sort: this.sort, |
166 | search: this.search | 166 | search: this.search |
167 | }) | 167 | }).subscribe({ |
168 | .subscribe({ | 168 | next: async resultList => { |
169 | next: async resultList => { | 169 | this.totalRecords = resultList.total |
170 | this.totalRecords = resultList.total | ||
171 | 170 | ||
172 | this.blocklist = resultList.data | 171 | this.blocklist = resultList.data |
173 | 172 | ||
174 | for (const element of this.blocklist) { | 173 | for (const element of this.blocklist) { |
175 | Object.assign(element, { | 174 | Object.assign(element, { |
176 | reasonHtml: await this.toHtml(element.reason) | 175 | reasonHtml: await this.toHtml(element.reason) |
177 | }) | 176 | }) |
178 | } | 177 | } |
179 | }, | 178 | }, |
180 | 179 | ||
181 | error: err => this.notifier.error(err.message) | 180 | error: err => this.notifier.error(err.message) |
182 | }) | 181 | }) |
183 | } | 182 | } |
184 | } | 183 | } |
diff --git a/client/src/app/+admin/overview/comments/video-comment-list.component.html b/client/src/app/+admin/overview/comments/video-comment-list.component.html index d2ca5f700..b0d8131bf 100644 --- a/client/src/app/+admin/overview/comments/video-comment-list.component.html +++ b/client/src/app/+admin/overview/comments/video-comment-list.component.html | |||
@@ -13,14 +13,14 @@ | |||
13 | [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" [selectionPageOnly]="true" | 13 | [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" [selectionPageOnly]="true" |
14 | [showCurrentPageReport]="true" i18n-currentPageReportTemplate | 14 | [showCurrentPageReport]="true" i18n-currentPageReportTemplate |
15 | currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} comments" | 15 | currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} comments" |
16 | [expandedRowKeys]="expandedRows" [(selection)]="selectedComments" | 16 | [expandedRowKeys]="expandedRows" [(selection)]="selectedRows" |
17 | > | 17 | > |
18 | <ng-template pTemplate="caption"> | 18 | <ng-template pTemplate="caption"> |
19 | <div class="caption"> | 19 | <div class="caption"> |
20 | <div> | 20 | <div> |
21 | <my-action-dropdown | 21 | <my-action-dropdown |
22 | *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange" | 22 | *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange" |
23 | [actions]="bulkCommentActions" [entry]="selectedComments" | 23 | [actions]="bulkActions" [entry]="selectedRows" |
24 | > | 24 | > |
25 | </my-action-dropdown> | 25 | </my-action-dropdown> |
26 | </div> | 26 | </div> |
diff --git a/client/src/app/+admin/overview/comments/video-comment-list.component.ts b/client/src/app/+admin/overview/comments/video-comment-list.component.ts index c95d2ffeb..28efdc076 100644 --- a/client/src/app/+admin/overview/comments/video-comment-list.component.ts +++ b/client/src/app/+admin/overview/comments/video-comment-list.component.ts | |||
@@ -14,7 +14,7 @@ import { prepareIcu } from '@app/helpers' | |||
14 | templateUrl: './video-comment-list.component.html', | 14 | templateUrl: './video-comment-list.component.html', |
15 | styleUrls: [ '../../../shared/shared-moderation/moderation.scss', './video-comment-list.component.scss' ] | 15 | styleUrls: [ '../../../shared/shared-moderation/moderation.scss', './video-comment-list.component.scss' ] |
16 | }) | 16 | }) |
17 | export class VideoCommentListComponent extends RestTable implements OnInit { | 17 | export class VideoCommentListComponent extends RestTable <VideoCommentAdmin> implements OnInit { |
18 | comments: VideoCommentAdmin[] | 18 | comments: VideoCommentAdmin[] |
19 | totalRecords = 0 | 19 | totalRecords = 0 |
20 | sort: SortMeta = { field: 'createdAt', order: -1 } | 20 | sort: SortMeta = { field: 'createdAt', order: -1 } |
@@ -40,8 +40,7 @@ export class VideoCommentListComponent extends RestTable implements OnInit { | |||
40 | } | 40 | } |
41 | ] | 41 | ] |
42 | 42 | ||
43 | selectedComments: VideoCommentAdmin[] = [] | 43 | bulkActions: DropdownAction<VideoCommentAdmin[]>[] = [] |
44 | bulkCommentActions: DropdownAction<VideoCommentAdmin[]>[] = [] | ||
45 | 44 | ||
46 | inputFilters: AdvancedInputFilter[] = [ | 45 | inputFilters: AdvancedInputFilter[] = [ |
47 | { | 46 | { |
@@ -100,7 +99,7 @@ export class VideoCommentListComponent extends RestTable implements OnInit { | |||
100 | ngOnInit () { | 99 | ngOnInit () { |
101 | this.initialize() | 100 | this.initialize() |
102 | 101 | ||
103 | this.bulkCommentActions = [ | 102 | this.bulkActions = [ |
104 | { | 103 | { |
105 | label: $localize`Delete`, | 104 | label: $localize`Delete`, |
106 | handler: comments => this.removeComments(comments), | 105 | handler: comments => this.removeComments(comments), |
@@ -118,11 +117,7 @@ export class VideoCommentListComponent extends RestTable implements OnInit { | |||
118 | return this.markdownRenderer.textMarkdownToHTML({ markdown: text, withHtml: true, withEmoji: true }) | 117 | return this.markdownRenderer.textMarkdownToHTML({ markdown: text, withHtml: true, withEmoji: true }) |
119 | } | 118 | } |
120 | 119 | ||
121 | isInSelectionMode () { | 120 | protected reloadDataInternal () { |
122 | return this.selectedComments.length !== 0 | ||
123 | } | ||
124 | |||
125 | reloadData () { | ||
126 | this.videoCommentService.getAdminVideoComments({ | 121 | this.videoCommentService.getAdminVideoComments({ |
127 | pagination: this.pagination, | 122 | pagination: this.pagination, |
128 | sort: this.sort, | 123 | sort: this.sort, |
@@ -162,7 +157,7 @@ export class VideoCommentListComponent extends RestTable implements OnInit { | |||
162 | 157 | ||
163 | error: err => this.notifier.error(err.message), | 158 | error: err => this.notifier.error(err.message), |
164 | 159 | ||
165 | complete: () => this.selectedComments = [] | 160 | complete: () => this.selectedRows = [] |
166 | }) | 161 | }) |
167 | } | 162 | } |
168 | 163 | ||
diff --git a/client/src/app/+admin/overview/users/user-list/user-list.component.html b/client/src/app/+admin/overview/users/user-list/user-list.component.html index a96ce561c..7eb5e0fc7 100644 --- a/client/src/app/+admin/overview/users/user-list/user-list.component.html +++ b/client/src/app/+admin/overview/users/user-list/user-list.component.html | |||
@@ -6,7 +6,7 @@ | |||
6 | <p-table | 6 | <p-table |
7 | [value]="users" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start" | 7 | [value]="users" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start" |
8 | [rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order" dataKey="id" [resizableColumns]="true" | 8 | [rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order" dataKey="id" [resizableColumns]="true" |
9 | [(selection)]="selectedUsers" [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" [selectionPageOnly]="true" | 9 | [(selection)]="selectedRows" [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" [selectionPageOnly]="true" |
10 | [showCurrentPageReport]="true" i18n-currentPageReportTemplate | 10 | [showCurrentPageReport]="true" i18n-currentPageReportTemplate |
11 | currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} users" | 11 | currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} users" |
12 | [expandedRowKeys]="expandedRows" | 12 | [expandedRowKeys]="expandedRows" |
@@ -16,7 +16,7 @@ | |||
16 | <div class="left-buttons"> | 16 | <div class="left-buttons"> |
17 | <my-action-dropdown | 17 | <my-action-dropdown |
18 | *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange" | 18 | *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange" |
19 | [actions]="bulkUserActions" [entry]="selectedUsers" | 19 | [actions]="bulkActions" [entry]="selectedRows" |
20 | > | 20 | > |
21 | </my-action-dropdown> | 21 | </my-action-dropdown> |
22 | 22 | ||
@@ -95,7 +95,7 @@ | |||
95 | <div class="chip two-lines"> | 95 | <div class="chip two-lines"> |
96 | <my-actor-avatar [actor]="user?.account" actorType="account" size="32"></my-actor-avatar> | 96 | <my-actor-avatar [actor]="user?.account" actorType="account" size="32"></my-actor-avatar> |
97 | <div> | 97 | <div> |
98 | <span class="user-table-primary-text">{{ user.account.displayName }}</span> | 98 | <span>{{ user.account.displayName }}</span> |
99 | <span class="muted">{{ user.username }}</span> | 99 | <span class="muted">{{ user.username }}</span> |
100 | </div> | 100 | </div> |
101 | </div> | 101 | </div> |
@@ -110,23 +110,10 @@ | |||
110 | <span *ngIf="!user.blocked" class="pt-badge" [ngClass]="getRoleClass(user.role.id)">{{ user.role.label }}</span> | 110 | <span *ngIf="!user.blocked" class="pt-badge" [ngClass]="getRoleClass(user.role.id)">{{ user.role.label }}</span> |
111 | </td> | 111 | </td> |
112 | 112 | ||
113 | <td *ngIf="isSelected('email')" [title]="user.email"> | 113 | <td *ngIf="isSelected('email')"> |
114 | <ng-container *ngIf="!requiresEmailVerification || user.blocked; else emailWithVerificationStatus"> | 114 | <my-user-email-info [entry]="user" [requiresEmailVerification]="requiresEmailVerification"></my-user-email-info> |
115 | <a class="table-email" [href]="'mailto:' + user.email">{{ user.email }}</a> | ||
116 | </ng-container> | ||
117 | </td> | 115 | </td> |
118 | 116 | ||
119 | <ng-template #emailWithVerificationStatus> | ||
120 | <td *ngIf="user.emailVerified === false; else emailVerifiedNotFalse" i18n-title title="User's email must be verified to login"> | ||
121 | <em>? {{ user.email }}</em> | ||
122 | </td> | ||
123 | <ng-template #emailVerifiedNotFalse> | ||
124 | <td i18n-title title="User's email is verified / User can login without email verification"> | ||
125 | ✓ {{ user.email }} | ||
126 | </td> | ||
127 | </ng-template> | ||
128 | </ng-template> | ||
129 | |||
130 | <td *ngIf="isSelected('quota')"> | 117 | <td *ngIf="isSelected('quota')"> |
131 | <div class="progress" i18n-title title="Total video quota"> | 118 | <div class="progress" i18n-title title="Total video quota"> |
132 | <div class="progress-bar" role="progressbar" [style]="{ width: getUserVideoQuotaPercentage(user) + '%' }" | 119 | <div class="progress-bar" role="progressbar" [style]="{ width: getUserVideoQuotaPercentage(user) + '%' }" |
diff --git a/client/src/app/+admin/overview/users/user-list/user-list.component.scss b/client/src/app/+admin/overview/users/user-list/user-list.component.scss index 23e0d29ee..2a3b955d2 100644 --- a/client/src/app/+admin/overview/users/user-list/user-list.component.scss +++ b/client/src/app/+admin/overview/users/user-list/user-list.component.scss | |||
@@ -10,12 +10,6 @@ tr.banned > td { | |||
10 | background-color: lighten($color: $red, $amount: 40) !important; | 10 | background-color: lighten($color: $red, $amount: 40) !important; |
11 | } | 11 | } |
12 | 12 | ||
13 | .table-email { | ||
14 | @include disable-default-a-behaviour; | ||
15 | |||
16 | color: pvar(--mainForegroundColor); | ||
17 | } | ||
18 | |||
19 | .banned-info { | 13 | .banned-info { |
20 | font-style: italic; | 14 | font-style: italic; |
21 | } | 15 | } |
@@ -37,10 +31,6 @@ my-global-icon { | |||
37 | width: 18px; | 31 | width: 18px; |
38 | } | 32 | } |
39 | 33 | ||
40 | .chip { | ||
41 | @include chip; | ||
42 | } | ||
43 | |||
44 | .progress { | 34 | .progress { |
45 | @include progressbar($small: true); | 35 | @include progressbar($small: true); |
46 | 36 | ||
diff --git a/client/src/app/+admin/overview/users/user-list/user-list.component.ts b/client/src/app/+admin/overview/users/user-list/user-list.component.ts index 99987fdff..19420b748 100644 --- a/client/src/app/+admin/overview/users/user-list/user-list.component.ts +++ b/client/src/app/+admin/overview/users/user-list/user-list.component.ts | |||
@@ -22,7 +22,7 @@ type UserForList = User & { | |||
22 | templateUrl: './user-list.component.html', | 22 | templateUrl: './user-list.component.html', |
23 | styleUrls: [ './user-list.component.scss' ] | 23 | styleUrls: [ './user-list.component.scss' ] |
24 | }) | 24 | }) |
25 | export class UserListComponent extends RestTable implements OnInit { | 25 | export class UserListComponent extends RestTable <User> implements OnInit { |
26 | private static readonly LOCAL_STORAGE_SELECTED_COLUMNS_KEY = 'admin-user-list-selected-columns' | 26 | private static readonly LOCAL_STORAGE_SELECTED_COLUMNS_KEY = 'admin-user-list-selected-columns' |
27 | 27 | ||
28 | @ViewChild('userBanModal', { static: true }) userBanModal: UserBanModalComponent | 28 | @ViewChild('userBanModal', { static: true }) userBanModal: UserBanModalComponent |
@@ -35,8 +35,7 @@ export class UserListComponent extends RestTable implements OnInit { | |||
35 | 35 | ||
36 | highlightBannedUsers = false | 36 | highlightBannedUsers = false |
37 | 37 | ||
38 | selectedUsers: User[] = [] | 38 | bulkActions: DropdownAction<User[]>[][] = [] |
39 | bulkUserActions: DropdownAction<User[]>[][] = [] | ||
40 | columns: { id: string, label: string }[] | 39 | columns: { id: string, label: string }[] |
41 | 40 | ||
42 | inputFilters: AdvancedInputFilter[] = [ | 41 | inputFilters: AdvancedInputFilter[] = [ |
@@ -95,7 +94,7 @@ export class UserListComponent extends RestTable implements OnInit { | |||
95 | 94 | ||
96 | this.initialize() | 95 | this.initialize() |
97 | 96 | ||
98 | this.bulkUserActions = [ | 97 | this.bulkActions = [ |
99 | [ | 98 | [ |
100 | { | 99 | { |
101 | label: $localize`Delete`, | 100 | label: $localize`Delete`, |
@@ -249,7 +248,7 @@ export class UserListComponent extends RestTable implements OnInit { | |||
249 | const res = await this.confirmService.confirm(message, $localize`Delete`) | 248 | const res = await this.confirmService.confirm(message, $localize`Delete`) |
250 | if (res === false) return | 249 | if (res === false) return |
251 | 250 | ||
252 | this.userAdminService.removeUser(users) | 251 | this.userAdminService.removeUsers(users) |
253 | .subscribe({ | 252 | .subscribe({ |
254 | next: () => { | 253 | next: () => { |
255 | this.notifier.success( | 254 | this.notifier.success( |
@@ -284,13 +283,7 @@ export class UserListComponent extends RestTable implements OnInit { | |||
284 | }) | 283 | }) |
285 | } | 284 | } |
286 | 285 | ||
287 | isInSelectionMode () { | 286 | protected reloadDataInternal () { |
288 | return this.selectedUsers.length !== 0 | ||
289 | } | ||
290 | |||
291 | protected reloadData () { | ||
292 | this.selectedUsers = [] | ||
293 | |||
294 | this.userAdminService.getUsers({ | 287 | this.userAdminService.getUsers({ |
295 | pagination: this.pagination, | 288 | pagination: this.pagination, |
296 | sort: this.sort, | 289 | sort: this.sort, |
diff --git a/client/src/app/+admin/overview/videos/video-list.component.html b/client/src/app/+admin/overview/videos/video-list.component.html index a6cd2e257..5b8405ad9 100644 --- a/client/src/app/+admin/overview/videos/video-list.component.html +++ b/client/src/app/+admin/overview/videos/video-list.component.html | |||
@@ -6,7 +6,7 @@ | |||
6 | <p-table | 6 | <p-table |
7 | [value]="videos" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start" | 7 | [value]="videos" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start" |
8 | [rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order" dataKey="id" [resizableColumns]="true" | 8 | [rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order" dataKey="id" [resizableColumns]="true" |
9 | [(selection)]="selectedVideos" [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" [selectionPageOnly]="true" | 9 | [(selection)]="selectedRows" [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" [selectionPageOnly]="true" |
10 | [showCurrentPageReport]="true" i18n-currentPageReportTemplate | 10 | [showCurrentPageReport]="true" i18n-currentPageReportTemplate |
11 | currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} videos" | 11 | currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} videos" |
12 | [expandedRowKeys]="expandedRows" [ngClass]="{ loading: loading }" | 12 | [expandedRowKeys]="expandedRows" [ngClass]="{ loading: loading }" |
@@ -16,7 +16,7 @@ | |||
16 | <div class="left-buttons"> | 16 | <div class="left-buttons"> |
17 | <my-action-dropdown | 17 | <my-action-dropdown |
18 | *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange" | 18 | *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange" |
19 | [actions]="bulkVideoActions" [entry]="selectedVideos" | 19 | [actions]="bulkActions" [entry]="selectedRows" |
20 | > | 20 | > |
21 | </my-action-dropdown> | 21 | </my-action-dropdown> |
22 | </div> | 22 | </div> |
diff --git a/client/src/app/+admin/overview/videos/video-list.component.ts b/client/src/app/+admin/overview/videos/video-list.component.ts index 4d3e9873c..1ea295499 100644 --- a/client/src/app/+admin/overview/videos/video-list.component.ts +++ b/client/src/app/+admin/overview/videos/video-list.component.ts | |||
@@ -17,7 +17,7 @@ import { VideoAdminService } from './video-admin.service' | |||
17 | templateUrl: './video-list.component.html', | 17 | templateUrl: './video-list.component.html', |
18 | styleUrls: [ './video-list.component.scss' ] | 18 | styleUrls: [ './video-list.component.scss' ] |
19 | }) | 19 | }) |
20 | export class VideoListComponent extends RestTable implements OnInit { | 20 | export class VideoListComponent extends RestTable <Video> implements OnInit { |
21 | @ViewChild('videoBlockModal') videoBlockModal: VideoBlockComponent | 21 | @ViewChild('videoBlockModal') videoBlockModal: VideoBlockComponent |
22 | 22 | ||
23 | videos: Video[] = [] | 23 | videos: Video[] = [] |
@@ -26,9 +26,7 @@ export class VideoListComponent extends RestTable implements OnInit { | |||
26 | sort: SortMeta = { field: 'publishedAt', order: -1 } | 26 | sort: SortMeta = { field: 'publishedAt', order: -1 } |
27 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } | 27 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } |
28 | 28 | ||
29 | bulkVideoActions: DropdownAction<Video[]>[][] = [] | 29 | bulkActions: DropdownAction<Video[]>[][] = [] |
30 | |||
31 | selectedVideos: Video[] = [] | ||
32 | 30 | ||
33 | inputFilters: AdvancedInputFilter[] | 31 | inputFilters: AdvancedInputFilter[] |
34 | 32 | ||
@@ -72,7 +70,7 @@ export class VideoListComponent extends RestTable implements OnInit { | |||
72 | 70 | ||
73 | this.inputFilters = this.videoAdminService.buildAdminInputFilter() | 71 | this.inputFilters = this.videoAdminService.buildAdminInputFilter() |
74 | 72 | ||
75 | this.bulkVideoActions = [ | 73 | this.bulkActions = [ |
76 | [ | 74 | [ |
77 | { | 75 | { |
78 | label: $localize`Delete`, | 76 | label: $localize`Delete`, |
@@ -126,10 +124,6 @@ export class VideoListComponent extends RestTable implements OnInit { | |||
126 | return 'VideoListComponent' | 124 | return 'VideoListComponent' |
127 | } | 125 | } |
128 | 126 | ||
129 | isInSelectionMode () { | ||
130 | return this.selectedVideos.length !== 0 | ||
131 | } | ||
132 | |||
133 | getPrivacyBadgeClass (video: Video) { | 127 | getPrivacyBadgeClass (video: Video) { |
134 | if (video.privacy.id === VideoPrivacy.PUBLIC) return 'badge-green' | 128 | if (video.privacy.id === VideoPrivacy.PUBLIC) return 'badge-green' |
135 | 129 | ||
@@ -189,9 +183,23 @@ export class VideoListComponent extends RestTable implements OnInit { | |||
189 | return files.reduce((p, f) => p += f.size, 0) | 183 | return files.reduce((p, f) => p += f.size, 0) |
190 | } | 184 | } |
191 | 185 | ||
192 | reloadData () { | 186 | async removeVideoFile (video: Video, file: VideoFile, type: 'hls' | 'webtorrent') { |
193 | this.selectedVideos = [] | 187 | const message = $localize`Are you sure you want to delete this ${file.resolution.label} file?` |
188 | const res = await this.confirmService.confirm(message, $localize`Delete file`) | ||
189 | if (res === false) return | ||
190 | |||
191 | this.videoService.removeFile(video.uuid, file.id, type) | ||
192 | .subscribe({ | ||
193 | next: () => { | ||
194 | this.notifier.success($localize`File removed.`) | ||
195 | this.reloadData() | ||
196 | }, | ||
197 | |||
198 | error: err => this.notifier.error(err.message) | ||
199 | }) | ||
200 | } | ||
194 | 201 | ||
202 | protected reloadDataInternal () { | ||
195 | this.loading = true | 203 | this.loading = true |
196 | 204 | ||
197 | this.videoAdminService.getAdminVideos({ | 205 | this.videoAdminService.getAdminVideos({ |
@@ -209,22 +217,6 @@ export class VideoListComponent extends RestTable implements OnInit { | |||
209 | }) | 217 | }) |
210 | } | 218 | } |
211 | 219 | ||
212 | async removeVideoFile (video: Video, file: VideoFile, type: 'hls' | 'webtorrent') { | ||
213 | const message = $localize`Are you sure you want to delete this ${file.resolution.label} file?` | ||
214 | const res = await this.confirmService.confirm(message, $localize`Delete file`) | ||
215 | if (res === false) return | ||
216 | |||
217 | this.videoService.removeFile(video.uuid, file.id, type) | ||
218 | .subscribe({ | ||
219 | next: () => { | ||
220 | this.notifier.success($localize`File removed.`) | ||
221 | this.reloadData() | ||
222 | }, | ||
223 | |||
224 | error: err => this.notifier.error(err.message) | ||
225 | }) | ||
226 | } | ||
227 | |||
228 | private async removeVideos (videos: Video[]) { | 220 | private async removeVideos (videos: Video[]) { |
229 | const message = prepareIcu($localize`Are you sure you want to delete {count, plural, =1 {this video} other {these {count} videos}}?`)( | 221 | const message = prepareIcu($localize`Are you sure you want to delete {count, plural, =1 {this video} other {these {count} videos}}?`)( |
230 | { count: videos.length }, | 222 | { count: videos.length }, |
diff --git a/client/src/app/+admin/shared/shared-admin.module.ts b/client/src/app/+admin/shared/shared-admin.module.ts index bef7d54ef..a5c300d12 100644 --- a/client/src/app/+admin/shared/shared-admin.module.ts +++ b/client/src/app/+admin/shared/shared-admin.module.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
2 | import { SharedMainModule } from '../../shared/shared-main/shared-main.module' | 2 | import { SharedMainModule } from '../../shared/shared-main/shared-main.module' |
3 | import { UserEmailInfoComponent } from './user-email-info.component' | ||
3 | import { UserRealQuotaInfoComponent } from './user-real-quota-info.component' | 4 | import { UserRealQuotaInfoComponent } from './user-real-quota-info.component' |
4 | 5 | ||
5 | @NgModule({ | 6 | @NgModule({ |
@@ -8,11 +9,13 @@ import { UserRealQuotaInfoComponent } from './user-real-quota-info.component' | |||
8 | ], | 9 | ], |
9 | 10 | ||
10 | declarations: [ | 11 | declarations: [ |
11 | UserRealQuotaInfoComponent | 12 | UserRealQuotaInfoComponent, |
13 | UserEmailInfoComponent | ||
12 | ], | 14 | ], |
13 | 15 | ||
14 | exports: [ | 16 | exports: [ |
15 | UserRealQuotaInfoComponent | 17 | UserRealQuotaInfoComponent, |
18 | UserEmailInfoComponent | ||
16 | ], | 19 | ], |
17 | 20 | ||
18 | providers: [] | 21 | providers: [] |
diff --git a/client/src/app/+admin/shared/user-email-info.component.html b/client/src/app/+admin/shared/user-email-info.component.html new file mode 100644 index 000000000..244240619 --- /dev/null +++ b/client/src/app/+admin/shared/user-email-info.component.html | |||
@@ -0,0 +1,13 @@ | |||
1 | <ng-container> | ||
2 | <a [href]="'mailto:' + entry.email" [title]="getTitle()"> | ||
3 | <ng-container *ngIf="!requiresEmailVerification"> | ||
4 | {{ entry.email }} | ||
5 | </ng-container> | ||
6 | |||
7 | <ng-container *ngIf="requiresEmailVerification"> | ||
8 | <em *ngIf="!entry.emailVerified">? {{ entry.email }}</em> | ||
9 | |||
10 | <ng-container *ngIf="entry.emailVerified === true">✓ {{ entry.email }}</ng-container> | ||
11 | </ng-container> | ||
12 | </a> | ||
13 | </ng-container> | ||
diff --git a/client/src/app/+admin/shared/user-email-info.component.scss b/client/src/app/+admin/shared/user-email-info.component.scss new file mode 100644 index 000000000..d34947edd --- /dev/null +++ b/client/src/app/+admin/shared/user-email-info.component.scss | |||
@@ -0,0 +1,10 @@ | |||
1 | @use '_variables' as *; | ||
2 | @use '_mixins' as *; | ||
3 | |||
4 | a { | ||
5 | color: pvar(--mainForegroundColor); | ||
6 | |||
7 | &:hover { | ||
8 | text-decoration: underline; | ||
9 | } | ||
10 | } | ||
diff --git a/client/src/app/+admin/shared/user-email-info.component.ts b/client/src/app/+admin/shared/user-email-info.component.ts new file mode 100644 index 000000000..e33948b60 --- /dev/null +++ b/client/src/app/+admin/shared/user-email-info.component.ts | |||
@@ -0,0 +1,20 @@ | |||
1 | import { Component, Input } from '@angular/core' | ||
2 | import { User, UserRegistration } from '@shared/models/users' | ||
3 | |||
4 | @Component({ | ||
5 | selector: 'my-user-email-info', | ||
6 | templateUrl: './user-email-info.component.html', | ||
7 | styleUrls: [ './user-email-info.component.scss' ] | ||
8 | }) | ||
9 | export class UserEmailInfoComponent { | ||
10 | @Input() entry: User | UserRegistration | ||
11 | @Input() requiresEmailVerification: boolean | ||
12 | |||
13 | getTitle () { | ||
14 | if (this.entry.emailVerified) { | ||
15 | return $localize`User email has been verified` | ||
16 | } | ||
17 | |||
18 | return $localize`User email hasn't been verified` | ||
19 | } | ||
20 | } | ||
diff --git a/client/src/app/+admin/system/jobs/job.service.ts b/client/src/app/+admin/system/jobs/job.service.ts index ef8ddd3b4..031e2bad8 100644 --- a/client/src/app/+admin/system/jobs/job.service.ts +++ b/client/src/app/+admin/system/jobs/job.service.ts | |||
@@ -19,7 +19,7 @@ export class JobService { | |||
19 | private restExtractor: RestExtractor | 19 | private restExtractor: RestExtractor |
20 | ) {} | 20 | ) {} |
21 | 21 | ||
22 | getJobs (options: { | 22 | listJobs (options: { |
23 | jobState?: JobStateClient | 23 | jobState?: JobStateClient |
24 | jobType: JobTypeClient | 24 | jobType: JobTypeClient |
25 | pagination: RestPagination | 25 | pagination: RestPagination |
diff --git a/client/src/app/+admin/system/jobs/jobs.component.ts b/client/src/app/+admin/system/jobs/jobs.component.ts index b8f3c3a68..6e10c81ff 100644 --- a/client/src/app/+admin/system/jobs/jobs.component.ts +++ b/client/src/app/+admin/system/jobs/jobs.component.ts | |||
@@ -120,12 +120,12 @@ export class JobsComponent extends RestTable implements OnInit { | |||
120 | this.reloadData() | 120 | this.reloadData() |
121 | } | 121 | } |
122 | 122 | ||
123 | protected reloadData () { | 123 | protected reloadDataInternal () { |
124 | let jobState = this.jobState as JobState | 124 | let jobState = this.jobState as JobState |
125 | if (this.jobState === 'all') jobState = null | 125 | if (this.jobState === 'all') jobState = null |
126 | 126 | ||
127 | this.jobsService | 127 | this.jobsService |
128 | .getJobs({ | 128 | .listJobs({ |
129 | jobState, | 129 | jobState, |
130 | jobType: this.jobType, | 130 | jobType: this.jobType, |
131 | pagination: this.pagination, | 131 | pagination: this.pagination, |
diff --git a/client/src/app/+login/login.component.ts b/client/src/app/+login/login.component.ts index c1705807f..c03af38f2 100644 --- a/client/src/app/+login/login.component.ts +++ b/client/src/app/+login/login.component.ts | |||
@@ -1,3 +1,4 @@ | |||
1 | import { environment } from 'src/environments/environment' | ||
1 | import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core' | 2 | import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core' |
2 | import { ActivatedRoute, Router } from '@angular/router' | 3 | import { ActivatedRoute, Router } from '@angular/router' |
3 | import { AuthService, Notifier, RedirectService, SessionStorageService, UserService } from '@app/core' | 4 | import { AuthService, Notifier, RedirectService, SessionStorageService, UserService } from '@app/core' |
@@ -7,8 +8,8 @@ import { USER_OTP_TOKEN_VALIDATOR } from '@app/shared/form-validators/user-valid | |||
7 | import { FormReactive, FormReactiveService, InputTextComponent } from '@app/shared/shared-forms' | 8 | import { FormReactive, FormReactiveService, InputTextComponent } from '@app/shared/shared-forms' |
8 | import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance' | 9 | import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance' |
9 | import { NgbAccordion, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' | 10 | import { NgbAccordion, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' |
10 | import { PluginsManager } from '@root-helpers/plugins-manager' | 11 | import { getExternalAuthHref } from '@shared/core-utils' |
11 | import { RegisteredExternalAuthConfig, ServerConfig } from '@shared/models' | 12 | import { RegisteredExternalAuthConfig, ServerConfig, ServerErrorCode } from '@shared/models' |
12 | 13 | ||
13 | @Component({ | 14 | @Component({ |
14 | selector: 'my-login', | 15 | selector: 'my-login', |
@@ -119,7 +120,7 @@ export class LoginComponent extends FormReactive implements OnInit, AfterViewIni | |||
119 | } | 120 | } |
120 | 121 | ||
121 | getAuthHref (auth: RegisteredExternalAuthConfig) { | 122 | getAuthHref (auth: RegisteredExternalAuthConfig) { |
122 | return PluginsManager.getExternalAuthHref(auth) | 123 | return getExternalAuthHref(environment.apiUrl, auth) |
123 | } | 124 | } |
124 | 125 | ||
125 | login () { | 126 | login () { |
@@ -196,6 +197,8 @@ The link will expire within 1 hour.` | |||
196 | } | 197 | } |
197 | 198 | ||
198 | private handleError (err: any) { | 199 | private handleError (err: any) { |
200 | console.log(err) | ||
201 | |||
199 | if (this.authService.isOTPMissingError(err)) { | 202 | if (this.authService.isOTPMissingError(err)) { |
200 | this.otpStep = true | 203 | this.otpStep = true |
201 | 204 | ||
@@ -207,8 +210,26 @@ The link will expire within 1 hour.` | |||
207 | return | 210 | return |
208 | } | 211 | } |
209 | 212 | ||
210 | if (err.message.indexOf('credentials are invalid') !== -1) this.error = $localize`Incorrect username or password.` | 213 | if (err.message.includes('credentials are invalid')) { |
211 | else if (err.message.indexOf('blocked') !== -1) this.error = $localize`Your account is blocked.` | 214 | this.error = $localize`Incorrect username or password.` |
212 | else this.error = err.message | 215 | return |
216 | } | ||
217 | |||
218 | if (err.message.includes('blocked')) { | ||
219 | this.error = $localize`Your account is blocked.` | ||
220 | return | ||
221 | } | ||
222 | |||
223 | if (err.body?.code === ServerErrorCode.ACCOUNT_WAITING_FOR_APPROVAL) { | ||
224 | this.error = $localize`This account is awaiting approval by moderators.` | ||
225 | return | ||
226 | } | ||
227 | |||
228 | if (err.body?.code === ServerErrorCode.ACCOUNT_APPROVAL_REJECTED) { | ||
229 | this.error = $localize`Registration approval has been rejected for this account.` | ||
230 | return | ||
231 | } | ||
232 | |||
233 | this.error = err.message | ||
213 | } | 234 | } |
214 | } | 235 | } |
diff --git a/client/src/app/+my-library/my-ownership/my-ownership.component.scss b/client/src/app/+my-library/my-ownership/my-ownership.component.scss index a8450ff1b..98bed226d 100644 --- a/client/src/app/+my-library/my-ownership/my-ownership.component.scss +++ b/client/src/app/+my-library/my-ownership/my-ownership.component.scss | |||
@@ -2,10 +2,6 @@ | |||
2 | @use '_miniature' as *; | 2 | @use '_miniature' as *; |
3 | @use '_mixins' as *; | 3 | @use '_mixins' as *; |
4 | 4 | ||
5 | .chip { | ||
6 | @include chip; | ||
7 | } | ||
8 | |||
9 | .video-table-video { | 5 | .video-table-video { |
10 | display: inline-flex; | 6 | display: inline-flex; |
11 | 7 | ||
diff --git a/client/src/app/+my-library/my-ownership/my-ownership.component.ts b/client/src/app/+my-library/my-ownership/my-ownership.component.ts index 7ea940ceb..8d6a42dfb 100644 --- a/client/src/app/+my-library/my-ownership/my-ownership.component.ts +++ b/client/src/app/+my-library/my-ownership/my-ownership.component.ts | |||
@@ -59,7 +59,7 @@ export class MyOwnershipComponent extends RestTable implements OnInit { | |||
59 | }) | 59 | }) |
60 | } | 60 | } |
61 | 61 | ||
62 | protected reloadData () { | 62 | protected reloadDataInternal () { |
63 | return this.videoOwnershipService.getOwnershipChanges(this.pagination, this.sort) | 63 | return this.videoOwnershipService.getOwnershipChanges(this.pagination, this.sort) |
64 | .subscribe({ | 64 | .subscribe({ |
65 | next: resultList => { | 65 | next: resultList => { |
diff --git a/client/src/app/+my-library/my-video-channel-syncs/my-video-channel-syncs.component.ts b/client/src/app/+my-library/my-video-channel-syncs/my-video-channel-syncs.component.ts index d18e78201..74dbe222d 100644 --- a/client/src/app/+my-library/my-video-channel-syncs/my-video-channel-syncs.component.ts +++ b/client/src/app/+my-library/my-video-channel-syncs/my-video-channel-syncs.component.ts | |||
@@ -68,7 +68,7 @@ export class MyVideoChannelSyncsComponent extends RestTable implements OnInit { | |||
68 | ] | 68 | ] |
69 | } | 69 | } |
70 | 70 | ||
71 | protected reloadData () { | 71 | protected reloadDataInternal () { |
72 | this.error = undefined | 72 | this.error = undefined |
73 | 73 | ||
74 | this.authService.userInformationLoaded | 74 | this.authService.userInformationLoaded |
diff --git a/client/src/app/+my-library/my-video-imports/my-video-imports.component.ts b/client/src/app/+my-library/my-video-imports/my-video-imports.component.ts index 46d689bd1..7d82f62b9 100644 --- a/client/src/app/+my-library/my-video-imports/my-video-imports.component.ts +++ b/client/src/app/+my-library/my-video-imports/my-video-imports.component.ts | |||
@@ -90,7 +90,7 @@ export class MyVideoImportsComponent extends RestTable implements OnInit { | |||
90 | }) | 90 | }) |
91 | } | 91 | } |
92 | 92 | ||
93 | protected reloadData () { | 93 | protected reloadDataInternal () { |
94 | this.videoImportService.getMyVideoImports(this.pagination, this.sort, this.search) | 94 | this.videoImportService.getMyVideoImports(this.pagination, this.sort, this.search) |
95 | .subscribe({ | 95 | .subscribe({ |
96 | next: resultList => { | 96 | next: resultList => { |
diff --git a/client/src/app/+signup/+register/register.component.html b/client/src/app/+signup/+register/register.component.html index bafb96a49..86763e801 100644 --- a/client/src/app/+signup/+register/register.component.html +++ b/client/src/app/+signup/+register/register.component.html | |||
@@ -5,29 +5,34 @@ | |||
5 | </div> | 5 | </div> |
6 | 6 | ||
7 | <ng-container *ngIf="!signupDisabled"> | 7 | <ng-container *ngIf="!signupDisabled"> |
8 | <h1 i18n class="title-page-v2"> | 8 | <h1 class="title-page-v2"> |
9 | <strong class="underline-orange">{{ instanceName }}</strong> | 9 | <strong class="underline-orange">{{ instanceName }}</strong> |
10 | > | 10 | > |
11 | Create an account | 11 | <my-signup-label [requiresApproval]="requiresApproval"></my-signup-label> |
12 | </h1> | 12 | </h1> |
13 | 13 | ||
14 | <div class="register-content"> | 14 | <div class="register-content"> |
15 | <my-custom-stepper linear> | 15 | <my-custom-stepper linear> |
16 | 16 | ||
17 | <cdk-step i18n-label label="About" [editable]="!signupSuccess"> | 17 | <cdk-step i18n-label label="About" [editable]="!signupSuccess"> |
18 | <my-signup-step-title mascotImageName="about" i18n> | 18 | <my-signup-step-title mascotImageName="about"> |
19 | <strong>Create an account</strong> | 19 | <strong> |
20 | <div>on {{ instanceName }}</div> | 20 | <my-signup-label [requiresApproval]="requiresApproval"></my-signup-label> |
21 | </strong> | ||
22 | |||
23 | <div i18n>on {{ instanceName }}</div> | ||
21 | </my-signup-step-title> | 24 | </my-signup-step-title> |
22 | 25 | ||
23 | <my-register-step-about [videoUploadDisabled]="videoUploadDisabled"></my-register-step-about> | 26 | <my-register-step-about [requiresApproval]="requiresApproval" [videoUploadDisabled]="videoUploadDisabled"></my-register-step-about> |
24 | 27 | ||
25 | <div class="step-buttons"> | 28 | <div class="step-buttons"> |
26 | <a i18n class="skip-step underline-orange" routerLink="/login"> | 29 | <a i18n class="skip-step underline-orange" routerLink="/login"> |
27 | <strong>I already have an account</strong>, I log in | 30 | <strong>I already have an account</strong>, I log in |
28 | </a> | 31 | </a> |
29 | 32 | ||
30 | <button i18n cdkStepperNext>Create an account</button> | 33 | <button cdkStepperNext> |
34 | <my-signup-label [requiresApproval]="requiresApproval"></my-signup-label> | ||
35 | </button> | ||
31 | </div> | 36 | </div> |
32 | </cdk-step> | 37 | </cdk-step> |
33 | 38 | ||
@@ -44,8 +49,8 @@ | |||
44 | ></my-instance-about-accordion> | 49 | ></my-instance-about-accordion> |
45 | 50 | ||
46 | <my-register-step-terms | 51 | <my-register-step-terms |
47 | [hasCodeOfConduct]="!!aboutHtml.codeOfConduct" | 52 | [hasCodeOfConduct]="!!aboutHtml.codeOfConduct" [minimumAge]="minimumAge" [instanceName]="instanceName" |
48 | [minimumAge]="minimumAge" | 53 | [requiresApproval]="requiresApproval" |
49 | (formBuilt)="onTermsFormBuilt($event)" (termsClick)="onTermsClick()" (codeOfConductClick)="onCodeOfConductClick()" | 54 | (formBuilt)="onTermsFormBuilt($event)" (termsClick)="onTermsClick()" (codeOfConductClick)="onCodeOfConductClick()" |
50 | ></my-register-step-terms> | 55 | ></my-register-step-terms> |
51 | 56 | ||
@@ -94,14 +99,15 @@ | |||
94 | <div class="skip-step-description" i18n>You will be able to create a channel later</div> | 99 | <div class="skip-step-description" i18n>You will be able to create a channel later</div> |
95 | </div> | 100 | </div> |
96 | 101 | ||
97 | <button cdkStepperNext [disabled]="!formStepChannel || !formStepChannel.valid || hasSameChannelAndAccountNames()" (click)="signup()" i18n> | 102 | <button cdkStepperNext [disabled]="!formStepChannel || !formStepChannel.valid || hasSameChannelAndAccountNames()" (click)="signup()"> |
98 | Create my account | 103 | <my-signup-label [requiresApproval]="requiresApproval"></my-signup-label> |
99 | </button> | 104 | </button> |
100 | </div> | 105 | </div> |
101 | </cdk-step> | 106 | </cdk-step> |
102 | 107 | ||
103 | <cdk-step #lastStep i18n-label label="Done!" [editable]="false"> | 108 | <cdk-step #lastStep i18n-label label="Done!" [editable]="false"> |
104 | <div *ngIf="!signupSuccess && !signupError" class="done-loader"> | 109 | <!-- Account creation can be a little bit long so display a loader --> |
110 | <div *ngIf="!requiresApproval && !signupSuccess && !signupError" class="done-loader"> | ||
105 | <my-loader [loading]="true"></my-loader> | 111 | <my-loader [loading]="true"></my-loader> |
106 | 112 | ||
107 | <div i18n>PeerTube is creating your account...</div> | 113 | <div i18n>PeerTube is creating your account...</div> |
@@ -109,7 +115,10 @@ | |||
109 | 115 | ||
110 | <div *ngIf="signupError" class="alert alert-danger">{{ signupError }}</div> | 116 | <div *ngIf="signupError" class="alert alert-danger">{{ signupError }}</div> |
111 | 117 | ||
112 | <my-signup-success *ngIf="signupSuccess" [requiresEmailVerification]="requiresEmailVerification"></my-signup-success> | 118 | <my-signup-success-before-email |
119 | *ngIf="signupSuccess" | ||
120 | [requiresEmailVerification]="requiresEmailVerification" [requiresApproval]="requiresApproval" [instanceName]="instanceName" | ||
121 | ></my-signup-success-before-email> | ||
113 | 122 | ||
114 | <div *ngIf="signupError" class="steps-button"> | 123 | <div *ngIf="signupError" class="steps-button"> |
115 | <button cdkStepperPrevious>{{ defaultPreviousStepButtonLabel }}</button> | 124 | <button cdkStepperPrevious>{{ defaultPreviousStepButtonLabel }}</button> |
diff --git a/client/src/app/+signup/+register/register.component.ts b/client/src/app/+signup/+register/register.component.ts index 958770ebf..9259d902c 100644 --- a/client/src/app/+signup/+register/register.component.ts +++ b/client/src/app/+signup/+register/register.component.ts | |||
@@ -5,10 +5,10 @@ import { ActivatedRoute } from '@angular/router' | |||
5 | import { AuthService } from '@app/core' | 5 | import { AuthService } from '@app/core' |
6 | import { HooksService } from '@app/core/plugins/hooks.service' | 6 | import { HooksService } from '@app/core/plugins/hooks.service' |
7 | import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance' | 7 | import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance' |
8 | import { UserSignupService } from '@app/shared/shared-users' | ||
9 | import { NgbAccordion } from '@ng-bootstrap/ng-bootstrap' | 8 | import { NgbAccordion } from '@ng-bootstrap/ng-bootstrap' |
10 | import { UserRegister } from '@shared/models' | 9 | import { UserRegister } from '@shared/models' |
11 | import { ServerConfig } from '@shared/models/server' | 10 | import { ServerConfig } from '@shared/models/server' |
11 | import { SignupService } from '../shared/signup.service' | ||
12 | 12 | ||
13 | @Component({ | 13 | @Component({ |
14 | selector: 'my-register', | 14 | selector: 'my-register', |
@@ -53,7 +53,7 @@ export class RegisterComponent implements OnInit { | |||
53 | constructor ( | 53 | constructor ( |
54 | private route: ActivatedRoute, | 54 | private route: ActivatedRoute, |
55 | private authService: AuthService, | 55 | private authService: AuthService, |
56 | private userSignupService: UserSignupService, | 56 | private signupService: SignupService, |
57 | private hooks: HooksService | 57 | private hooks: HooksService |
58 | ) { } | 58 | ) { } |
59 | 59 | ||
@@ -61,6 +61,10 @@ export class RegisterComponent implements OnInit { | |||
61 | return this.serverConfig.signup.requiresEmailVerification | 61 | return this.serverConfig.signup.requiresEmailVerification |
62 | } | 62 | } |
63 | 63 | ||
64 | get requiresApproval () { | ||
65 | return this.serverConfig.signup.requiresApproval | ||
66 | } | ||
67 | |||
64 | get minimumAge () { | 68 | get minimumAge () { |
65 | return this.serverConfig.signup.minimumAge | 69 | return this.serverConfig.signup.minimumAge |
66 | } | 70 | } |
@@ -132,42 +136,49 @@ export class RegisterComponent implements OnInit { | |||
132 | skipChannelCreation () { | 136 | skipChannelCreation () { |
133 | this.formStepChannel.reset() | 137 | this.formStepChannel.reset() |
134 | this.lastStep.select() | 138 | this.lastStep.select() |
139 | |||
135 | this.signup() | 140 | this.signup() |
136 | } | 141 | } |
137 | 142 | ||
138 | async signup () { | 143 | async signup () { |
139 | this.signupError = undefined | 144 | this.signupError = undefined |
140 | 145 | ||
141 | const body: UserRegister = await this.hooks.wrapObject( | 146 | const termsForm = this.formStepTerms.value |
147 | const userForm = this.formStepUser.value | ||
148 | const channelForm = this.formStepChannel?.value | ||
149 | |||
150 | const channel = this.formStepChannel?.value?.name | ||
151 | ? { name: channelForm?.name, displayName: channelForm?.displayName } | ||
152 | : undefined | ||
153 | |||
154 | const body = await this.hooks.wrapObject( | ||
142 | { | 155 | { |
143 | ...this.formStepUser.value, | 156 | username: userForm.username, |
157 | password: userForm.password, | ||
158 | email: userForm.email, | ||
159 | displayName: userForm.displayName, | ||
160 | |||
161 | registrationReason: termsForm.registrationReason, | ||
144 | 162 | ||
145 | channel: this.formStepChannel?.value?.name | 163 | channel |
146 | ? this.formStepChannel.value | ||
147 | : undefined | ||
148 | }, | 164 | }, |
149 | 'signup', | 165 | 'signup', |
150 | 'filter:api.signup.registration.create.params' | 166 | 'filter:api.signup.registration.create.params' |
151 | ) | 167 | ) |
152 | 168 | ||
153 | this.userSignupService.signup(body).subscribe({ | 169 | const obs = this.requiresApproval |
170 | ? this.signupService.requestSignup(body) | ||
171 | : this.signupService.directSignup(body) | ||
172 | |||
173 | obs.subscribe({ | ||
154 | next: () => { | 174 | next: () => { |
155 | if (this.requiresEmailVerification) { | 175 | if (this.requiresEmailVerification || this.requiresApproval) { |
156 | this.signupSuccess = true | 176 | this.signupSuccess = true |
157 | return | 177 | return |
158 | } | 178 | } |
159 | 179 | ||
160 | // Auto login | 180 | // Auto login |
161 | this.authService.login({ username: body.username, password: body.password }) | 181 | this.autoLogin(body) |
162 | .subscribe({ | ||
163 | next: () => { | ||
164 | this.signupSuccess = true | ||
165 | }, | ||
166 | |||
167 | error: err => { | ||
168 | this.signupError = err.message | ||
169 | } | ||
170 | }) | ||
171 | }, | 182 | }, |
172 | 183 | ||
173 | error: err => { | 184 | error: err => { |
@@ -175,4 +186,17 @@ export class RegisterComponent implements OnInit { | |||
175 | } | 186 | } |
176 | }) | 187 | }) |
177 | } | 188 | } |
189 | |||
190 | private autoLogin (body: UserRegister) { | ||
191 | this.authService.login({ username: body.username, password: body.password }) | ||
192 | .subscribe({ | ||
193 | next: () => { | ||
194 | this.signupSuccess = true | ||
195 | }, | ||
196 | |||
197 | error: err => { | ||
198 | this.signupError = err.message | ||
199 | } | ||
200 | }) | ||
201 | } | ||
178 | } | 202 | } |
diff --git a/client/src/app/+signup/+register/shared/index.ts b/client/src/app/+signup/+register/shared/index.ts new file mode 100644 index 000000000..affb54bf4 --- /dev/null +++ b/client/src/app/+signup/+register/shared/index.ts | |||
@@ -0,0 +1 @@ | |||
export * from './register-validators' | |||
diff --git a/client/src/app/+signup/+register/shared/register-validators.ts b/client/src/app/+signup/+register/shared/register-validators.ts new file mode 100644 index 000000000..f14803b68 --- /dev/null +++ b/client/src/app/+signup/+register/shared/register-validators.ts | |||
@@ -0,0 +1,18 @@ | |||
1 | import { Validators } from '@angular/forms' | ||
2 | import { BuildFormValidator } from '@app/shared/form-validators' | ||
3 | |||
4 | export const REGISTER_TERMS_VALIDATOR: BuildFormValidator = { | ||
5 | VALIDATORS: [ Validators.requiredTrue ], | ||
6 | MESSAGES: { | ||
7 | required: $localize`You must agree with the instance terms in order to register on it.` | ||
8 | } | ||
9 | } | ||
10 | |||
11 | export const REGISTER_REASON_VALIDATOR: BuildFormValidator = { | ||
12 | VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ], | ||
13 | MESSAGES: { | ||
14 | required: $localize`Registration reason is required.`, | ||
15 | minlength: $localize`Registration reason must be at least 2 characters long.`, | ||
16 | maxlength: $localize`Registration reason cannot be more than 3000 characters long.` | ||
17 | } | ||
18 | } | ||
diff --git a/client/src/app/+signup/+register/steps/register-step-about.component.html b/client/src/app/+signup/+register/steps/register-step-about.component.html index 769fe3127..580e8a92c 100644 --- a/client/src/app/+signup/+register/steps/register-step-about.component.html +++ b/client/src/app/+signup/+register/steps/register-step-about.component.html | |||
@@ -13,6 +13,10 @@ | |||
13 | <li i18n>Have access to your <strong>watch history</strong></li> | 13 | <li i18n>Have access to your <strong>watch history</strong></li> |
14 | <li *ngIf="!videoUploadDisabled" i18n>Create your channel to <strong>publish videos</strong></li> | 14 | <li *ngIf="!videoUploadDisabled" i18n>Create your channel to <strong>publish videos</strong></li> |
15 | </ul> | 15 | </ul> |
16 | |||
17 | <p *ngIf="requiresApproval" i18n> | ||
18 | Moderators of {{ instanceName }} will have to approve your registration request once you have finished to fill the form. | ||
19 | </p> | ||
16 | </div> | 20 | </div> |
17 | 21 | ||
18 | <div> | 22 | <div> |
diff --git a/client/src/app/+signup/+register/steps/register-step-about.component.ts b/client/src/app/+signup/+register/steps/register-step-about.component.ts index 9a0941016..b176ffa59 100644 --- a/client/src/app/+signup/+register/steps/register-step-about.component.ts +++ b/client/src/app/+signup/+register/steps/register-step-about.component.ts | |||
@@ -7,6 +7,7 @@ import { ServerService } from '@app/core' | |||
7 | styleUrls: [ './register-step-about.component.scss' ] | 7 | styleUrls: [ './register-step-about.component.scss' ] |
8 | }) | 8 | }) |
9 | export class RegisterStepAboutComponent { | 9 | export class RegisterStepAboutComponent { |
10 | @Input() requiresApproval: boolean | ||
10 | @Input() videoUploadDisabled: boolean | 11 | @Input() videoUploadDisabled: boolean |
11 | 12 | ||
12 | constructor (private serverService: ServerService) { | 13 | constructor (private serverService: ServerService) { |
diff --git a/client/src/app/+signup/+register/steps/register-step-channel.component.ts b/client/src/app/+signup/+register/steps/register-step-channel.component.ts index df92c5145..478ca0177 100644 --- a/client/src/app/+signup/+register/steps/register-step-channel.component.ts +++ b/client/src/app/+signup/+register/steps/register-step-channel.component.ts | |||
@@ -2,9 +2,9 @@ import { concat, of } from 'rxjs' | |||
2 | import { pairwise } from 'rxjs/operators' | 2 | import { pairwise } from 'rxjs/operators' |
3 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' | 3 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' |
4 | import { FormGroup } from '@angular/forms' | 4 | import { FormGroup } from '@angular/forms' |
5 | import { SignupService } from '@app/+signup/shared/signup.service' | ||
5 | import { VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR, VIDEO_CHANNEL_NAME_VALIDATOR } from '@app/shared/form-validators/video-channel-validators' | 6 | import { VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR, VIDEO_CHANNEL_NAME_VALIDATOR } from '@app/shared/form-validators/video-channel-validators' |
6 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' | 7 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
7 | import { UserSignupService } from '@app/shared/shared-users' | ||
8 | 8 | ||
9 | @Component({ | 9 | @Component({ |
10 | selector: 'my-register-step-channel', | 10 | selector: 'my-register-step-channel', |
@@ -20,7 +20,7 @@ export class RegisterStepChannelComponent extends FormReactive implements OnInit | |||
20 | 20 | ||
21 | constructor ( | 21 | constructor ( |
22 | protected formReactiveService: FormReactiveService, | 22 | protected formReactiveService: FormReactiveService, |
23 | private userSignupService: UserSignupService | 23 | private signupService: SignupService |
24 | ) { | 24 | ) { |
25 | super() | 25 | super() |
26 | } | 26 | } |
@@ -51,7 +51,7 @@ export class RegisterStepChannelComponent extends FormReactive implements OnInit | |||
51 | private onDisplayNameChange (oldDisplayName: string, newDisplayName: string) { | 51 | private onDisplayNameChange (oldDisplayName: string, newDisplayName: string) { |
52 | const name = this.form.value['name'] || '' | 52 | const name = this.form.value['name'] || '' |
53 | 53 | ||
54 | const newName = this.userSignupService.getNewUsername(oldDisplayName, newDisplayName, name) | 54 | const newName = this.signupService.getNewUsername(oldDisplayName, newDisplayName, name) |
55 | this.form.patchValue({ name: newName }) | 55 | this.form.patchValue({ name: newName }) |
56 | } | 56 | } |
57 | } | 57 | } |
diff --git a/client/src/app/+signup/+register/steps/register-step-terms.component.html b/client/src/app/+signup/+register/steps/register-step-terms.component.html index cbfb32518..1d753a3f2 100644 --- a/client/src/app/+signup/+register/steps/register-step-terms.component.html +++ b/client/src/app/+signup/+register/steps/register-step-terms.component.html | |||
@@ -1,4 +1,16 @@ | |||
1 | <form role="form" [formGroup]="form"> | 1 | <form role="form" [formGroup]="form"> |
2 | |||
3 | <div *ngIf="requiresApproval" class="form-group"> | ||
4 | <label i18n for="registrationReason">Why do you want to join {{ instanceName }}?</label> | ||
5 | |||
6 | <textarea | ||
7 | id="registrationReason" formControlName="registrationReason" class="form-control" rows="4" | ||
8 | [ngClass]="{ 'input-error': formErrors['registrationReason'] }" | ||
9 | ></textarea> | ||
10 | |||
11 | <div *ngIf="formErrors.registrationReason" class="form-error">{{ formErrors.registrationReason }}</div> | ||
12 | </div> | ||
13 | |||
2 | <div class="form-group"> | 14 | <div class="form-group"> |
3 | <my-peertube-checkbox inputName="terms" formControlName="terms"> | 15 | <my-peertube-checkbox inputName="terms" formControlName="terms"> |
4 | <ng-template ptTemplate="label"> | 16 | <ng-template ptTemplate="label"> |
@@ -6,7 +18,7 @@ | |||
6 | I am at least {{ minimumAge }} years old and agree | 18 | I am at least {{ minimumAge }} years old and agree |
7 | to the <a class="link-orange" (click)="onTermsClick($event)" href='#'>Terms</a> | 19 | to the <a class="link-orange" (click)="onTermsClick($event)" href='#'>Terms</a> |
8 | <ng-container *ngIf="hasCodeOfConduct"> and to the <a class="link-orange" (click)="onCodeOfConductClick($event)" href='#'>Code of Conduct</a></ng-container> | 20 | <ng-container *ngIf="hasCodeOfConduct"> and to the <a class="link-orange" (click)="onCodeOfConductClick($event)" href='#'>Code of Conduct</a></ng-container> |
9 | of this instance | 21 | of {{ instanceName }} |
10 | </ng-container> | 22 | </ng-container> |
11 | </ng-template> | 23 | </ng-template> |
12 | </my-peertube-checkbox> | 24 | </my-peertube-checkbox> |
diff --git a/client/src/app/+signup/+register/steps/register-step-terms.component.ts b/client/src/app/+signup/+register/steps/register-step-terms.component.ts index 2df963b30..1b1fb49ee 100644 --- a/client/src/app/+signup/+register/steps/register-step-terms.component.ts +++ b/client/src/app/+signup/+register/steps/register-step-terms.component.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' | 1 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' |
2 | import { FormGroup } from '@angular/forms' | 2 | import { FormGroup } from '@angular/forms' |
3 | import { USER_TERMS_VALIDATOR } from '@app/shared/form-validators/user-validators' | ||
4 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' | 3 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
4 | import { REGISTER_REASON_VALIDATOR, REGISTER_TERMS_VALIDATOR } from '../shared' | ||
5 | 5 | ||
6 | @Component({ | 6 | @Component({ |
7 | selector: 'my-register-step-terms', | 7 | selector: 'my-register-step-terms', |
@@ -10,7 +10,9 @@ import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' | |||
10 | }) | 10 | }) |
11 | export class RegisterStepTermsComponent extends FormReactive implements OnInit { | 11 | export class RegisterStepTermsComponent extends FormReactive implements OnInit { |
12 | @Input() hasCodeOfConduct = false | 12 | @Input() hasCodeOfConduct = false |
13 | @Input() requiresApproval: boolean | ||
13 | @Input() minimumAge = 16 | 14 | @Input() minimumAge = 16 |
15 | @Input() instanceName: string | ||
14 | 16 | ||
15 | @Output() formBuilt = new EventEmitter<FormGroup>() | 17 | @Output() formBuilt = new EventEmitter<FormGroup>() |
16 | @Output() termsClick = new EventEmitter<void>() | 18 | @Output() termsClick = new EventEmitter<void>() |
@@ -28,7 +30,11 @@ export class RegisterStepTermsComponent extends FormReactive implements OnInit { | |||
28 | 30 | ||
29 | ngOnInit () { | 31 | ngOnInit () { |
30 | this.buildForm({ | 32 | this.buildForm({ |
31 | terms: USER_TERMS_VALIDATOR | 33 | terms: REGISTER_TERMS_VALIDATOR, |
34 | |||
35 | registrationReason: this.requiresApproval | ||
36 | ? REGISTER_REASON_VALIDATOR | ||
37 | : null | ||
32 | }) | 38 | }) |
33 | 39 | ||
34 | setTimeout(() => this.formBuilt.emit(this.form)) | 40 | setTimeout(() => this.formBuilt.emit(this.form)) |
diff --git a/client/src/app/+signup/+register/steps/register-step-user.component.ts b/client/src/app/+signup/+register/steps/register-step-user.component.ts index 822f8f5c5..0a5d2e437 100644 --- a/client/src/app/+signup/+register/steps/register-step-user.component.ts +++ b/client/src/app/+signup/+register/steps/register-step-user.component.ts | |||
@@ -2,6 +2,7 @@ import { concat, of } from 'rxjs' | |||
2 | import { pairwise } from 'rxjs/operators' | 2 | import { pairwise } from 'rxjs/operators' |
3 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' | 3 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' |
4 | import { FormGroup } from '@angular/forms' | 4 | import { FormGroup } from '@angular/forms' |
5 | import { SignupService } from '@app/+signup/shared/signup.service' | ||
5 | import { | 6 | import { |
6 | USER_DISPLAY_NAME_REQUIRED_VALIDATOR, | 7 | USER_DISPLAY_NAME_REQUIRED_VALIDATOR, |
7 | USER_EMAIL_VALIDATOR, | 8 | USER_EMAIL_VALIDATOR, |
@@ -9,7 +10,6 @@ import { | |||
9 | USER_USERNAME_VALIDATOR | 10 | USER_USERNAME_VALIDATOR |
10 | } from '@app/shared/form-validators/user-validators' | 11 | } from '@app/shared/form-validators/user-validators' |
11 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' | 12 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
12 | import { UserSignupService } from '@app/shared/shared-users' | ||
13 | 13 | ||
14 | @Component({ | 14 | @Component({ |
15 | selector: 'my-register-step-user', | 15 | selector: 'my-register-step-user', |
@@ -24,7 +24,7 @@ export class RegisterStepUserComponent extends FormReactive implements OnInit { | |||
24 | 24 | ||
25 | constructor ( | 25 | constructor ( |
26 | protected formReactiveService: FormReactiveService, | 26 | protected formReactiveService: FormReactiveService, |
27 | private userSignupService: UserSignupService | 27 | private signupService: SignupService |
28 | ) { | 28 | ) { |
29 | super() | 29 | super() |
30 | } | 30 | } |
@@ -57,7 +57,7 @@ export class RegisterStepUserComponent extends FormReactive implements OnInit { | |||
57 | private onDisplayNameChange (oldDisplayName: string, newDisplayName: string) { | 57 | private onDisplayNameChange (oldDisplayName: string, newDisplayName: string) { |
58 | const username = this.form.value['username'] || '' | 58 | const username = this.form.value['username'] || '' |
59 | 59 | ||
60 | const newUsername = this.userSignupService.getNewUsername(oldDisplayName, newDisplayName, username) | 60 | const newUsername = this.signupService.getNewUsername(oldDisplayName, newDisplayName, username) |
61 | this.form.patchValue({ username: newUsername }) | 61 | this.form.patchValue({ username: newUsername }) |
62 | } | 62 | } |
63 | } | 63 | } |
diff --git a/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts b/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts index 06905f678..75b599e0e 100644 --- a/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts +++ b/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts | |||
@@ -1,8 +1,8 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { SignupService } from '@app/+signup/shared/signup.service' | ||
2 | import { Notifier, RedirectService, ServerService } from '@app/core' | 3 | import { Notifier, RedirectService, ServerService } from '@app/core' |
3 | import { USER_EMAIL_VALIDATOR } from '@app/shared/form-validators/user-validators' | 4 | import { USER_EMAIL_VALIDATOR } from '@app/shared/form-validators/user-validators' |
4 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' | 5 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
5 | import { UserSignupService } from '@app/shared/shared-users' | ||
6 | 6 | ||
7 | @Component({ | 7 | @Component({ |
8 | selector: 'my-verify-account-ask-send-email', | 8 | selector: 'my-verify-account-ask-send-email', |
@@ -15,7 +15,7 @@ export class VerifyAccountAskSendEmailComponent extends FormReactive implements | |||
15 | 15 | ||
16 | constructor ( | 16 | constructor ( |
17 | protected formReactiveService: FormReactiveService, | 17 | protected formReactiveService: FormReactiveService, |
18 | private userSignupService: UserSignupService, | 18 | private signupService: SignupService, |
19 | private serverService: ServerService, | 19 | private serverService: ServerService, |
20 | private notifier: Notifier, | 20 | private notifier: Notifier, |
21 | private redirectService: RedirectService | 21 | private redirectService: RedirectService |
@@ -34,7 +34,7 @@ export class VerifyAccountAskSendEmailComponent extends FormReactive implements | |||
34 | 34 | ||
35 | askSendVerifyEmail () { | 35 | askSendVerifyEmail () { |
36 | const email = this.form.value['verify-email-email'] | 36 | const email = this.form.value['verify-email-email'] |
37 | this.userSignupService.askSendVerifyEmail(email) | 37 | this.signupService.askSendVerifyEmail(email) |
38 | .subscribe({ | 38 | .subscribe({ |
39 | next: () => { | 39 | next: () => { |
40 | this.notifier.success($localize`An email with verification link will be sent to ${email}.`) | 40 | this.notifier.success($localize`An email with verification link will be sent to ${email}.`) |
diff --git a/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.html b/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.html index 122f3c28c..8c8b1098e 100644 --- a/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.html +++ b/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.html | |||
@@ -1,14 +1,19 @@ | |||
1 | <div class="margin-content"> | 1 | <div *ngIf="loaded" class="margin-content"> |
2 | <h1 i18n class="title-page">Verify account email confirmation</h1> | 2 | <h1 i18n class="title-page">Verify email</h1> |
3 | 3 | ||
4 | <my-signup-success i18n *ngIf="!isPendingEmail && success" [requiresEmailVerification]="false"> | 4 | <my-signup-success-after-email |
5 | </my-signup-success> | 5 | *ngIf="displaySignupSuccess()" |
6 | [requiresApproval]="isRegistrationRequest() && requiresApproval" | ||
7 | > | ||
8 | </my-signup-success-after-email> | ||
6 | 9 | ||
7 | <div i18n class="alert alert-success" *ngIf="isPendingEmail && success">Email updated.</div> | 10 | <div i18n class="alert alert-success" *ngIf="!isRegistrationRequest() && isPendingEmail && success">Email updated.</div> |
8 | 11 | ||
9 | <div class="alert alert-danger" *ngIf="failed"> | 12 | <div class="alert alert-danger" *ngIf="failed"> |
10 | <span i18n>An error occurred.</span> | 13 | <span i18n>An error occurred.</span> |
11 | 14 | ||
12 | <a i18n class="ms-1 link-orange" routerLink="/verify-account/ask-send-email" [queryParams]="{ isPendingEmail: isPendingEmail }">Request new verification email</a> | 15 | <a i18n class="ms-1 link-orange" routerLink="/verify-account/ask-send-email"> |
16 | Request a new verification email | ||
17 | </a> | ||
13 | </div> | 18 | </div> |
14 | </div> | 19 | </div> |
diff --git a/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.ts b/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.ts index 88efce4a1..faf663391 100644 --- a/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.ts +++ b/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { ActivatedRoute } from '@angular/router' | 2 | import { ActivatedRoute } from '@angular/router' |
3 | import { AuthService, Notifier } from '@app/core' | 3 | import { SignupService } from '@app/+signup/shared/signup.service' |
4 | import { UserSignupService } from '@app/shared/shared-users' | 4 | import { AuthService, Notifier, ServerService } from '@app/core' |
5 | 5 | ||
6 | @Component({ | 6 | @Component({ |
7 | selector: 'my-verify-account-email', | 7 | selector: 'my-verify-account-email', |
@@ -13,32 +13,82 @@ export class VerifyAccountEmailComponent implements OnInit { | |||
13 | failed = false | 13 | failed = false |
14 | isPendingEmail = false | 14 | isPendingEmail = false |
15 | 15 | ||
16 | requiresApproval: boolean | ||
17 | loaded = false | ||
18 | |||
16 | private userId: number | 19 | private userId: number |
20 | private registrationId: number | ||
17 | private verificationString: string | 21 | private verificationString: string |
18 | 22 | ||
19 | constructor ( | 23 | constructor ( |
20 | private userSignupService: UserSignupService, | 24 | private signupService: SignupService, |
25 | private server: ServerService, | ||
21 | private authService: AuthService, | 26 | private authService: AuthService, |
22 | private notifier: Notifier, | 27 | private notifier: Notifier, |
23 | private route: ActivatedRoute | 28 | private route: ActivatedRoute |
24 | ) { | 29 | ) { |
25 | } | 30 | } |
26 | 31 | ||
32 | get instanceName () { | ||
33 | return this.server.getHTMLConfig().instance.name | ||
34 | } | ||
35 | |||
27 | ngOnInit () { | 36 | ngOnInit () { |
28 | const queryParams = this.route.snapshot.queryParams | 37 | const queryParams = this.route.snapshot.queryParams |
38 | |||
39 | this.server.getConfig().subscribe(config => { | ||
40 | this.requiresApproval = config.signup.requiresApproval | ||
41 | |||
42 | this.loaded = true | ||
43 | }) | ||
44 | |||
29 | this.userId = queryParams['userId'] | 45 | this.userId = queryParams['userId'] |
46 | this.registrationId = queryParams['registrationId'] | ||
47 | |||
30 | this.verificationString = queryParams['verificationString'] | 48 | this.verificationString = queryParams['verificationString'] |
49 | |||
31 | this.isPendingEmail = queryParams['isPendingEmail'] === 'true' | 50 | this.isPendingEmail = queryParams['isPendingEmail'] === 'true' |
32 | 51 | ||
33 | if (!this.userId || !this.verificationString) { | 52 | if (!this.verificationString) { |
34 | this.notifier.error($localize`Unable to find user id or verification string.`) | 53 | this.notifier.error($localize`Unable to find verification string in URL query.`) |
35 | } else { | 54 | return |
36 | this.verifyEmail() | 55 | } |
56 | |||
57 | if (!this.userId && !this.registrationId) { | ||
58 | this.notifier.error($localize`Unable to find user id or registration id in URL query.`) | ||
59 | return | ||
37 | } | 60 | } |
61 | |||
62 | this.verifyEmail() | ||
63 | } | ||
64 | |||
65 | isRegistrationRequest () { | ||
66 | return !!this.registrationId | ||
67 | } | ||
68 | |||
69 | displaySignupSuccess () { | ||
70 | if (!this.success) return false | ||
71 | if (!this.isRegistrationRequest() && this.isPendingEmail) return false | ||
72 | |||
73 | return true | ||
38 | } | 74 | } |
39 | 75 | ||
40 | verifyEmail () { | 76 | verifyEmail () { |
41 | this.userSignupService.verifyEmail(this.userId, this.verificationString, this.isPendingEmail) | 77 | if (this.isRegistrationRequest()) { |
78 | return this.verifyRegistrationEmail() | ||
79 | } | ||
80 | |||
81 | return this.verifyUserEmail() | ||
82 | } | ||
83 | |||
84 | private verifyUserEmail () { | ||
85 | const options = { | ||
86 | userId: this.userId, | ||
87 | verificationString: this.verificationString, | ||
88 | isPendingEmail: this.isPendingEmail | ||
89 | } | ||
90 | |||
91 | this.signupService.verifyUserEmail(options) | ||
42 | .subscribe({ | 92 | .subscribe({ |
43 | next: () => { | 93 | next: () => { |
44 | if (this.authService.isLoggedIn()) { | 94 | if (this.authService.isLoggedIn()) { |
@@ -55,4 +105,24 @@ export class VerifyAccountEmailComponent implements OnInit { | |||
55 | } | 105 | } |
56 | }) | 106 | }) |
57 | } | 107 | } |
108 | |||
109 | private verifyRegistrationEmail () { | ||
110 | const options = { | ||
111 | registrationId: this.registrationId, | ||
112 | verificationString: this.verificationString | ||
113 | } | ||
114 | |||
115 | this.signupService.verifyRegistrationEmail(options) | ||
116 | .subscribe({ | ||
117 | next: () => { | ||
118 | this.success = true | ||
119 | }, | ||
120 | |||
121 | error: err => { | ||
122 | this.failed = true | ||
123 | |||
124 | this.notifier.error(err.message) | ||
125 | } | ||
126 | }) | ||
127 | } | ||
58 | } | 128 | } |
diff --git a/client/src/app/+signup/shared/shared-signup.module.ts b/client/src/app/+signup/shared/shared-signup.module.ts index 0aa08f3e2..0600f0af8 100644 --- a/client/src/app/+signup/shared/shared-signup.module.ts +++ b/client/src/app/+signup/shared/shared-signup.module.ts | |||
@@ -5,7 +5,9 @@ import { SharedMainModule } from '@app/shared/shared-main' | |||
5 | import { SharedUsersModule } from '@app/shared/shared-users' | 5 | import { SharedUsersModule } from '@app/shared/shared-users' |
6 | import { SignupMascotComponent } from './signup-mascot.component' | 6 | import { SignupMascotComponent } from './signup-mascot.component' |
7 | import { SignupStepTitleComponent } from './signup-step-title.component' | 7 | import { SignupStepTitleComponent } from './signup-step-title.component' |
8 | import { SignupSuccessComponent } from './signup-success.component' | 8 | import { SignupSuccessBeforeEmailComponent } from './signup-success-before-email.component' |
9 | import { SignupSuccessAfterEmailComponent } from './signup-success-after-email.component' | ||
10 | import { SignupService } from './signup.service' | ||
9 | 11 | ||
10 | @NgModule({ | 12 | @NgModule({ |
11 | imports: [ | 13 | imports: [ |
@@ -16,7 +18,8 @@ import { SignupSuccessComponent } from './signup-success.component' | |||
16 | ], | 18 | ], |
17 | 19 | ||
18 | declarations: [ | 20 | declarations: [ |
19 | SignupSuccessComponent, | 21 | SignupSuccessBeforeEmailComponent, |
22 | SignupSuccessAfterEmailComponent, | ||
20 | SignupStepTitleComponent, | 23 | SignupStepTitleComponent, |
21 | SignupMascotComponent | 24 | SignupMascotComponent |
22 | ], | 25 | ], |
@@ -26,12 +29,14 @@ import { SignupSuccessComponent } from './signup-success.component' | |||
26 | SharedFormModule, | 29 | SharedFormModule, |
27 | SharedGlobalIconModule, | 30 | SharedGlobalIconModule, |
28 | 31 | ||
29 | SignupSuccessComponent, | 32 | SignupSuccessBeforeEmailComponent, |
33 | SignupSuccessAfterEmailComponent, | ||
30 | SignupStepTitleComponent, | 34 | SignupStepTitleComponent, |
31 | SignupMascotComponent | 35 | SignupMascotComponent |
32 | ], | 36 | ], |
33 | 37 | ||
34 | providers: [ | 38 | providers: [ |
39 | SignupService | ||
35 | ] | 40 | ] |
36 | }) | 41 | }) |
37 | export class SharedSignupModule { } | 42 | export class SharedSignupModule { } |
diff --git a/client/src/app/+signup/shared/signup-success-after-email.component.html b/client/src/app/+signup/shared/signup-success-after-email.component.html new file mode 100644 index 000000000..1c3536ada --- /dev/null +++ b/client/src/app/+signup/shared/signup-success-after-email.component.html | |||
@@ -0,0 +1,21 @@ | |||
1 | <my-signup-step-title mascotImageName="success"> | ||
2 | <strong i18n>Email verified!</strong> | ||
3 | </my-signup-step-title> | ||
4 | |||
5 | <div class="alert pt-alert-primary"> | ||
6 | <ng-container *ngIf="requiresApproval"> | ||
7 | <p i18n>Your email has been verified and your account request has been sent!</p> | ||
8 | |||
9 | <p i18n> | ||
10 | A moderator will check your registration request soon and you'll receive an email when it will be accepted or rejected. | ||
11 | </p> | ||
12 | </ng-container> | ||
13 | |||
14 | <ng-container *ngIf="!requiresApproval"> | ||
15 | <p i18n>Your email has been verified and your account has been created!</p> | ||
16 | |||
17 | <p i18n> | ||
18 | If you need help to use PeerTube, you can have a look at the <a class="link-orange" href="https://docs.joinpeertube.org/use-setup-account" target="_blank" rel="noopener noreferrer">documentation</a>. | ||
19 | </p> | ||
20 | </ng-container> | ||
21 | </div> | ||
diff --git a/client/src/app/+signup/shared/signup-success-after-email.component.ts b/client/src/app/+signup/shared/signup-success-after-email.component.ts new file mode 100644 index 000000000..3d72fdae9 --- /dev/null +++ b/client/src/app/+signup/shared/signup-success-after-email.component.ts | |||
@@ -0,0 +1,10 @@ | |||
1 | import { Component, Input } from '@angular/core' | ||
2 | |||
3 | @Component({ | ||
4 | selector: 'my-signup-success-after-email', | ||
5 | templateUrl: './signup-success-after-email.component.html', | ||
6 | styleUrls: [ './signup-success.component.scss' ] | ||
7 | }) | ||
8 | export class SignupSuccessAfterEmailComponent { | ||
9 | @Input() requiresApproval: boolean | ||
10 | } | ||
diff --git a/client/src/app/+signup/shared/signup-success-before-email.component.html b/client/src/app/+signup/shared/signup-success-before-email.component.html new file mode 100644 index 000000000..b9668ee82 --- /dev/null +++ b/client/src/app/+signup/shared/signup-success-before-email.component.html | |||
@@ -0,0 +1,35 @@ | |||
1 | <my-signup-step-title mascotImageName="success"> | ||
2 | <ng-container *ngIf="requiresApproval"> | ||
3 | <strong i18n>Account request sent</strong> | ||
4 | </ng-container> | ||
5 | |||
6 | <ng-container *ngIf="!requiresApproval" i18n> | ||
7 | <strong>Welcome</strong> | ||
8 | <div>on {{ instanceName }}</div> | ||
9 | </ng-container> | ||
10 | </my-signup-step-title> | ||
11 | |||
12 | <div class="alert pt-alert-primary"> | ||
13 | <p *ngIf="requiresApproval" i18n>Your account request has been sent!</p> | ||
14 | <p *ngIf="!requiresApproval" i18n>Your account has been created!</p> | ||
15 | |||
16 | <ng-container *ngIf="requiresEmailVerification"> | ||
17 | <p i18n *ngIf="requiresApproval"> | ||
18 | <strong>Check your emails</strong> to validate your account and complete your registration request. | ||
19 | </p> | ||
20 | |||
21 | <p i18n *ngIf="!requiresApproval"> | ||
22 | <strong>Check your emails</strong> to validate your account and complete your registration. | ||
23 | </p> | ||
24 | </ng-container> | ||
25 | |||
26 | <ng-container *ngIf="!requiresEmailVerification"> | ||
27 | <p i18n *ngIf="requiresApproval"> | ||
28 | A moderator will check your registration request soon and you'll receive an email when it will be accepted or rejected. | ||
29 | </p> | ||
30 | |||
31 | <p *ngIf="!requiresApproval" i18n> | ||
32 | If you need help to use PeerTube, you can have a look at the <a class="link-orange" href="https://docs.joinpeertube.org/use-setup-account" target="_blank" rel="noopener noreferrer">documentation</a>. | ||
33 | </p> | ||
34 | </ng-container> | ||
35 | </div> | ||
diff --git a/client/src/app/+signup/shared/signup-success-before-email.component.ts b/client/src/app/+signup/shared/signup-success-before-email.component.ts new file mode 100644 index 000000000..d72462340 --- /dev/null +++ b/client/src/app/+signup/shared/signup-success-before-email.component.ts | |||
@@ -0,0 +1,12 @@ | |||
1 | import { Component, Input } from '@angular/core' | ||
2 | |||
3 | @Component({ | ||
4 | selector: 'my-signup-success-before-email', | ||
5 | templateUrl: './signup-success-before-email.component.html', | ||
6 | styleUrls: [ './signup-success.component.scss' ] | ||
7 | }) | ||
8 | export class SignupSuccessBeforeEmailComponent { | ||
9 | @Input() requiresApproval: boolean | ||
10 | @Input() requiresEmailVerification: boolean | ||
11 | @Input() instanceName: string | ||
12 | } | ||
diff --git a/client/src/app/+signup/shared/signup-success.component.html b/client/src/app/+signup/shared/signup-success.component.html deleted file mode 100644 index c14889c72..000000000 --- a/client/src/app/+signup/shared/signup-success.component.html +++ /dev/null | |||
@@ -1,22 +0,0 @@ | |||
1 | <my-signup-step-title mascotImageName="success" i18n> | ||
2 | <strong>Welcome</strong> | ||
3 | <div>on {{ instanceName }}</div> | ||
4 | </my-signup-step-title> | ||
5 | |||
6 | <div class="alert pt-alert-primary"> | ||
7 | <p i18n>Your account has been created!</p> | ||
8 | |||
9 | <p i18n *ngIf="requiresEmailVerification"> | ||
10 | <strong>Check your emails</strong> to validate your account and complete your inscription. | ||
11 | </p> | ||
12 | |||
13 | <ng-container *ngIf="!requiresEmailVerification"> | ||
14 | <p i18n> | ||
15 | If you need help to use PeerTube, you can have a look at the <a class="link-orange" href="https://docs.joinpeertube.org/use-setup-account" target="_blank" rel="noopener noreferrer">documentation</a>. | ||
16 | </p> | ||
17 | |||
18 | <p i18n> | ||
19 | To help moderators and other users to know <strong>who you are</strong>, don't forget to <a class="link-orange" routerLink="/my-account/settings">set up your account profile</a> by adding an <strong>avatar</strong> and a <strong>description</strong>. | ||
20 | </p> | ||
21 | </ng-container> | ||
22 | </div> | ||
diff --git a/client/src/app/+signup/shared/signup-success.component.ts b/client/src/app/+signup/shared/signup-success.component.ts deleted file mode 100644 index a03f3819d..000000000 --- a/client/src/app/+signup/shared/signup-success.component.ts +++ /dev/null | |||
@@ -1,19 +0,0 @@ | |||
1 | import { Component, Input } from '@angular/core' | ||
2 | import { ServerService } from '@app/core' | ||
3 | |||
4 | @Component({ | ||
5 | selector: 'my-signup-success', | ||
6 | templateUrl: './signup-success.component.html', | ||
7 | styleUrls: [ './signup-success.component.scss' ] | ||
8 | }) | ||
9 | export class SignupSuccessComponent { | ||
10 | @Input() requiresEmailVerification: boolean | ||
11 | |||
12 | constructor (private serverService: ServerService) { | ||
13 | |||
14 | } | ||
15 | |||
16 | get instanceName () { | ||
17 | return this.serverService.getHTMLConfig().instance.name | ||
18 | } | ||
19 | } | ||
diff --git a/client/src/app/shared/shared-users/user-signup.service.ts b/client/src/app/+signup/shared/signup.service.ts index 46fe34af1..f647298be 100644 --- a/client/src/app/shared/shared-users/user-signup.service.ts +++ b/client/src/app/+signup/shared/signup.service.ts | |||
@@ -2,17 +2,18 @@ import { catchError, tap } from 'rxjs/operators' | |||
2 | import { HttpClient } from '@angular/common/http' | 2 | import { HttpClient } from '@angular/common/http' |
3 | import { Injectable } from '@angular/core' | 3 | import { Injectable } from '@angular/core' |
4 | import { RestExtractor, UserService } from '@app/core' | 4 | import { RestExtractor, UserService } from '@app/core' |
5 | import { UserRegister } from '@shared/models' | 5 | import { UserRegister, UserRegistrationRequest } from '@shared/models' |
6 | 6 | ||
7 | @Injectable() | 7 | @Injectable() |
8 | export class UserSignupService { | 8 | export class SignupService { |
9 | |||
9 | constructor ( | 10 | constructor ( |
10 | private authHttp: HttpClient, | 11 | private authHttp: HttpClient, |
11 | private restExtractor: RestExtractor, | 12 | private restExtractor: RestExtractor, |
12 | private userService: UserService | 13 | private userService: UserService |
13 | ) { } | 14 | ) { } |
14 | 15 | ||
15 | signup (userCreate: UserRegister) { | 16 | directSignup (userCreate: UserRegister) { |
16 | return this.authHttp.post(UserService.BASE_USERS_URL + 'register', userCreate) | 17 | return this.authHttp.post(UserService.BASE_USERS_URL + 'register', userCreate) |
17 | .pipe( | 18 | .pipe( |
18 | tap(() => this.userService.setSignupInThisSession(true)), | 19 | tap(() => this.userService.setSignupInThisSession(true)), |
@@ -20,8 +21,21 @@ export class UserSignupService { | |||
20 | ) | 21 | ) |
21 | } | 22 | } |
22 | 23 | ||
23 | verifyEmail (userId: number, verificationString: string, isPendingEmail: boolean) { | 24 | requestSignup (userCreate: UserRegistrationRequest) { |
24 | const url = `${UserService.BASE_USERS_URL}/${userId}/verify-email` | 25 | return this.authHttp.post(UserService.BASE_USERS_URL + 'registrations/request', userCreate) |
26 | .pipe(catchError(err => this.restExtractor.handleError(err))) | ||
27 | } | ||
28 | |||
29 | // --------------------------------------------------------------------------- | ||
30 | |||
31 | verifyUserEmail (options: { | ||
32 | userId: number | ||
33 | verificationString: string | ||
34 | isPendingEmail: boolean | ||
35 | }) { | ||
36 | const { userId, verificationString, isPendingEmail } = options | ||
37 | |||
38 | const url = `${UserService.BASE_USERS_URL}${userId}/verify-email` | ||
25 | const body = { | 39 | const body = { |
26 | verificationString, | 40 | verificationString, |
27 | isPendingEmail | 41 | isPendingEmail |
@@ -31,13 +45,28 @@ export class UserSignupService { | |||
31 | .pipe(catchError(res => this.restExtractor.handleError(res))) | 45 | .pipe(catchError(res => this.restExtractor.handleError(res))) |
32 | } | 46 | } |
33 | 47 | ||
48 | verifyRegistrationEmail (options: { | ||
49 | registrationId: number | ||
50 | verificationString: string | ||
51 | }) { | ||
52 | const { registrationId, verificationString } = options | ||
53 | |||
54 | const url = `${UserService.BASE_USERS_URL}registrations/${registrationId}/verify-email` | ||
55 | const body = { verificationString } | ||
56 | |||
57 | return this.authHttp.post(url, body) | ||
58 | .pipe(catchError(res => this.restExtractor.handleError(res))) | ||
59 | } | ||
60 | |||
34 | askSendVerifyEmail (email: string) { | 61 | askSendVerifyEmail (email: string) { |
35 | const url = UserService.BASE_USERS_URL + '/ask-send-verify-email' | 62 | const url = UserService.BASE_USERS_URL + 'ask-send-verify-email' |
36 | 63 | ||
37 | return this.authHttp.post(url, { email }) | 64 | return this.authHttp.post(url, { email }) |
38 | .pipe(catchError(err => this.restExtractor.handleError(err))) | 65 | .pipe(catchError(err => this.restExtractor.handleError(err))) |
39 | } | 66 | } |
40 | 67 | ||
68 | // --------------------------------------------------------------------------- | ||
69 | |||
41 | getNewUsername (oldDisplayName: string, newDisplayName: string, currentUsername: string) { | 70 | getNewUsername (oldDisplayName: string, newDisplayName: string, currentUsername: string) { |
42 | // Don't update display name, the user seems to have changed it | 71 | // Don't update display name, the user seems to have changed it |
43 | if (this.displayNameToUsername(oldDisplayName) !== currentUsername) return currentUsername | 72 | if (this.displayNameToUsername(oldDisplayName) !== currentUsername) return currentUsername |
diff --git a/client/src/app/+videos/+video-watch/video-watch.component.ts b/client/src/app/+videos/+video-watch/video-watch.component.ts index 94853423b..84548de97 100644 --- a/client/src/app/+videos/+video-watch/video-watch.component.ts +++ b/client/src/app/+videos/+video-watch/video-watch.component.ts | |||
@@ -133,8 +133,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
133 | this.loadRouteParams() | 133 | this.loadRouteParams() |
134 | this.loadRouteQuery() | 134 | this.loadRouteQuery() |
135 | 135 | ||
136 | this.initHotkeys() | ||
137 | |||
138 | this.theaterEnabled = getStoredTheater() | 136 | this.theaterEnabled = getStoredTheater() |
139 | 137 | ||
140 | this.hooks.runAction('action:video-watch.init', 'video-watch') | 138 | this.hooks.runAction('action:video-watch.init', 'video-watch') |
@@ -295,6 +293,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
295 | subtitle: queryParams.subtitle, | 293 | subtitle: queryParams.subtitle, |
296 | 294 | ||
297 | playerMode: queryParams.mode, | 295 | playerMode: queryParams.mode, |
296 | playbackRate: queryParams.playbackRate, | ||
298 | peertubeLink: false | 297 | peertubeLink: false |
299 | } | 298 | } |
300 | 299 | ||
@@ -406,6 +405,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
406 | if (res === false) return this.location.back() | 405 | if (res === false) return this.location.back() |
407 | } | 406 | } |
408 | 407 | ||
408 | this.buildHotkeysHelp(video) | ||
409 | |||
409 | this.buildPlayer({ urlOptions, loggedInOrAnonymousUser, forceAutoplay }) | 410 | this.buildPlayer({ urlOptions, loggedInOrAnonymousUser, forceAutoplay }) |
410 | .catch(err => logger.error('Cannot build the player', err)) | 411 | .catch(err => logger.error('Cannot build the player', err)) |
411 | 412 | ||
@@ -657,6 +658,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
657 | muted: urlOptions.muted, | 658 | muted: urlOptions.muted, |
658 | loop: urlOptions.loop, | 659 | loop: urlOptions.loop, |
659 | subtitle: urlOptions.subtitle, | 660 | subtitle: urlOptions.subtitle, |
661 | playbackRate: urlOptions.playbackRate, | ||
660 | 662 | ||
661 | peertubeLink: urlOptions.peertubeLink, | 663 | peertubeLink: urlOptions.peertubeLink, |
662 | 664 | ||
@@ -785,33 +787,43 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
785 | this.video.viewers = newViewers | 787 | this.video.viewers = newViewers |
786 | } | 788 | } |
787 | 789 | ||
788 | private initHotkeys () { | 790 | private buildHotkeysHelp (video: Video) { |
791 | if (this.hotkeys.length !== 0) { | ||
792 | this.hotkeysService.remove(this.hotkeys) | ||
793 | } | ||
794 | |||
789 | this.hotkeys = [ | 795 | this.hotkeys = [ |
790 | // These hotkeys are managed by the player | 796 | // These hotkeys are managed by the player |
791 | new Hotkey('f', e => e, undefined, $localize`Enter/exit fullscreen`), | 797 | new Hotkey('f', e => e, undefined, $localize`Enter/exit fullscreen`), |
792 | new Hotkey('space', e => e, undefined, $localize`Play/Pause the video`), | 798 | new Hotkey('space', e => e, undefined, $localize`Play/Pause the video`), |
793 | new Hotkey('m', e => e, undefined, $localize`Mute/unmute the video`), | 799 | new Hotkey('m', e => e, undefined, $localize`Mute/unmute the video`), |
794 | 800 | ||
795 | new Hotkey('0-9', e => e, undefined, $localize`Skip to a percentage of the video: 0 is 0% and 9 is 90%`), | ||
796 | |||
797 | new Hotkey('up', e => e, undefined, $localize`Increase the volume`), | 801 | new Hotkey('up', e => e, undefined, $localize`Increase the volume`), |
798 | new Hotkey('down', e => e, undefined, $localize`Decrease the volume`), | 802 | new Hotkey('down', e => e, undefined, $localize`Decrease the volume`), |
799 | 803 | ||
800 | new Hotkey('right', e => e, undefined, $localize`Seek the video forward`), | ||
801 | new Hotkey('left', e => e, undefined, $localize`Seek the video backward`), | ||
802 | |||
803 | new Hotkey('>', e => e, undefined, $localize`Increase playback rate`), | ||
804 | new Hotkey('<', e => e, undefined, $localize`Decrease playback rate`), | ||
805 | |||
806 | new Hotkey(',', e => e, undefined, $localize`Navigate in the video to the previous frame`), | ||
807 | new Hotkey('.', e => e, undefined, $localize`Navigate in the video to the next frame`), | ||
808 | |||
809 | new Hotkey('t', e => { | 804 | new Hotkey('t', e => { |
810 | this.theaterEnabled = !this.theaterEnabled | 805 | this.theaterEnabled = !this.theaterEnabled |
811 | return false | 806 | return false |
812 | }, undefined, $localize`Toggle theater mode`) | 807 | }, undefined, $localize`Toggle theater mode`) |
813 | ] | 808 | ] |
814 | 809 | ||
810 | if (!video.isLive) { | ||
811 | this.hotkeys = this.hotkeys.concat([ | ||
812 | // These hotkeys are also managed by the player but only for VOD | ||
813 | |||
814 | new Hotkey('0-9', e => e, undefined, $localize`Skip to a percentage of the video: 0 is 0% and 9 is 90%`), | ||
815 | |||
816 | new Hotkey('right', e => e, undefined, $localize`Seek the video forward`), | ||
817 | new Hotkey('left', e => e, undefined, $localize`Seek the video backward`), | ||
818 | |||
819 | new Hotkey('>', e => e, undefined, $localize`Increase playback rate`), | ||
820 | new Hotkey('<', e => e, undefined, $localize`Decrease playback rate`), | ||
821 | |||
822 | new Hotkey(',', e => e, undefined, $localize`Navigate in the video to the previous frame`), | ||
823 | new Hotkey('.', e => e, undefined, $localize`Navigate in the video to the next frame`) | ||
824 | ]) | ||
825 | } | ||
826 | |||
815 | if (this.isUserLoggedIn()) { | 827 | if (this.isUserLoggedIn()) { |
816 | this.hotkeys = this.hotkeys.concat([ | 828 | this.hotkeys = this.hotkeys.concat([ |
817 | new Hotkey('shift+s', () => { | 829 | new Hotkey('shift+s', () => { |
diff --git a/client/src/app/+videos/video-list/videos-list-common-page.component.ts b/client/src/app/+videos/video-list/videos-list-common-page.component.ts index c8fa8ef30..bafe30fd7 100644 --- a/client/src/app/+videos/video-list/videos-list-common-page.component.ts +++ b/client/src/app/+videos/video-list/videos-list-common-page.component.ts | |||
@@ -177,6 +177,9 @@ export class VideosListCommonPageComponent implements OnInit, OnDestroy, Disable | |||
177 | case 'best': | 177 | case 'best': |
178 | return '-hot' | 178 | return '-hot' |
179 | 179 | ||
180 | case 'name': | ||
181 | return 'name' | ||
182 | |||
180 | default: | 183 | default: |
181 | return '-' + algorithm as VideoSortField | 184 | return '-' + algorithm as VideoSortField |
182 | } | 185 | } |
diff --git a/client/src/app/core/auth/auth.service.ts b/client/src/app/core/auth/auth.service.ts index 4de28e51e..ed7eabb76 100644 --- a/client/src/app/core/auth/auth.service.ts +++ b/client/src/app/core/auth/auth.service.ts | |||
@@ -5,10 +5,11 @@ import { HttpClient, HttpErrorResponse, HttpHeaders, HttpParams } from '@angular | |||
5 | import { Injectable } from '@angular/core' | 5 | import { Injectable } from '@angular/core' |
6 | import { Router } from '@angular/router' | 6 | import { Router } from '@angular/router' |
7 | import { Notifier } from '@app/core/notification/notifier.service' | 7 | import { Notifier } from '@app/core/notification/notifier.service' |
8 | import { logger, OAuthUserTokens, objectToUrlEncoded, peertubeLocalStorage } from '@root-helpers/index' | 8 | import { logger, OAuthUserTokens, objectToUrlEncoded, peertubeLocalStorage, PluginsManager } from '@root-helpers/index' |
9 | import { HttpStatusCode, MyUser as UserServerModel, OAuthClientLocal, User, UserLogin, UserRefreshToken } from '@shared/models' | 9 | import { HttpStatusCode, MyUser as UserServerModel, OAuthClientLocal, User, UserLogin, UserRefreshToken } from '@shared/models' |
10 | import { environment } from '../../../environments/environment' | 10 | import { environment } from '../../../environments/environment' |
11 | import { RestExtractor } from '../rest/rest-extractor.service' | 11 | import { RestExtractor } from '../rest/rest-extractor.service' |
12 | import { ServerService } from '../server' | ||
12 | import { AuthStatus } from './auth-status.model' | 13 | import { AuthStatus } from './auth-status.model' |
13 | import { AuthUser } from './auth-user.model' | 14 | import { AuthUser } from './auth-user.model' |
14 | 15 | ||
@@ -44,6 +45,7 @@ export class AuthService { | |||
44 | private refreshingTokenObservable: Observable<any> | 45 | private refreshingTokenObservable: Observable<any> |
45 | 46 | ||
46 | constructor ( | 47 | constructor ( |
48 | private serverService: ServerService, | ||
47 | private http: HttpClient, | 49 | private http: HttpClient, |
48 | private notifier: Notifier, | 50 | private notifier: Notifier, |
49 | private hotkeysService: HotkeysService, | 51 | private hotkeysService: HotkeysService, |
@@ -213,25 +215,28 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular | |||
213 | const headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded') | 215 | const headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded') |
214 | 216 | ||
215 | this.refreshingTokenObservable = this.http.post<UserRefreshToken>(AuthService.BASE_TOKEN_URL, body, { headers }) | 217 | this.refreshingTokenObservable = this.http.post<UserRefreshToken>(AuthService.BASE_TOKEN_URL, body, { headers }) |
216 | .pipe( | 218 | .pipe( |
217 | map(res => this.handleRefreshToken(res)), | 219 | map(res => this.handleRefreshToken(res)), |
218 | tap(() => { | 220 | tap(() => { |
219 | this.refreshingTokenObservable = null | 221 | this.refreshingTokenObservable = null |
220 | }), | 222 | }), |
221 | catchError(err => { | 223 | catchError(err => { |
222 | this.refreshingTokenObservable = null | 224 | this.refreshingTokenObservable = null |
223 | 225 | ||
224 | logger.error(err) | 226 | logger.error(err) |
225 | logger.info('Cannot refresh token -> logout...') | 227 | logger.info('Cannot refresh token -> logout...') |
226 | this.logout() | 228 | this.logout() |
227 | this.router.navigate([ '/login' ]) | 229 | |
228 | 230 | const externalLoginUrl = PluginsManager.getDefaultLoginHref(environment.apiUrl, this.serverService.getHTMLConfig()) | |
229 | return observableThrowError(() => ({ | 231 | if (externalLoginUrl) window.location.href = externalLoginUrl |
230 | error: $localize`You need to reconnect.` | 232 | else this.router.navigate([ '/login' ]) |
231 | })) | 233 | |
232 | }), | 234 | return observableThrowError(() => ({ |
233 | share() | 235 | error: $localize`You need to reconnect.` |
234 | ) | 236 | })) |
237 | }), | ||
238 | share() | ||
239 | ) | ||
235 | 240 | ||
236 | return this.refreshingTokenObservable | 241 | return this.refreshingTokenObservable |
237 | } | 242 | } |
diff --git a/client/src/app/core/renderer/linkifier.service.ts b/client/src/app/core/renderer/linkifier.service.ts index 78df92cc9..d99591d6c 100644 --- a/client/src/app/core/renderer/linkifier.service.ts +++ b/client/src/app/core/renderer/linkifier.service.ts | |||
@@ -15,7 +15,7 @@ export class LinkifierService { | |||
15 | }, | 15 | }, |
16 | formatHref: { | 16 | formatHref: { |
17 | mention: (href: string) => { | 17 | mention: (href: string) => { |
18 | return getAbsoluteAPIUrl() + '/services/redirect/accounts/' + href.substr(1) | 18 | return getAbsoluteAPIUrl() + '/services/redirect/accounts/' + href.substring(1) |
19 | } | 19 | } |
20 | } | 20 | } |
21 | } | 21 | } |
diff --git a/client/src/app/core/renderer/markdown.service.ts b/client/src/app/core/renderer/markdown.service.ts index a5fd72862..dd23a1b01 100644 --- a/client/src/app/core/renderer/markdown.service.ts +++ b/client/src/app/core/renderer/markdown.service.ts | |||
@@ -64,8 +64,8 @@ export class MarkdownService { | |||
64 | 64 | ||
65 | textMarkdownToHTML (options: { | 65 | textMarkdownToHTML (options: { |
66 | markdown: string | 66 | markdown: string |
67 | withHtml?: boolean | 67 | withHtml?: boolean // default false |
68 | withEmoji?: boolean | 68 | withEmoji?: boolean // default false |
69 | }) { | 69 | }) { |
70 | const { markdown, withHtml = false, withEmoji = false } = options | 70 | const { markdown, withHtml = false, withEmoji = false } = options |
71 | 71 | ||
@@ -76,8 +76,8 @@ export class MarkdownService { | |||
76 | 76 | ||
77 | enhancedMarkdownToHTML (options: { | 77 | enhancedMarkdownToHTML (options: { |
78 | markdown: string | 78 | markdown: string |
79 | withHtml?: boolean | 79 | withHtml?: boolean // default false |
80 | withEmoji?: boolean | 80 | withEmoji?: boolean // default false |
81 | }) { | 81 | }) { |
82 | const { markdown, withHtml = false, withEmoji = false } = options | 82 | const { markdown, withHtml = false, withEmoji = false } = options |
83 | 83 | ||
@@ -99,6 +99,8 @@ export class MarkdownService { | |||
99 | return this.render({ name: 'customPageMarkdownIt', markdown, withEmoji: true, additionalAllowedTags }) | 99 | return this.render({ name: 'customPageMarkdownIt', markdown, withEmoji: true, additionalAllowedTags }) |
100 | } | 100 | } |
101 | 101 | ||
102 | // --------------------------------------------------------------------------- | ||
103 | |||
102 | processVideoTimestamps (videoShortUUID: string, html: string) { | 104 | processVideoTimestamps (videoShortUUID: string, html: string) { |
103 | return html.replace(/((\d{1,2}):)?(\d{1,2}):(\d{1,2})/g, function (str, _, h, m, s) { | 105 | return html.replace(/((\d{1,2}):)?(\d{1,2}):(\d{1,2})/g, function (str, _, h, m, s) { |
104 | const t = (3600 * +(h || 0)) + (60 * +(m || 0)) + (+(s || 0)) | 106 | const t = (3600 * +(h || 0)) + (60 * +(m || 0)) + (+(s || 0)) |
diff --git a/client/src/app/core/rest/rest-extractor.service.ts b/client/src/app/core/rest/rest-extractor.service.ts index de3f2bfff..daed7f178 100644 --- a/client/src/app/core/rest/rest-extractor.service.ts +++ b/client/src/app/core/rest/rest-extractor.service.ts | |||
@@ -87,7 +87,11 @@ export class RestExtractor { | |||
87 | 87 | ||
88 | if (err.status !== undefined) { | 88 | if (err.status !== undefined) { |
89 | const errorMessage = this.buildServerErrorMessage(err) | 89 | const errorMessage = this.buildServerErrorMessage(err) |
90 | logger.error(`Backend returned code ${err.status}, errorMessage is: ${errorMessage}`) | 90 | |
91 | const message = `Backend returned code ${err.status}, errorMessage is: ${errorMessage}` | ||
92 | |||
93 | if (err.status === HttpStatusCode.NOT_FOUND_404) logger.clientError(message) | ||
94 | else logger.error(message) | ||
91 | 95 | ||
92 | return errorMessage | 96 | return errorMessage |
93 | } | 97 | } |
diff --git a/client/src/app/core/rest/rest-table.ts b/client/src/app/core/rest/rest-table.ts index ec5646b5d..707110d7f 100644 --- a/client/src/app/core/rest/rest-table.ts +++ b/client/src/app/core/rest/rest-table.ts | |||
@@ -7,7 +7,7 @@ import { RestPagination } from './rest-pagination' | |||
7 | 7 | ||
8 | const debugLogger = debug('peertube:tables:RestTable') | 8 | const debugLogger = debug('peertube:tables:RestTable') |
9 | 9 | ||
10 | export abstract class RestTable { | 10 | export abstract class RestTable <T = unknown> { |
11 | 11 | ||
12 | abstract totalRecords: number | 12 | abstract totalRecords: number |
13 | abstract sort: SortMeta | 13 | abstract sort: SortMeta |
@@ -17,6 +17,8 @@ export abstract class RestTable { | |||
17 | rowsPerPage = this.rowsPerPageOptions[0] | 17 | rowsPerPage = this.rowsPerPageOptions[0] |
18 | expandedRows = {} | 18 | expandedRows = {} |
19 | 19 | ||
20 | selectedRows: T[] = [] | ||
21 | |||
20 | search: string | 22 | search: string |
21 | 23 | ||
22 | protected route: ActivatedRoute | 24 | protected route: ActivatedRoute |
@@ -75,7 +77,17 @@ export abstract class RestTable { | |||
75 | this.reloadData() | 77 | this.reloadData() |
76 | } | 78 | } |
77 | 79 | ||
78 | protected abstract reloadData (): void | 80 | isInSelectionMode () { |
81 | return this.selectedRows.length !== 0 | ||
82 | } | ||
83 | |||
84 | protected abstract reloadDataInternal (): void | ||
85 | |||
86 | protected reloadData () { | ||
87 | this.selectedRows = [] | ||
88 | |||
89 | this.reloadDataInternal() | ||
90 | } | ||
79 | 91 | ||
80 | private getSortLocalStorageKey () { | 92 | private getSortLocalStorageKey () { |
81 | return 'rest-table-sort-' + this.getIdentifier() | 93 | return 'rest-table-sort-' + this.getIdentifier() |
diff --git a/client/src/app/menu/menu.component.html b/client/src/app/menu/menu.component.html index c5d08ab75..15b1a3c4a 100644 --- a/client/src/app/menu/menu.component.html +++ b/client/src/app/menu/menu.component.html | |||
@@ -103,7 +103,9 @@ | |||
103 | <a i18n *ngIf="!getExternalLoginHref()" routerLink="/login" class="peertube-button-link orange-button">Login</a> | 103 | <a i18n *ngIf="!getExternalLoginHref()" routerLink="/login" class="peertube-button-link orange-button">Login</a> |
104 | <a i18n *ngIf="getExternalLoginHref()" [href]="getExternalLoginHref()" class="peertube-button-link orange-button">Login</a> | 104 | <a i18n *ngIf="getExternalLoginHref()" [href]="getExternalLoginHref()" class="peertube-button-link orange-button">Login</a> |
105 | 105 | ||
106 | <a i18n *ngIf="isRegistrationAllowed()" routerLink="/signup" class="peertube-button-link create-account-button">Create an account</a> | 106 | <a *ngIf="isRegistrationAllowed()" routerLink="/signup" class="peertube-button-link create-account-button"> |
107 | <my-signup-label [requiresApproval]="requiresApproval"></my-signup-label> | ||
108 | </a> | ||
107 | </div> | 109 | </div> |
108 | 110 | ||
109 | <ng-container *ngFor="let menuSection of menuSections" > | 111 | <ng-container *ngFor="let menuSection of menuSections" > |
diff --git a/client/src/app/menu/menu.component.ts b/client/src/app/menu/menu.component.ts index 63f01df92..fc6d74cff 100644 --- a/client/src/app/menu/menu.component.ts +++ b/client/src/app/menu/menu.component.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import { HotkeysService } from 'angular2-hotkeys' | 1 | import { HotkeysService } from 'angular2-hotkeys' |
2 | import * as debug from 'debug' | 2 | import * as debug from 'debug' |
3 | import { switchMap } from 'rxjs/operators' | 3 | import { switchMap } from 'rxjs/operators' |
4 | import { environment } from 'src/environments/environment' | ||
4 | import { ViewportScroller } from '@angular/common' | 5 | import { ViewportScroller } from '@angular/common' |
5 | import { Component, OnInit, ViewChild } from '@angular/core' | 6 | import { Component, OnInit, ViewChild } from '@angular/core' |
6 | import { Router } from '@angular/router' | 7 | import { Router } from '@angular/router' |
@@ -91,6 +92,10 @@ export class MenuComponent implements OnInit { | |||
91 | return this.languageChooserModal.getCurrentLanguage() | 92 | return this.languageChooserModal.getCurrentLanguage() |
92 | } | 93 | } |
93 | 94 | ||
95 | get requiresApproval () { | ||
96 | return this.serverConfig.signup.requiresApproval | ||
97 | } | ||
98 | |||
94 | ngOnInit () { | 99 | ngOnInit () { |
95 | this.htmlServerConfig = this.serverService.getHTMLConfig() | 100 | this.htmlServerConfig = this.serverService.getHTMLConfig() |
96 | this.currentInterfaceLanguage = this.languageChooserModal.getCurrentLanguage() | 101 | this.currentInterfaceLanguage = this.languageChooserModal.getCurrentLanguage() |
@@ -131,12 +136,7 @@ export class MenuComponent implements OnInit { | |||
131 | } | 136 | } |
132 | 137 | ||
133 | getExternalLoginHref () { | 138 | getExternalLoginHref () { |
134 | if (!this.serverConfig || this.serverConfig.client.menu.login.redirectOnSingleExternalAuth !== true) return undefined | 139 | return PluginsManager.getDefaultLoginHref(environment.apiUrl, this.serverConfig) |
135 | |||
136 | const externalAuths = this.serverConfig.plugin.registeredExternalAuths | ||
137 | if (externalAuths.length !== 1) return undefined | ||
138 | |||
139 | return PluginsManager.getExternalAuthHref(externalAuths[0]) | ||
140 | } | 140 | } |
141 | 141 | ||
142 | isRegistrationAllowed () { | 142 | isRegistrationAllowed () { |
diff --git a/client/src/app/shared/form-validators/form-validator.model.ts b/client/src/app/shared/form-validators/form-validator.model.ts index 31c253b9b..1e4bba86b 100644 --- a/client/src/app/shared/form-validators/form-validator.model.ts +++ b/client/src/app/shared/form-validators/form-validator.model.ts | |||
@@ -12,5 +12,5 @@ export type BuildFormArgument = { | |||
12 | } | 12 | } |
13 | 13 | ||
14 | export type BuildFormDefaultValues = { | 14 | export type BuildFormDefaultValues = { |
15 | [ name: string ]: number | string | string[] | BuildFormDefaultValues | 15 | [ name: string ]: boolean | number | string | string[] | BuildFormDefaultValues |
16 | } | 16 | } |
diff --git a/client/src/app/shared/form-validators/user-validators.ts b/client/src/app/shared/form-validators/user-validators.ts index b93de75ea..ed6e0582e 100644 --- a/client/src/app/shared/form-validators/user-validators.ts +++ b/client/src/app/shared/form-validators/user-validators.ts | |||
@@ -136,13 +136,6 @@ export const USER_DESCRIPTION_VALIDATOR: BuildFormValidator = { | |||
136 | } | 136 | } |
137 | } | 137 | } |
138 | 138 | ||
139 | export const USER_TERMS_VALIDATOR: BuildFormValidator = { | ||
140 | VALIDATORS: [ Validators.requiredTrue ], | ||
141 | MESSAGES: { | ||
142 | required: $localize`You must agree with the instance terms in order to register on it.` | ||
143 | } | ||
144 | } | ||
145 | |||
146 | export const USER_BAN_REASON_VALIDATOR: BuildFormValidator = { | 139 | export const USER_BAN_REASON_VALIDATOR: BuildFormValidator = { |
147 | VALIDATORS: [ | 140 | VALIDATORS: [ |
148 | Validators.minLength(3), | 141 | Validators.minLength(3), |
diff --git a/client/src/app/shared/shared-abuse-list/abuse-details.component.html b/client/src/app/shared/shared-abuse-list/abuse-details.component.html index 089be501d..2d3e26a25 100644 --- a/client/src/app/shared/shared-abuse-list/abuse-details.component.html +++ b/client/src/app/shared/shared-abuse-list/abuse-details.component.html | |||
@@ -8,7 +8,7 @@ | |||
8 | 8 | ||
9 | <span class="moderation-expanded-text"> | 9 | <span class="moderation-expanded-text"> |
10 | <a [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reporter:"' + abuse.reporterAccount.displayName + '"' }" | 10 | <a [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reporter:"' + abuse.reporterAccount.displayName + '"' }" |
11 | class="chip" | 11 | class="chip me-1" |
12 | > | 12 | > |
13 | <my-actor-avatar size="18" [actor]="abuse.reporterAccount" actorType="account"></my-actor-avatar> | 13 | <my-actor-avatar size="18" [actor]="abuse.reporterAccount" actorType="account"></my-actor-avatar> |
14 | <div> | 14 | <div> |
@@ -29,7 +29,7 @@ | |||
29 | <span class="moderation-expanded-label" i18n>Reportee</span> | 29 | <span class="moderation-expanded-label" i18n>Reportee</span> |
30 | <span class="moderation-expanded-text"> | 30 | <span class="moderation-expanded-text"> |
31 | <a [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reportee:"' +abuse.flaggedAccount.displayName + '"' }" | 31 | <a [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reportee:"' +abuse.flaggedAccount.displayName + '"' }" |
32 | class="chip" | 32 | class="chip me-1" |
33 | > | 33 | > |
34 | <my-actor-avatar size="18" [actor]="abuse.flaggedAccount" actorType="account"></my-actor-avatar> | 34 | <my-actor-avatar size="18" [actor]="abuse.flaggedAccount" actorType="account"></my-actor-avatar> |
35 | <div> | 35 | <div> |
@@ -63,7 +63,7 @@ | |||
63 | <div *ngIf="predefinedReasons" class="mt-2 d-flex"> | 63 | <div *ngIf="predefinedReasons" class="mt-2 d-flex"> |
64 | <span> | 64 | <span> |
65 | <a *ngFor="let reason of predefinedReasons" [routerLink]="[ '.' ]" | 65 | <a *ngFor="let reason of predefinedReasons" [routerLink]="[ '.' ]" |
66 | [queryParams]="{ 'search': 'tag:' + reason.id }" class="chip rectangular bg-secondary text-light" | 66 | [queryParams]="{ 'search': 'tag:' + reason.id }" class="pt-badge badge-secondary" |
67 | > | 67 | > |
68 | <div>{{ reason.label }}</div> | 68 | <div>{{ reason.label }}</div> |
69 | </a> | 69 | </a> |
diff --git a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts index 569a37b17..d8470e927 100644 --- a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts +++ b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts | |||
@@ -175,7 +175,7 @@ export class AbuseListTableComponent extends RestTable implements OnInit { | |||
175 | return Actor.IS_LOCAL(abuse.reporterAccount.host) | 175 | return Actor.IS_LOCAL(abuse.reporterAccount.host) |
176 | } | 176 | } |
177 | 177 | ||
178 | protected reloadData () { | 178 | protected reloadDataInternal () { |
179 | debugLogger('Loading data.') | 179 | debugLogger('Loading data.') |
180 | 180 | ||
181 | const options = { | 181 | const options = { |
diff --git a/client/src/app/shared/shared-custom-markup/custom-markup-container.component.ts b/client/src/app/shared/shared-custom-markup/custom-markup-container.component.ts index 4e802b14d..b2ee2d8f2 100644 --- a/client/src/app/shared/shared-custom-markup/custom-markup-container.component.ts +++ b/client/src/app/shared/shared-custom-markup/custom-markup-container.component.ts | |||
@@ -6,9 +6,9 @@ import { CustomMarkupService } from './custom-markup.service' | |||
6 | templateUrl: './custom-markup-container.component.html' | 6 | templateUrl: './custom-markup-container.component.html' |
7 | }) | 7 | }) |
8 | export class CustomMarkupContainerComponent implements OnChanges { | 8 | export class CustomMarkupContainerComponent implements OnChanges { |
9 | @ViewChild('contentWrapper') contentWrapper: ElementRef<HTMLInputElement> | 9 | @ViewChild('contentWrapper', { static: true }) contentWrapper: ElementRef<HTMLInputElement> |
10 | 10 | ||
11 | @Input() content: string | 11 | @Input() content: string | HTMLDivElement |
12 | 12 | ||
13 | displayed = false | 13 | displayed = false |
14 | 14 | ||
@@ -17,17 +17,23 @@ export class CustomMarkupContainerComponent implements OnChanges { | |||
17 | ) { } | 17 | ) { } |
18 | 18 | ||
19 | async ngOnChanges () { | 19 | async ngOnChanges () { |
20 | await this.buildElement() | 20 | await this.rebuild() |
21 | } | 21 | } |
22 | 22 | ||
23 | private async buildElement () { | 23 | private async rebuild () { |
24 | if (!this.content) return | 24 | if (this.content instanceof HTMLDivElement) { |
25 | return this.loadElement(this.content) | ||
26 | } | ||
25 | 27 | ||
26 | const { rootElement, componentsLoaded } = await this.customMarkupService.buildElement(this.content) | 28 | const { rootElement, componentsLoaded } = await this.customMarkupService.buildElement(this.content) |
27 | this.contentWrapper.nativeElement.appendChild(rootElement) | ||
28 | |||
29 | await componentsLoaded | 29 | await componentsLoaded |
30 | 30 | ||
31 | return this.loadElement(rootElement) | ||
32 | } | ||
33 | |||
34 | private loadElement (el: HTMLDivElement) { | ||
35 | this.contentWrapper.nativeElement.appendChild(el) | ||
36 | |||
31 | this.displayed = true | 37 | this.displayed = true |
32 | } | 38 | } |
33 | } | 39 | } |
diff --git a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/button-markup.component.ts b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/button-markup.component.ts index 1af060548..264dd9577 100644 --- a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/button-markup.component.ts +++ b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/button-markup.component.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { Component, Input } from '@angular/core' | 1 | import { ChangeDetectionStrategy, Component, Input } from '@angular/core' |
2 | import { VideoChannel } from '../../shared-main' | 2 | import { VideoChannel } from '../../shared-main' |
3 | import { CustomMarkupComponent } from './shared' | 3 | import { CustomMarkupComponent } from './shared' |
4 | 4 | ||
@@ -9,7 +9,8 @@ import { CustomMarkupComponent } from './shared' | |||
9 | @Component({ | 9 | @Component({ |
10 | selector: 'my-button-markup', | 10 | selector: 'my-button-markup', |
11 | templateUrl: 'button-markup.component.html', | 11 | templateUrl: 'button-markup.component.html', |
12 | styleUrls: [ 'button-markup.component.scss' ] | 12 | styleUrls: [ 'button-markup.component.scss' ], |
13 | changeDetection: ChangeDetectionStrategy.OnPush | ||
13 | }) | 14 | }) |
14 | export class ButtonMarkupComponent implements CustomMarkupComponent { | 15 | export class ButtonMarkupComponent implements CustomMarkupComponent { |
15 | @Input() theme: 'primary' | 'secondary' | 16 | @Input() theme: 'primary' | 'secondary' |
diff --git a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.ts b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.ts index ba12b7139..1e7860750 100644 --- a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.ts +++ b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { from } from 'rxjs' | 1 | import { from } from 'rxjs' |
2 | import { finalize, map, switchMap, tap } from 'rxjs/operators' | 2 | import { finalize, map, switchMap, tap } from 'rxjs/operators' |
3 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' | 3 | import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core' |
4 | import { MarkdownService, Notifier, UserService } from '@app/core' | 4 | import { MarkdownService, Notifier, UserService } from '@app/core' |
5 | import { FindInBulkService } from '@app/shared/shared-search' | 5 | import { FindInBulkService } from '@app/shared/shared-search' |
6 | import { VideoSortField } from '@shared/models' | 6 | import { VideoSortField } from '@shared/models' |
@@ -14,7 +14,8 @@ import { CustomMarkupComponent } from './shared' | |||
14 | @Component({ | 14 | @Component({ |
15 | selector: 'my-channel-miniature-markup', | 15 | selector: 'my-channel-miniature-markup', |
16 | templateUrl: 'channel-miniature-markup.component.html', | 16 | templateUrl: 'channel-miniature-markup.component.html', |
17 | styleUrls: [ 'channel-miniature-markup.component.scss' ] | 17 | styleUrls: [ 'channel-miniature-markup.component.scss' ], |
18 | changeDetection: ChangeDetectionStrategy.OnPush | ||
18 | }) | 19 | }) |
19 | export class ChannelMiniatureMarkupComponent implements CustomMarkupComponent, OnInit { | 20 | export class ChannelMiniatureMarkupComponent implements CustomMarkupComponent, OnInit { |
20 | @Input() name: string | 21 | @Input() name: string |
diff --git a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/playlist-miniature-markup.component.ts b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/playlist-miniature-markup.component.ts index 07fa6fd2d..ab52e7e37 100644 --- a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/playlist-miniature-markup.component.ts +++ b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/playlist-miniature-markup.component.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { finalize } from 'rxjs/operators' | 1 | import { finalize } from 'rxjs/operators' |
2 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' | 2 | import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core' |
3 | import { Notifier } from '@app/core' | 3 | import { Notifier } from '@app/core' |
4 | import { FindInBulkService } from '@app/shared/shared-search' | 4 | import { FindInBulkService } from '@app/shared/shared-search' |
5 | import { MiniatureDisplayOptions } from '../../shared-video-miniature' | 5 | import { MiniatureDisplayOptions } from '../../shared-video-miniature' |
@@ -13,7 +13,8 @@ import { CustomMarkupComponent } from './shared' | |||
13 | @Component({ | 13 | @Component({ |
14 | selector: 'my-playlist-miniature-markup', | 14 | selector: 'my-playlist-miniature-markup', |
15 | templateUrl: 'playlist-miniature-markup.component.html', | 15 | templateUrl: 'playlist-miniature-markup.component.html', |
16 | styleUrls: [ 'playlist-miniature-markup.component.scss' ] | 16 | styleUrls: [ 'playlist-miniature-markup.component.scss' ], |
17 | changeDetection: ChangeDetectionStrategy.OnPush | ||
17 | }) | 18 | }) |
18 | export class PlaylistMiniatureMarkupComponent implements CustomMarkupComponent, OnInit { | 19 | export class PlaylistMiniatureMarkupComponent implements CustomMarkupComponent, OnInit { |
19 | @Input() uuid: string | 20 | @Input() uuid: string |
diff --git a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/video-miniature-markup.component.ts b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/video-miniature-markup.component.ts index cbbacf77c..c37666359 100644 --- a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/video-miniature-markup.component.ts +++ b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/video-miniature-markup.component.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { finalize } from 'rxjs/operators' | 1 | import { finalize } from 'rxjs/operators' |
2 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' | 2 | import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core' |
3 | import { AuthService, Notifier } from '@app/core' | 3 | import { AuthService, Notifier } from '@app/core' |
4 | import { FindInBulkService } from '@app/shared/shared-search' | 4 | import { FindInBulkService } from '@app/shared/shared-search' |
5 | import { Video } from '../../shared-main' | 5 | import { Video } from '../../shared-main' |
@@ -13,7 +13,8 @@ import { CustomMarkupComponent } from './shared' | |||
13 | @Component({ | 13 | @Component({ |
14 | selector: 'my-video-miniature-markup', | 14 | selector: 'my-video-miniature-markup', |
15 | templateUrl: 'video-miniature-markup.component.html', | 15 | templateUrl: 'video-miniature-markup.component.html', |
16 | styleUrls: [ 'video-miniature-markup.component.scss' ] | 16 | styleUrls: [ 'video-miniature-markup.component.scss' ], |
17 | changeDetection: ChangeDetectionStrategy.OnPush | ||
17 | }) | 18 | }) |
18 | export class VideoMiniatureMarkupComponent implements CustomMarkupComponent, OnInit { | 19 | export class VideoMiniatureMarkupComponent implements CustomMarkupComponent, OnInit { |
19 | @Input() uuid: string | 20 | @Input() uuid: string |
diff --git a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/videos-list-markup.component.ts b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/videos-list-markup.component.ts index 7d3498d4c..70e88ea51 100644 --- a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/videos-list-markup.component.ts +++ b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/videos-list-markup.component.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { finalize } from 'rxjs/operators' | 1 | import { finalize } from 'rxjs/operators' |
2 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' | 2 | import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core' |
3 | import { AuthService, Notifier } from '@app/core' | 3 | import { AuthService, Notifier } from '@app/core' |
4 | import { VideoSortField } from '@shared/models' | 4 | import { VideoSortField } from '@shared/models' |
5 | import { Video, VideoService } from '../../shared-main' | 5 | import { Video, VideoService } from '../../shared-main' |
@@ -13,7 +13,8 @@ import { CustomMarkupComponent } from './shared' | |||
13 | @Component({ | 13 | @Component({ |
14 | selector: 'my-videos-list-markup', | 14 | selector: 'my-videos-list-markup', |
15 | templateUrl: 'videos-list-markup.component.html', | 15 | templateUrl: 'videos-list-markup.component.html', |
16 | styleUrls: [ 'videos-list-markup.component.scss' ] | 16 | styleUrls: [ 'videos-list-markup.component.scss' ], |
17 | changeDetection: ChangeDetectionStrategy.OnPush | ||
17 | }) | 18 | }) |
18 | export class VideosListMarkupComponent implements CustomMarkupComponent, OnInit { | 19 | export class VideosListMarkupComponent implements CustomMarkupComponent, OnInit { |
19 | @Input() sort: string | 20 | @Input() sort: string |
diff --git a/client/src/app/shared/shared-forms/markdown-textarea.component.ts b/client/src/app/shared/shared-forms/markdown-textarea.component.ts index e3371f22c..c6527e169 100644 --- a/client/src/app/shared/shared-forms/markdown-textarea.component.ts +++ b/client/src/app/shared/shared-forms/markdown-textarea.component.ts | |||
@@ -31,6 +31,8 @@ export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit { | |||
31 | @Input() markdownType: 'text' | 'enhanced' = 'text' | 31 | @Input() markdownType: 'text' | 'enhanced' = 'text' |
32 | @Input() customMarkdownRenderer?: (text: string) => Promise<string | HTMLElement> | 32 | @Input() customMarkdownRenderer?: (text: string) => Promise<string | HTMLElement> |
33 | 33 | ||
34 | @Input() debounceTime = 150 | ||
35 | |||
34 | @Input() markdownVideo: Video | 36 | @Input() markdownVideo: Video |
35 | 37 | ||
36 | @Input() name = 'description' | 38 | @Input() name = 'description' |
@@ -59,7 +61,7 @@ export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit { | |||
59 | ngOnInit () { | 61 | ngOnInit () { |
60 | this.contentChanged | 62 | this.contentChanged |
61 | .pipe( | 63 | .pipe( |
62 | debounceTime(150), | 64 | debounceTime(this.debounceTime), |
63 | distinctUntilChanged() | 65 | distinctUntilChanged() |
64 | ) | 66 | ) |
65 | .subscribe(() => this.updatePreviews()) | 67 | .subscribe(() => this.updatePreviews()) |
diff --git a/client/src/app/shared/shared-instance/instance-features-table.component.html b/client/src/app/shared/shared-instance/instance-features-table.component.html index 6c05764df..205f2bc97 100644 --- a/client/src/app/shared/shared-instance/instance-features-table.component.html +++ b/client/src/app/shared/shared-instance/instance-features-table.component.html | |||
@@ -18,10 +18,9 @@ | |||
18 | </tr> | 18 | </tr> |
19 | 19 | ||
20 | <tr> | 20 | <tr> |
21 | <th i18n class="label" scope="row">User registration allowed</th> | 21 | <th i18n class="label" scope="row">User registration</th> |
22 | <td> | 22 | |
23 | <my-feature-boolean [value]="serverConfig.signup.allowed"></my-feature-boolean> | 23 | <td class="value">{{ buildRegistrationLabel() }}</td> |
24 | </td> | ||
25 | </tr> | 24 | </tr> |
26 | 25 | ||
27 | <tr> | 26 | <tr> |
diff --git a/client/src/app/shared/shared-instance/instance-features-table.component.ts b/client/src/app/shared/shared-instance/instance-features-table.component.ts index e405c5790..c3df7c594 100644 --- a/client/src/app/shared/shared-instance/instance-features-table.component.ts +++ b/client/src/app/shared/shared-instance/instance-features-table.component.ts | |||
@@ -56,6 +56,15 @@ export class InstanceFeaturesTableComponent implements OnInit { | |||
56 | if (policy === 'display') return $localize`Displayed` | 56 | if (policy === 'display') return $localize`Displayed` |
57 | } | 57 | } |
58 | 58 | ||
59 | buildRegistrationLabel () { | ||
60 | const config = this.serverConfig.signup | ||
61 | |||
62 | if (config.allowed !== true) return $localize`Disabled` | ||
63 | if (config.requiresApproval === true) return $localize`Requires approval by moderators` | ||
64 | |||
65 | return $localize`Enabled` | ||
66 | } | ||
67 | |||
59 | getServerVersionAndCommit () { | 68 | getServerVersionAndCommit () { |
60 | return this.serverService.getServerVersionAndCommit() | 69 | return this.serverService.getServerVersionAndCommit() |
61 | } | 70 | } |
diff --git a/client/src/app/shared/shared-instance/instance.service.ts b/client/src/app/shared/shared-instance/instance.service.ts index 89f47db24..f5b2e05db 100644 --- a/client/src/app/shared/shared-instance/instance.service.ts +++ b/client/src/app/shared/shared-instance/instance.service.ts | |||
@@ -7,6 +7,11 @@ import { peertubeTranslate } from '@shared/core-utils/i18n' | |||
7 | import { About } from '@shared/models' | 7 | import { About } from '@shared/models' |
8 | import { environment } from '../../../environments/environment' | 8 | import { environment } from '../../../environments/environment' |
9 | 9 | ||
10 | export type AboutHTML = Pick<About['instance'], | ||
11 | 'terms' | 'codeOfConduct' | 'moderationInformation' | 'administrator' | 'creationReason' | | ||
12 | 'maintenanceLifetime' | 'businessModel' | 'hardwareInformation' | ||
13 | > | ||
14 | |||
10 | @Injectable() | 15 | @Injectable() |
11 | export class InstanceService { | 16 | export class InstanceService { |
12 | private static BASE_CONFIG_URL = environment.apiUrl + '/api/v1/config' | 17 | private static BASE_CONFIG_URL = environment.apiUrl + '/api/v1/config' |
@@ -39,7 +44,7 @@ export class InstanceService { | |||
39 | } | 44 | } |
40 | 45 | ||
41 | async buildHtml (about: About) { | 46 | async buildHtml (about: About) { |
42 | const html = { | 47 | const html: AboutHTML = { |
43 | terms: '', | 48 | terms: '', |
44 | codeOfConduct: '', | 49 | codeOfConduct: '', |
45 | moderationInformation: '', | 50 | moderationInformation: '', |
diff --git a/client/src/app/shared/shared-main/account/index.ts b/client/src/app/shared/shared-main/account/index.ts index b80ddb9f5..dd41a5f05 100644 --- a/client/src/app/shared/shared-main/account/index.ts +++ b/client/src/app/shared/shared-main/account/index.ts | |||
@@ -1,3 +1,4 @@ | |||
1 | export * from './account.model' | 1 | export * from './account.model' |
2 | export * from './account.service' | 2 | export * from './account.service' |
3 | export * from './actor.model' | 3 | export * from './actor.model' |
4 | export * from './signup-label.component' | ||
diff --git a/client/src/app/shared/shared-main/account/signup-label.component.html b/client/src/app/shared/shared-main/account/signup-label.component.html new file mode 100644 index 000000000..35d6c5360 --- /dev/null +++ b/client/src/app/shared/shared-main/account/signup-label.component.html | |||
@@ -0,0 +1,2 @@ | |||
1 | <ng-container i18n *ngIf="requiresApproval">Request an account</ng-container> | ||
2 | <ng-container i18n *ngIf="!requiresApproval">Create an account</ng-container> | ||
diff --git a/client/src/app/shared/shared-main/account/signup-label.component.ts b/client/src/app/shared/shared-main/account/signup-label.component.ts new file mode 100644 index 000000000..caacb9c6f --- /dev/null +++ b/client/src/app/shared/shared-main/account/signup-label.component.ts | |||
@@ -0,0 +1,9 @@ | |||
1 | import { Component, Input } from '@angular/core' | ||
2 | |||
3 | @Component({ | ||
4 | selector: 'my-signup-label', | ||
5 | templateUrl: './signup-label.component.html' | ||
6 | }) | ||
7 | export class SignupLabelComponent { | ||
8 | @Input() requiresApproval: boolean | ||
9 | } | ||
diff --git a/client/src/app/shared/shared-main/shared-main.module.ts b/client/src/app/shared/shared-main/shared-main.module.ts index c1523bc50..eb1642d97 100644 --- a/client/src/app/shared/shared-main/shared-main.module.ts +++ b/client/src/app/shared/shared-main/shared-main.module.ts | |||
@@ -16,7 +16,7 @@ import { | |||
16 | import { LoadingBarModule } from '@ngx-loading-bar/core' | 16 | import { LoadingBarModule } from '@ngx-loading-bar/core' |
17 | import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client' | 17 | import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client' |
18 | import { SharedGlobalIconModule } from '../shared-icons' | 18 | import { SharedGlobalIconModule } from '../shared-icons' |
19 | import { AccountService } from './account' | 19 | import { AccountService, SignupLabelComponent } from './account' |
20 | import { | 20 | import { |
21 | AutofocusDirective, | 21 | AutofocusDirective, |
22 | BytesPipe, | 22 | BytesPipe, |
@@ -113,6 +113,8 @@ import { VideoChannelService } from './video-channel' | |||
113 | UserQuotaComponent, | 113 | UserQuotaComponent, |
114 | UserNotificationsComponent, | 114 | UserNotificationsComponent, |
115 | 115 | ||
116 | SignupLabelComponent, | ||
117 | |||
116 | EmbedComponent, | 118 | EmbedComponent, |
117 | 119 | ||
118 | PluginPlaceholderComponent, | 120 | PluginPlaceholderComponent, |
@@ -171,6 +173,8 @@ import { VideoChannelService } from './video-channel' | |||
171 | UserQuotaComponent, | 173 | UserQuotaComponent, |
172 | UserNotificationsComponent, | 174 | UserNotificationsComponent, |
173 | 175 | ||
176 | SignupLabelComponent, | ||
177 | |||
174 | EmbedComponent, | 178 | EmbedComponent, |
175 | 179 | ||
176 | PluginPlaceholderComponent, | 180 | PluginPlaceholderComponent, |
diff --git a/client/src/app/shared/shared-main/users/user-notification.model.ts b/client/src/app/shared/shared-main/users/user-notification.model.ts index bf8870a79..96e7b4dd0 100644 --- a/client/src/app/shared/shared-main/users/user-notification.model.ts +++ b/client/src/app/shared/shared-main/users/user-notification.model.ts | |||
@@ -83,6 +83,11 @@ export class UserNotification implements UserNotificationServer { | |||
83 | latestVersion: string | 83 | latestVersion: string |
84 | } | 84 | } |
85 | 85 | ||
86 | registration?: { | ||
87 | id: number | ||
88 | username: string | ||
89 | } | ||
90 | |||
86 | createdAt: string | 91 | createdAt: string |
87 | updatedAt: string | 92 | updatedAt: string |
88 | 93 | ||
@@ -97,6 +102,8 @@ export class UserNotification implements UserNotificationServer { | |||
97 | 102 | ||
98 | accountUrl?: string | 103 | accountUrl?: string |
99 | 104 | ||
105 | registrationsUrl?: string | ||
106 | |||
100 | videoImportIdentifier?: string | 107 | videoImportIdentifier?: string |
101 | videoImportUrl?: string | 108 | videoImportUrl?: string |
102 | 109 | ||
@@ -135,6 +142,7 @@ export class UserNotification implements UserNotificationServer { | |||
135 | 142 | ||
136 | this.plugin = hash.plugin | 143 | this.plugin = hash.plugin |
137 | this.peertube = hash.peertube | 144 | this.peertube = hash.peertube |
145 | this.registration = hash.registration | ||
138 | 146 | ||
139 | this.createdAt = hash.createdAt | 147 | this.createdAt = hash.createdAt |
140 | this.updatedAt = hash.updatedAt | 148 | this.updatedAt = hash.updatedAt |
@@ -208,6 +216,10 @@ export class UserNotification implements UserNotificationServer { | |||
208 | this.accountUrl = this.buildAccountUrl(this.account) | 216 | this.accountUrl = this.buildAccountUrl(this.account) |
209 | break | 217 | break |
210 | 218 | ||
219 | case UserNotificationType.NEW_USER_REGISTRATION_REQUEST: | ||
220 | this.registrationsUrl = '/admin/moderation/registrations/list' | ||
221 | break | ||
222 | |||
211 | case UserNotificationType.NEW_FOLLOW: | 223 | case UserNotificationType.NEW_FOLLOW: |
212 | this.accountUrl = this.buildAccountUrl(this.actorFollow.follower) | 224 | this.accountUrl = this.buildAccountUrl(this.actorFollow.follower) |
213 | break | 225 | break |
diff --git a/client/src/app/shared/shared-main/users/user-notifications.component.html b/client/src/app/shared/shared-main/users/user-notifications.component.html index e7cdb0183..a51e08292 100644 --- a/client/src/app/shared/shared-main/users/user-notifications.component.html +++ b/client/src/app/shared/shared-main/users/user-notifications.component.html | |||
@@ -215,6 +215,14 @@ | |||
215 | </div> | 215 | </div> |
216 | </ng-container> | 216 | </ng-container> |
217 | 217 | ||
218 | <ng-container *ngSwitchCase="20"> <!-- UserNotificationType.NEW_USER_REGISTRATION_REQUEST --> | ||
219 | <my-global-icon iconName="user-add" aria-hidden="true"></my-global-icon> | ||
220 | |||
221 | <div class="message" i18n> | ||
222 | User <a (click)="markAsRead(notification)" [routerLink]="notification.registrationsUrl">{{ notification.registration.username }}</a> wants to register on your instance | ||
223 | </div> | ||
224 | </ng-container> | ||
225 | |||
218 | <ng-container *ngSwitchDefault> | 226 | <ng-container *ngSwitchDefault> |
219 | <my-global-icon iconName="alert" aria-hidden="true"></my-global-icon> | 227 | <my-global-icon iconName="alert" aria-hidden="true"></my-global-icon> |
220 | 228 | ||
diff --git a/client/src/app/shared/shared-moderation/account-blocklist.component.scss b/client/src/app/shared/shared-moderation/account-blocklist.component.scss index 8b1239d34..00aaf3b9c 100644 --- a/client/src/app/shared/shared-moderation/account-blocklist.component.scss +++ b/client/src/app/shared/shared-moderation/account-blocklist.component.scss | |||
@@ -1,10 +1,6 @@ | |||
1 | @use '_variables' as *; | 1 | @use '_variables' as *; |
2 | @use '_mixins' as *; | 2 | @use '_mixins' as *; |
3 | 3 | ||
4 | .chip { | ||
5 | @include chip; | ||
6 | } | ||
7 | |||
8 | .unblock-button { | 4 | .unblock-button { |
9 | @include peertube-button; | 5 | @include peertube-button; |
10 | @include grey-button; | 6 | @include grey-button; |
diff --git a/client/src/app/shared/shared-moderation/account-blocklist.component.ts b/client/src/app/shared/shared-moderation/account-blocklist.component.ts index 9ed00bc12..38dbbff78 100644 --- a/client/src/app/shared/shared-moderation/account-blocklist.component.ts +++ b/client/src/app/shared/shared-moderation/account-blocklist.component.ts | |||
@@ -48,7 +48,7 @@ export class GenericAccountBlocklistComponent extends RestTable implements OnIni | |||
48 | ) | 48 | ) |
49 | } | 49 | } |
50 | 50 | ||
51 | protected reloadData () { | 51 | protected reloadDataInternal () { |
52 | const operation = this.mode === BlocklistComponentType.Account | 52 | const operation = this.mode === BlocklistComponentType.Account |
53 | ? this.blocklistService.getUserAccountBlocklist({ | 53 | ? this.blocklistService.getUserAccountBlocklist({ |
54 | pagination: this.pagination, | 54 | pagination: this.pagination, |
diff --git a/client/src/app/shared/shared-moderation/moderation.scss b/client/src/app/shared/shared-moderation/moderation.scss index eaf5a8250..7c1e308cf 100644 --- a/client/src/app/shared/shared-moderation/moderation.scss +++ b/client/src/app/shared/shared-moderation/moderation.scss | |||
@@ -40,10 +40,6 @@ | |||
40 | } | 40 | } |
41 | } | 41 | } |
42 | 42 | ||
43 | .chip { | ||
44 | @include chip; | ||
45 | } | ||
46 | |||
47 | my-action-dropdown.show { | 43 | my-action-dropdown.show { |
48 | ::ng-deep .dropdown-root { | 44 | ::ng-deep .dropdown-root { |
49 | display: block !important; | 45 | display: block !important; |
diff --git a/client/src/app/shared/shared-moderation/server-blocklist.component.scss b/client/src/app/shared/shared-moderation/server-blocklist.component.scss index e29668a23..1a6b0435f 100644 --- a/client/src/app/shared/shared-moderation/server-blocklist.component.scss +++ b/client/src/app/shared/shared-moderation/server-blocklist.component.scss | |||
@@ -24,7 +24,3 @@ a { | |||
24 | .block-button { | 24 | .block-button { |
25 | @include create-button; | 25 | @include create-button; |
26 | } | 26 | } |
27 | |||
28 | .chip { | ||
29 | @include chip; | ||
30 | } | ||
diff --git a/client/src/app/shared/shared-moderation/server-blocklist.component.ts b/client/src/app/shared/shared-moderation/server-blocklist.component.ts index 1ba7a1b4d..f1bcbd561 100644 --- a/client/src/app/shared/shared-moderation/server-blocklist.component.ts +++ b/client/src/app/shared/shared-moderation/server-blocklist.component.ts | |||
@@ -75,7 +75,7 @@ export class GenericServerBlocklistComponent extends RestTable implements OnInit | |||
75 | }) | 75 | }) |
76 | } | 76 | } |
77 | 77 | ||
78 | protected reloadData () { | 78 | protected reloadDataInternal () { |
79 | const operation = this.mode === BlocklistComponentType.Account | 79 | const operation = this.mode === BlocklistComponentType.Account |
80 | ? this.blocklistService.getUserServerBlocklist({ | 80 | ? this.blocklistService.getUserServerBlocklist({ |
81 | pagination: this.pagination, | 81 | pagination: this.pagination, |
diff --git a/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts b/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts index c69a45c25..50dccf862 100644 --- a/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts +++ b/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts | |||
@@ -105,7 +105,7 @@ export class UserModerationDropdownComponent implements OnInit, OnChanges { | |||
105 | const res = await this.confirmService.confirm(message, $localize`Delete ${user.username}`) | 105 | const res = await this.confirmService.confirm(message, $localize`Delete ${user.username}`) |
106 | if (res === false) return | 106 | if (res === false) return |
107 | 107 | ||
108 | this.userAdminService.removeUser(user) | 108 | this.userAdminService.removeUsers(user) |
109 | .subscribe({ | 109 | .subscribe({ |
110 | next: () => { | 110 | next: () => { |
111 | this.notifier.success($localize`User ${user.username} deleted.`) | 111 | this.notifier.success($localize`User ${user.username} deleted.`) |
diff --git a/client/src/app/shared/shared-users/index.ts b/client/src/app/shared/shared-users/index.ts index 20e60486d..95d90e49e 100644 --- a/client/src/app/shared/shared-users/index.ts +++ b/client/src/app/shared/shared-users/index.ts | |||
@@ -1,5 +1,4 @@ | |||
1 | export * from './user-admin.service' | 1 | export * from './user-admin.service' |
2 | export * from './user-signup.service' | ||
3 | export * from './two-factor.service' | 2 | export * from './two-factor.service' |
4 | 3 | ||
5 | export * from './shared-users.module' | 4 | export * from './shared-users.module' |
diff --git a/client/src/app/shared/shared-users/shared-users.module.ts b/client/src/app/shared/shared-users/shared-users.module.ts index 5a1675dc9..efffc6026 100644 --- a/client/src/app/shared/shared-users/shared-users.module.ts +++ b/client/src/app/shared/shared-users/shared-users.module.ts | |||
@@ -1,9 +1,7 @@ | |||
1 | |||
2 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
3 | import { SharedMainModule } from '../shared-main/shared-main.module' | 2 | import { SharedMainModule } from '../shared-main/shared-main.module' |
4 | import { TwoFactorService } from './two-factor.service' | 3 | import { TwoFactorService } from './two-factor.service' |
5 | import { UserAdminService } from './user-admin.service' | 4 | import { UserAdminService } from './user-admin.service' |
6 | import { UserSignupService } from './user-signup.service' | ||
7 | 5 | ||
8 | @NgModule({ | 6 | @NgModule({ |
9 | imports: [ | 7 | imports: [ |
@@ -15,7 +13,6 @@ import { UserSignupService } from './user-signup.service' | |||
15 | exports: [], | 13 | exports: [], |
16 | 14 | ||
17 | providers: [ | 15 | providers: [ |
18 | UserSignupService, | ||
19 | UserAdminService, | 16 | UserAdminService, |
20 | TwoFactorService | 17 | TwoFactorService |
21 | ] | 18 | ] |
diff --git a/client/src/app/shared/shared-users/user-admin.service.ts b/client/src/app/shared/shared-users/user-admin.service.ts index 0b04023a3..6224f0bd5 100644 --- a/client/src/app/shared/shared-users/user-admin.service.ts +++ b/client/src/app/shared/shared-users/user-admin.service.ts | |||
@@ -64,7 +64,7 @@ export class UserAdminService { | |||
64 | ) | 64 | ) |
65 | } | 65 | } |
66 | 66 | ||
67 | removeUser (usersArg: UserServerModel | UserServerModel[]) { | 67 | removeUsers (usersArg: UserServerModel | UserServerModel[]) { |
68 | const users = arrayify(usersArg) | 68 | const users = arrayify(usersArg) |
69 | 69 | ||
70 | return from(users) | 70 | return from(users) |
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.html b/client/src/app/shared/shared-video-miniature/video-miniature.component.html index 6fdf24b2d..227c12130 100644 --- a/client/src/app/shared/shared-video-miniature/video-miniature.component.html +++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.html | |||
@@ -53,8 +53,8 @@ | |||
53 | <ng-container *ngIf="displayOptions.state">{{ getStateLabel(video) }}</ng-container> | 53 | <ng-container *ngIf="displayOptions.state">{{ getStateLabel(video) }}</ng-container> |
54 | </div> | 54 | </div> |
55 | 55 | ||
56 | <div *ngIf="containedInPlaylists" class="video-contained-in-playlists"> | 56 | <div *ngIf="containedInPlaylists" class="fs-6"> |
57 | <a *ngFor="let playlist of containedInPlaylists" class="chip rectangular bg-secondary text-light" [routerLink]="['/w/p/', playlist.playlistShortUUID]"> | 57 | <a *ngFor="let playlist of containedInPlaylists" class="pt-badge badge-secondary" [routerLink]="['/w/p/', playlist.playlistShortUUID]"> |
58 | {{ playlist.playlistDisplayName }} | 58 | {{ playlist.playlistDisplayName }} |
59 | </a> | 59 | </a> |
60 | </div> | 60 | </div> |
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.scss b/client/src/app/shared/shared-video-miniature/video-miniature.component.scss index ba2adfc5a..a397efdca 100644 --- a/client/src/app/shared/shared-video-miniature/video-miniature.component.scss +++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.scss | |||
@@ -4,10 +4,6 @@ | |||
4 | 4 | ||
5 | $more-button-width: 40px; | 5 | $more-button-width: 40px; |
6 | 6 | ||
7 | .chip { | ||
8 | @include chip; | ||
9 | } | ||
10 | |||
11 | .video-miniature { | 7 | .video-miniature { |
12 | font-size: 14px; | 8 | font-size: 14px; |
13 | } | 9 | } |
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.ts b/client/src/app/shared/shared-video-miniature/video-miniature.component.ts index 85c63c173..706227e66 100644 --- a/client/src/app/shared/shared-video-miniature/video-miniature.component.ts +++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.ts | |||
@@ -314,6 +314,6 @@ export class VideoMiniatureComponent implements OnInit { | |||
314 | this.cd.markForCheck() | 314 | this.cd.markForCheck() |
315 | }) | 315 | }) |
316 | 316 | ||
317 | this.videoPlaylistService.runPlaylistCheck(this.video.id) | 317 | this.videoPlaylistService.runVideoExistsInPlaylistCheck(this.video.id) |
318 | } | 318 | } |
319 | } | 319 | } |
diff --git a/client/src/app/shared/shared-video-miniature/videos-list.component.ts b/client/src/app/shared/shared-video-miniature/videos-list.component.ts index d5cdd958e..7b832263e 100644 --- a/client/src/app/shared/shared-video-miniature/videos-list.component.ts +++ b/client/src/app/shared/shared-video-miniature/videos-list.component.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import * as debug from 'debug' | 1 | import * as debug from 'debug' |
2 | import { fromEvent, Observable, Subject, Subscription } from 'rxjs' | 2 | import { fromEvent, Observable, Subject, Subscription } from 'rxjs' |
3 | import { debounceTime, switchMap } from 'rxjs/operators' | 3 | import { concatMap, debounceTime, map, switchMap } from 'rxjs/operators' |
4 | import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core' | 4 | import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core' |
5 | import { ActivatedRoute } from '@angular/router' | 5 | import { ActivatedRoute } from '@angular/router' |
6 | import { | 6 | import { |
@@ -111,6 +111,8 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy { | |||
111 | 111 | ||
112 | private lastQueryLength: number | 112 | private lastQueryLength: number |
113 | 113 | ||
114 | private videoRequests = new Subject<{ reset: boolean, obs: Observable<ResultList<Video>> }>() | ||
115 | |||
114 | constructor ( | 116 | constructor ( |
115 | private notifier: Notifier, | 117 | private notifier: Notifier, |
116 | private authService: AuthService, | 118 | private authService: AuthService, |
@@ -124,6 +126,8 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy { | |||
124 | } | 126 | } |
125 | 127 | ||
126 | ngOnInit () { | 128 | ngOnInit () { |
129 | this.subscribeToVideoRequests() | ||
130 | |||
127 | const hiddenFilters = this.hideScopeFilter | 131 | const hiddenFilters = this.hideScopeFilter |
128 | ? [ 'scope' ] | 132 | ? [ 'scope' ] |
129 | : [] | 133 | : [] |
@@ -228,30 +232,12 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy { | |||
228 | } | 232 | } |
229 | 233 | ||
230 | loadMoreVideos (reset = false) { | 234 | loadMoreVideos (reset = false) { |
231 | if (reset) this.hasDoneFirstQuery = false | 235 | if (reset) { |
232 | 236 | this.hasDoneFirstQuery = false | |
233 | this.getVideosObservableFunction(this.pagination, this.filters) | 237 | this.videos = [] |
234 | .subscribe({ | 238 | } |
235 | next: ({ data }) => { | ||
236 | this.hasDoneFirstQuery = true | ||
237 | this.lastQueryLength = data.length | ||
238 | |||
239 | if (reset) this.videos = [] | ||
240 | this.videos = this.videos.concat(data) | ||
241 | |||
242 | if (this.groupByDate) this.buildGroupedDateLabels() | ||
243 | |||
244 | this.onDataSubject.next(data) | ||
245 | this.videosLoaded.emit(this.videos) | ||
246 | }, | ||
247 | |||
248 | error: err => { | ||
249 | const message = $localize`Cannot load more videos. Try again later.` | ||
250 | 239 | ||
251 | logger.error(message, err) | 240 | this.videoRequests.next({ reset, obs: this.getVideosObservableFunction(this.pagination, this.filters) }) |
252 | this.notifier.error(message) | ||
253 | } | ||
254 | }) | ||
255 | } | 241 | } |
256 | 242 | ||
257 | reloadVideos () { | 243 | reloadVideos () { |
@@ -423,4 +409,30 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy { | |||
423 | this.onFiltersChanged(true) | 409 | this.onFiltersChanged(true) |
424 | }) | 410 | }) |
425 | } | 411 | } |
412 | |||
413 | private subscribeToVideoRequests () { | ||
414 | this.videoRequests | ||
415 | .pipe(concatMap(({ reset, obs }) => obs.pipe(map(({ data }) => ({ data, reset }))))) | ||
416 | .subscribe({ | ||
417 | next: ({ data, reset }) => { | ||
418 | this.hasDoneFirstQuery = true | ||
419 | this.lastQueryLength = data.length | ||
420 | |||
421 | if (reset) this.videos = [] | ||
422 | this.videos = this.videos.concat(data) | ||
423 | |||
424 | if (this.groupByDate) this.buildGroupedDateLabels() | ||
425 | |||
426 | this.onDataSubject.next(data) | ||
427 | this.videosLoaded.emit(this.videos) | ||
428 | }, | ||
429 | |||
430 | error: err => { | ||
431 | const message = $localize`Cannot load more videos. Try again later.` | ||
432 | |||
433 | logger.error(message, err) | ||
434 | this.notifier.error(message) | ||
435 | } | ||
436 | }) | ||
437 | } | ||
426 | } | 438 | } |
diff --git a/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts b/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts index 2fc39fc75..f802416a4 100644 --- a/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts +++ b/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts | |||
@@ -81,7 +81,7 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit, | |||
81 | .subscribe(result => { | 81 | .subscribe(result => { |
82 | this.playlistsData = result.data | 82 | this.playlistsData = result.data |
83 | 83 | ||
84 | this.videoPlaylistService.runPlaylistCheck(this.video.id) | 84 | this.videoPlaylistService.runVideoExistsInPlaylistCheck(this.video.id) |
85 | }) | 85 | }) |
86 | 86 | ||
87 | this.videoPlaylistSearchChanged | 87 | this.videoPlaylistSearchChanged |
@@ -129,7 +129,7 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit, | |||
129 | .subscribe(playlistsResult => { | 129 | .subscribe(playlistsResult => { |
130 | this.playlistsData = playlistsResult.data | 130 | this.playlistsData = playlistsResult.data |
131 | 131 | ||
132 | this.videoPlaylistService.runPlaylistCheck(this.video.id) | 132 | this.videoPlaylistService.runVideoExistsInPlaylistCheck(this.video.id) |
133 | }) | 133 | }) |
134 | } | 134 | } |
135 | 135 | ||
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist.service.ts b/client/src/app/shared/shared-video-playlist/video-playlist.service.ts index 330a51f91..bc9fb0d74 100644 --- a/client/src/app/shared/shared-video-playlist/video-playlist.service.ts +++ b/client/src/app/shared/shared-video-playlist/video-playlist.service.ts | |||
@@ -206,7 +206,15 @@ export class VideoPlaylistService { | |||
206 | stopTimestamp: body.stopTimestamp | 206 | stopTimestamp: body.stopTimestamp |
207 | }) | 207 | }) |
208 | 208 | ||
209 | this.runPlaylistCheck(body.videoId) | 209 | this.runVideoExistsInPlaylistCheck(body.videoId) |
210 | |||
211 | if (this.myAccountPlaylistCache) { | ||
212 | const playlist = this.myAccountPlaylistCache.data.find(p => p.id === playlistId) | ||
213 | if (!playlist) return | ||
214 | |||
215 | const otherPlaylists = this.myAccountPlaylistCache.data.filter(p => p !== playlist) | ||
216 | this.myAccountPlaylistCache.data = [ playlist, ...otherPlaylists ] | ||
217 | } | ||
210 | }), | 218 | }), |
211 | catchError(err => this.restExtractor.handleError(err)) | 219 | catchError(err => this.restExtractor.handleError(err)) |
212 | ) | 220 | ) |
@@ -225,7 +233,7 @@ export class VideoPlaylistService { | |||
225 | elem.stopTimestamp = body.stopTimestamp | 233 | elem.stopTimestamp = body.stopTimestamp |
226 | } | 234 | } |
227 | 235 | ||
228 | this.runPlaylistCheck(videoId) | 236 | this.runVideoExistsInPlaylistCheck(videoId) |
229 | }), | 237 | }), |
230 | catchError(err => this.restExtractor.handleError(err)) | 238 | catchError(err => this.restExtractor.handleError(err)) |
231 | ) | 239 | ) |
@@ -242,7 +250,7 @@ export class VideoPlaylistService { | |||
242 | .filter(e => e.playlistElementId !== playlistElementId) | 250 | .filter(e => e.playlistElementId !== playlistElementId) |
243 | } | 251 | } |
244 | 252 | ||
245 | this.runPlaylistCheck(videoId) | 253 | this.runVideoExistsInPlaylistCheck(videoId) |
246 | }), | 254 | }), |
247 | catchError(err => this.restExtractor.handleError(err)) | 255 | catchError(err => this.restExtractor.handleError(err)) |
248 | ) | 256 | ) |
@@ -296,7 +304,7 @@ export class VideoPlaylistService { | |||
296 | return obs | 304 | return obs |
297 | } | 305 | } |
298 | 306 | ||
299 | runPlaylistCheck (videoId: number) { | 307 | runVideoExistsInPlaylistCheck (videoId: number) { |
300 | debugLogger('Running playlist check.') | 308 | debugLogger('Running playlist check.') |
301 | 309 | ||
302 | if (this.videoExistsCache[videoId]) { | 310 | if (this.videoExistsCache[videoId]) { |
diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts index 56310c4e9..2781850b9 100644 --- a/client/src/assets/player/peertube-player-manager.ts +++ b/client/src/assets/player/peertube-player-manager.ts | |||
@@ -11,6 +11,7 @@ import './shared/control-bar/p2p-info-button' | |||
11 | import './shared/control-bar/peertube-link-button' | 11 | import './shared/control-bar/peertube-link-button' |
12 | import './shared/control-bar/peertube-load-progress-bar' | 12 | import './shared/control-bar/peertube-load-progress-bar' |
13 | import './shared/control-bar/theater-button' | 13 | import './shared/control-bar/theater-button' |
14 | import './shared/control-bar/peertube-live-display' | ||
14 | import './shared/settings/resolution-menu-button' | 15 | import './shared/settings/resolution-menu-button' |
15 | import './shared/settings/resolution-menu-item' | 16 | import './shared/settings/resolution-menu-item' |
16 | import './shared/settings/settings-dialog' | 17 | import './shared/settings/settings-dialog' |
@@ -96,6 +97,10 @@ export class PeertubePlayerManager { | |||
96 | videojs(options.common.playerElement, videojsOptions, function (this: videojs.Player) { | 97 | videojs(options.common.playerElement, videojsOptions, function (this: videojs.Player) { |
97 | const player = this | 98 | const player = this |
98 | 99 | ||
100 | if (!isNaN(+options.common.playbackRate)) { | ||
101 | player.playbackRate(+options.common.playbackRate) | ||
102 | } | ||
103 | |||
99 | let alreadyFallback = false | 104 | let alreadyFallback = false |
100 | 105 | ||
101 | const handleError = () => { | 106 | const handleError = () => { |
@@ -118,7 +123,7 @@ export class PeertubePlayerManager { | |||
118 | self.addContextMenu(videojsOptionsBuilder, player, options.common) | 123 | self.addContextMenu(videojsOptionsBuilder, player, options.common) |
119 | 124 | ||
120 | if (isMobile()) player.peertubeMobile() | 125 | if (isMobile()) player.peertubeMobile() |
121 | if (options.common.enableHotkeys === true) player.peerTubeHotkeysPlugin() | 126 | if (options.common.enableHotkeys === true) player.peerTubeHotkeysPlugin({ isLive: options.common.isLive }) |
122 | if (options.common.controlBar === false) player.controlBar.addClass('control-bar-hidden') | 127 | if (options.common.controlBar === false) player.controlBar.addClass('control-bar-hidden') |
123 | 128 | ||
124 | player.bezels() | 129 | player.bezels() |
diff --git a/client/src/assets/player/shared/control-bar/index.ts b/client/src/assets/player/shared/control-bar/index.ts index db5b8938d..e71e90713 100644 --- a/client/src/assets/player/shared/control-bar/index.ts +++ b/client/src/assets/player/shared/control-bar/index.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | export * from './next-previous-video-button' | 1 | export * from './next-previous-video-button' |
2 | export * from './p2p-info-button' | 2 | export * from './p2p-info-button' |
3 | export * from './peertube-link-button' | 3 | export * from './peertube-link-button' |
4 | export * from './peertube-live-display' | ||
4 | export * from './peertube-load-progress-bar' | 5 | export * from './peertube-load-progress-bar' |
5 | export * from './theater-button' | 6 | export * from './theater-button' |
diff --git a/client/src/assets/player/shared/control-bar/peertube-live-display.ts b/client/src/assets/player/shared/control-bar/peertube-live-display.ts new file mode 100644 index 000000000..649eb0b00 --- /dev/null +++ b/client/src/assets/player/shared/control-bar/peertube-live-display.ts | |||
@@ -0,0 +1,93 @@ | |||
1 | import videojs from 'video.js' | ||
2 | import { PeerTubeLinkButtonOptions } from '../../types' | ||
3 | |||
4 | const ClickableComponent = videojs.getComponent('ClickableComponent') | ||
5 | |||
6 | class PeerTubeLiveDisplay extends ClickableComponent { | ||
7 | private interval: any | ||
8 | |||
9 | private contentEl_: any | ||
10 | |||
11 | constructor (player: videojs.Player, options?: PeerTubeLinkButtonOptions) { | ||
12 | super(player, options as any) | ||
13 | |||
14 | this.interval = this.setInterval(() => this.updateClass(), 1000) | ||
15 | |||
16 | this.show() | ||
17 | this.updateSync(true) | ||
18 | } | ||
19 | |||
20 | dispose () { | ||
21 | if (this.interval) { | ||
22 | this.clearInterval(this.interval) | ||
23 | this.interval = undefined | ||
24 | } | ||
25 | |||
26 | this.contentEl_ = null | ||
27 | |||
28 | super.dispose() | ||
29 | } | ||
30 | |||
31 | createEl () { | ||
32 | const el = super.createEl('div', { | ||
33 | className: 'vjs-live-control vjs-control' | ||
34 | }) | ||
35 | |||
36 | this.contentEl_ = videojs.dom.createEl('div', { | ||
37 | className: 'vjs-live-display' | ||
38 | }, { | ||
39 | 'aria-live': 'off' | ||
40 | }) | ||
41 | |||
42 | this.contentEl_.appendChild(videojs.dom.createEl('span', { | ||
43 | className: 'vjs-control-text', | ||
44 | textContent: `${this.localize('Stream Type')}\u00a0` | ||
45 | })) | ||
46 | |||
47 | this.contentEl_.appendChild(document.createTextNode(this.localize('LIVE'))) | ||
48 | |||
49 | el.appendChild(this.contentEl_) | ||
50 | return el | ||
51 | } | ||
52 | |||
53 | handleClick () { | ||
54 | const hlsjs = this.getHLSJS() | ||
55 | if (!hlsjs) return | ||
56 | |||
57 | this.player().currentTime(hlsjs.liveSyncPosition) | ||
58 | this.player().play() | ||
59 | this.updateSync(true) | ||
60 | } | ||
61 | |||
62 | private updateClass () { | ||
63 | const hlsjs = this.getHLSJS() | ||
64 | if (!hlsjs) return | ||
65 | |||
66 | // Not loaded yet | ||
67 | if (this.player().currentTime() === 0) return | ||
68 | |||
69 | const isSync = Math.abs(this.player().currentTime() - hlsjs.liveSyncPosition) < 10 | ||
70 | this.updateSync(isSync) | ||
71 | } | ||
72 | |||
73 | private updateSync (isSync: boolean) { | ||
74 | if (isSync) { | ||
75 | this.addClass('synced-with-live-edge') | ||
76 | this.removeAttribute('title') | ||
77 | this.disable() | ||
78 | } else { | ||
79 | this.removeClass('synced-with-live-edge') | ||
80 | this.setAttribute('title', this.localize('Go back to the live')) | ||
81 | this.enable() | ||
82 | } | ||
83 | } | ||
84 | |||
85 | private getHLSJS () { | ||
86 | const p2pMediaLoader = this.player()?.p2pMediaLoader | ||
87 | if (!p2pMediaLoader) return undefined | ||
88 | |||
89 | return p2pMediaLoader().getHLSJS() | ||
90 | } | ||
91 | } | ||
92 | |||
93 | videojs.registerComponent('PeerTubeLiveDisplay', PeerTubeLiveDisplay) | ||
diff --git a/client/src/assets/player/shared/hotkeys/peertube-hotkeys-plugin.ts b/client/src/assets/player/shared/hotkeys/peertube-hotkeys-plugin.ts index ec1e1038b..f5b4b3919 100644 --- a/client/src/assets/player/shared/hotkeys/peertube-hotkeys-plugin.ts +++ b/client/src/assets/player/shared/hotkeys/peertube-hotkeys-plugin.ts | |||
@@ -4,6 +4,10 @@ type KeyHandler = { accept: (event: KeyboardEvent) => boolean, cb: (e: KeyboardE | |||
4 | 4 | ||
5 | const Plugin = videojs.getPlugin('plugin') | 5 | const Plugin = videojs.getPlugin('plugin') |
6 | 6 | ||
7 | export type HotkeysOptions = { | ||
8 | isLive: boolean | ||
9 | } | ||
10 | |||
7 | class PeerTubeHotkeysPlugin extends Plugin { | 11 | class PeerTubeHotkeysPlugin extends Plugin { |
8 | private static readonly VOLUME_STEP = 0.1 | 12 | private static readonly VOLUME_STEP = 0.1 |
9 | private static readonly SEEK_STEP = 5 | 13 | private static readonly SEEK_STEP = 5 |
@@ -12,9 +16,13 @@ class PeerTubeHotkeysPlugin extends Plugin { | |||
12 | 16 | ||
13 | private readonly handlers: KeyHandler[] | 17 | private readonly handlers: KeyHandler[] |
14 | 18 | ||
15 | constructor (player: videojs.Player, options: videojs.PlayerOptions) { | 19 | private readonly isLive: boolean |
20 | |||
21 | constructor (player: videojs.Player, options: videojs.PlayerOptions & HotkeysOptions) { | ||
16 | super(player, options) | 22 | super(player, options) |
17 | 23 | ||
24 | this.isLive = options.isLive | ||
25 | |||
18 | this.handlers = this.buildHandlers() | 26 | this.handlers = this.buildHandlers() |
19 | 27 | ||
20 | this.handleKeyFunction = (event: KeyboardEvent) => this.onKeyDown(event) | 28 | this.handleKeyFunction = (event: KeyboardEvent) => this.onKeyDown(event) |
@@ -68,28 +76,6 @@ class PeerTubeHotkeysPlugin extends Plugin { | |||
68 | } | 76 | } |
69 | }, | 77 | }, |
70 | 78 | ||
71 | // Rewind | ||
72 | { | ||
73 | accept: e => this.isNaked(e, 'ArrowLeft') || this.isNaked(e, 'MediaRewind'), | ||
74 | cb: e => { | ||
75 | e.preventDefault() | ||
76 | |||
77 | const target = Math.max(0, this.player.currentTime() - PeerTubeHotkeysPlugin.SEEK_STEP) | ||
78 | this.player.currentTime(target) | ||
79 | } | ||
80 | }, | ||
81 | |||
82 | // Forward | ||
83 | { | ||
84 | accept: e => this.isNaked(e, 'ArrowRight') || this.isNaked(e, 'MediaForward'), | ||
85 | cb: e => { | ||
86 | e.preventDefault() | ||
87 | |||
88 | const target = Math.min(this.player.duration(), this.player.currentTime() + PeerTubeHotkeysPlugin.SEEK_STEP) | ||
89 | this.player.currentTime(target) | ||
90 | } | ||
91 | }, | ||
92 | |||
93 | // Fullscreen | 79 | // Fullscreen |
94 | { | 80 | { |
95 | // f key or Ctrl + Enter | 81 | // f key or Ctrl + Enter |
@@ -116,6 +102,8 @@ class PeerTubeHotkeysPlugin extends Plugin { | |||
116 | { | 102 | { |
117 | accept: e => e.key === '>', | 103 | accept: e => e.key === '>', |
118 | cb: () => { | 104 | cb: () => { |
105 | if (this.isLive) return | ||
106 | |||
119 | const target = Math.min(this.player.playbackRate() + 0.1, 5) | 107 | const target = Math.min(this.player.playbackRate() + 0.1, 5) |
120 | 108 | ||
121 | this.player.playbackRate(parseFloat(target.toFixed(2))) | 109 | this.player.playbackRate(parseFloat(target.toFixed(2))) |
@@ -126,6 +114,8 @@ class PeerTubeHotkeysPlugin extends Plugin { | |||
126 | { | 114 | { |
127 | accept: e => e.key === '<', | 115 | accept: e => e.key === '<', |
128 | cb: () => { | 116 | cb: () => { |
117 | if (this.isLive) return | ||
118 | |||
129 | const target = Math.max(this.player.playbackRate() - 0.1, 0.10) | 119 | const target = Math.max(this.player.playbackRate() - 0.1, 0.10) |
130 | 120 | ||
131 | this.player.playbackRate(parseFloat(target.toFixed(2))) | 121 | this.player.playbackRate(parseFloat(target.toFixed(2))) |
@@ -136,6 +126,8 @@ class PeerTubeHotkeysPlugin extends Plugin { | |||
136 | { | 126 | { |
137 | accept: e => e.key === ',', | 127 | accept: e => e.key === ',', |
138 | cb: () => { | 128 | cb: () => { |
129 | if (this.isLive) return | ||
130 | |||
139 | this.player.pause() | 131 | this.player.pause() |
140 | 132 | ||
141 | // Calculate movement distance (assuming 30 fps) | 133 | // Calculate movement distance (assuming 30 fps) |
@@ -148,6 +140,8 @@ class PeerTubeHotkeysPlugin extends Plugin { | |||
148 | { | 140 | { |
149 | accept: e => e.key === '.', | 141 | accept: e => e.key === '.', |
150 | cb: () => { | 142 | cb: () => { |
143 | if (this.isLive) return | ||
144 | |||
151 | this.player.pause() | 145 | this.player.pause() |
152 | 146 | ||
153 | // Calculate movement distance (assuming 30 fps) | 147 | // Calculate movement distance (assuming 30 fps) |
@@ -157,11 +151,47 @@ class PeerTubeHotkeysPlugin extends Plugin { | |||
157 | } | 151 | } |
158 | ] | 152 | ] |
159 | 153 | ||
154 | if (this.isLive) return handlers | ||
155 | |||
156 | return handlers.concat(this.buildVODHandlers()) | ||
157 | } | ||
158 | |||
159 | private buildVODHandlers () { | ||
160 | const handlers: KeyHandler[] = [ | ||
161 | // Rewind | ||
162 | { | ||
163 | accept: e => this.isNaked(e, 'ArrowLeft') || this.isNaked(e, 'MediaRewind'), | ||
164 | cb: e => { | ||
165 | if (this.isLive) return | ||
166 | |||
167 | e.preventDefault() | ||
168 | |||
169 | const target = Math.max(0, this.player.currentTime() - PeerTubeHotkeysPlugin.SEEK_STEP) | ||
170 | this.player.currentTime(target) | ||
171 | } | ||
172 | }, | ||
173 | |||
174 | // Forward | ||
175 | { | ||
176 | accept: e => this.isNaked(e, 'ArrowRight') || this.isNaked(e, 'MediaForward'), | ||
177 | cb: e => { | ||
178 | if (this.isLive) return | ||
179 | |||
180 | e.preventDefault() | ||
181 | |||
182 | const target = Math.min(this.player.duration(), this.player.currentTime() + PeerTubeHotkeysPlugin.SEEK_STEP) | ||
183 | this.player.currentTime(target) | ||
184 | } | ||
185 | } | ||
186 | ] | ||
187 | |||
160 | // 0-9 key handlers | 188 | // 0-9 key handlers |
161 | for (let i = 0; i < 10; i++) { | 189 | for (let i = 0; i < 10; i++) { |
162 | handlers.push({ | 190 | handlers.push({ |
163 | accept: e => this.isNakedOrShift(e, i + ''), | 191 | accept: e => this.isNakedOrShift(e, i + ''), |
164 | cb: e => { | 192 | cb: e => { |
193 | if (this.isLive) return | ||
194 | |||
165 | e.preventDefault() | 195 | e.preventDefault() |
166 | 196 | ||
167 | this.player.currentTime(this.player.duration() * i * 0.1) | 197 | this.player.currentTime(this.player.duration() * i * 0.1) |
diff --git a/client/src/assets/player/shared/manager-options/control-bar-options-builder.ts b/client/src/assets/player/shared/manager-options/control-bar-options-builder.ts index 27f366732..26f923e92 100644 --- a/client/src/assets/player/shared/manager-options/control-bar-options-builder.ts +++ b/client/src/assets/player/shared/manager-options/control-bar-options-builder.ts | |||
@@ -30,10 +30,7 @@ export class ControlBarOptionsBuilder { | |||
30 | } | 30 | } |
31 | 31 | ||
32 | Object.assign(children, { | 32 | Object.assign(children, { |
33 | currentTimeDisplay: {}, | 33 | ...this.getTimeControls(), |
34 | timeDivider: {}, | ||
35 | durationDisplay: {}, | ||
36 | liveDisplay: {}, | ||
37 | 34 | ||
38 | flexibleWidthSpacer: {}, | 35 | flexibleWidthSpacer: {}, |
39 | 36 | ||
@@ -74,7 +71,9 @@ export class ControlBarOptionsBuilder { | |||
74 | private getSettingsButton () { | 71 | private getSettingsButton () { |
75 | const settingEntries: string[] = [] | 72 | const settingEntries: string[] = [] |
76 | 73 | ||
77 | settingEntries.push('playbackRateMenuButton') | 74 | if (!this.options.isLive) { |
75 | settingEntries.push('playbackRateMenuButton') | ||
76 | } | ||
78 | 77 | ||
79 | if (this.options.captions === true) settingEntries.push('captionsButton') | 78 | if (this.options.captions === true) settingEntries.push('captionsButton') |
80 | 79 | ||
@@ -90,7 +89,23 @@ export class ControlBarOptionsBuilder { | |||
90 | } | 89 | } |
91 | } | 90 | } |
92 | 91 | ||
92 | private getTimeControls () { | ||
93 | if (this.options.isLive) { | ||
94 | return { | ||
95 | peerTubeLiveDisplay: {} | ||
96 | } | ||
97 | } | ||
98 | |||
99 | return { | ||
100 | currentTimeDisplay: {}, | ||
101 | timeDivider: {}, | ||
102 | durationDisplay: {} | ||
103 | } | ||
104 | } | ||
105 | |||
93 | private getProgressControl () { | 106 | private getProgressControl () { |
107 | if (this.options.isLive) return {} | ||
108 | |||
94 | const loadProgressBar = this.mode === 'webtorrent' | 109 | const loadProgressBar = this.mode === 'webtorrent' |
95 | ? 'peerTubeLoadProgressBar' | 110 | ? 'peerTubeLoadProgressBar' |
96 | : 'loadProgressBar' | 111 | : 'loadProgressBar' |
diff --git a/client/src/assets/player/shared/p2p-media-loader/hls-plugin.ts b/client/src/assets/player/shared/p2p-media-loader/hls-plugin.ts index a14beb347..7f7d90ab9 100644 --- a/client/src/assets/player/shared/p2p-media-loader/hls-plugin.ts +++ b/client/src/assets/player/shared/p2p-media-loader/hls-plugin.ts | |||
@@ -281,8 +281,8 @@ class Html5Hlsjs { | |||
281 | if (this.errorCounts[data.type]) this.errorCounts[data.type] += 1 | 281 | if (this.errorCounts[data.type]) this.errorCounts[data.type] += 1 |
282 | else this.errorCounts[data.type] = 1 | 282 | else this.errorCounts[data.type] = 1 |
283 | 283 | ||
284 | if (data.fatal) logger.warn(error.message) | 284 | if (data.fatal) logger.error(error.message, { currentTime: this.player.currentTime(), data }) |
285 | else logger.error(error.message, { data }) | 285 | else logger.warn(error.message) |
286 | 286 | ||
287 | if (data.type === Hlsjs.ErrorTypes.NETWORK_ERROR) { | 287 | if (data.type === Hlsjs.ErrorTypes.NETWORK_ERROR) { |
288 | error.code = 2 | 288 | error.code = 2 |
diff --git a/client/src/assets/player/shared/stats/stats-card.ts b/client/src/assets/player/shared/stats/stats-card.ts index f23ae48be..471a5e46c 100644 --- a/client/src/assets/player/shared/stats/stats-card.ts +++ b/client/src/assets/player/shared/stats/stats-card.ts | |||
@@ -182,7 +182,7 @@ class StatsCard extends Component { | |||
182 | let colorSpace = 'unknown' | 182 | let colorSpace = 'unknown' |
183 | let codecs = 'unknown' | 183 | let codecs = 'unknown' |
184 | 184 | ||
185 | if (metadata?.streams[0]) { | 185 | if (metadata?.streams?.[0]) { |
186 | const stream = metadata.streams[0] | 186 | const stream = metadata.streams[0] |
187 | 187 | ||
188 | colorSpace = stream['color_space'] !== 'unknown' | 188 | colorSpace = stream['color_space'] !== 'unknown' |
@@ -193,7 +193,7 @@ class StatsCard extends Component { | |||
193 | } | 193 | } |
194 | 194 | ||
195 | const resolution = videoFile?.resolution.label + videoFile?.fps | 195 | const resolution = videoFile?.resolution.label + videoFile?.fps |
196 | const buffer = this.timeRangesToString(this.player().buffered()) | 196 | const buffer = this.timeRangesToString(this.player_.buffered()) |
197 | const progress = this.player_.webtorrent().getTorrent()?.progress | 197 | const progress = this.player_.webtorrent().getTorrent()?.progress |
198 | 198 | ||
199 | return { | 199 | return { |
diff --git a/client/src/assets/player/types/manager-options.ts b/client/src/assets/player/types/manager-options.ts index 3057a5adb..3fbcec29c 100644 --- a/client/src/assets/player/types/manager-options.ts +++ b/client/src/assets/player/types/manager-options.ts | |||
@@ -29,6 +29,8 @@ export interface CustomizationOptions { | |||
29 | resume?: string | 29 | resume?: string |
30 | 30 | ||
31 | peertubeLink: boolean | 31 | peertubeLink: boolean |
32 | |||
33 | playbackRate?: number | string | ||
32 | } | 34 | } |
33 | 35 | ||
34 | export interface CommonOptions extends CustomizationOptions { | 36 | export interface CommonOptions extends CustomizationOptions { |
diff --git a/client/src/assets/player/types/peertube-videojs-typings.ts b/client/src/assets/player/types/peertube-videojs-typings.ts index c60154f3b..5674f78cb 100644 --- a/client/src/assets/player/types/peertube-videojs-typings.ts +++ b/client/src/assets/player/types/peertube-videojs-typings.ts | |||
@@ -3,6 +3,7 @@ import videojs from 'video.js' | |||
3 | import { Engine } from '@peertube/p2p-media-loader-hlsjs' | 3 | import { Engine } from '@peertube/p2p-media-loader-hlsjs' |
4 | import { VideoFile, VideoPlaylist, VideoPlaylistElement } from '@shared/models' | 4 | import { VideoFile, VideoPlaylist, VideoPlaylistElement } from '@shared/models' |
5 | import { PeerTubeDockPluginOptions } from '../shared/dock/peertube-dock-plugin' | 5 | import { PeerTubeDockPluginOptions } from '../shared/dock/peertube-dock-plugin' |
6 | import { HotkeysOptions } from '../shared/hotkeys/peertube-hotkeys-plugin' | ||
6 | import { Html5Hlsjs } from '../shared/p2p-media-loader/hls-plugin' | 7 | import { Html5Hlsjs } from '../shared/p2p-media-loader/hls-plugin' |
7 | import { P2pMediaLoaderPlugin } from '../shared/p2p-media-loader/p2p-media-loader-plugin' | 8 | import { P2pMediaLoaderPlugin } from '../shared/p2p-media-loader/p2p-media-loader-plugin' |
8 | import { RedundancyUrlManager } from '../shared/p2p-media-loader/redundancy-url-manager' | 9 | import { RedundancyUrlManager } from '../shared/p2p-media-loader/redundancy-url-manager' |
@@ -44,7 +45,7 @@ declare module 'video.js' { | |||
44 | 45 | ||
45 | bezels (): void | 46 | bezels (): void |
46 | peertubeMobile (): void | 47 | peertubeMobile (): void |
47 | peerTubeHotkeysPlugin (): void | 48 | peerTubeHotkeysPlugin (options?: HotkeysOptions): void |
48 | 49 | ||
49 | stats (options?: StatsCardOptions): StatsForNerdsPlugin | 50 | stats (options?: StatsCardOptions): StatsForNerdsPlugin |
50 | 51 | ||
diff --git a/client/src/root-helpers/logger.ts b/client/src/root-helpers/logger.ts index d1fdf73aa..618be62cd 100644 --- a/client/src/root-helpers/logger.ts +++ b/client/src/root-helpers/logger.ts | |||
@@ -27,6 +27,10 @@ class Logger { | |||
27 | warn (message: LoggerMessage, meta?: LoggerMeta) { | 27 | warn (message: LoggerMessage, meta?: LoggerMeta) { |
28 | this.runHooks('warn', message, meta) | 28 | this.runHooks('warn', message, meta) |
29 | 29 | ||
30 | this.clientWarn(message, meta) | ||
31 | } | ||
32 | |||
33 | clientWarn (message: LoggerMessage, meta?: LoggerMeta) { | ||
30 | if (meta) console.warn(message, meta) | 34 | if (meta) console.warn(message, meta) |
31 | else console.warn(message) | 35 | else console.warn(message) |
32 | } | 36 | } |
@@ -34,6 +38,10 @@ class Logger { | |||
34 | error (message: LoggerMessage, meta?: LoggerMeta) { | 38 | error (message: LoggerMessage, meta?: LoggerMeta) { |
35 | this.runHooks('error', message, meta) | 39 | this.runHooks('error', message, meta) |
36 | 40 | ||
41 | this.clientError(message, meta) | ||
42 | } | ||
43 | |||
44 | clientError (message: LoggerMessage, meta?: LoggerMeta) { | ||
37 | if (meta) console.error(message, meta) | 45 | if (meta) console.error(message, meta) |
38 | else console.error(message) | 46 | else console.error(message) |
39 | } | 47 | } |
diff --git a/client/src/root-helpers/plugins-manager.ts b/client/src/root-helpers/plugins-manager.ts index 6c64e2b01..e5b06a94c 100644 --- a/client/src/root-helpers/plugins-manager.ts +++ b/client/src/root-helpers/plugins-manager.ts | |||
@@ -3,7 +3,7 @@ import * as debug from 'debug' | |||
3 | import { firstValueFrom, ReplaySubject } from 'rxjs' | 3 | import { firstValueFrom, ReplaySubject } from 'rxjs' |
4 | import { first, shareReplay } from 'rxjs/operators' | 4 | import { first, shareReplay } from 'rxjs/operators' |
5 | import { RegisterClientHelpers } from 'src/types/register-client-option.model' | 5 | import { RegisterClientHelpers } from 'src/types/register-client-option.model' |
6 | import { getHookType, internalRunHook } from '@shared/core-utils/plugins/hooks' | 6 | import { getExternalAuthHref, getHookType, internalRunHook } from '@shared/core-utils/plugins/hooks' |
7 | import { | 7 | import { |
8 | ClientHookName, | 8 | ClientHookName, |
9 | clientHookObject, | 9 | clientHookObject, |
@@ -16,7 +16,6 @@ import { | |||
16 | RegisterClientRouteOptions, | 16 | RegisterClientRouteOptions, |
17 | RegisterClientSettingsScriptOptions, | 17 | RegisterClientSettingsScriptOptions, |
18 | RegisterClientVideoFieldOptions, | 18 | RegisterClientVideoFieldOptions, |
19 | RegisteredExternalAuthConfig, | ||
20 | ServerConfigPlugin | 19 | ServerConfigPlugin |
21 | } from '@shared/models' | 20 | } from '@shared/models' |
22 | import { environment } from '../environments/environment' | 21 | import { environment } from '../environments/environment' |
@@ -94,9 +93,13 @@ class PluginsManager { | |||
94 | return isTheme ? '/themes' : '/plugins' | 93 | return isTheme ? '/themes' : '/plugins' |
95 | } | 94 | } |
96 | 95 | ||
97 | static getExternalAuthHref (auth: RegisteredExternalAuthConfig) { | 96 | static getDefaultLoginHref (apiUrl: string, serverConfig: HTMLServerConfig) { |
98 | return environment.apiUrl + `/plugins/${auth.name}/${auth.version}/auth/${auth.authName}` | 97 | if (!serverConfig || serverConfig.client.menu.login.redirectOnSingleExternalAuth !== true) return undefined |
99 | 98 | ||
99 | const externalAuths = serverConfig.plugin.registeredExternalAuths | ||
100 | if (externalAuths.length !== 1) return undefined | ||
101 | |||
102 | return getExternalAuthHref(apiUrl, externalAuths[0]) | ||
100 | } | 103 | } |
101 | 104 | ||
102 | loadPluginsList (config: HTMLServerConfig) { | 105 | loadPluginsList (config: HTMLServerConfig) { |
diff --git a/client/src/sass/class-helpers.scss b/client/src/sass/class-helpers.scss index bc965331a..feb3a6de2 100644 --- a/client/src/sass/class-helpers.scss +++ b/client/src/sass/class-helpers.scss | |||
@@ -284,3 +284,9 @@ label + .form-group-description { | |||
284 | border: 2px solid pvar(--mainColorLightest); | 284 | border: 2px solid pvar(--mainColorLightest); |
285 | } | 285 | } |
286 | } | 286 | } |
287 | |||
288 | // --------------------------------------------------------------------------- | ||
289 | |||
290 | .chip { | ||
291 | @include chip; | ||
292 | } | ||
diff --git a/client/src/sass/include/_badges.scss b/client/src/sass/include/_badges.scss index 4bc70d4a9..7efd2fb81 100644 --- a/client/src/sass/include/_badges.scss +++ b/client/src/sass/include/_badges.scss | |||
@@ -9,6 +9,10 @@ | |||
9 | font-weight: $font-semibold; | 9 | font-weight: $font-semibold; |
10 | line-height: 1.1; | 10 | line-height: 1.1; |
11 | 11 | ||
12 | &.badge-fs-normal { | ||
13 | font-size: 100%; | ||
14 | } | ||
15 | |||
12 | &.badge-primary { | 16 | &.badge-primary { |
13 | color: pvar(--mainBackgroundColor); | 17 | color: pvar(--mainBackgroundColor); |
14 | background-color: pvar(--mainColor); | 18 | background-color: pvar(--mainColor); |
diff --git a/client/src/sass/include/_fonts.scss b/client/src/sass/include/_fonts.scss index e5a40af34..514261d01 100644 --- a/client/src/sass/include/_fonts.scss +++ b/client/src/sass/include/_fonts.scss | |||
@@ -15,7 +15,3 @@ | |||
15 | font-display: swap; | 15 | font-display: swap; |
16 | src: url('../fonts/source-sans/WOFF2/VAR/SourceSans3VF-Italic.ttf.woff2') format('woff2'); | 16 | src: url('../fonts/source-sans/WOFF2/VAR/SourceSans3VF-Italic.ttf.woff2') format('woff2'); |
17 | } | 17 | } |
18 | |||
19 | @mixin muted { | ||
20 | color: pvar(--greyForegroundColor) !important; | ||
21 | } | ||
diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss index b5ccb6598..8816437d9 100644 --- a/client/src/sass/include/_mixins.scss +++ b/client/src/sass/include/_mixins.scss | |||
@@ -36,6 +36,10 @@ | |||
36 | max-height: $font-size * $number-of-lines; | 36 | max-height: $font-size * $number-of-lines; |
37 | } | 37 | } |
38 | 38 | ||
39 | @mixin muted { | ||
40 | color: pvar(--greyForegroundColor) !important; | ||
41 | } | ||
42 | |||
39 | @mixin fade-text ($fade-after, $background-color) { | 43 | @mixin fade-text ($fade-after, $background-color) { |
40 | position: relative; | 44 | position: relative; |
41 | overflow: hidden; | 45 | overflow: hidden; |
@@ -791,51 +795,39 @@ | |||
791 | } | 795 | } |
792 | 796 | ||
793 | @mixin chip { | 797 | @mixin chip { |
794 | --chip-radius: 5rem; | 798 | --avatar-size: 1.2rem; |
795 | --chip-padding: .2rem .4rem; | ||
796 | $avatar-height: 1.2rem; | ||
797 | 799 | ||
798 | align-items: center; | ||
799 | border-radius: var(--chip-radius); | ||
800 | display: inline-flex; | 800 | display: inline-flex; |
801 | font-size: 90%; | ||
802 | color: pvar(--mainForegroundColor); | 801 | color: pvar(--mainForegroundColor); |
803 | height: $avatar-height; | 802 | height: var(--avatar-size); |
804 | line-height: 1rem; | ||
805 | margin: .1rem; | ||
806 | max-width: 320px; | 803 | max-width: 320px; |
807 | overflow: hidden; | 804 | overflow: hidden; |
808 | padding: var(--chip-padding); | ||
809 | text-decoration: none; | 805 | text-decoration: none; |
810 | text-overflow: ellipsis; | 806 | text-overflow: ellipsis; |
811 | vertical-align: middle; | 807 | vertical-align: middle; |
812 | white-space: nowrap; | 808 | white-space: nowrap; |
813 | 809 | ||
814 | &.rectangular { | ||
815 | --chip-radius: .2rem; | ||
816 | --chip-padding: .2rem .3rem; | ||
817 | } | ||
818 | |||
819 | my-actor-avatar { | 810 | my-actor-avatar { |
820 | @include margin-left(-.4rem); | ||
821 | @include margin-right(.2rem); | 811 | @include margin-right(.2rem); |
812 | |||
813 | border-radius: 5rem; | ||
814 | width: var(--avatar-size); | ||
815 | height: var(--avatar-size); | ||
822 | } | 816 | } |
823 | 817 | ||
824 | &.two-lines { | 818 | &.two-lines { |
825 | $avatar-height: 2rem; | 819 | --avatar-size: 2rem; |
826 | 820 | ||
827 | height: $avatar-height; | 821 | font-size: 14px; |
822 | line-height: 1rem; | ||
828 | 823 | ||
829 | my-actor-avatar { | 824 | my-actor-avatar { |
830 | display: inline-block; | 825 | display: inline-block; |
831 | } | 826 | } |
832 | 827 | ||
833 | div { | 828 | > div { |
834 | margin: 0 .1rem; | ||
835 | |||
836 | display: flex; | 829 | display: flex; |
837 | flex-direction: column; | 830 | flex-direction: column; |
838 | height: $avatar-height; | ||
839 | justify-content: center; | 831 | justify-content: center; |
840 | } | 832 | } |
841 | } | 833 | } |
diff --git a/client/src/sass/player/control-bar.scss b/client/src/sass/player/control-bar.scss index 0082378e4..96b3adf66 100644 --- a/client/src/sass/player/control-bar.scss +++ b/client/src/sass/player/control-bar.scss | |||
@@ -153,8 +153,25 @@ | |||
153 | } | 153 | } |
154 | 154 | ||
155 | .vjs-live-control { | 155 | .vjs-live-control { |
156 | line-height: $control-bar-height; | 156 | padding: 5px 7px; |
157 | min-width: 4em; | 157 | border-radius: 3px; |
158 | height: fit-content; | ||
159 | margin: auto 10px; | ||
160 | font-weight: bold; | ||
161 | max-width: fit-content; | ||
162 | opacity: 1 !important; | ||
163 | line-height: normal; | ||
164 | position: relative; | ||
165 | top: -1px; | ||
166 | |||
167 | &.synced-with-live-edge { | ||
168 | background: #d7281c; | ||
169 | } | ||
170 | |||
171 | &:not(.synced-with-live-edge) { | ||
172 | cursor: pointer; | ||
173 | background: #80807f; | ||
174 | } | ||
158 | } | 175 | } |
159 | 176 | ||
160 | .vjs-peertube { | 177 | .vjs-peertube { |
diff --git a/client/src/sass/primeng-custom.scss b/client/src/sass/primeng-custom.scss index 88f6efb6a..ee66a9db3 100644 --- a/client/src/sass/primeng-custom.scss +++ b/client/src/sass/primeng-custom.scss | |||
@@ -294,6 +294,7 @@ body .p-datepicker .p-datepicker-header .p-datepicker-title select:focus { | |||
294 | body .p-datepicker table { | 294 | body .p-datepicker table { |
295 | font-size: 14px; | 295 | font-size: 14px; |
296 | margin: 0.857em 0 0 0; | 296 | margin: 0.857em 0 0 0; |
297 | table-layout: fixed; | ||
297 | } | 298 | } |
298 | body .p-datepicker table th { | 299 | body .p-datepicker table th { |
299 | padding: 0.5em; | 300 | padding: 0.5em; |
diff --git a/client/src/standalone/videos/shared/player-manager-options.ts b/client/src/standalone/videos/shared/player-manager-options.ts index b0bdb2dd9..f09c86d14 100644 --- a/client/src/standalone/videos/shared/player-manager-options.ts +++ b/client/src/standalone/videos/shared/player-manager-options.ts | |||
@@ -38,6 +38,7 @@ export class PlayerManagerOptions { | |||
38 | private enableApi = false | 38 | private enableApi = false |
39 | private startTime: number | string = 0 | 39 | private startTime: number | string = 0 |
40 | private stopTime: number | string | 40 | private stopTime: number | string |
41 | private playbackRate: number | string | ||
41 | 42 | ||
42 | private title: boolean | 43 | private title: boolean |
43 | private warningTitle: boolean | 44 | private warningTitle: boolean |
@@ -130,6 +131,7 @@ export class PlayerManagerOptions { | |||
130 | this.subtitle = getParamString(params, 'subtitle') | 131 | this.subtitle = getParamString(params, 'subtitle') |
131 | this.startTime = getParamString(params, 'start') | 132 | this.startTime = getParamString(params, 'start') |
132 | this.stopTime = getParamString(params, 'stop') | 133 | this.stopTime = getParamString(params, 'stop') |
134 | this.playbackRate = getParamString(params, 'playbackRate') | ||
133 | 135 | ||
134 | this.bigPlayBackgroundColor = getParamString(params, 'bigPlayBackgroundColor') | 136 | this.bigPlayBackgroundColor = getParamString(params, 'bigPlayBackgroundColor') |
135 | this.foregroundColor = getParamString(params, 'foregroundColor') | 137 | this.foregroundColor = getParamString(params, 'foregroundColor') |
@@ -210,6 +212,8 @@ export class PlayerManagerOptions { | |||
210 | ? playlistTracker.getCurrentElement().stopTimestamp | 212 | ? playlistTracker.getCurrentElement().stopTimestamp |
211 | : this.stopTime, | 213 | : this.stopTime, |
212 | 214 | ||
215 | playbackRate: this.playbackRate, | ||
216 | |||
213 | videoCaptions, | 217 | videoCaptions, |
214 | inactivityTimeout: 2500, | 218 | inactivityTimeout: 2500, |
215 | videoViewUrl: this.videoFetcher.getVideoViewsUrl(video.uuid), | 219 | videoViewUrl: this.videoFetcher.getVideoViewsUrl(video.uuid), |
diff --git a/client/yarn.lock b/client/yarn.lock index b680bfdfb..1799df7b1 100644 --- a/client/yarn.lock +++ b/client/yarn.lock | |||
@@ -302,6 +302,11 @@ | |||
302 | dependencies: | 302 | dependencies: |
303 | tslib "^2.3.0" | 303 | tslib "^2.3.0" |
304 | 304 | ||
305 | "@arr/every@^1.0.0": | ||
306 | version "1.0.1" | ||
307 | resolved "https://registry.yarnpkg.com/@arr/every/-/every-1.0.1.tgz#22fe1f8e6355beca6c7c7bde965eb15cf994387b" | ||
308 | integrity sha512-UQFQ6SgyJ6LX42W8rHCs8KVc0JS0tzVL9ct4XYedJukskYVWTo49tNiMEK9C2HTyarbNiT/RVIRSY82vH+6sTg== | ||
309 | |||
305 | "@assemblyscript/loader@^0.10.1": | 310 | "@assemblyscript/loader@^0.10.1": |
306 | version "0.10.1" | 311 | version "0.10.1" |
307 | resolved "https://registry.yarnpkg.com/@assemblyscript/loader/-/loader-0.10.1.tgz#70e45678f06c72fa2e350e8553ec4a4d72b92e06" | 312 | resolved "https://registry.yarnpkg.com/@assemblyscript/loader/-/loader-0.10.1.tgz#70e45678f06c72fa2e350e8553ec4a4d72b92e06" |
@@ -1789,10 +1794,22 @@ | |||
1789 | read-package-json-fast "^2.0.3" | 1794 | read-package-json-fast "^2.0.3" |
1790 | which "^2.0.2" | 1795 | which "^2.0.2" |
1791 | 1796 | ||
1792 | "@peertube/p2p-media-loader-core@^1.0.13", "@peertube/p2p-media-loader-core@^1.0.8": | 1797 | "@peertube/maildev@^1.2.0": |
1793 | version "1.0.13" | 1798 | version "1.2.0" |
1794 | resolved "https://registry.yarnpkg.com/@peertube/p2p-media-loader-core/-/p2p-media-loader-core-1.0.13.tgz#36744a291b69c001b2562c1a93017979f8534ff8" | 1799 | resolved "https://registry.yarnpkg.com/@peertube/maildev/-/maildev-1.2.0.tgz#f25ee9fa6a45c0a6bc99c5392f63139eaa8eb088" |
1795 | integrity sha512-ArSAaeuxwwBAG0Xd3Gj0TzKObLfJFYzHz9+fREvmUf+GZQEG6qGwWmrdVWL6xjPiEuo6LdFeCOnHSQzAbj/ptg== | 1800 | integrity sha512-VGog0A2gk0P8UnP0ZjCoYQumELiqqQY5i+gt18avTC7NJNJLUxMRMI045NAVSDFVbqt2EJJPsbZf3LFjUWRtmw== |
1801 | dependencies: | ||
1802 | async "^3.1.0" | ||
1803 | commander "^8.3.0" | ||
1804 | mailparser-mit "^1.0.0" | ||
1805 | rimraf "^3.0.2" | ||
1806 | smtp-server "^3.9.0" | ||
1807 | wildstring "1.0.9" | ||
1808 | |||
1809 | "@peertube/p2p-media-loader-core@^1.0.14": | ||
1810 | version "1.0.14" | ||
1811 | resolved "https://registry.yarnpkg.com/@peertube/p2p-media-loader-core/-/p2p-media-loader-core-1.0.14.tgz#b4442dd343d6b30a51502e1240275eb98ef2c788" | ||
1812 | integrity sha512-tjQv1CNziNY+zYzcL1h4q6AA2WuBUZnBIeVyjWR/EsO1EEC1VMdvPsL02cqYLz9yvIxgycjeTsWCm6XDqNgXRw== | ||
1796 | dependencies: | 1813 | dependencies: |
1797 | bittorrent-tracker "^9.19.0" | 1814 | bittorrent-tracker "^9.19.0" |
1798 | debug "^4.3.4" | 1815 | debug "^4.3.4" |
@@ -1800,12 +1817,12 @@ | |||
1800 | sha.js "^2.4.11" | 1817 | sha.js "^2.4.11" |
1801 | simple-peer "^9.11.1" | 1818 | simple-peer "^9.11.1" |
1802 | 1819 | ||
1803 | "@peertube/p2p-media-loader-hlsjs@^1.0.13": | 1820 | "@peertube/p2p-media-loader-hlsjs@^1.0.14": |
1804 | version "1.0.13" | 1821 | version "1.0.14" |
1805 | resolved "https://registry.yarnpkg.com/@peertube/p2p-media-loader-hlsjs/-/p2p-media-loader-hlsjs-1.0.13.tgz#5305e2008041d01850802544d1c49298f79dd67a" | 1822 | resolved "https://registry.yarnpkg.com/@peertube/p2p-media-loader-hlsjs/-/p2p-media-loader-hlsjs-1.0.14.tgz#829629a57608b0e30f4b50bc98578e6bee9f8b9b" |
1806 | integrity sha512-2BO2oaRsSHEhLkgi2iw1r4n1Yqq1EnyoOgOZccPDqjmHUsZSV/wNrno8WYr6LsleudrHA26Imu57hVD1jDx7lg== | 1823 | integrity sha512-ySUVgUvAFXCE5E94xxjfywQ8xzk3jy9UGVkgi5Oqq+QeY7uG+o7CZ+LsQ/RjXgWBD70tEnyyfADHtL+9FCnwyQ== |
1807 | dependencies: | 1824 | dependencies: |
1808 | "@peertube/p2p-media-loader-core" "^1.0.8" | 1825 | "@peertube/p2p-media-loader-core" "^1.0.14" |
1809 | debug "^4.3.4" | 1826 | debug "^4.3.4" |
1810 | events "^3.3.0" | 1827 | events "^3.3.0" |
1811 | m3u8-parser "^4.7.1" | 1828 | m3u8-parser "^4.7.1" |
@@ -1830,6 +1847,16 @@ | |||
1830 | tokenizr "^1.6.4" | 1847 | tokenizr "^1.6.4" |
1831 | xmldom "^0.6.0" | 1848 | xmldom "^0.6.0" |
1832 | 1849 | ||
1850 | "@polka/parse@^1.0.0-next.0": | ||
1851 | version "1.0.0-next.0" | ||
1852 | resolved "https://registry.yarnpkg.com/@polka/parse/-/parse-1.0.0-next.0.tgz#3551d792acdf4ad0b053072e57498cbe32e45a94" | ||
1853 | integrity sha512-zcPNrc3PNrRLSCQ7ca8XR7h18VxdPIXhn+yvrYMdUFCHM7mhXGSPw5xBdbcf/dQ1cI4uE8pDfmm5uU+HX+WfFg== | ||
1854 | |||
1855 | "@polka/url@^0.5.0": | ||
1856 | version "0.5.0" | ||
1857 | resolved "https://registry.yarnpkg.com/@polka/url/-/url-0.5.0.tgz#b21510597fd601e5d7c95008b76bf0d254ebfd31" | ||
1858 | integrity sha512-oZLYFEAzUKyi3SKnXvj32ZCEGH6RDnao7COuCVhDydMS9NrCSVXhM79VaKyP5+Zc33m0QXEd2DN3UkU7OsHcfw== | ||
1859 | |||
1833 | "@polka/url@^1.0.0-next.20": | 1860 | "@polka/url@^1.0.0-next.20": |
1834 | version "1.0.0-next.21" | 1861 | version "1.0.0-next.21" |
1835 | resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.21.tgz#5de5a2385a35309427f6011992b544514d559aa1" | 1862 | resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.21.tgz#5de5a2385a35309427f6011992b544514d559aa1" |
@@ -2021,6 +2048,11 @@ | |||
2021 | dependencies: | 2048 | dependencies: |
2022 | "@types/node" "*" | 2049 | "@types/node" "*" |
2023 | 2050 | ||
2051 | "@types/gitconfiglocal@^2.0.1": | ||
2052 | version "2.0.1" | ||
2053 | resolved "https://registry.yarnpkg.com/@types/gitconfiglocal/-/gitconfiglocal-2.0.1.tgz#c134f9fb03d71917afa35c14f3b82085520509a6" | ||
2054 | integrity sha512-AYC38la5dRwIfbrZhPNIvlGHlIbH+kdl2j8A37twoCQyhKPPoRPfVmoBZKajpLIfV7SMboU6MZ6w/RmZLH68IQ== | ||
2055 | |||
2024 | "@types/html-minifier-terser@^6.0.0": | 2056 | "@types/html-minifier-terser@^6.0.0": |
2025 | version "6.1.0" | 2057 | version "6.1.0" |
2026 | resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#4fc33a00c1d0c16987b1a20cf92d20614c55ac35" | 2058 | resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#4fc33a00c1d0c16987b1a20cf92d20614c55ac35" |
@@ -2127,9 +2159,9 @@ | |||
2127 | "@types/lodash" "*" | 2159 | "@types/lodash" "*" |
2128 | 2160 | ||
2129 | "@types/lodash@*": | 2161 | "@types/lodash@*": |
2130 | version "4.14.189" | 2162 | version "4.14.191" |
2131 | resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.189.tgz#975ff8c38da5ae58b751127b19ad5e44b5b7f6d2" | 2163 | resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.191.tgz#09511e7f7cba275acd8b419ddac8da9a6a79e2fa" |
2132 | integrity sha512-kb9/98N6X8gyME9Cf7YaqIMvYGnBSWqEci6tiettE6iJWH1XdJz/PO8LB0GtLCG7x8dU3KWhZT+lA1a35127tA== | 2164 | integrity sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ== |
2133 | 2165 | ||
2134 | "@types/magnet-uri@*": | 2166 | "@types/magnet-uri@*": |
2135 | version "5.1.3" | 2167 | version "5.1.3" |
@@ -2162,9 +2194,9 @@ | |||
2162 | integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== | 2194 | integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== |
2163 | 2195 | ||
2164 | "@types/mocha@^10.0.0": | 2196 | "@types/mocha@^10.0.0": |
2165 | version "10.0.0" | 2197 | version "10.0.1" |
2166 | resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-10.0.0.tgz#3d9018c575f0e3f7386c1de80ee66cc21fbb7a52" | 2198 | resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-10.0.1.tgz#2f4f65bb08bc368ac39c96da7b2f09140b26851b" |
2167 | integrity sha512-rADY+HtTOA52l9VZWtgQfn4p+UDVM2eDVkMZT1I6syp0YKxW2F9v+0pbRZLsvskhQv/vMb6ZfCay81GHbz5SHg== | 2199 | integrity sha512-/fvYntiO1GeICvqbQ3doGDIP97vWmvFt83GKguJ6prmQM2iXZfFcq6YE8KteFyRtX2/h5Hf91BYvPodJKFYv5Q== |
2168 | 2200 | ||
2169 | "@types/mousetrap@^1.6.9": | 2201 | "@types/mousetrap@^1.6.9": |
2170 | version "1.6.11" | 2202 | version "1.6.11" |
@@ -2177,9 +2209,9 @@ | |||
2177 | integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== | 2209 | integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== |
2178 | 2210 | ||
2179 | "@types/node@*", "@types/node@^18.0.0": | 2211 | "@types/node@*", "@types/node@^18.0.0": |
2180 | version "18.11.9" | 2212 | version "18.11.18" |
2181 | resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.9.tgz#02d013de7058cea16d36168ef2fc653464cfbad4" | 2213 | resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.18.tgz#8dfb97f0da23c2293e554c5a50d61ef134d7697f" |
2182 | integrity sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg== | 2214 | integrity sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA== |
2183 | 2215 | ||
2184 | "@types/node@^17.0.42": | 2216 | "@types/node@^17.0.42": |
2185 | version "17.0.45" | 2217 | version "17.0.45" |
@@ -2380,9 +2412,9 @@ | |||
2380 | integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== | 2412 | integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== |
2381 | 2413 | ||
2382 | "@types/yargs@^17.0.8": | 2414 | "@types/yargs@^17.0.8": |
2383 | version "17.0.13" | 2415 | version "17.0.19" |
2384 | resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.13.tgz#34cced675ca1b1d51fcf4d34c3c6f0fa142a5c76" | 2416 | resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.19.tgz#8dbecdc9ab48bee0cb74f6e3327de3fa0d0c98ae" |
2385 | integrity sha512-9sWaruZk2JGxIQU+IhI1fhPYRcQ0UuTNuKuCW9bR5fp7qi2Llf7WDzNa17Cy7TKnh3cdxDOiyTu6gaLS0eDatg== | 2417 | integrity sha512-cAx3qamwaYX9R0fzOIZAlFpo4A+1uBVCxqpKz9D26uTF4srRXaGTTsikQmaotCtNdbhzyUH7ft6p9ktz9s6UNQ== |
2386 | dependencies: | 2418 | dependencies: |
2387 | "@types/yargs-parser" "*" | 2419 | "@types/yargs-parser" "*" |
2388 | 2420 | ||
@@ -2509,22 +2541,26 @@ | |||
2509 | is-function "^1.0.1" | 2541 | is-function "^1.0.1" |
2510 | 2542 | ||
2511 | "@wdio/browserstack-service@^7.25.2": | 2543 | "@wdio/browserstack-service@^7.25.2": |
2512 | version "7.26.0" | 2544 | version "7.29.1" |
2513 | resolved "https://registry.yarnpkg.com/@wdio/browserstack-service/-/browserstack-service-7.26.0.tgz#d303c5998e565734bd7f5c23fc9b291a588b7c21" | 2545 | resolved "https://registry.yarnpkg.com/@wdio/browserstack-service/-/browserstack-service-7.29.1.tgz#46282aa07b7c11a51ebac0bff1f12f1badd6e264" |
2514 | integrity sha512-hRKmg4u/DRNZm1EJGaYESAH6GsCPCtBm15fP9ngm/HFUG084thFfrD8Tt09hO+KSNoK4tXl4k1ZHZ4akrOq9KA== | 2546 | integrity sha512-1+MoqlIXIjbh1oEOZcvtemij+Yz/CB6orZjeT3WCoA9oY8Ul8EeIHhfF7GxmE6u0OVofjmC+wfO5NlHYCKgL1w== |
2515 | dependencies: | 2547 | dependencies: |
2516 | "@types/node" "^18.0.0" | 2548 | "@types/gitconfiglocal" "^2.0.1" |
2517 | "@wdio/logger" "7.26.0" | 2549 | "@wdio/logger" "7.26.0" |
2550 | "@wdio/reporter" "7.25.4" | ||
2518 | "@wdio/types" "7.26.0" | 2551 | "@wdio/types" "7.26.0" |
2519 | browserstack-local "^1.4.5" | 2552 | browserstack-local "^1.4.5" |
2520 | form-data "^4.0.0" | 2553 | form-data "^4.0.0" |
2554 | git-repo-info "^2.1.1" | ||
2555 | gitconfiglocal "^2.1.0" | ||
2521 | got "^11.0.2" | 2556 | got "^11.0.2" |
2522 | webdriverio "7.26.0" | 2557 | uuid "^8.3.2" |
2558 | webdriverio "7.29.1" | ||
2523 | 2559 | ||
2524 | "@wdio/cli@^7.25.2": | 2560 | "@wdio/cli@^7.25.2": |
2525 | version "7.26.0" | 2561 | version "7.29.1" |
2526 | resolved "https://registry.yarnpkg.com/@wdio/cli/-/cli-7.26.0.tgz#20c690a5ede4a35cb2f84da9041c250a6013bc54" | 2562 | resolved "https://registry.yarnpkg.com/@wdio/cli/-/cli-7.29.1.tgz#1b47f5a45f21754d42be814dbae94ff723a6a1a2" |
2527 | integrity sha512-xG+ZIzPqzz/Tvhfrogd8oNvTXzzdE+cbkmTHjMGo1hnmnoAQPeAEcV/QqaX5CHFE9DjaguEeadqjcZikB5U2GQ== | 2563 | integrity sha512-dldHNYlnuFUG10TlENbeL41tujqgYD7S/9nzV1J/szBryCO6AIVz/QWn/AUv3zrsO2sn8TNF8BMEXRvLgCxyeg== |
2528 | dependencies: | 2564 | dependencies: |
2529 | "@types/ejs" "^3.0.5" | 2565 | "@types/ejs" "^3.0.5" |
2530 | "@types/fs-extra" "^9.0.4" | 2566 | "@types/fs-extra" "^9.0.4" |
@@ -2536,7 +2572,7 @@ | |||
2536 | "@types/recursive-readdir" "^2.2.0" | 2572 | "@types/recursive-readdir" "^2.2.0" |
2537 | "@wdio/config" "7.26.0" | 2573 | "@wdio/config" "7.26.0" |
2538 | "@wdio/logger" "7.26.0" | 2574 | "@wdio/logger" "7.26.0" |
2539 | "@wdio/protocols" "7.22.0" | 2575 | "@wdio/protocols" "7.27.0" |
2540 | "@wdio/types" "7.26.0" | 2576 | "@wdio/types" "7.26.0" |
2541 | "@wdio/utils" "7.26.0" | 2577 | "@wdio/utils" "7.26.0" |
2542 | async-exit-hook "^2.0.1" | 2578 | async-exit-hook "^2.0.1" |
@@ -2551,7 +2587,7 @@ | |||
2551 | lodash.union "^4.6.0" | 2587 | lodash.union "^4.6.0" |
2552 | mkdirp "^1.0.4" | 2588 | mkdirp "^1.0.4" |
2553 | recursive-readdir "^2.2.2" | 2589 | recursive-readdir "^2.2.2" |
2554 | webdriverio "7.26.0" | 2590 | webdriverio "7.29.1" |
2555 | yargs "^17.0.0" | 2591 | yargs "^17.0.0" |
2556 | yarn-install "^1.0.0" | 2592 | yarn-install "^1.0.0" |
2557 | 2593 | ||
@@ -2567,14 +2603,14 @@ | |||
2567 | glob "^8.0.3" | 2603 | glob "^8.0.3" |
2568 | 2604 | ||
2569 | "@wdio/local-runner@^7.25.2": | 2605 | "@wdio/local-runner@^7.25.2": |
2570 | version "7.26.0" | 2606 | version "7.29.1" |
2571 | resolved "https://registry.yarnpkg.com/@wdio/local-runner/-/local-runner-7.26.0.tgz#a056c6e9d73c7f48e54fe3f07ce573a90dae26ab" | 2607 | resolved "https://registry.yarnpkg.com/@wdio/local-runner/-/local-runner-7.29.1.tgz#f93a2953847b4271b59ba1b9635920e8046f0e55" |
2572 | integrity sha512-GdCP7Y8s8qvoctC0WaSGBSmTSbVw74WEJm6Y3n3DpoCI8ABFNkQlhFlqJH+taQDs3sRVEM65bHGcU4C4FOVWXQ== | 2608 | integrity sha512-4w9Dsp9/4+MEU8yG7M8ynsCqpSP6UbKqZ2M/gWpvkvy57rb3eS9evFdIFfRzuQmbsztG9qeAlGILwlZ4/oaopg== |
2573 | dependencies: | 2609 | dependencies: |
2574 | "@types/stream-buffers" "^3.0.3" | 2610 | "@types/stream-buffers" "^3.0.3" |
2575 | "@wdio/logger" "7.26.0" | 2611 | "@wdio/logger" "7.26.0" |
2576 | "@wdio/repl" "7.26.0" | 2612 | "@wdio/repl" "7.26.0" |
2577 | "@wdio/runner" "7.26.0" | 2613 | "@wdio/runner" "7.29.1" |
2578 | "@wdio/types" "7.26.0" | 2614 | "@wdio/types" "7.26.0" |
2579 | async-exit-hook "^2.0.1" | 2615 | async-exit-hook "^2.0.1" |
2580 | split2 "^4.0.0" | 2616 | split2 "^4.0.0" |
@@ -2602,10 +2638,10 @@ | |||
2602 | expect-webdriverio "^3.0.0" | 2638 | expect-webdriverio "^3.0.0" |
2603 | mocha "^10.0.0" | 2639 | mocha "^10.0.0" |
2604 | 2640 | ||
2605 | "@wdio/protocols@7.22.0": | 2641 | "@wdio/protocols@7.27.0": |
2606 | version "7.22.0" | 2642 | version "7.27.0" |
2607 | resolved "https://registry.yarnpkg.com/@wdio/protocols/-/protocols-7.22.0.tgz#d89faef687cb08981d734bbc5e5dffc6fb5a064c" | 2643 | resolved "https://registry.yarnpkg.com/@wdio/protocols/-/protocols-7.27.0.tgz#8e2663ec877dce7a5f76b021209c18dd0132e853" |
2608 | integrity sha512-8EXRR+Ymdwousm/VGtW3H1hwxZ/1g1H99A1lF0U4GuJ5cFWHCd0IVE5H31Z52i8ZruouW8jueMkGZPSo2IIUSQ== | 2644 | integrity sha512-hT/U22R5i3HhwPjkaKAG0yd59eaOaZB0eibRj2+esCImkb5Y6rg8FirrlYRxIGFVBl0+xZV0jKHzR5+o097nvg== |
2609 | 2645 | ||
2610 | "@wdio/repl@7.26.0": | 2646 | "@wdio/repl@7.26.0": |
2611 | version "7.26.0" | 2647 | version "7.26.0" |
@@ -2614,10 +2650,26 @@ | |||
2614 | dependencies: | 2650 | dependencies: |
2615 | "@wdio/utils" "7.26.0" | 2651 | "@wdio/utils" "7.26.0" |
2616 | 2652 | ||
2617 | "@wdio/reporter@7.26.0": | 2653 | "@wdio/reporter@7.25.4": |
2618 | version "7.26.0" | 2654 | version "7.25.4" |
2619 | resolved "https://registry.yarnpkg.com/@wdio/reporter/-/reporter-7.26.0.tgz#26c0e7114a4c1e7b29a79e4d178e5312e04d7934" | 2655 | resolved "https://registry.yarnpkg.com/@wdio/reporter/-/reporter-7.25.4.tgz#b6a69652dd0c4ec131255000af128eac403a18b9" |
2620 | integrity sha512-kEb7i1A4V4E1wJgdyvLsDbap4cEp1fPZslErGtbAbK+9HI8Lt/SlTZCiOpZbvhgzvawEqOV6UqxZT1RsL8wZWw== | 2656 | integrity sha512-M37qzEmF5qNffyZmRQGjDlrXqWW21EFvgW8wsv1b/NtfpZc0c0MoRpeh6BnvX1KcE4nCXfjXgSJPOqV4ZCzUEQ== |
2657 | dependencies: | ||
2658 | "@types/diff" "^5.0.0" | ||
2659 | "@types/node" "^18.0.0" | ||
2660 | "@types/object-inspect" "^1.8.0" | ||
2661 | "@types/supports-color" "^8.1.0" | ||
2662 | "@types/tmp" "^0.2.0" | ||
2663 | "@wdio/types" "7.25.4" | ||
2664 | diff "^5.0.0" | ||
2665 | fs-extra "^10.0.0" | ||
2666 | object-inspect "^1.10.3" | ||
2667 | supports-color "8.1.1" | ||
2668 | |||
2669 | "@wdio/reporter@7.29.1": | ||
2670 | version "7.29.1" | ||
2671 | resolved "https://registry.yarnpkg.com/@wdio/reporter/-/reporter-7.29.1.tgz#7fc2e3b7aa3843172dcd97221c44257384cbbd27" | ||
2672 | integrity sha512-mpusCpbw7RxnJSDu9qa1qv5IfEMCh7377y1Typ4J2TlMy+78CQzGZ8coEXjBxLcqijTUwcyyoLNI5yRSvbDExw== | ||
2621 | dependencies: | 2673 | dependencies: |
2622 | "@types/diff" "^5.0.0" | 2674 | "@types/diff" "^5.0.0" |
2623 | "@types/node" "^18.0.0" | 2675 | "@types/node" "^18.0.0" |
@@ -2630,10 +2682,10 @@ | |||
2630 | object-inspect "^1.10.3" | 2682 | object-inspect "^1.10.3" |
2631 | supports-color "8.1.1" | 2683 | supports-color "8.1.1" |
2632 | 2684 | ||
2633 | "@wdio/runner@7.26.0": | 2685 | "@wdio/runner@7.29.1": |
2634 | version "7.26.0" | 2686 | version "7.29.1" |
2635 | resolved "https://registry.yarnpkg.com/@wdio/runner/-/runner-7.26.0.tgz#c0b2848dc885b655e8690d3e0381dfb0ad221af5" | 2687 | resolved "https://registry.yarnpkg.com/@wdio/runner/-/runner-7.29.1.tgz#9fd2fa6dd28b8b130a10d23452eb155e1e887576" |
2636 | integrity sha512-DhQiOs10oPeLlv7/R+997arPg5OY7iEgespGkn6r+kdx2o+awxa6PFegQrjJmRKUmNv3TTuKXHouP34TbR/8sw== | 2688 | integrity sha512-lJEk/HJ5IiuvAJws8zTx9XL5LJuoexvjWIZmOmFJ6Gv8qRpUx6b0n+JM7vhhbTeIqs4QLXOwTQUHlDDRldQlzQ== |
2637 | dependencies: | 2689 | dependencies: |
2638 | "@wdio/config" "7.26.0" | 2690 | "@wdio/config" "7.26.0" |
2639 | "@wdio/logger" "7.26.0" | 2691 | "@wdio/logger" "7.26.0" |
@@ -2641,21 +2693,41 @@ | |||
2641 | "@wdio/utils" "7.26.0" | 2693 | "@wdio/utils" "7.26.0" |
2642 | deepmerge "^4.0.0" | 2694 | deepmerge "^4.0.0" |
2643 | gaze "^1.1.2" | 2695 | gaze "^1.1.2" |
2644 | webdriver "7.26.0" | 2696 | webdriver "7.27.0" |
2645 | webdriverio "7.26.0" | 2697 | webdriverio "7.29.1" |
2698 | |||
2699 | "@wdio/shared-store-service@^7.25.2": | ||
2700 | version "7.29.1" | ||
2701 | resolved "https://registry.yarnpkg.com/@wdio/shared-store-service/-/shared-store-service-7.29.1.tgz#c43a3dbc7d47c8334970bc173e963688977e8a79" | ||
2702 | integrity sha512-13VOxyz956DSs2wloQ8gtyEx42zjAuOg+N8/4tGk1p2igPzHB2qUiY/P0yi6zamxYGb6PKLIumIeUjitWHtyWA== | ||
2703 | dependencies: | ||
2704 | "@polka/parse" "^1.0.0-next.0" | ||
2705 | "@wdio/logger" "7.26.0" | ||
2706 | "@wdio/types" "7.26.0" | ||
2707 | got "^11.0.2" | ||
2708 | polka "^0.5.2" | ||
2709 | webdriverio "7.29.1" | ||
2646 | 2710 | ||
2647 | "@wdio/spec-reporter@^7.25.1": | 2711 | "@wdio/spec-reporter@^7.25.1": |
2648 | version "7.26.0" | 2712 | version "7.29.1" |
2649 | resolved "https://registry.yarnpkg.com/@wdio/spec-reporter/-/spec-reporter-7.26.0.tgz#13eaa5a0fd089684d4c1bcd8ac11dc8646afb5b7" | 2713 | resolved "https://registry.yarnpkg.com/@wdio/spec-reporter/-/spec-reporter-7.29.1.tgz#08e13c02ea0876672226d5a2c326dda7e1a66c8e" |
2650 | integrity sha512-oisyVWn+MRoq0We0qORoDHNk+iKr7CFG4+IE5GCRecR8cgP7dUjVXZcEbn6blgRpry4jOxsAl24frfaPDOsZVA== | 2714 | integrity sha512-bwSGM72QrDedqacY7Wq9Gn86VgRwIGPYzZtcaD7aDnvppCuV8Z/31Wpdfen+CzUk2+whXjXKe66ohPyl9TG5+w== |
2651 | dependencies: | 2715 | dependencies: |
2652 | "@types/easy-table" "^1.2.0" | 2716 | "@types/easy-table" "^1.2.0" |
2653 | "@wdio/reporter" "7.26.0" | 2717 | "@wdio/reporter" "7.29.1" |
2654 | "@wdio/types" "7.26.0" | 2718 | "@wdio/types" "7.26.0" |
2655 | chalk "^4.0.0" | 2719 | chalk "^4.0.0" |
2656 | easy-table "^1.1.1" | 2720 | easy-table "^1.1.1" |
2657 | pretty-ms "^7.0.0" | 2721 | pretty-ms "^7.0.0" |
2658 | 2722 | ||
2723 | "@wdio/types@7.25.4": | ||
2724 | version "7.25.4" | ||
2725 | resolved "https://registry.yarnpkg.com/@wdio/types/-/types-7.25.4.tgz#6f8f028e3108dc880de5068264695f1572e65352" | ||
2726 | integrity sha512-muvNmq48QZCvocctnbe0URq2FjJjUPIG4iLoeMmyF0AQgdbjaUkMkw3BHYNHVTbSOU9WMsr2z8alhj/I2H6NRQ== | ||
2727 | dependencies: | ||
2728 | "@types/node" "^18.0.0" | ||
2729 | got "^11.8.1" | ||
2730 | |||
2659 | "@wdio/types@7.26.0": | 2731 | "@wdio/types@7.26.0": |
2660 | version "7.26.0" | 2732 | version "7.26.0" |
2661 | resolved "https://registry.yarnpkg.com/@wdio/types/-/types-7.26.0.tgz#70bc879c5dbe316a0eebbac4a46f0f66430b1d84" | 2733 | resolved "https://registry.yarnpkg.com/@wdio/types/-/types-7.26.0.tgz#70bc879c5dbe316a0eebbac4a46f0f66430b1d84" |
@@ -2882,6 +2954,11 @@ addr-to-ip-port@^1.0.1, addr-to-ip-port@^1.5.4: | |||
2882 | resolved "https://registry.yarnpkg.com/addr-to-ip-port/-/addr-to-ip-port-1.5.4.tgz#9542b1c6219fdb8c9ce6cc72c14ee880ab7ddd88" | 2954 | resolved "https://registry.yarnpkg.com/addr-to-ip-port/-/addr-to-ip-port-1.5.4.tgz#9542b1c6219fdb8c9ce6cc72c14ee880ab7ddd88" |
2883 | integrity sha512-ByxmJgv8vjmDcl3IDToxL2yrWFrRtFpZAToY0f46XFXl8zS081t7El5MXIodwm7RC6DhHBRoOSMLFSPKCtHukg== | 2955 | integrity sha512-ByxmJgv8vjmDcl3IDToxL2yrWFrRtFpZAToY0f46XFXl8zS081t7El5MXIodwm7RC6DhHBRoOSMLFSPKCtHukg== |
2884 | 2956 | ||
2957 | addressparser@^1.0.1: | ||
2958 | version "1.0.1" | ||
2959 | resolved "https://registry.yarnpkg.com/addressparser/-/addressparser-1.0.1.tgz#47afbe1a2a9262191db6838e4fd1d39b40821746" | ||
2960 | integrity sha512-aQX7AISOMM7HFE0iZ3+YnD07oIeJqWGVnJ+ZIKaBZAk03ftmVYVqsGas/rbXKR21n4D/hKCSHypvcyOkds/xzg== | ||
2961 | |||
2885 | adjust-sourcemap-loader@^4.0.0: | 2962 | adjust-sourcemap-loader@^4.0.0: |
2886 | version "4.0.0" | 2963 | version "4.0.0" |
2887 | resolved "https://registry.yarnpkg.com/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz#fc4a0fd080f7d10471f30a7320f25560ade28c99" | 2964 | resolved "https://registry.yarnpkg.com/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz#fc4a0fd080f7d10471f30a7320f25560ade28c99" |
@@ -3057,9 +3134,9 @@ ansi-styles@^5.0.0: | |||
3057 | integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== | 3134 | integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== |
3058 | 3135 | ||
3059 | anymatch@~3.1.2: | 3136 | anymatch@~3.1.2: |
3060 | version "3.1.2" | 3137 | version "3.1.3" |
3061 | resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" | 3138 | resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" |
3062 | integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== | 3139 | integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== |
3063 | dependencies: | 3140 | dependencies: |
3064 | normalize-path "^3.0.0" | 3141 | normalize-path "^3.0.0" |
3065 | picomatch "^2.0.4" | 3142 | picomatch "^2.0.4" |
@@ -3188,7 +3265,7 @@ async-exit-hook@^2.0.1: | |||
3188 | resolved "https://registry.yarnpkg.com/async-exit-hook/-/async-exit-hook-2.0.1.tgz#8bd8b024b0ec9b1c01cccb9af9db29bd717dfaf3" | 3265 | resolved "https://registry.yarnpkg.com/async-exit-hook/-/async-exit-hook-2.0.1.tgz#8bd8b024b0ec9b1c01cccb9af9db29bd717dfaf3" |
3189 | integrity sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw== | 3266 | integrity sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw== |
3190 | 3267 | ||
3191 | async@^3.2.3: | 3268 | async@^3.1.0, async@^3.2.3: |
3192 | version "3.2.4" | 3269 | version "3.2.4" |
3193 | resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" | 3270 | resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" |
3194 | integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== | 3271 | integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== |
@@ -3327,6 +3404,11 @@ balanced-match@^2.0.0: | |||
3327 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-2.0.0.tgz#dc70f920d78db8b858535795867bf48f820633d9" | 3404 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-2.0.0.tgz#dc70f920d78db8b858535795867bf48f820633d9" |
3328 | integrity sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA== | 3405 | integrity sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA== |
3329 | 3406 | ||
3407 | base32.js@0.1.0: | ||
3408 | version "0.1.0" | ||
3409 | resolved "https://registry.yarnpkg.com/base32.js/-/base32.js-0.1.0.tgz#b582dec693c2f11e893cf064ee6ac5b6131a2202" | ||
3410 | integrity sha512-n3TkB02ixgBOhTvANakDb4xaMXnYUVkNoRFJjQflcqMQhyEKxEHdj3E6N8t8sUQ0mjH/3/JxzlXuz3ul/J90pQ== | ||
3411 | |||
3330 | base64-js@^1.2.0, base64-js@^1.3.1: | 3412 | base64-js@^1.2.0, base64-js@^1.3.1: |
3331 | version "1.5.1" | 3413 | version "1.5.1" |
3332 | resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" | 3414 | resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" |
@@ -3933,9 +4015,9 @@ chunk-store-stream@^4.3.0: | |||
3933 | readable-stream "^3.6.0" | 4015 | readable-stream "^3.6.0" |
3934 | 4016 | ||
3935 | ci-info@^3.2.0: | 4017 | ci-info@^3.2.0: |
3936 | version "3.6.1" | 4018 | version "3.7.1" |
3937 | resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.6.1.tgz#7594f1c95cb7fdfddee7af95a13af7dbc67afdcf" | 4019 | resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.7.1.tgz#708a6cdae38915d597afdf3b145f2f8e1ff55f3f" |
3938 | integrity sha512-up5ggbaDqOqJ4UqLKZ2naVkyqSJQgJi5lwD6b6mM748ysrghDBX0bx/qJTUHzw7zu6Mq4gycviSF5hJnwceD8w== | 4020 | integrity sha512-4jYS4MOAaCIStSRwiuxc4B8MYhIe676yO1sYGzARnjXkWpmzZMMYxY6zu8WYWDhSuth5zhrQ1rhNSibyyvv4/w== |
3939 | 4021 | ||
3940 | clean-css@5.2.0: | 4022 | clean-css@5.2.0: |
3941 | version "5.2.0" | 4023 | version "5.2.0" |
@@ -4504,16 +4586,18 @@ decompress-response@^6.0.0: | |||
4504 | mimic-response "^3.1.0" | 4586 | mimic-response "^3.1.0" |
4505 | 4587 | ||
4506 | deep-equal@^2.0.5: | 4588 | deep-equal@^2.0.5: |
4507 | version "2.1.0" | 4589 | version "2.2.0" |
4508 | resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.1.0.tgz#5ba60402cf44ab92c2c07f3f3312c3d857a0e1dd" | 4590 | resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.2.0.tgz#5caeace9c781028b9ff459f33b779346637c43e6" |
4509 | integrity sha512-2pxgvWu3Alv1PoWEyVg7HS8YhGlUFUV7N5oOvfL6d+7xAmLSemMwv/c8Zv/i9KFzxV5Kt5CAvQc70fLwVuf4UA== | 4591 | integrity sha512-RdpzE0Hv4lhowpIUKKMJfeH6C1pXdtT1/it80ubgWqwI3qpuxUBpC1S4hnHg+zjnuOoDkzUtUCEEkG+XG5l3Mw== |
4510 | dependencies: | 4592 | dependencies: |
4511 | call-bind "^1.0.2" | 4593 | call-bind "^1.0.2" |
4512 | es-get-iterator "^1.1.2" | 4594 | es-get-iterator "^1.1.2" |
4513 | get-intrinsic "^1.1.3" | 4595 | get-intrinsic "^1.1.3" |
4514 | is-arguments "^1.1.1" | 4596 | is-arguments "^1.1.1" |
4597 | is-array-buffer "^3.0.1" | ||
4515 | is-date-object "^1.0.5" | 4598 | is-date-object "^1.0.5" |
4516 | is-regex "^1.1.4" | 4599 | is-regex "^1.1.4" |
4600 | is-shared-array-buffer "^1.0.2" | ||
4517 | isarray "^2.0.5" | 4601 | isarray "^2.0.5" |
4518 | object-is "^1.1.5" | 4602 | object-is "^1.1.5" |
4519 | object-keys "^1.1.1" | 4603 | object-keys "^1.1.1" |
@@ -4522,7 +4606,7 @@ deep-equal@^2.0.5: | |||
4522 | side-channel "^1.0.4" | 4606 | side-channel "^1.0.4" |
4523 | which-boxed-primitive "^1.0.2" | 4607 | which-boxed-primitive "^1.0.2" |
4524 | which-collection "^1.0.1" | 4608 | which-collection "^1.0.1" |
4525 | which-typed-array "^1.1.8" | 4609 | which-typed-array "^1.1.9" |
4526 | 4610 | ||
4527 | deep-is@^0.1.3: | 4611 | deep-is@^0.1.3: |
4528 | version "0.1.4" | 4612 | version "0.1.4" |
@@ -4606,21 +4690,21 @@ devtools-protocol@0.0.981744: | |||
4606 | resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.981744.tgz#9960da0370284577d46c28979a0b32651022bacf" | 4690 | resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.981744.tgz#9960da0370284577d46c28979a0b32651022bacf" |
4607 | integrity sha512-0cuGS8+jhR67Fy7qG3i3Pc7Aw494sb9yG9QgpG97SFVWwolgYjlhJg7n+UaHxOQT30d1TYu/EYe9k01ivLErIg== | 4691 | integrity sha512-0cuGS8+jhR67Fy7qG3i3Pc7Aw494sb9yG9QgpG97SFVWwolgYjlhJg7n+UaHxOQT30d1TYu/EYe9k01ivLErIg== |
4608 | 4692 | ||
4609 | devtools-protocol@^0.0.1069585: | 4693 | devtools-protocol@^0.0.1085790: |
4610 | version "0.0.1069585" | 4694 | version "0.0.1085790" |
4611 | resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.1069585.tgz#c9a9f330462aabf054d581f254b13774297b84f2" | 4695 | resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.1085790.tgz#315e4700eb960cf111cc908b9be2caca2257cb13" |
4612 | integrity sha512-sHmkZB6immWQWU4Wx3ogXwxjQUvQc92MmUDL52+q1z2hQmvpOcvDmbsjwX7QZOPTA32dMV7fgT6zUytcpPzy4A== | 4696 | integrity sha512-f5kfwdOTxPqX5v8ZfAAl9xBgoEVazBYtIONDWIRqYbb7yjOIcnk6vpzCgBCQvav5AuBRLzyUGG0V74OAx93LoA== |
4613 | 4697 | ||
4614 | devtools@7.26.0: | 4698 | devtools@7.28.1: |
4615 | version "7.26.0" | 4699 | version "7.28.1" |
4616 | resolved "https://registry.yarnpkg.com/devtools/-/devtools-7.26.0.tgz#3d568aea2238d190ad0cd71c00483c07c707124a" | 4700 | resolved "https://registry.yarnpkg.com/devtools/-/devtools-7.28.1.tgz#9699e0ca41c9a3adfa351d8afac2928f8e1d381c" |
4617 | integrity sha512-+8HNbNpzgo4Sn+WcrvXuwsHW9XPJfLo4bs9lgs6DPJHIIDXYJXQGsd7940wMX0Rp0D2vHXA4ibK0oTI5rogM3Q== | 4701 | integrity sha512-sDoszzrXDMLiBQqsg9A5gDqDBwhH4sjYzJIW15lQinB8qgNs0y4o1zdfNlqiKs4HstCA2uFixQeibbDCyMa7hQ== |
4618 | dependencies: | 4702 | dependencies: |
4619 | "@types/node" "^18.0.0" | 4703 | "@types/node" "^18.0.0" |
4620 | "@types/ua-parser-js" "^0.7.33" | 4704 | "@types/ua-parser-js" "^0.7.33" |
4621 | "@wdio/config" "7.26.0" | 4705 | "@wdio/config" "7.26.0" |
4622 | "@wdio/logger" "7.26.0" | 4706 | "@wdio/logger" "7.26.0" |
4623 | "@wdio/protocols" "7.22.0" | 4707 | "@wdio/protocols" "7.27.0" |
4624 | "@wdio/types" "7.26.0" | 4708 | "@wdio/types" "7.26.0" |
4625 | "@wdio/utils" "7.26.0" | 4709 | "@wdio/utils" "7.26.0" |
4626 | chrome-launcher "^0.15.0" | 4710 | chrome-launcher "^0.15.0" |
@@ -4923,18 +5007,19 @@ es-abstract@^1.19.0, es-abstract@^1.20.4: | |||
4923 | unbox-primitive "^1.0.2" | 5007 | unbox-primitive "^1.0.2" |
4924 | 5008 | ||
4925 | es-get-iterator@^1.1.2: | 5009 | es-get-iterator@^1.1.2: |
4926 | version "1.1.2" | 5010 | version "1.1.3" |
4927 | resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.2.tgz#9234c54aba713486d7ebde0220864af5e2b283f7" | 5011 | resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.3.tgz#3ef87523c5d464d41084b2c3c9c214f1199763d6" |
4928 | integrity sha512-+DTO8GYwbMCwbywjimwZMHp8AuYXOS2JZFWoi2AlPOS3ebnII9w/NLpNZtA7A0YLaVDw+O7KFCeoIV7OPvM7hQ== | 5012 | integrity sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw== |
4929 | dependencies: | 5013 | dependencies: |
4930 | call-bind "^1.0.2" | 5014 | call-bind "^1.0.2" |
4931 | get-intrinsic "^1.1.0" | 5015 | get-intrinsic "^1.1.3" |
4932 | has-symbols "^1.0.1" | 5016 | has-symbols "^1.0.3" |
4933 | is-arguments "^1.1.0" | 5017 | is-arguments "^1.1.1" |
4934 | is-map "^2.0.2" | 5018 | is-map "^2.0.2" |
4935 | is-set "^2.0.2" | 5019 | is-set "^2.0.2" |
4936 | is-string "^1.0.5" | 5020 | is-string "^1.0.7" |
4937 | isarray "^2.0.5" | 5021 | isarray "^2.0.5" |
5022 | stop-iteration-iterator "^1.0.0" | ||
4938 | 5023 | ||
4939 | es-module-lexer@^0.9.0: | 5024 | es-module-lexer@^0.9.0: |
4940 | version "0.9.3" | 5025 | version "0.9.3" |
@@ -5104,7 +5189,7 @@ escape-string-regexp@4.0.0, escape-string-regexp@^4.0.0: | |||
5104 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" | 5189 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" |
5105 | integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== | 5190 | integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== |
5106 | 5191 | ||
5107 | escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: | 5192 | escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5, escape-string-regexp@~1.0.5: |
5108 | version "1.0.5" | 5193 | version "1.0.5" |
5109 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" | 5194 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" |
5110 | integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== | 5195 | integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== |
@@ -5404,7 +5489,7 @@ express@^4.17.3: | |||
5404 | utils-merge "1.0.1" | 5489 | utils-merge "1.0.1" |
5405 | vary "~1.1.2" | 5490 | vary "~1.1.2" |
5406 | 5491 | ||
5407 | extend@~3.0.2: | 5492 | extend@~3.0.0, extend@~3.0.2: |
5408 | version "3.0.2" | 5493 | version "3.0.2" |
5409 | resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" | 5494 | resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" |
5410 | integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== | 5495 | integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== |
@@ -5856,6 +5941,18 @@ getpass@^0.1.1: | |||
5856 | dependencies: | 5941 | dependencies: |
5857 | assert-plus "^1.0.0" | 5942 | assert-plus "^1.0.0" |
5858 | 5943 | ||
5944 | git-repo-info@^2.1.1: | ||
5945 | version "2.1.1" | ||
5946 | resolved "https://registry.yarnpkg.com/git-repo-info/-/git-repo-info-2.1.1.tgz#220ffed8cbae74ef8a80e3052f2ccb5179aed058" | ||
5947 | integrity sha512-8aCohiDo4jwjOwma4FmYFd3i97urZulL8XL24nIPxuE+GZnfsAyy/g2Shqx6OjUiFKUXZM+Yy+KHnOmmA3FVcg== | ||
5948 | |||
5949 | gitconfiglocal@^2.1.0: | ||
5950 | version "2.1.0" | ||
5951 | resolved "https://registry.yarnpkg.com/gitconfiglocal/-/gitconfiglocal-2.1.0.tgz#07c28685c55cc5338b27b5acbcfe34aeb92e43d1" | ||
5952 | integrity sha512-qoerOEliJn3z+Zyn1HW2F6eoYJqKwS6MgC9cztTLUB/xLWX8gD/6T60pKn4+t/d6tP7JlybI7Z3z+I572CR/Vg== | ||
5953 | dependencies: | ||
5954 | ini "^1.3.2" | ||
5955 | |||
5859 | glob-parent@^5.1.2, glob-parent@~5.1.2: | 5956 | glob-parent@^5.1.2, glob-parent@~5.1.2: |
5860 | version "5.1.2" | 5957 | version "5.1.2" |
5861 | resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" | 5958 | resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" |
@@ -5887,7 +5984,7 @@ glob@7.2.0: | |||
5887 | once "^1.3.0" | 5984 | once "^1.3.0" |
5888 | path-is-absolute "^1.0.0" | 5985 | path-is-absolute "^1.0.0" |
5889 | 5986 | ||
5890 | glob@8.0.3, glob@^8.0.1, glob@^8.0.3: | 5987 | glob@8.0.3, glob@^8.0.1: |
5891 | version "8.0.3" | 5988 | version "8.0.3" |
5892 | resolved "https://registry.yarnpkg.com/glob/-/glob-8.0.3.tgz#415c6eb2deed9e502c68fa44a272e6da6eeca42e" | 5989 | resolved "https://registry.yarnpkg.com/glob/-/glob-8.0.3.tgz#415c6eb2deed9e502c68fa44a272e6da6eeca42e" |
5893 | integrity sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ== | 5990 | integrity sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ== |
@@ -5910,6 +6007,17 @@ glob@^7.0.5, glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: | |||
5910 | once "^1.3.0" | 6007 | once "^1.3.0" |
5911 | path-is-absolute "^1.0.0" | 6008 | path-is-absolute "^1.0.0" |
5912 | 6009 | ||
6010 | glob@^8.0.3: | ||
6011 | version "8.1.0" | ||
6012 | resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" | ||
6013 | integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== | ||
6014 | dependencies: | ||
6015 | fs.realpath "^1.0.0" | ||
6016 | inflight "^1.0.4" | ||
6017 | inherits "2" | ||
6018 | minimatch "^5.0.1" | ||
6019 | once "^1.3.0" | ||
6020 | |||
5913 | glob@~7.1.1: | 6021 | glob@~7.1.1: |
5914 | version "7.1.7" | 6022 | version "7.1.7" |
5915 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" | 6023 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" |
@@ -6002,7 +6110,7 @@ gopd@^1.0.1: | |||
6002 | dependencies: | 6110 | dependencies: |
6003 | get-intrinsic "^1.1.3" | 6111 | get-intrinsic "^1.1.3" |
6004 | 6112 | ||
6005 | got@11.8.5, got@^11.0.2, got@^11.8.1: | 6113 | got@11.8.5: |
6006 | version "11.8.5" | 6114 | version "11.8.5" |
6007 | resolved "https://registry.yarnpkg.com/got/-/got-11.8.5.tgz#ce77d045136de56e8f024bebb82ea349bc730046" | 6115 | resolved "https://registry.yarnpkg.com/got/-/got-11.8.5.tgz#ce77d045136de56e8f024bebb82ea349bc730046" |
6008 | integrity sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ== | 6116 | integrity sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ== |
@@ -6019,6 +6127,23 @@ got@11.8.5, got@^11.0.2, got@^11.8.1: | |||
6019 | p-cancelable "^2.0.0" | 6127 | p-cancelable "^2.0.0" |
6020 | responselike "^2.0.0" | 6128 | responselike "^2.0.0" |
6021 | 6129 | ||
6130 | got@^11.0.2, got@^11.8.1: | ||
6131 | version "11.8.6" | ||
6132 | resolved "https://registry.yarnpkg.com/got/-/got-11.8.6.tgz#276e827ead8772eddbcfc97170590b841823233a" | ||
6133 | integrity sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g== | ||
6134 | dependencies: | ||
6135 | "@sindresorhus/is" "^4.0.0" | ||
6136 | "@szmarczak/http-timer" "^4.0.5" | ||
6137 | "@types/cacheable-request" "^6.0.1" | ||
6138 | "@types/responselike" "^1.0.0" | ||
6139 | cacheable-lookup "^5.0.3" | ||
6140 | cacheable-request "^7.0.2" | ||
6141 | decompress-response "^6.0.0" | ||
6142 | http2-wrapper "^1.0.0-beta.5.2" | ||
6143 | lowercase-keys "^2.0.0" | ||
6144 | p-cancelable "^2.0.0" | ||
6145 | responselike "^2.0.0" | ||
6146 | |||
6022 | graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: | 6147 | graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: |
6023 | version "4.2.10" | 6148 | version "4.2.10" |
6024 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" | 6149 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" |
@@ -6093,7 +6218,7 @@ has-property-descriptors@^1.0.0: | |||
6093 | dependencies: | 6218 | dependencies: |
6094 | get-intrinsic "^1.1.1" | 6219 | get-intrinsic "^1.1.1" |
6095 | 6220 | ||
6096 | has-symbols@^1.0.1, has-symbols@^1.0.2, has-symbols@^1.0.3: | 6221 | has-symbols@^1.0.2, has-symbols@^1.0.3: |
6097 | version "1.0.3" | 6222 | version "1.0.3" |
6098 | resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" | 6223 | resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" |
6099 | integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== | 6224 | integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== |
@@ -6347,7 +6472,7 @@ humanize-ms@^1.2.1: | |||
6347 | dependencies: | 6472 | dependencies: |
6348 | ms "^2.0.0" | 6473 | ms "^2.0.0" |
6349 | 6474 | ||
6350 | iconv-lite@0.4.24, iconv-lite@^0.4.24: | 6475 | iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@~0.4.24: |
6351 | version "0.4.24" | 6476 | version "0.4.24" |
6352 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" | 6477 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" |
6353 | integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== | 6478 | integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== |
@@ -6469,7 +6594,7 @@ ini@3.0.0: | |||
6469 | resolved "https://registry.yarnpkg.com/ini/-/ini-3.0.0.tgz#2f6de95006923aa75feed8894f5686165adc08f1" | 6594 | resolved "https://registry.yarnpkg.com/ini/-/ini-3.0.0.tgz#2f6de95006923aa75feed8894f5686165adc08f1" |
6470 | integrity sha512-TxYQaeNW/N8ymDvwAxPyRbhMBtnEwuvaTYpOQkFx1nSeusgezHniEc/l35Vo4iCq/mMiTJbpD7oYxN98hFlfmw== | 6595 | integrity sha512-TxYQaeNW/N8ymDvwAxPyRbhMBtnEwuvaTYpOQkFx1nSeusgezHniEc/l35Vo4iCq/mMiTJbpD7oYxN98hFlfmw== |
6471 | 6596 | ||
6472 | ini@^1.3.5: | 6597 | ini@^1.3.2, ini@^1.3.5: |
6473 | version "1.3.8" | 6598 | version "1.3.8" |
6474 | resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" | 6599 | resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" |
6475 | integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== | 6600 | integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== |
@@ -6504,6 +6629,15 @@ internal-slot@^1.0.3: | |||
6504 | has "^1.0.3" | 6629 | has "^1.0.3" |
6505 | side-channel "^1.0.4" | 6630 | side-channel "^1.0.4" |
6506 | 6631 | ||
6632 | internal-slot@^1.0.4: | ||
6633 | version "1.0.4" | ||
6634 | resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.4.tgz#8551e7baf74a7a6ba5f749cfb16aa60722f0d6f3" | ||
6635 | integrity sha512-tA8URYccNzMo94s5MQZgH8NB/XTa6HsOo0MLfXTKKEnHVVdegzaQoFZ7Jp44bdvLvY2waT5dc+j5ICEswhi7UQ== | ||
6636 | dependencies: | ||
6637 | get-intrinsic "^1.1.3" | ||
6638 | has "^1.0.3" | ||
6639 | side-channel "^1.0.4" | ||
6640 | |||
6507 | interpret@^2.2.0: | 6641 | interpret@^2.2.0: |
6508 | version "2.2.0" | 6642 | version "2.2.0" |
6509 | resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" | 6643 | resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" |
@@ -6556,7 +6690,12 @@ ipaddr.js@1.9.1: | |||
6556 | resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.0.1.tgz#eca256a7a877e917aeb368b0a7497ddf42ef81c0" | 6690 | resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.0.1.tgz#eca256a7a877e917aeb368b0a7497ddf42ef81c0" |
6557 | integrity sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng== | 6691 | integrity sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng== |
6558 | 6692 | ||
6559 | is-arguments@^1.1.0, is-arguments@^1.1.1: | 6693 | ipv6-normalize@1.0.1: |
6694 | version "1.0.1" | ||
6695 | resolved "https://registry.yarnpkg.com/ipv6-normalize/-/ipv6-normalize-1.0.1.tgz#1b3258290d365fa83239e89907dde4592e7620a8" | ||
6696 | integrity sha512-Bm6H79i01DjgGTCWjUuCjJ6QDo1HB96PT/xCYuyJUP9WFbVDrLSbG4EZCvOCun2rNswZb0c3e4Jt/ws795esHA== | ||
6697 | |||
6698 | is-arguments@^1.1.1: | ||
6560 | version "1.1.1" | 6699 | version "1.1.1" |
6561 | resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" | 6700 | resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" |
6562 | integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== | 6701 | integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== |
@@ -6564,6 +6703,15 @@ is-arguments@^1.1.0, is-arguments@^1.1.1: | |||
6564 | call-bind "^1.0.2" | 6703 | call-bind "^1.0.2" |
6565 | has-tostringtag "^1.0.0" | 6704 | has-tostringtag "^1.0.0" |
6566 | 6705 | ||
6706 | is-array-buffer@^3.0.1: | ||
6707 | version "3.0.1" | ||
6708 | resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.1.tgz#deb1db4fcae48308d54ef2442706c0393997052a" | ||
6709 | integrity sha512-ASfLknmY8Xa2XtB4wmbz13Wu202baeA18cJBCeCy0wXUHZF0IPyVEXqKEcd+t2fNSLLL1vC6k7lxZEojNbISXQ== | ||
6710 | dependencies: | ||
6711 | call-bind "^1.0.2" | ||
6712 | get-intrinsic "^1.1.3" | ||
6713 | is-typed-array "^1.1.10" | ||
6714 | |||
6567 | is-arrayish@^0.2.1: | 6715 | is-arrayish@^0.2.1: |
6568 | version "0.2.1" | 6716 | version "0.2.1" |
6569 | resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" | 6717 | resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" |
@@ -7508,6 +7656,16 @@ magnet-uri@^6.2.0: | |||
7508 | bep53-range "^1.1.0" | 7656 | bep53-range "^1.1.0" |
7509 | thirty-two "^1.0.2" | 7657 | thirty-two "^1.0.2" |
7510 | 7658 | ||
7659 | mailparser-mit@^1.0.0: | ||
7660 | version "1.0.0" | ||
7661 | resolved "https://registry.yarnpkg.com/mailparser-mit/-/mailparser-mit-1.0.0.tgz#19df8436c2a02e1d34a03ec518a2eb065e0a94a4" | ||
7662 | integrity sha512-sckRITNb3VCT1sQ275g47MAN786pQ5lU20bLY5f794dF/ARGzuVATQ64gO13FOw8jayjFT10e5ttsripKGGXcw== | ||
7663 | dependencies: | ||
7664 | addressparser "^1.0.1" | ||
7665 | iconv-lite "~0.4.24" | ||
7666 | mime "^1.6.0" | ||
7667 | uue "^3.1.0" | ||
7668 | |||
7511 | make-dir@^2.1.0: | 7669 | make-dir@^2.1.0: |
7512 | version "2.1.0" | 7670 | version "2.1.0" |
7513 | resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" | 7671 | resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" |
@@ -7576,6 +7734,13 @@ marky@^1.2.2: | |||
7576 | resolved "https://registry.yarnpkg.com/marky/-/marky-1.2.5.tgz#55796b688cbd72390d2d399eaaf1832c9413e3c0" | 7734 | resolved "https://registry.yarnpkg.com/marky/-/marky-1.2.5.tgz#55796b688cbd72390d2d399eaaf1832c9413e3c0" |
7577 | integrity sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q== | 7735 | integrity sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q== |
7578 | 7736 | ||
7737 | matchit@^1.0.0: | ||
7738 | version "1.1.0" | ||
7739 | resolved "https://registry.yarnpkg.com/matchit/-/matchit-1.1.0.tgz#c4ccf17d9c824cc1301edbcffde9b75a61d10a7c" | ||
7740 | integrity sha512-+nGYoOlfHmxe5BW5tE0EMJppXEwdSf8uBA1GTZC7Q77kbT35+VKLYJMzVNWCHSsga1ps1tPYFtFyvxvKzWVmMA== | ||
7741 | dependencies: | ||
7742 | "@arr/every" "^1.0.0" | ||
7743 | |||
7579 | mathml-tag-names@^2.1.3: | 7744 | mathml-tag-names@^2.1.3: |
7580 | version "2.1.3" | 7745 | version "2.1.3" |
7581 | resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" | 7746 | resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" |
@@ -7679,7 +7844,7 @@ mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, | |||
7679 | dependencies: | 7844 | dependencies: |
7680 | mime-db "1.52.0" | 7845 | mime-db "1.52.0" |
7681 | 7846 | ||
7682 | mime@1.6.0, mime@^1.4.1: | 7847 | mime@1.6.0, mime@^1.4.1, mime@^1.6.0: |
7683 | version "1.6.0" | 7848 | version "1.6.0" |
7684 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" | 7849 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" |
7685 | integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== | 7850 | integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== |
@@ -7740,7 +7905,7 @@ minimatch@5.0.1: | |||
7740 | dependencies: | 7905 | dependencies: |
7741 | brace-expansion "^2.0.1" | 7906 | brace-expansion "^2.0.1" |
7742 | 7907 | ||
7743 | minimatch@5.1.0, minimatch@^5.0.0, minimatch@^5.0.1, minimatch@^5.1.0: | 7908 | minimatch@5.1.0: |
7744 | version "5.1.0" | 7909 | version "5.1.0" |
7745 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.0.tgz#1717b464f4971b144f6aabe8f2d0b8e4511e09c7" | 7910 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.0.tgz#1717b464f4971b144f6aabe8f2d0b8e4511e09c7" |
7746 | integrity sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg== | 7911 | integrity sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg== |
@@ -7754,6 +7919,13 @@ minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: | |||
7754 | dependencies: | 7919 | dependencies: |
7755 | brace-expansion "^1.1.7" | 7920 | brace-expansion "^1.1.7" |
7756 | 7921 | ||
7922 | minimatch@^5.0.0, minimatch@^5.0.1, minimatch@^5.1.0: | ||
7923 | version "5.1.6" | ||
7924 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" | ||
7925 | integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== | ||
7926 | dependencies: | ||
7927 | brace-expansion "^2.0.1" | ||
7928 | |||
7757 | minimatch@~3.0.2: | 7929 | minimatch@~3.0.2: |
7758 | version "3.0.8" | 7930 | version "3.0.8" |
7759 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.8.tgz#5e6a59bd11e2ab0de1cfb843eb2d82e546c321c1" | 7931 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.8.tgz#5e6a59bd11e2ab0de1cfb843eb2d82e546c321c1" |
@@ -7848,9 +8020,9 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: | |||
7848 | integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== | 8020 | integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== |
7849 | 8021 | ||
7850 | mocha@^10.0.0: | 8022 | mocha@^10.0.0: |
7851 | version "10.1.0" | 8023 | version "10.2.0" |
7852 | resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.1.0.tgz#dbf1114b7c3f9d0ca5de3133906aea3dfc89ef7a" | 8024 | resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.2.0.tgz#1fd4a7c32ba5ac372e03a17eef435bd00e5c68b8" |
7853 | integrity sha512-vUF7IYxEoN7XhQpFLxQAEMtE4W91acW4B6En9l97MwE9stL1A9gusXfoHZCLVHDUJ/7V5+lbCM6yMqzo5vNymg== | 8025 | integrity sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg== |
7854 | dependencies: | 8026 | dependencies: |
7855 | ansi-colors "4.1.1" | 8027 | ansi-colors "4.1.1" |
7856 | browser-stdout "1.3.1" | 8028 | browser-stdout "1.3.1" |
@@ -8080,6 +8252,11 @@ node-releases@^2.0.6: | |||
8080 | resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503" | 8252 | resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503" |
8081 | integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg== | 8253 | integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg== |
8082 | 8254 | ||
8255 | nodemailer@6.7.3: | ||
8256 | version "6.7.3" | ||
8257 | resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.7.3.tgz#b73f9a81b9c8fa8acb4ea14b608f5e725ea8e018" | ||
8258 | integrity sha512-KUdDsspqx89sD4UUyUKzdlUOper3hRkDVkrKh/89G+d9WKsU5ox51NWS4tB1XR5dPUdR4SP0E3molyEfOvSa3g== | ||
8259 | |||
8083 | nopt@^6.0.0: | 8260 | nopt@^6.0.0: |
8084 | version "6.0.0" | 8261 | version "6.0.0" |
8085 | resolved "https://registry.yarnpkg.com/nopt/-/nopt-6.0.0.tgz#245801d8ebf409c6df22ab9d95b65e1309cdb16d" | 8262 | resolved "https://registry.yarnpkg.com/nopt/-/nopt-6.0.0.tgz#245801d8ebf409c6df22ab9d95b65e1309cdb16d" |
@@ -8267,7 +8444,12 @@ oauth-sign@~0.9.0: | |||
8267 | resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" | 8444 | resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" |
8268 | integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== | 8445 | integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== |
8269 | 8446 | ||
8270 | object-inspect@^1.10.3, object-inspect@^1.12.2, object-inspect@^1.9.0: | 8447 | object-inspect@^1.10.3, object-inspect@^1.9.0: |
8448 | version "1.12.3" | ||
8449 | resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" | ||
8450 | integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== | ||
8451 | |||
8452 | object-inspect@^1.12.2: | ||
8271 | version "1.12.2" | 8453 | version "1.12.2" |
8272 | resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" | 8454 | resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" |
8273 | integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== | 8455 | integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== |
@@ -8775,6 +8957,14 @@ pngjs@^5.0.0: | |||
8775 | resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb" | 8957 | resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb" |
8776 | integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw== | 8958 | integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw== |
8777 | 8959 | ||
8960 | polka@^0.5.2: | ||
8961 | version "0.5.2" | ||
8962 | resolved "https://registry.yarnpkg.com/polka/-/polka-0.5.2.tgz#588bee0c5806dbc6c64958de3a1251860e9f2e26" | ||
8963 | integrity sha512-FVg3vDmCqP80tOrs+OeNlgXYmFppTXdjD5E7I4ET1NjvtNmQrb1/mJibybKkb/d4NA7YWAr1ojxuhpL3FHqdlw== | ||
8964 | dependencies: | ||
8965 | "@polka/url" "^0.5.0" | ||
8966 | trouter "^2.0.1" | ||
8967 | |||
8778 | postcss-attribute-case-insensitive@^5.0.2: | 8968 | postcss-attribute-case-insensitive@^5.0.2: |
8779 | version "5.0.2" | 8969 | version "5.0.2" |
8780 | resolved "https://registry.yarnpkg.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.2.tgz#03d761b24afc04c09e757e92ff53716ae8ea2741" | 8970 | resolved "https://registry.yarnpkg.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.2.tgz#03d761b24afc04c09e757e92ff53716ae8ea2741" |
@@ -9285,9 +9475,9 @@ qs@~6.5.2: | |||
9285 | integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA== | 9475 | integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA== |
9286 | 9476 | ||
9287 | query-selector-shadow-dom@^1.0.0: | 9477 | query-selector-shadow-dom@^1.0.0: |
9288 | version "1.0.0" | 9478 | version "1.0.1" |
9289 | resolved "https://registry.yarnpkg.com/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.0.tgz#8fa7459a4620f094457640e74e953a9dbe61a38e" | 9479 | resolved "https://registry.yarnpkg.com/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.1.tgz#1c7b0058eff4881ac44f45d8f84ede32e9a2f349" |
9290 | integrity sha512-bK0/0cCI+R8ZmOF1QjT7HupDUYCxbf/9TJgAmSXQxZpftXmTAeil9DRoCnTDkWbvOyZzhcMBwKpptWcdkGFIMg== | 9480 | integrity sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw== |
9291 | 9481 | ||
9292 | querystring@0.2.0: | 9482 | querystring@0.2.0: |
9293 | version "0.2.0" | 9483 | version "0.2.0" |
@@ -9736,9 +9926,9 @@ responselike@^2.0.0: | |||
9736 | lowercase-keys "^2.0.0" | 9926 | lowercase-keys "^2.0.0" |
9737 | 9927 | ||
9738 | resq@^1.9.1: | 9928 | resq@^1.9.1: |
9739 | version "1.10.2" | 9929 | version "1.11.0" |
9740 | resolved "https://registry.yarnpkg.com/resq/-/resq-1.10.2.tgz#cedf4f20d53f6e574b1e12afbda446ad9576c193" | 9930 | resolved "https://registry.yarnpkg.com/resq/-/resq-1.11.0.tgz#edec8c58be9af800fd628118c0ca8815283de196" |
9741 | integrity sha512-HmgVS3j+FLrEDBTDYysPdPVF9/hioDMJ/otOiQDKqk77YfZeeLOj0qi34yObumcud1gBpk+wpBTEg4kMicD++A== | 9931 | integrity sha512-G10EBz+zAAy3zUd/CDoBbXRL6ia9kOo3xRHrMDsHljI0GDkhYlyjwoCx5+3eCC4swi1uCoZQhskuJkj7Gp57Bw== |
9742 | dependencies: | 9932 | dependencies: |
9743 | fast-deep-equal "^2.0.1" | 9933 | fast-deep-equal "^2.0.1" |
9744 | 9934 | ||
@@ -9835,13 +10025,20 @@ rxjs@6.6.7: | |||
9835 | dependencies: | 10025 | dependencies: |
9836 | tslib "^1.9.0" | 10026 | tslib "^1.9.0" |
9837 | 10027 | ||
9838 | rxjs@^7.3.0, rxjs@^7.4.0, rxjs@^7.5.5: | 10028 | rxjs@^7.3.0, rxjs@^7.4.0: |
9839 | version "7.5.7" | 10029 | version "7.5.7" |
9840 | resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.7.tgz#2ec0d57fdc89ece220d2e702730ae8f1e49def39" | 10030 | resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.7.tgz#2ec0d57fdc89ece220d2e702730ae8f1e49def39" |
9841 | integrity sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA== | 10031 | integrity sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA== |
9842 | dependencies: | 10032 | dependencies: |
9843 | tslib "^2.1.0" | 10033 | tslib "^2.1.0" |
9844 | 10034 | ||
10035 | rxjs@^7.5.5: | ||
10036 | version "7.8.0" | ||
10037 | resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.0.tgz#90a938862a82888ff4c7359811a595e14e1e09a4" | ||
10038 | integrity sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg== | ||
10039 | dependencies: | ||
10040 | tslib "^2.1.0" | ||
10041 | |||
9845 | safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: | 10042 | safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: |
9846 | version "5.1.2" | 10043 | version "5.1.2" |
9847 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" | 10044 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" |
@@ -10191,6 +10388,15 @@ smart-buffer@^4.2.0: | |||
10191 | resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" | 10388 | resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" |
10192 | integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== | 10389 | integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== |
10193 | 10390 | ||
10391 | smtp-server@^3.9.0: | ||
10392 | version "3.11.0" | ||
10393 | resolved "https://registry.yarnpkg.com/smtp-server/-/smtp-server-3.11.0.tgz#8820c191124fab37a8f16c8325a7f1fd38092c4f" | ||
10394 | integrity sha512-j/W6mEKeMNKuiM9oCAAjm87agPEN1O3IU4cFLT4ZOCyyq3UXN7HiIXF+q7izxJcYSar15B/JaSxcijoPCR8Tag== | ||
10395 | dependencies: | ||
10396 | base32.js "0.1.0" | ||
10397 | ipv6-normalize "1.0.1" | ||
10398 | nodemailer "6.7.3" | ||
10399 | |||
10194 | socket.io-client@^4.5.4: | 10400 | socket.io-client@^4.5.4: |
10195 | version "4.5.4" | 10401 | version "4.5.4" |
10196 | resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.5.4.tgz#d3cde8a06a6250041ba7390f08d2468ccebc5ac9" | 10402 | resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.5.4.tgz#d3cde8a06a6250041ba7390f08d2468ccebc5ac9" |
@@ -10420,6 +10626,13 @@ statuses@2.0.1: | |||
10420 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" | 10626 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" |
10421 | integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== | 10627 | integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== |
10422 | 10628 | ||
10629 | stop-iteration-iterator@^1.0.0: | ||
10630 | version "1.0.0" | ||
10631 | resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz#6a60be0b4ee757d1ed5254858ec66b10c49285e4" | ||
10632 | integrity sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ== | ||
10633 | dependencies: | ||
10634 | internal-slot "^1.0.4" | ||
10635 | |||
10423 | stream-browserify@^3.0.0: | 10636 | stream-browserify@^3.0.0: |
10424 | version "3.0.0" | 10637 | version "3.0.0" |
10425 | resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-3.0.0.tgz#22b0a2850cdf6503e73085da1fc7b7d0c2122f2f" | 10638 | resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-3.0.0.tgz#22b0a2850cdf6503e73085da1fc7b7d0c2122f2f" |
@@ -10980,6 +11193,13 @@ trim-newlines@^3.0.0: | |||
10980 | resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144" | 11193 | resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144" |
10981 | integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw== | 11194 | integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw== |
10982 | 11195 | ||
11196 | trouter@^2.0.1: | ||
11197 | version "2.0.1" | ||
11198 | resolved "https://registry.yarnpkg.com/trouter/-/trouter-2.0.1.tgz#2726a5f8558e090d24c3a393f09eaab1df232df6" | ||
11199 | integrity sha512-kr8SKKw94OI+xTGOkfsvwZQ8mWoikZDd2n8XZHjJVZUARZT+4/VV6cacRS6CLsH9bNm+HFIPU1Zx4CnNnb4qlQ== | ||
11200 | dependencies: | ||
11201 | matchit "^1.0.0" | ||
11202 | |||
10983 | ts-loader@^9.3.0: | 11203 | ts-loader@^9.3.0: |
10984 | version "9.4.1" | 11204 | version "9.4.1" |
10985 | resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.4.1.tgz#b6f3d82db0eac5a8295994f8cb5e4940ff6b1060" | 11205 | resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.4.1.tgz#b6f3d82db0eac5a8295994f8cb5e4940ff6b1060" |
@@ -11280,6 +11500,14 @@ utp-native@^2.5.3: | |||
11280 | timeout-refresh "^1.0.0" | 11500 | timeout-refresh "^1.0.0" |
11281 | unordered-set "^2.0.1" | 11501 | unordered-set "^2.0.1" |
11282 | 11502 | ||
11503 | uue@^3.1.0: | ||
11504 | version "3.1.2" | ||
11505 | resolved "https://registry.yarnpkg.com/uue/-/uue-3.1.2.tgz#e99368414e87200012eb37de4dbaebaa1c742ad2" | ||
11506 | integrity sha512-axKLXVqwtdI/czrjG0X8hyV1KLgeWx8F4KvSbvVCnS+RUvsQMGRjx0kfuZDXXqj0LYvVJmx3B9kWlKtEdRrJLg== | ||
11507 | dependencies: | ||
11508 | escape-string-regexp "~1.0.5" | ||
11509 | extend "~3.0.0" | ||
11510 | |||
11283 | uuid@8.3.2, uuid@^8.3.2: | 11511 | uuid@8.3.2, uuid@^8.3.2: |
11284 | version "8.3.2" | 11512 | version "8.3.2" |
11285 | resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" | 11513 | resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" |
@@ -11415,31 +11643,31 @@ wdio-geckodriver-service@^3.0.2: | |||
11415 | split2 "^4.1.0" | 11643 | split2 "^4.1.0" |
11416 | tcp-port-used "^1.0.2" | 11644 | tcp-port-used "^1.0.2" |
11417 | 11645 | ||
11418 | webdriver@7.26.0: | 11646 | webdriver@7.27.0: |
11419 | version "7.26.0" | 11647 | version "7.27.0" |
11420 | resolved "https://registry.yarnpkg.com/webdriver/-/webdriver-7.26.0.tgz#cc20640ee9906c0126044449dfe9562b6277d14e" | 11648 | resolved "https://registry.yarnpkg.com/webdriver/-/webdriver-7.27.0.tgz#41d23a6c38bd79ea868f0b9fb9c9e3d4b6e4f8bd" |
11421 | integrity sha512-T21T31wq29D/rmpFHcAahhdrvfsfXsLs/LBe2su7wL725ptOEoSssuDXjXMkwjf9MSUIXnTcUIz8oJGbKRUMwQ== | 11649 | integrity sha512-870uIBnrGJ86g3DdYjM+PHhqdWf6NxysSme1KIs6irWxK+LqcaWKWhN75PldE+04xJB2mVWt1tKn0NBBFTWeMg== |
11422 | dependencies: | 11650 | dependencies: |
11423 | "@types/node" "^18.0.0" | 11651 | "@types/node" "^18.0.0" |
11424 | "@wdio/config" "7.26.0" | 11652 | "@wdio/config" "7.26.0" |
11425 | "@wdio/logger" "7.26.0" | 11653 | "@wdio/logger" "7.26.0" |
11426 | "@wdio/protocols" "7.22.0" | 11654 | "@wdio/protocols" "7.27.0" |
11427 | "@wdio/types" "7.26.0" | 11655 | "@wdio/types" "7.26.0" |
11428 | "@wdio/utils" "7.26.0" | 11656 | "@wdio/utils" "7.26.0" |
11429 | got "^11.0.2" | 11657 | got "^11.0.2" |
11430 | ky "0.30.0" | 11658 | ky "0.30.0" |
11431 | lodash.merge "^4.6.1" | 11659 | lodash.merge "^4.6.1" |
11432 | 11660 | ||
11433 | webdriverio@7.26.0: | 11661 | webdriverio@7.29.1: |
11434 | version "7.26.0" | 11662 | version "7.29.1" |
11435 | resolved "https://registry.yarnpkg.com/webdriverio/-/webdriverio-7.26.0.tgz#d6036d950ef96fb6cc29c6c5c9cfc452fcafa59a" | 11663 | resolved "https://registry.yarnpkg.com/webdriverio/-/webdriverio-7.29.1.tgz#f71c9de317326cff36d22f6277669477e5340c6f" |
11436 | integrity sha512-7m9TeP871aYxZYKBI4GDh5aQZLN9Fd/PASu5K/jEIT65J4OBB6g5ZaycGFOmfNHCfjWKjwPXZuKiN1f2mcrcRg== | 11664 | integrity sha512-2xhoaZvV0tzOgnj8H/B4Yol8LTcIrWdfTdfe01d+ERtdzKCoqimmPNP4vpr2lVRVKL/TW4rfoBTBNvDUaJHe2g== |
11437 | dependencies: | 11665 | dependencies: |
11438 | "@types/aria-query" "^5.0.0" | 11666 | "@types/aria-query" "^5.0.0" |
11439 | "@types/node" "^18.0.0" | 11667 | "@types/node" "^18.0.0" |
11440 | "@wdio/config" "7.26.0" | 11668 | "@wdio/config" "7.26.0" |
11441 | "@wdio/logger" "7.26.0" | 11669 | "@wdio/logger" "7.26.0" |
11442 | "@wdio/protocols" "7.22.0" | 11670 | "@wdio/protocols" "7.27.0" |
11443 | "@wdio/repl" "7.26.0" | 11671 | "@wdio/repl" "7.26.0" |
11444 | "@wdio/types" "7.26.0" | 11672 | "@wdio/types" "7.26.0" |
11445 | "@wdio/utils" "7.26.0" | 11673 | "@wdio/utils" "7.26.0" |
@@ -11447,8 +11675,8 @@ webdriverio@7.26.0: | |||
11447 | aria-query "^5.0.0" | 11675 | aria-query "^5.0.0" |
11448 | css-shorthand-properties "^1.1.1" | 11676 | css-shorthand-properties "^1.1.1" |
11449 | css-value "^0.0.1" | 11677 | css-value "^0.0.1" |
11450 | devtools "7.26.0" | 11678 | devtools "7.28.1" |
11451 | devtools-protocol "^0.0.1069585" | 11679 | devtools-protocol "^0.0.1085790" |
11452 | fs-extra "^10.0.0" | 11680 | fs-extra "^10.0.0" |
11453 | grapheme-splitter "^1.0.2" | 11681 | grapheme-splitter "^1.0.2" |
11454 | lodash.clonedeep "^4.5.0" | 11682 | lodash.clonedeep "^4.5.0" |
@@ -11461,7 +11689,7 @@ webdriverio@7.26.0: | |||
11461 | resq "^1.9.1" | 11689 | resq "^1.9.1" |
11462 | rgb2hex "0.2.5" | 11690 | rgb2hex "0.2.5" |
11463 | serialize-error "^8.0.0" | 11691 | serialize-error "^8.0.0" |
11464 | webdriver "7.26.0" | 11692 | webdriver "7.27.0" |
11465 | 11693 | ||
11466 | webidl-conversions@^3.0.0: | 11694 | webidl-conversions@^3.0.0: |
11467 | version "3.0.1" | 11695 | version "3.0.1" |
@@ -11732,7 +11960,7 @@ which-module@^2.0.0: | |||
11732 | resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" | 11960 | resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" |
11733 | integrity sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q== | 11961 | integrity sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q== |
11734 | 11962 | ||
11735 | which-typed-array@^1.1.8: | 11963 | which-typed-array@^1.1.9: |
11736 | version "1.1.9" | 11964 | version "1.1.9" |
11737 | resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.9.tgz#307cf898025848cf995e795e8423c7f337efbde6" | 11965 | resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.9.tgz#307cf898025848cf995e795e8423c7f337efbde6" |
11738 | integrity sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA== | 11966 | integrity sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA== |
@@ -11770,6 +11998,11 @@ wildcard@^2.0.0: | |||
11770 | resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec" | 11998 | resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec" |
11771 | integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw== | 11999 | integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw== |
11772 | 12000 | ||
12001 | wildstring@1.0.9: | ||
12002 | version "1.0.9" | ||
12003 | resolved "https://registry.yarnpkg.com/wildstring/-/wildstring-1.0.9.tgz#82a696d5653c7d4ec9ba716859b6b53aba2761c5" | ||
12004 | integrity sha512-XBNxKIMLO6uVHf1Xvo++HGWAZZoiVCHmEMCmZJzJ82vQsuUJCLw13Gzq0mRCATk7a3+ZcgeOKSDioavuYqtlfA== | ||
12005 | |||
11773 | word-wrap@^1.2.3: | 12006 | word-wrap@^1.2.3: |
11774 | version "1.2.3" | 12007 | version "1.2.3" |
11775 | resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" | 12008 | resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" |