From 22a16e36f6526887ed8f5e5d3c9f9e5da0b4a8cd Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 21 Aug 2018 16:18:59 +0200 Subject: [PATCH] Add local user subscriptions --- .../account-video-channels.component.scss | 2 +- .../+my-account/my-account-routing.module.ts | 10 +++ .../my-account-subscriptions.component.html | 23 ++++++ .../my-account-subscriptions.component.scss | 49 ++++++++++++ .../my-account-subscriptions.component.ts | 30 ++++++++ ...-account-video-channel-update.component.ts | 4 +- .../my-account-video-channels.component.scss | 15 +--- .../my-account-videos.component.scss | 6 +- .../app/+my-account/my-account.component.html | 6 +- .../src/app/+my-account/my-account.module.ts | 4 +- .../shared/actor-avatar-info.component.scss | 2 +- .../video-channels.component.html | 3 +- .../video-channels.component.scss | 15 ++++ client/src/app/menu/menu.component.html | 5 ++ client/src/app/menu/menu.component.scss | 6 ++ client/src/app/shared/shared.module.ts | 9 ++- .../src/app/shared/user-subscription/index.ts | 2 + .../subscribe-button.component.html | 15 ++++ .../subscribe-button.component.scss | 37 ++++++++++ .../subscribe-button.component.ts | 74 +++++++++++++++++++ .../user-subscription.service.ts | 66 +++++++++++++++++ .../video-channel/video-channel.service.ts | 30 ++++---- .../app/shared/video/abstract-video-list.html | 2 +- .../app/shared/video/abstract-video-list.ts | 2 + .../app/shared/video/video-details.model.ts | 12 +-- .../video/video-miniature.component.html | 8 +- .../video/video-miniature.component.scss | 3 +- .../shared/video/video-miniature.component.ts | 35 ++++++++- client/src/app/shared/video/video.model.ts | 8 +- client/src/app/shared/video/video.service.ts | 18 +++++ .../+video-watch/video-watch.component.html | 9 ++- .../+video-watch/video-watch.component.scss | 17 +++-- .../video-user-subscriptions.component.ts | 57 ++++++++++++++ .../src/app/videos/videos-routing.module.ts | 15 ++-- client/src/app/videos/videos.module.ts | 4 +- client/src/assets/images/menu/podcasts.svg | 26 +++++++ .../src/assets/images/menu/subscriptions.svg | 26 +++++++ client/src/sass/application.scss | 7 ++ client/src/sass/include/_mixins.scss | 57 +++++++++----- client/src/sass/include/_variables.scss | 2 + .../job-queue/handlers/activitypub-follow.ts | 15 ++-- server/models/activitypub/actor-follow.ts | 9 ++- 42 files changed, 647 insertions(+), 98 deletions(-) create mode 100644 client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.html create mode 100644 client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.scss create mode 100644 client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.ts create mode 100644 client/src/app/shared/user-subscription/index.ts create mode 100644 client/src/app/shared/user-subscription/subscribe-button.component.html create mode 100644 client/src/app/shared/user-subscription/subscribe-button.component.scss create mode 100644 client/src/app/shared/user-subscription/subscribe-button.component.ts create mode 100644 client/src/app/shared/user-subscription/user-subscription.service.ts create mode 100644 client/src/app/videos/video-list/video-user-subscriptions.component.ts create mode 100644 client/src/assets/images/menu/podcasts.svg create mode 100644 client/src/assets/images/menu/subscriptions.svg diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss b/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss index c9c7fa8eb..39c0840e4 100644 --- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss +++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss @@ -2,7 +2,7 @@ @import '_mixins'; .row { - text-align: center; + justify-content: center; } a.video-channel { diff --git a/client/src/app/+my-account/my-account-routing.module.ts b/client/src/app/+my-account/my-account-routing.module.ts index 6f0806e8a..c1c979151 100644 --- a/client/src/app/+my-account/my-account-routing.module.ts +++ b/client/src/app/+my-account/my-account-routing.module.ts @@ -9,6 +9,7 @@ import { MyAccountVideoChannelsComponent } from '@app/+my-account/my-account-vid import { MyAccountVideoChannelCreateComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channel-create.component' import { MyAccountVideoChannelUpdateComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channel-update.component' import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-video-imports/my-account-video-imports.component' +import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-subscriptions/my-account-subscriptions.component' const myAccountRoutes: Routes = [ { @@ -74,6 +75,15 @@ const myAccountRoutes: Routes = [ title: 'Account video imports' } } + }, + { + path: 'subscriptions', + component: MyAccountSubscriptionsComponent, + data: { + meta: { + title: 'Account subscriptions' + } + } } ] } diff --git a/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.html b/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.html new file mode 100644 index 000000000..4c68cd1a5 --- /dev/null +++ b/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.html @@ -0,0 +1,23 @@ +
+ +
diff --git a/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.scss b/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.scss new file mode 100644 index 000000000..2fbfa335b --- /dev/null +++ b/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.scss @@ -0,0 +1,49 @@ +@import '_variables'; +@import '_mixins'; + +.video-channel { + @include row-blocks; + + img { + @include avatar(80px); + + margin-right: 10px; + } + + .video-channel-info { + flex-grow: 1; + + a.video-channel-names { + @include disable-default-a-behaviour; + + width: fit-content; + display: flex; + align-items: baseline; + color: #000; + + .video-channel-display-name { + font-weight: $font-semibold; + font-size: 18px; + } + + .video-channel-name { + font-size: 14px; + color: $grey-actor-name; + margin-left: 5px; + } + } + } + + .actor-owner { + @include actor-owner; + } + + my-subscribe-button { + /deep/ span[role=button] { + padding: 7px 12px; + font-size: 16px; + } + } +} + + diff --git a/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.ts b/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.ts new file mode 100644 index 000000000..1e94cf90b --- /dev/null +++ b/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.ts @@ -0,0 +1,30 @@ +import { Component, OnInit } from '@angular/core' +import { NotificationsService } from 'angular2-notifications' +import { VideoChannel } from '@app/shared/video-channel/video-channel.model' +import { I18n } from '@ngx-translate/i18n-polyfill' +import { UserSubscriptionService } from '@app/shared/user-subscription' + +@Component({ + selector: 'my-account-subscriptions', + templateUrl: './my-account-subscriptions.component.html', + styleUrls: [ './my-account-subscriptions.component.scss' ] +}) +export class MyAccountSubscriptionsComponent implements OnInit { + videoChannels: VideoChannel[] = [] + + constructor ( + private userSubscriptionService: UserSubscriptionService, + private notificationsService: NotificationsService, + private i18n: I18n + ) {} + + ngOnInit () { + this.userSubscriptionService.listSubscriptions() + .subscribe( + res => { console.log(res); this.videoChannels = res.data }, + + error => this.notificationsService.error(this.i18n('Error'), error.message) + ) + } + +} diff --git a/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-update.component.ts b/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-update.component.ts index e25037e24..56697030b 100644 --- a/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-update.component.ts +++ b/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-update.component.ts @@ -78,7 +78,7 @@ export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelE support: body.support || null } - this.videoChannelService.updateVideoChannel(this.videoChannelToUpdate.uuid, videoChannelUpdate).subscribe( + this.videoChannelService.updateVideoChannel(this.videoChannelToUpdate.name, videoChannelUpdate).subscribe( () => { this.authService.refreshUserInformation() this.notificationsService.success( @@ -93,7 +93,7 @@ export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelE } onAvatarChange (formData: FormData) { - this.videoChannelService.changeVideoChannelAvatar(this.videoChannelToUpdate.uuid, formData) + this.videoChannelService.changeVideoChannelAvatar(this.videoChannelToUpdate.name, formData) .subscribe( data => { this.notificationsService.success(this.i18n('Success'), this.i18n('Avatar changed.')) diff --git a/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.scss b/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.scss index f8fd2684e..5c892be01 100644 --- a/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.scss +++ b/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.scss @@ -12,11 +12,7 @@ } .video-channel { - display: flex; - min-height: 130px; - padding-bottom: 20px; - margin-bottom: 20px; - border-bottom: 1px solid #C6C6C6; + @include row-blocks; img { @include avatar(80px); @@ -42,7 +38,7 @@ .video-channel-name { font-size: 14px; - color: #777272; + color: $grey-actor-name; margin-left: 5px; } } @@ -64,12 +60,9 @@ } .video-channel { - flex-direction: column; - height: auto; - text-align: center; - .video-channel-names { - justify-content: center; + flex-direction: column; + align-items: center !important; } img { diff --git a/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss b/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss index 64a04fa20..cd805be73 100644 --- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss +++ b/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss @@ -42,11 +42,7 @@ } .video { - display: flex; - min-height: 130px; - padding-bottom: 20px; - margin-bottom: 20px; - border-bottom: 1px solid #C6C6C6; + @include row-blocks; &:first-child { margin-top: 47px; diff --git a/client/src/app/+my-account/my-account.component.html b/client/src/app/+my-account/my-account.component.html index ddb0570db..74742649c 100644 --- a/client/src/app/+my-account/my-account.component.html +++ b/client/src/app/+my-account/my-account.component.html @@ -2,11 +2,13 @@
diff --git a/client/src/app/+my-account/my-account.module.ts b/client/src/app/+my-account/my-account.module.ts index 29b49e8d9..c93f38d4b 100644 --- a/client/src/app/+my-account/my-account.module.ts +++ b/client/src/app/+my-account/my-account.module.ts @@ -14,6 +14,7 @@ import { MyAccountVideoChannelUpdateComponent } from '@app/+my-account/my-accoun import { ActorAvatarInfoComponent } from '@app/+my-account/shared/actor-avatar-info.component' import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-video-imports/my-account-video-imports.component' import { MyAccountDangerZoneComponent } from '@app/+my-account/my-account-settings/my-account-danger-zone' +import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-subscriptions/my-account-subscriptions.component' @NgModule({ imports: [ @@ -34,7 +35,8 @@ import { MyAccountDangerZoneComponent } from '@app/+my-account/my-account-settin MyAccountVideoChannelUpdateComponent, ActorAvatarInfoComponent, MyAccountVideoImportsComponent, - MyAccountDangerZoneComponent + MyAccountDangerZoneComponent, + MyAccountSubscriptionsComponent ], exports: [ diff --git a/client/src/app/+my-account/shared/actor-avatar-info.component.scss b/client/src/app/+my-account/shared/actor-avatar-info.component.scss index 36a792f82..0b0c83de5 100644 --- a/client/src/app/+my-account/shared/actor-avatar-info.component.scss +++ b/client/src/app/+my-account/shared/actor-avatar-info.component.scss @@ -25,7 +25,7 @@ position: relative; top: 2px; font-size: 14px; - color: #777272; + color: $grey-actor-name; } } diff --git a/client/src/app/+video-channels/video-channels.component.html b/client/src/app/+video-channels/video-channels.component.html index 5a69a82a0..1941a2eab 100644 --- a/client/src/app/+video-channels/video-channels.component.html +++ b/client/src/app/+video-channels/video-channels.component.html @@ -8,6 +8,8 @@
{{ videoChannel.displayName }}
{{ videoChannel.nameWithHost }}
+ +
{{ videoChannel.followersCount }} subscribers
@@ -20,7 +22,6 @@
diff --git a/client/src/app/+video-channels/video-channels.component.scss b/client/src/app/+video-channels/video-channels.component.scss index 909b65bc7..a63b1ec06 100644 --- a/client/src/app/+video-channels/video-channels.component.scss +++ b/client/src/app/+video-channels/video-channels.component.scss @@ -3,4 +3,19 @@ .sub-menu { @include sub-menu-with-actor; + + .actor, .actor-info { + width: 100%; + } + + .actor-name { + flex-grow: 1; + } + + my-subscribe-button { + /deep/ span[role=button] { + padding: 7px 12px; + font-size: 16px; + } + } } \ No newline at end of file diff --git a/client/src/app/menu/menu.component.html b/client/src/app/menu/menu.component.html index 7edcdf501..bd03af9b3 100644 --- a/client/src/app/menu/menu.component.html +++ b/client/src/app/menu/menu.component.html @@ -42,6 +42,11 @@
Videos
+ + + Subscriptions + + Trending diff --git a/client/src/app/menu/menu.component.scss b/client/src/app/menu/menu.component.scss index 39f1e9be0..606fea961 100644 --- a/client/src/app/menu/menu.component.scss +++ b/client/src/app/menu/menu.component.scss @@ -135,6 +135,12 @@ menu { margin-right: 18px; + &.icon-videos-subscriptions { + position: relative; + top: -2px; + background-image: url('../../assets/images/menu/subscriptions.svg'); + } + &.icon-videos-trending { position: relative; top: -2px; diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts index 722415a06..9bc7ad88b 100644 --- a/client/src/app/shared/shared.module.ts +++ b/client/src/app/shared/shared.module.ts @@ -36,7 +36,8 @@ import { ReactiveFileComponent, ResetPasswordValidatorsService, UserValidatorsService, - VideoAbuseValidatorsService, VideoBlacklistValidatorsService, + VideoAbuseValidatorsService, + VideoBlacklistValidatorsService, VideoChannelValidatorsService, VideoCommentValidatorsService, VideoValidatorsService @@ -49,6 +50,7 @@ import { PeertubeCheckboxComponent } from '@app/shared/forms/peertube-checkbox.c import { VideoImportService } from '@app/shared/video-import/video-import.service' import { ActionDropdownComponent } from '@app/shared/buttons/action-dropdown.component' import { NgbDropdownModule, NgbModalModule, NgbPopoverModule, NgbTabsetModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap' +import { SubscribeButtonComponent, UserSubscriptionService } from '@app/shared/user-subscription' @NgModule({ imports: [ @@ -83,7 +85,8 @@ import { NgbDropdownModule, NgbModalModule, NgbPopoverModule, NgbTabsetModule, N InfiniteScrollerDirective, HelpComponent, ReactiveFileComponent, - PeertubeCheckboxComponent + PeertubeCheckboxComponent, + SubscribeButtonComponent ], exports: [ @@ -115,6 +118,7 @@ import { NgbDropdownModule, NgbModalModule, NgbPopoverModule, NgbTabsetModule, N HelpComponent, ReactiveFileComponent, PeertubeCheckboxComponent, + SubscribeButtonComponent, NumberFormatterPipe, ObjectLengthPipe, @@ -134,6 +138,7 @@ import { NgbDropdownModule, NgbModalModule, NgbPopoverModule, NgbTabsetModule, N VideoChannelService, VideoCaptionService, VideoImportService, + UserSubscriptionService, FormValidatorService, CustomConfigValidatorsService, diff --git a/client/src/app/shared/user-subscription/index.ts b/client/src/app/shared/user-subscription/index.ts new file mode 100644 index 000000000..024b36a41 --- /dev/null +++ b/client/src/app/shared/user-subscription/index.ts @@ -0,0 +1,2 @@ +export * from './user-subscription.service' +export * from './subscribe-button.component' \ No newline at end of file diff --git a/client/src/app/shared/user-subscription/subscribe-button.component.html b/client/src/app/shared/user-subscription/subscribe-button.component.html new file mode 100644 index 000000000..63b313662 --- /dev/null +++ b/client/src/app/shared/user-subscription/subscribe-button.component.html @@ -0,0 +1,15 @@ + + + + Subscribed + Unsubscribe + + + {{ videoChannel.followersCount | myNumberFormatter }} + + diff --git a/client/src/app/shared/user-subscription/subscribe-button.component.scss b/client/src/app/shared/user-subscription/subscribe-button.component.scss new file mode 100644 index 000000000..9811fdc0c --- /dev/null +++ b/client/src/app/shared/user-subscription/subscribe-button.component.scss @@ -0,0 +1,37 @@ +@import '_variables'; +@import '_mixins'; + +.subscribe-button { + @include peertube-button; + @include orange-button; +} + +.unsubscribe-button { + @include peertube-button; + @include grey-button +} + +.subscribe-button, +.unsubscribe-button { + padding: 3px 7px; +} + +.unsubscribe-button { + .subscribed { + display: inline; + } + + .unsubscribe { + display: none; + } + + &:hover { + .subscribed { + display: none; + } + + .unsubscribe { + display: inline; + } + } +} \ No newline at end of file diff --git a/client/src/app/shared/user-subscription/subscribe-button.component.ts b/client/src/app/shared/user-subscription/subscribe-button.component.ts new file mode 100644 index 000000000..46d6dbaf7 --- /dev/null +++ b/client/src/app/shared/user-subscription/subscribe-button.component.ts @@ -0,0 +1,74 @@ +import { Component, Input, OnInit } from '@angular/core' +import { AuthService } from '@app/core' +import { RestExtractor } from '@app/shared/rest' +import { RedirectService } from '@app/core/routing/redirect.service' +import { UserSubscriptionService } from '@app/shared/user-subscription/user-subscription.service' +import { VideoChannel } from '@app/shared/video-channel/video-channel.model' +import { NotificationsService } from 'angular2-notifications' +import { I18n } from '@ngx-translate/i18n-polyfill' + +@Component({ + selector: 'my-subscribe-button', + templateUrl: './subscribe-button.component.html', + styleUrls: [ './subscribe-button.component.scss' ] +}) +export class SubscribeButtonComponent implements OnInit { + @Input() videoChannel: VideoChannel + @Input() displayFollowers = false + + subscribed: boolean + + constructor ( + private authService: AuthService, + private restExtractor: RestExtractor, + private redirectService: RedirectService, + private notificationsService: NotificationsService, + private userSubscriptionService: UserSubscriptionService, + private i18n: I18n + ) { } + + get uri () { + return this.videoChannel.name + '@' + this.videoChannel.host + } + + ngOnInit () { + this.userSubscriptionService.isSubscriptionExists(this.uri) + .subscribe( + exists => this.subscribed = exists, + + err => this.notificationsService.error(this.i18n('Error'), err.message) + ) + } + + subscribe () { + this.userSubscriptionService.addSubscription(this.uri) + .subscribe( + () => { + this.subscribed = true + + this.notificationsService.success( + this.i18n('Subscribed'), + this.i18n('Subscribed to {{nameWithHost}}', { nameWithHost: this.videoChannel.displayName }) + ) + }, + + err => this.notificationsService.error(this.i18n('Error'), err.message) + ) + } + + unsubscribe () { + this.userSubscriptionService.deleteSubscription(this.uri) + .subscribe( + () => { + this.subscribed = false + + this.notificationsService.success( + this.i18n('Unsubscribed'), + this.i18n('Unsubscribed from {{nameWithHost}}', { nameWithHost: this.videoChannel.displayName }) + ) + }, + + err => this.notificationsService.error(this.i18n('Error'), err.message) + ) + } +} diff --git a/client/src/app/shared/user-subscription/user-subscription.service.ts b/client/src/app/shared/user-subscription/user-subscription.service.ts new file mode 100644 index 000000000..3103706d1 --- /dev/null +++ b/client/src/app/shared/user-subscription/user-subscription.service.ts @@ -0,0 +1,66 @@ +import { catchError, map } from 'rxjs/operators' +import { HttpClient } from '@angular/common/http' +import { Injectable } from '@angular/core' +import { ResultList } from '../../../../../shared' +import { environment } from '../../../environments/environment' +import { RestExtractor } from '../rest' +import { Observable, of } from 'rxjs' +import { VideoChannel } from '@app/shared/video-channel/video-channel.model' +import { VideoChannelService } from '@app/shared/video-channel/video-channel.service' +import { VideoChannel as VideoChannelServer } from '../../../../../shared/models/videos' + +@Injectable() +export class UserSubscriptionService { + static BASE_USER_SUBSCRIPTIONS_URL = environment.apiUrl + '/api/v1/users/me/subscriptions' + + constructor ( + private authHttp: HttpClient, + private restExtractor: RestExtractor + ) { + } + + deleteSubscription (nameWithHost: string) { + const url = UserSubscriptionService.BASE_USER_SUBSCRIPTIONS_URL + '/' + nameWithHost + + return this.authHttp.delete(url) + .pipe( + map(this.restExtractor.extractDataBool), + catchError(err => this.restExtractor.handleError(err)) + ) + } + + addSubscription (nameWithHost: string) { + const url = UserSubscriptionService.BASE_USER_SUBSCRIPTIONS_URL + + const body = { uri: nameWithHost } + return this.authHttp.post(url, body) + .pipe( + map(this.restExtractor.extractDataBool), + catchError(err => this.restExtractor.handleError(err)) + ) + } + + listSubscriptions (): Observable> { + const url = UserSubscriptionService.BASE_USER_SUBSCRIPTIONS_URL + + return this.authHttp.get>(url) + .pipe( + map(res => VideoChannelService.extractVideoChannels(res)), + catchError(err => this.restExtractor.handleError(err)) + ) + } + + isSubscriptionExists (nameWithHost: string): Observable { + const url = UserSubscriptionService.BASE_USER_SUBSCRIPTIONS_URL + '/' + nameWithHost + + return this.authHttp.get(url) + .pipe( + map(this.restExtractor.extractDataBool), + catchError(err => { + if (err.status === 404) return of(false) + + return this.restExtractor.handleError(err) + }) + ) + } +} diff --git a/client/src/app/shared/video-channel/video-channel.service.ts b/client/src/app/shared/video-channel/video-channel.service.ts index 510dc9c3d..46b121790 100644 --- a/client/src/app/shared/video-channel/video-channel.service.ts +++ b/client/src/app/shared/video-channel/video-channel.service.ts @@ -22,6 +22,16 @@ export class VideoChannelService { private restExtractor: RestExtractor ) {} + static extractVideoChannels (result: ResultList) { + const videoChannels: VideoChannel[] = [] + + for (const videoChannelJSON of result.data) { + videoChannels.push(new VideoChannel(videoChannelJSON)) + } + + return { data: videoChannels, total: result.total } + } + getVideoChannel (videoChannelName: string) { return this.authHttp.get(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName) .pipe( @@ -34,7 +44,7 @@ export class VideoChannelService { listAccountVideoChannels (account: Account): Observable> { return this.authHttp.get>(AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/video-channels') .pipe( - map(res => this.extractVideoChannels(res)), + map(res => VideoChannelService.extractVideoChannels(res)), catchError(err => this.restExtractor.handleError(err)) ) } @@ -47,16 +57,16 @@ export class VideoChannelService { ) } - updateVideoChannel (videoChannelUUID: string, videoChannel: VideoChannelUpdate) { - return this.authHttp.put(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelUUID, videoChannel) + updateVideoChannel (videoChannelName: string, videoChannel: VideoChannelUpdate) { + return this.authHttp.put(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName, videoChannel) .pipe( map(this.restExtractor.extractDataBool), catchError(err => this.restExtractor.handleError(err)) ) } - changeVideoChannelAvatar (videoChannelUUID: string, avatarForm: FormData) { - const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelUUID + '/avatar/pick' + changeVideoChannelAvatar (videoChannelName: string, avatarForm: FormData) { + const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName + '/avatar/pick' return this.authHttp.post<{ avatar: Avatar }>(url, avatarForm) .pipe(catchError(err => this.restExtractor.handleError(err))) @@ -69,14 +79,4 @@ export class VideoChannelService { catchError(err => this.restExtractor.handleError(err)) ) } - - private extractVideoChannels (result: ResultList) { - const videoChannels: VideoChannel[] = [] - - for (const videoChannelJSON of result.data) { - videoChannels.push(new VideoChannel(videoChannelJSON)) - } - - return { data: videoChannels, total: result.total } - } } diff --git a/client/src/app/shared/video/abstract-video-list.html b/client/src/app/shared/video/abstract-video-list.html index e8ded6ab8..d4b00c07c 100644 --- a/client/src/app/shared/video/abstract-video-list.html +++ b/client/src/app/shared/video/abstract-video-list.html @@ -14,7 +14,7 @@
diff --git a/client/src/app/shared/video/abstract-video-list.ts b/client/src/app/shared/video/abstract-video-list.ts index 59d3c1ebe..b8fd7f8eb 100644 --- a/client/src/app/shared/video/abstract-video-list.ts +++ b/client/src/app/shared/video/abstract-video-list.ts @@ -11,6 +11,7 @@ import { VideoSortField } from './sort-field.type' import { Video } from './video.model' import { I18n } from '@ngx-translate/i18n-polyfill' import { ScreenService } from '@app/shared/misc/screen.service' +import { OwnerDisplayType } from '@app/shared/video/video-miniature.component' export abstract class AbstractVideoList implements OnInit, OnDestroy { private static LINES_PER_PAGE = 4 @@ -34,6 +35,7 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy { videoWidth: number videoHeight: number videoPages: Video[][] = [] + ownerDisplayType: OwnerDisplayType = 'account' protected baseVideoWidth = 215 protected baseVideoHeight = 230 diff --git a/client/src/app/shared/video/video-details.model.ts b/client/src/app/shared/video/video-details.model.ts index d346f985c..fa4ca7f93 100644 --- a/client/src/app/shared/video/video-details.model.ts +++ b/client/src/app/shared/video/video-details.model.ts @@ -1,14 +1,8 @@ -import { - UserRight, - VideoChannel, - VideoConstant, - VideoDetails as VideoDetailsServerModel, - VideoFile, - VideoState -} from '../../../../../shared' +import { UserRight, VideoConstant, VideoDetails as VideoDetailsServerModel, VideoFile, VideoState } from '../../../../../shared' import { AuthUser } from '../../core' import { Video } from '../../shared/video/video.model' import { Account } from '@app/shared/account/account.model' +import { VideoChannel } from '@app/shared/video-channel/video-channel.model' export class VideoDetails extends Video implements VideoDetailsServerModel { descriptionPath: string @@ -30,7 +24,7 @@ export class VideoDetails extends Video implements VideoDetailsServerModel { this.descriptionPath = hash.descriptionPath this.files = hash.files - this.channel = hash.channel + this.channel = new VideoChannel(hash.channel) this.account = new Account(hash.account) this.tags = hash.tags this.support = hash.support diff --git a/client/src/app/shared/video/video-miniature.component.html b/client/src/app/shared/video/video-miniature.component.html index 3010e5ccc..de84bccf9 100644 --- a/client/src/app/shared/video/video-miniature.component.html +++ b/client/src/app/shared/video/video-miniature.component.html @@ -10,6 +10,12 @@
{{ video.publishedAt | myFromNow }} - {{ video.views | myNumberFormatter }} views - + + + + {{ video.byVideoChannel }} +
diff --git a/client/src/app/shared/video/video-miniature.component.scss b/client/src/app/shared/video/video-miniature.component.scss index 588eea3a7..6883650f4 100644 --- a/client/src/app/shared/video/video-miniature.component.scss +++ b/client/src/app/shared/video/video-miniature.component.scss @@ -38,7 +38,8 @@ font-size: 13px; } - .video-miniature-account { + .video-miniature-account, + .video-miniature-channel { @include disable-default-a-behaviour; display: block; diff --git a/client/src/app/shared/video/video-miniature.component.ts b/client/src/app/shared/video/video-miniature.component.ts index d3f6dc1f6..07193ebd5 100644 --- a/client/src/app/shared/video/video-miniature.component.ts +++ b/client/src/app/shared/video/video-miniature.component.ts @@ -1,20 +1,51 @@ -import { Component, Input } from '@angular/core' +import { Component, Input, OnInit } from '@angular/core' import { User } from '../users' import { Video } from './video.model' import { ServerService } from '@app/core' +export type OwnerDisplayType = 'account' | 'videoChannel' | 'auto' + @Component({ selector: 'my-video-miniature', styleUrls: [ './video-miniature.component.scss' ], templateUrl: './video-miniature.component.html' }) -export class VideoMiniatureComponent { +export class VideoMiniatureComponent implements OnInit { @Input() user: User @Input() video: Video + @Input() ownerDisplayType: OwnerDisplayType = 'account' + + private ownerDisplayTypeChosen: 'account' | 'videoChannel' constructor (private serverService: ServerService) { } + ngOnInit () { + if (this.ownerDisplayType === 'account' || this.ownerDisplayType === 'videoChannel') { + this.ownerDisplayTypeChosen = this.ownerDisplayType + return + } + + // If the video channel name an UUID (not really displayable, we changed this behaviour in v1.0.0-beta.12) + // -> Use the account name + if ( + this.video.channel.name === `${this.video.account.name}_channel` || + this.video.channel.name.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/) + ) { + this.ownerDisplayTypeChosen = 'account' + } else { + this.ownerDisplayTypeChosen = 'videoChannel' + } + } + isVideoBlur () { return this.video.isVideoNSFWForUser(this.user, this.serverService.getConfig()) } + + displayOwnerAccount () { + return this.ownerDisplayTypeChosen === 'account' + } + + displayOwnerVideoChannel () { + return this.ownerDisplayTypeChosen === 'videoChannel' + } } diff --git a/client/src/app/shared/video/video.model.ts b/client/src/app/shared/video/video.model.ts index df8253301..d80c10459 100644 --- a/client/src/app/shared/video/video.model.ts +++ b/client/src/app/shared/video/video.model.ts @@ -8,9 +8,12 @@ import { Actor } from '@app/shared/actor/actor.model' import { VideoScheduleUpdate } from '../../../../../shared/models/videos/video-schedule-update.model' export class Video implements VideoServerModel { - by: string + byVideoChannel: string + byAccount: string + accountAvatarUrl: string videoChannelAvatarUrl: string + createdAt: Date updatedAt: Date publishedAt: Date @@ -110,7 +113,8 @@ export class Video implements VideoServerModel { this.account = hash.account this.channel = hash.channel - this.by = Actor.CREATE_BY_STRING(hash.account.name, hash.account.host) + this.byAccount = Actor.CREATE_BY_STRING(hash.account.name, hash.account.host) + this.byVideoChannel = Actor.CREATE_BY_STRING(hash.channel.name, hash.channel.host) this.accountAvatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.account) this.videoChannelAvatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.channel) diff --git a/client/src/app/shared/video/video.service.ts b/client/src/app/shared/video/video.service.ts index e44f1ee65..1a934c8e2 100644 --- a/client/src/app/shared/video/video.service.ts +++ b/client/src/app/shared/video/video.service.ts @@ -27,6 +27,7 @@ import { Account } from '@app/shared/account/account.model' import { AccountService } from '@app/shared/account/account.service' import { VideoChannelService } from '@app/shared/video-channel/video-channel.service' import { ServerService } from '@app/core' +import { UserSubscriptionService } from '@app/shared/user-subscription' @Injectable() export class VideoService { @@ -157,6 +158,23 @@ export class VideoService { ) } + getUserSubscriptionVideos ( + videoPagination: ComponentPagination, + sort: VideoSortField + ): Observable<{ videos: Video[], totalVideos: number }> { + const pagination = this.restService.componentPaginationToRestPagination(videoPagination) + + let params = new HttpParams() + params = this.restService.addRestGetParams(params, pagination, sort) + + return this.authHttp + .get>(UserSubscriptionService.BASE_USER_SUBSCRIPTIONS_URL + '/videos', { params }) + .pipe( + switchMap(res => this.extractVideos(res)), + catchError(err => this.restExtractor.handleError(err)) + ) + } + getVideos ( videoPagination: ComponentPagination, sort: VideoSortField, diff --git a/client/src/app/videos/+video-watch/video-watch.component.html b/client/src/app/videos/+video-watch/video-watch.component.html index c275258ef..8a49e3566 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.html +++ b/client/src/app/videos/+video-watch/video-watch.component.html @@ -42,16 +42,17 @@ Video channel avatar - + + diff --git a/client/src/app/videos/+video-watch/video-watch.component.scss b/client/src/app/videos/+video-watch/video-watch.component.scss index 1354de32e..5bf2f485a 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.scss +++ b/client/src/app/videos/+video-watch/video-watch.component.scss @@ -125,6 +125,14 @@ margin: -2px 2px 0 5px; } } + + my-subscribe-button { + /deep/ span[role=button] { + font-size: 13px !important; + } + + margin-left: 5px; + } } .video-info-by { @@ -369,7 +377,10 @@ .video-miniature-information { flex-grow: 1; - margin-left: 10px; + } + + .video-thumbnail { + margin-right: 10px } } } @@ -502,10 +513,6 @@ .other-videos { /deep/ .video-miniature { flex-direction: column; - - .video-miniature-information { - margin-left: 0 !important; - } } } diff --git a/client/src/app/videos/video-list/video-user-subscriptions.component.ts b/client/src/app/videos/video-list/video-user-subscriptions.component.ts new file mode 100644 index 000000000..6e8959c54 --- /dev/null +++ b/client/src/app/videos/video-list/video-user-subscriptions.component.ts @@ -0,0 +1,57 @@ +import { Component, OnDestroy, OnInit } from '@angular/core' +import { ActivatedRoute, Router } from '@angular/router' +import { immutableAssign } from '@app/shared/misc/utils' +import { Location } from '@angular/common' +import { NotificationsService } from 'angular2-notifications' +import { AuthService } from '../../core/auth' +import { AbstractVideoList } from '../../shared/video/abstract-video-list' +import { VideoSortField } from '../../shared/video/sort-field.type' +import { VideoService } from '../../shared/video/video.service' +import { I18n } from '@ngx-translate/i18n-polyfill' +import { ScreenService } from '@app/shared/misc/screen.service' +import { OwnerDisplayType } from '@app/shared/video/video-miniature.component' + +@Component({ + selector: 'my-videos-user-subscriptions', + styleUrls: [ '../../shared/video/abstract-video-list.scss' ], + templateUrl: '../../shared/video/abstract-video-list.html' +}) +export class VideoUserSubscriptionsComponent extends AbstractVideoList implements OnInit, OnDestroy { + titlePage: string + currentRoute = '/videos/subscriptions' + sort = '-publishedAt' as VideoSortField + ownerDisplayType: OwnerDisplayType = 'auto' + + constructor ( + protected router: Router, + protected route: ActivatedRoute, + protected notificationsService: NotificationsService, + protected authService: AuthService, + protected location: Location, + protected i18n: I18n, + protected screenService: ScreenService, + private videoService: VideoService + ) { + super() + + this.titlePage = i18n('Videos from your subscriptions') + } + + ngOnInit () { + super.ngOnInit() + } + + ngOnDestroy () { + super.ngOnDestroy() + } + + getVideosObservable (page: number) { + const newPagination = immutableAssign(this.pagination, { currentPage: page }) + + return this.videoService.getUserSubscriptionVideos(newPagination, this.sort) + } + + generateSyndicationList () { + // not implemented yet + } +} diff --git a/client/src/app/videos/videos-routing.module.ts b/client/src/app/videos/videos-routing.module.ts index 538a43c6d..18ed52570 100644 --- a/client/src/app/videos/videos-routing.module.ts +++ b/client/src/app/videos/videos-routing.module.ts @@ -5,6 +5,7 @@ import { MetaGuard } from '@ngx-meta/core' import { VideoRecentlyAddedComponent } from './video-list/video-recently-added.component' import { VideoTrendingComponent } from './video-list/video-trending.component' import { VideosComponent } from './videos.component' +import { VideoUserSubscriptionsComponent } from '@app/videos/video-list/video-user-subscriptions.component' const videosRoutes: Routes = [ { @@ -12,11 +13,6 @@ const videosRoutes: Routes = [ component: VideosComponent, canActivateChild: [ MetaGuard ], children: [ - { - path: 'list', - pathMatch: 'full', - redirectTo: 'recently-added' - }, { path: 'trending', component: VideoTrendingComponent, @@ -35,6 +31,15 @@ const videosRoutes: Routes = [ } } }, + { + path: 'subscriptions', + component: VideoUserSubscriptionsComponent, + data: { + meta: { + title: 'Subscriptions' + } + } + }, { path: 'local', component: VideoLocalComponent, diff --git a/client/src/app/videos/videos.module.ts b/client/src/app/videos/videos.module.ts index c38257e08..3c3877273 100644 --- a/client/src/app/videos/videos.module.ts +++ b/client/src/app/videos/videos.module.ts @@ -5,6 +5,7 @@ import { VideoRecentlyAddedComponent } from './video-list/video-recently-added.c import { VideoTrendingComponent } from './video-list/video-trending.component' import { VideosRoutingModule } from './videos-routing.module' import { VideosComponent } from './videos.component' +import { VideoUserSubscriptionsComponent } from '@app/videos/video-list/video-user-subscriptions.component' @NgModule({ imports: [ @@ -17,7 +18,8 @@ import { VideosComponent } from './videos.component' VideoTrendingComponent, VideoRecentlyAddedComponent, - VideoLocalComponent + VideoLocalComponent, + VideoUserSubscriptionsComponent ], exports: [ diff --git a/client/src/assets/images/menu/podcasts.svg b/client/src/assets/images/menu/podcasts.svg new file mode 100644 index 000000000..cd6efc54e --- /dev/null +++ b/client/src/assets/images/menu/podcasts.svg @@ -0,0 +1,26 @@ + + + + podcasts + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + diff --git a/client/src/assets/images/menu/subscriptions.svg b/client/src/assets/images/menu/subscriptions.svg new file mode 100644 index 000000000..cd6efc54e --- /dev/null +++ b/client/src/assets/images/menu/subscriptions.svg @@ -0,0 +1,26 @@ + + + + podcasts + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + diff --git a/client/src/sass/application.scss b/client/src/sass/application.scss index dc0ffe912..b2d7c2bec 100644 --- a/client/src/sass/application.scss +++ b/client/src/sass/application.scss @@ -83,6 +83,7 @@ label { display: flex; align-items: center; padding-left: $not-expanded-horizontal-margins; + padding-right: $not-expanded-horizontal-margins; } // Override some properties if the main content is expanded (no menu on the left) @@ -96,6 +97,7 @@ label { .sub-menu { padding-left: $expanded-horizontal-margins; + padding-right: $expanded-horizontal-margins; } } } @@ -294,6 +296,10 @@ table { .sub-menu { padding-left: 50px; + + .title-page { + font-size: 15px; + } } } } @@ -316,6 +322,7 @@ table { .sub-menu { padding-left: 15px; + padding-right: 15px; margin-bottom: 10px; } diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss index b0b0f544c..aafe478f9 100644 --- a/client/src/sass/include/_mixins.scss +++ b/client/src/sass/include/_mixins.scss @@ -335,6 +335,27 @@ font-size: 13px; } +@mixin actor-owner { + @include disable-default-a-behaviour; + + display: block; + font-size: 13px; + margin-top: 4px; + color: #000; + + span:hover { + opacity: 0.8; + } + + img { + @include avatar(18px); + + margin-left: 7px; + position: relative; + top: -2px; + } +} + @mixin sub-menu-with-actor { height: 160px; display: flex; @@ -371,7 +392,7 @@ position: relative; top: 3px; font-size: 14px; - color: #777272; + color: $grey-actor-name; } } @@ -380,24 +401,7 @@ } .actor-owner { - @include disable-default-a-behaviour; - - display: block; - font-size: 13px; - margin-top: 4px; - color: #000; - - span:hover { - opacity: 0.8; - } - - img { - @include avatar(18px); - - margin-left: 7px; - position: relative; - top: -2px; - } + @include actor-owner; } } } @@ -426,3 +430,18 @@ background-image: url($imageUrl); } } + +@mixin row-blocks { + display: flex; + min-height: 130px; + padding-bottom: 20px; + margin-bottom: 20px; + border-bottom: 1px solid #C6C6C6; + + @media screen and (max-width: 800px) { + flex-direction: column; + height: auto; + text-align: center; + align-items: center; + } +} \ No newline at end of file diff --git a/client/src/sass/include/_variables.scss b/client/src/sass/include/_variables.scss index f1f755126..e6db98642 100644 --- a/client/src/sass/include/_variables.scss +++ b/client/src/sass/include/_variables.scss @@ -12,6 +12,8 @@ $black-background: #000; $grey-background: #f6f2f2; $red-error: #FF0000; +$grey-actor-name: #777272; + $expanded-horizontal-margins: 150px; $not-expanded-horizontal-margins: 30px; diff --git a/server/lib/job-queue/handlers/activitypub-follow.ts b/server/lib/job-queue/handlers/activitypub-follow.ts index 282dde268..36d0f237b 100644 --- a/server/lib/job-queue/handlers/activitypub-follow.ts +++ b/server/lib/job-queue/handlers/activitypub-follow.ts @@ -1,7 +1,6 @@ import * as Bull from 'bull' import { logger } from '../../../helpers/logger' -import { getServerActor } from '../../../helpers/utils' -import { REMOTE_SCHEME, sequelizeTypescript } from '../../../initializers' +import { CONFIG, REMOTE_SCHEME, sequelizeTypescript } from '../../../initializers' import { sendFollow } from '../../activitypub/send' import { sanitizeHost } from '../../../helpers/core-utils' import { loadActorUrlOrGetFromWebfinger } from '../../../helpers/webfinger' @@ -22,10 +21,14 @@ async function processActivityPubFollow (job: Bull.Job) { logger.info('Processing ActivityPub follow in job %d.', job.id) - const sanitizedHost = sanitizeHost(host, REMOTE_SCHEME.HTTP) - - const actorUrl = await loadActorUrlOrGetFromWebfinger(payload.name + '@' + sanitizedHost) - const targetActor = await getOrCreateActorAndServerAndModel(actorUrl) + let targetActor: ActorModel + if (!host || host === CONFIG.WEBSERVER.HOST) { + targetActor = await ActorModel.loadLocalByName(payload.name) + } else { + const sanitizedHost = sanitizeHost(host, REMOTE_SCHEME.HTTP) + const actorUrl = await loadActorUrlOrGetFromWebfinger(payload.name + '@' + sanitizedHost) + targetActor = await getOrCreateActorAndServerAndModel(actorUrl) + } const fromActor = await ActorModel.load(payload.followerActorId) diff --git a/server/models/activitypub/actor-follow.ts b/server/models/activitypub/actor-follow.ts index 20d3aa5fc..b2d7ace66 100644 --- a/server/models/activitypub/actor-follow.ts +++ b/server/models/activitypub/actor-follow.ts @@ -29,6 +29,7 @@ import { getSort } from '../utils' import { ActorModel } from './actor' import { VideoChannelModel } from '../video/video-channel' import { IIncludeOptions } from '../../../node_modules/sequelize-typescript/lib/interfaces/IIncludeOptions' +import { AccountModel } from '../account/account' @Table({ tableName: 'actorFollow', @@ -262,7 +263,13 @@ export class ActorFollowModel extends Model { include: [ { model: VideoChannelModel, - required: true + required: true, + include: [ + { + model: AccountModel, + required: true + } + ] } ] } -- 2.41.0