Merge branch 'release/2.3.0' into develop

pull/2977/head
Chocobozzz 2020-07-20 17:00:58 +02:00
commit ec903c010e
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
22 changed files with 141 additions and 112 deletions

View File

@ -44,8 +44,8 @@
<td class="action-cell">
<ng-container *ngIf="follow.state === 'pending'">
<my-button i18n-label label="Accept" icon="tick" (click)="acceptFollower(follow)"></my-button>
<my-button i18n-label label="Refuse" icon="cross" (click)="rejectFollower(follow)"></my-button>
<my-button i18n-title title="Accept" icon="tick" (click)="acceptFollower(follow)"></my-button>
<my-button i18n-title title="Refuse" icon="cross" (click)="rejectFollower(follow)"></my-button>
</ng-container>
<my-delete-button *ngIf="follow.state === 'accepted'" (click)="deleteFollower(follow)"></my-delete-button>

View File

@ -95,7 +95,7 @@
<input
type="text" id="email" i18n-placeholder placeholder="mail@example.com" class="form-control"
formControlName="email" [ngClass]="{ 'input-error': formErrors['email'] }"
autocomplete="off" [readonly]="user.pluginAuth !== null"
autocomplete="off" [readonly]="user && user.pluginAuth !== null"
>
<div *ngIf="formErrors.email" class="form-error">
{{ formErrors.email }}

View File

@ -45,7 +45,7 @@
<td>{{ videoImport.createdAt | date: 'short' }}</td>
<td class="action-cell">
<my-edit-button *ngIf="isVideoImportSuccess(videoImport) && videoImport.video" [routerLink]="getEditVideoUrl(videoImport.video)"></my-edit-button>
<my-edit-button label=" " *ngIf="isVideoImportSuccess(videoImport) && videoImport.video" [routerLink]="getEditVideoUrl(videoImport.video)"></my-edit-button>
</td>
</tr>
</ng-template>

View File

@ -12,20 +12,12 @@ export class ChannelLazyLoadResolver implements Resolve<any> {
resolve (route: ActivatedRouteSnapshot) {
const url = route.params.url
const externalRedirect = route.params.externalRedirect
const fromPath = route.params.fromPath
if (!url) {
console.error('Could not find url param.', { params: route.params })
return this.router.navigateByUrl('/404')
}
if (externalRedirect === 'true') {
window.open(url)
this.router.navigateByUrl(fromPath)
return
}
return this.searchService.searchVideoChannels({ search: url })
.pipe(
map(result => {

View File

@ -35,15 +35,27 @@
<ng-container *ngFor="let result of results">
<div *ngIf="isVideoChannel(result)" class="entry video-channel">
<a [routerLink]="getChannelUrl(result)">
<a *ngIf="!isExternalChannelUrl()" [routerLink]="getChannelUrl(result)">
<img [src]="result.avatarUrl" alt="Avatar" />
</a>
<a *ngIf="isExternalChannelUrl()" [href]="getChannelUrl(result)" target="_blank">
<img [src]="result.avatarUrl" alt="Avatar" />
</a>
<div class="video-channel-info">
<a [routerLink]="getChannelUrl(result)" class="video-channel-names">
<a *ngIf="!isExternalChannelUrl()" [routerLink]="getChannelUrl(result)" class="video-channel-names">
<ng-container *ngTemplateOutlet="aContent"></ng-container>
</a>
<a *ngIf="isExternalChannelUrl()" [href]="getChannelUrl(result)" target="_blank" class="video-channel-names">
<ng-container *ngTemplateOutlet="aContent"></ng-container>
</a>
<ng-template #aContent>
<div class="video-channel-display-name">{{ result.displayName }}</div>
<div class="video-channel-name">{{ result.nameWithHost }}</div>
</a>
</ng-template>
<div i18n class="video-channel-followers">{{ result.followersCount }} subscribers</div>
</div>
@ -54,7 +66,7 @@
<div *ngIf="isVideo(result)" class="entry video">
<my-video-miniature
[video]="result" [user]="userMiniature" [displayAsRow]="true" [displayVideoActions]="!hideActions()"
[displayOptions]="videoDisplayOptions" [useLazyLoadUrl]="advancedSearch.searchTarget === 'search-index'"
[displayOptions]="videoDisplayOptions" [videoLinkType]="getVideoLinkType()"
(videoBlocked)="removeVideoFromArray(result)" (videoRemoved)="removeVideoFromArray(result)"
></my-video-miniature>
</div>

View File

@ -5,7 +5,7 @@ import { AuthService, ComponentPagination, HooksService, Notifier, ServerService
import { immutableAssign } from '@app/helpers'
import { Video, VideoChannel } from '@app/shared/shared-main'
import { AdvancedSearch, SearchService } from '@app/shared/shared-search'
import { MiniatureDisplayOptions } from '@app/shared/shared-video-miniature'
import { MiniatureDisplayOptions, VideoLinkType } from '@app/shared/shared-video-miniature'
import { MetaService } from '@ngx-meta/core'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { SearchTargetType, ServerConfig } from '@shared/models'
@ -119,6 +119,25 @@ export class SearchComponent implements OnInit, OnDestroy {
return this.authService.isLoggedIn()
}
getVideoLinkType (): VideoLinkType {
if (this.advancedSearch.searchTarget === 'search-index') {
const remoteUriConfig = this.serverConfig.search.remoteUri
// Redirect on the external instance if not allowed to fetch remote data
if ((!this.isUserLoggedIn() && !remoteUriConfig.anonymous) || !remoteUriConfig.users) {
return 'external'
}
return 'lazy-load'
}
return 'internal'
}
isExternalChannelUrl () {
return this.getVideoLinkType() === 'external'
}
search () {
forkJoin([
this.getVideosObs(),
@ -184,17 +203,16 @@ export class SearchComponent implements OnInit, OnDestroy {
}
getChannelUrl (channel: VideoChannel) {
if (this.advancedSearch.searchTarget === 'search-index' && channel.url) {
const remoteUriConfig = this.serverConfig.search.remoteUri
// Redirect on the external instance if not allowed to fetch remote data
const externalRedirect = (!this.authService.isLoggedIn() && !remoteUriConfig.anonymous) || !remoteUriConfig.users
const fromPath = window.location.pathname + window.location.search
return [ '/search/lazy-load-channel', { url: channel.url, externalRedirect, fromPath } ]
// Same algorithm than videos
if (this.getVideoLinkType() === 'external') {
return channel.url
}
return [ '/video-channels', channel.nameWithHost ]
if (this.getVideoLinkType() === 'internal') {
return [ '/video-channels', channel.nameWithHost ]
}
return [ '/search/lazy-load-channel', { url: channel.url } ]
}
hideActions () {

View File

@ -12,20 +12,12 @@ export class VideoLazyLoadResolver implements Resolve<any> {
resolve (route: ActivatedRouteSnapshot) {
const url = route.params.url
const externalRedirect = route.params.externalRedirect
const fromPath = route.params.fromPath
if (!url) {
console.error('Could not find url param.', { params: route.params })
return this.router.navigateByUrl('/404')
}
if (externalRedirect === 'true') {
window.open(url)
this.router.navigateByUrl(fromPath)
return
}
return this.searchService.searchVideos({ search: url })
.pipe(
map(result => {

View File

@ -43,7 +43,7 @@ $input-border-radius: 3px;
}
.grey-button {
padding: 0 12px 0 12px;
padding: 0 7px 0 12px;
}
}
}

View File

@ -1,4 +1,4 @@
<span class="action-button" [ngClass]="getClasses()" [title]="getTitle()">
<span class="action-button" [ngClass]="getClasses()" [title]="getTitle()" tabindex="0">
<my-global-icon *ngIf="!loading" [iconName]="icon"></my-global-icon>
<my-small-loader [loading]="loading"></my-small-loader>

View File

@ -1,4 +1,4 @@
<span class="action-button action-button-delete grey-button" [title]="title" role="button">
<span class="action-button action-button-delete grey-button" [title]="title" role="button" tabindex="0">
<my-global-icon iconName="delete" aria-hidden="true"></my-global-icon>
<span class="button-label" *ngIf="label">{{ label }}</span>

View File

@ -1,7 +1,12 @@
<a
[routerLink]="getVideoRouterLink()" [queryParams]="queryParams"
class="video-thumbnail"
>
<a *ngIf="!videoHref" [routerLink]="getVideoRouterLink()" [queryParams]="queryParams" class="video-thumbnail">
<ng-container *ngTemplateOutlet="aContent"></ng-container>
</a>
<a *ngIf="videoHref" [href]="videoHref" [target]="videoTarget" class="video-thumbnail">
<ng-container *ngTemplateOutlet="aContent"></ng-container>
</a>
<ng-template #aContent>
<img alt="" [attr.aria-label]="video.name" [attr.src]="getImageUrl()" [ngClass]="{ 'blur-filter': nsfw }" />
<div *ngIf="displayWatchLaterPlaylist" class="video-thumbnail-actions-overlay">
@ -30,4 +35,4 @@
<div class="progress-bar" *ngIf="video.userHistory?.currentTime">
<div [ngStyle]="{ 'width.%': getProgressPercent() }"></div>
</div>
</a>
</ng-template>

View File

@ -11,8 +11,11 @@ import { Video } from '../shared-main'
export class VideoThumbnailComponent {
@Input() video: Video
@Input() nsfw = false
@Input() routerLink: any[]
@Input() videoRouterLink: any[]
@Input() queryParams: { [ p: string ]: any }
@Input() videoHref: string
@Input() videoTarget: string
@Input() displayWatchLaterPlaylist: boolean
@Input() inWatchLaterPlaylist: boolean
@ -49,7 +52,7 @@ export class VideoThumbnailComponent {
}
getVideoRouterLink () {
if (this.routerLink) return this.routerLink
if (this.videoRouterLink) return this.videoRouterLink
return [ '/videos/watch', this.video.uuid ]
}

View File

@ -1,6 +1,6 @@
<div class="video-miniature" [ngClass]="{ 'display-as-row': displayAsRow, 'fit-width': fitWidth }" (mouseenter)="loadActions()">
<my-video-thumbnail
[video]="video" [nsfw]="isVideoBlur" [routerLink]="videoLink"
[video]="video" [nsfw]="isVideoBlur" [videoRouterLink]="videoRouterLink" [videoHref]="videoHref" [videoTarget]="videoTarget"
[displayWatchLaterPlaylist]="isWatchLaterPlaylistDisplayed()" [inWatchLaterPlaylist]="inWatchLaterPlaylist" (watchLaterClick)="onWatchLaterClick($event)"
>
<ng-container ngProjectAs="label-warning" *ngIf="displayOptions.privacyLabel && isUnlistedVideo()" i18n>Unlisted</ng-container>
@ -15,10 +15,12 @@
</a>
<div class="w-100 d-flex flex-column">
<a
tabindex="-1"
class="video-miniature-name"
[routerLink]="videoLink" [attr.title]="video.name" [ngClass]="{ 'blur-filter': isVideoBlur }"
<a *ngIf="!videoHref" tabindex="-1" class="video-miniature-name"
[routerLink]="videoRouterLink" [attr.title]="video.name" [ngClass]="{ 'blur-filter': isVideoBlur }"
>{{ video.name }}</a>
<a *ngIf="videoHref" tabindex="-1" class="video-miniature-name"
[href]="videoHref" [target]="videoTarget" [attr.title]="video.name" [ngClass]="{ 'blur-filter': isVideoBlur }"
>{{ video.name }}</a>
<span class="video-miniature-created-at-views">

View File

@ -29,6 +29,7 @@ export type MiniatureDisplayOptions = {
blacklistInfo?: boolean
nsfw?: boolean
}
export type VideoLinkType = 'internal' | 'lazy-load' | 'external'
@Component({
selector: 'my-video-miniature',
@ -55,7 +56,7 @@ export class VideoMiniatureComponent implements OnInit {
@Input() displayVideoActions = true
@Input() fitWidth = false
@Input() useLazyLoadUrl = false
@Input() videoLinkType: VideoLinkType = 'internal'
@Output() videoBlocked = new EventEmitter()
@Output() videoUnblocked = new EventEmitter()
@ -85,7 +86,9 @@ export class VideoMiniatureComponent implements OnInit {
playlistElementId?: number
}
videoLink: any[] = []
videoRouterLink: any[] = []
videoHref: string
videoTarget: string
private ownerDisplayTypeChosen: 'account' | 'videoChannel'
@ -125,18 +128,20 @@ export class VideoMiniatureComponent implements OnInit {
}
buildVideoLink () {
if (this.useLazyLoadUrl && this.video.url) {
const remoteUriConfig = this.serverConfig.search.remoteUri
// Redirect on the external instance if not allowed to fetch remote data
const externalRedirect = (!this.authService.isLoggedIn() && !remoteUriConfig.anonymous) || !remoteUriConfig.users
const fromPath = window.location.pathname + window.location.search
this.videoLink = [ '/search/lazy-load-video', { url: this.video.url, externalRedirect, fromPath } ]
if (this.videoLinkType === 'internal' || !this.video.url) {
this.videoRouterLink = [ '/videos/watch', this.video.uuid ]
return
}
this.videoLink = [ '/videos/watch', this.video.uuid ]
if (this.videoLinkType === 'external') {
this.videoRouterLink = null
this.videoHref = this.video.url
this.videoTarget = '_blank'
return
}
// Lazy load
this.videoRouterLink = [ '/search/lazy-load-video', { url: this.video.url } ]
}
displayOwnerAccount () {

View File

@ -10,14 +10,13 @@
position: fixed;
.action-button {
display: inline-block;
display: block;
margin-left: 55px;
}
.action-button-cancel-selection {
@include peertube-button;
@include grey-button;
margin-right: 10px;
}
}
}
@ -54,4 +53,8 @@
margin-top: 10px;
}
}
.action-selection-mode {
display: none; // disable for small screens
}
}

View File

@ -8,7 +8,7 @@
<my-video-thumbnail
*ngIf="playlistElement.video"
[video]="playlistElement.video" [nsfw]="isVideoBlur(playlistElement.video)"
[routerLink]="buildRouterLink()" [queryParams]="buildRouterQuery()"
[videoRouterLink]="buildRouterLink()" [queryParams]="buildRouterQuery()"
></my-video-thumbnail>
<div class="fake-thumbnail" *ngIf="!playlistElement.video"></div>

View File

@ -43,20 +43,24 @@ class P2pMediaLoaderPlugin extends Plugin {
// FIXME: typings https://github.com/Microsoft/TypeScript/issues/14080
if (!(videojs as any).Html5Hlsjs) {
const message = 'HLS.js does not seem to be supported.'
console.warn(message)
console.warn('HLS.js does not seem to be supported. Try to fallback to built in HLS.')
player.ready(() => player.trigger('error', new Error(message)))
return
if (!player.canPlayType('application/vnd.apple.mpegurl')) {
const message = 'Cannot fallback to built-in HLS'
console.warn(message)
player.ready(() => player.trigger('error', new Error(message)))
return
}
} else {
// FIXME: typings https://github.com/Microsoft/TypeScript/issues/14080
(videojs as any).Html5Hlsjs.addHook('beforeinitialize', (videojsPlayer: any, hlsjs: any) => {
this.hlsjs = hlsjs
})
initVideoJsContribHlsJsPlayer(player)
}
// FIXME: typings https://github.com/Microsoft/TypeScript/issues/14080
(videojs as any).Html5Hlsjs.addHook('beforeinitialize', (videojsPlayer: any, hlsjs: any) => {
this.hlsjs = hlsjs
})
initVideoJsContribHlsJsPlayer(player)
this.startTime = timeToInt(options.startTime)
player.src({
@ -64,11 +68,13 @@ class P2pMediaLoaderPlugin extends Plugin {
src: options.src
})
player.one('play', () => {
player.addClass('vjs-has-big-play-button-clicked')
})
player.ready(() => {
this.initializeCore()
player.ready(() => this.initialize())
if ((videojs as any).Html5Hlsjs) {
this.initializePlugin()
}
})
}
dispose () {
@ -82,7 +88,19 @@ class P2pMediaLoaderPlugin extends Plugin {
return this.hlsjs
}
private initialize () {
private initializeCore () {
this.player.one('play', () => {
this.player.addClass('vjs-has-big-play-button-clicked')
})
this.player.one('canplay', () => {
if (this.startTime) {
this.player.currentTime(this.startTime)
}
})
}
private initializePlugin () {
initHlsJsPlayer(this.hlsjs)
// FIXME: typings
@ -102,12 +120,6 @@ class P2pMediaLoaderPlugin extends Plugin {
this.statsP2PBytes.numPeers = 1 + this.options.redundancyUrlManager.countBaseUrls()
this.runStats()
this.player.one('canplay', () => {
if (this.startTime) {
this.player.currentTime(this.startTime)
}
})
}
private runStats () {

View File

@ -79,15 +79,6 @@ p-table {
tr {
&:hover {
background-color: pvar(--submenuColor) !important;
.action-cell {
.dropdown-root,
my-edit-button,
my-delete-button,
my-button {
display: inline-block !important;
}
}
}
td {
@ -164,18 +155,9 @@ p-table {
my-edit-button,
my-delete-button,
my-button {
display: none !important;
display: inline-block !important;
margin-left: 5px;
&.show {
display: inline-block !important;
}
// keep displaying on touchscreen
@media not all and (hover: hover) and (pointer: fine) {
display: inline-block !important;
}
:first-child {
margin-left: 0
}

View File

@ -340,7 +340,7 @@ async function askResetUserPassword (req: express.Request, res: express.Response
const verificationString = await Redis.Instance.setResetPasswordVerificationString(user.id)
const url = WEBSERVER.URL + '/reset-password?userId=' + user.id + '&verificationString=' + verificationString
await Emailer.Instance.addPasswordResetEmailJob(user.email, url)
await Emailer.Instance.addPasswordResetEmailJob(user.username, user.email, url)
return res.status(204).end()
}

View File

@ -427,12 +427,13 @@ class Emailer {
return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
}
addPasswordResetEmailJob (to: string, resetPasswordUrl: string) {
addPasswordResetEmailJob (username: string, to: string, resetPasswordUrl: string) {
const emailPayload: EmailPayload = {
template: 'password-reset',
to: [ to ],
subject: 'Reset your account password',
locals: {
username,
resetPasswordUrl
}
}
@ -454,12 +455,13 @@ class Emailer {
return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
}
addVerifyEmailJob (to: string, verifyEmailUrl: string) {
addVerifyEmailJob (username: string, to: string, verifyEmailUrl: string) {
const emailPayload: EmailPayload = {
template: 'verify-email',
to: [ to ],
subject: `Verify your email on ${WEBSERVER.HOST}`,
locals: {
username,
verifyEmailUrl
}
}

View File

@ -5,8 +5,8 @@ block title
block content
p.
A reset password procedure for your account ${to} has been requested on #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}].
Please follow #[a(href=resetPasswordUrl) this link] to reset it: #[a(href=resetPasswordUrl) #{resetPasswordUrl}]
A reset password procedure for your account #{username} has been requested on #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}].
Please follow #[a(href=resetPasswordUrl) this link] to reset it: #[a(href=resetPasswordUrl) #{resetPasswordUrl}]
(the link will expire within 1 hour)
p.
If you are not the person who initiated this request, please ignore this email.
If you are not the person who initiated this request, please ignore this email.

View File

@ -111,8 +111,9 @@ async function sendVerifyUserEmail (user: MUser, isPendingEmail = false) {
if (isPendingEmail) url += '&isPendingEmail=true'
const email = isPendingEmail ? user.pendingEmail : user.email
const username = user.username
await Emailer.Instance.addVerifyEmailJob(email, url)
await Emailer.Instance.addVerifyEmailJob(username, email, url)
}
// ---------------------------------------------------------------------------