<div class="actor-display-name">{{ videoChannel.displayName }}</div>
<div class="actor-name">{{ videoChannel.nameWithHost }}</div>
- <my-subscribe-button #subscribeButton *ngIf="isUserLoggedIn()" [videoChannel]="videoChannel"></my-subscribe-button>
+ <my-subscribe-button #subscribeButton [videoChannel]="videoChannel"></my-subscribe-button>
</div>
<div i18n class="actor-followers">{{ videoChannel.followersCount }} subscribers</div>
<div i18n class="video-channel-followers">{{ result.followersCount }} subscribers</div>
</div>
- <my-subscribe-button *ngIf="isUserLoggedIn()" [videoChannel]="result"></my-subscribe-button>
+ <my-subscribe-button [videoChannel]="result"></my-subscribe-button>
</div>
<div *ngIf="isVideo(result)" class="entry video">
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'
+import { SubscribeButtonComponent, RemoteSubscribeComponent, UserSubscriptionService } from '@app/shared/user-subscription'
import { InstanceFeaturesTableComponent } from '@app/shared/instance/instance-features-table.component'
import { OverviewService } from '@app/shared/overview'
ReactiveFileComponent,
PeertubeCheckboxComponent,
SubscribeButtonComponent,
+ RemoteSubscribeComponent,
InstanceFeaturesTableComponent
],
ReactiveFileComponent,
PeertubeCheckboxComponent,
SubscribeButtonComponent,
+ RemoteSubscribeComponent,
InstanceFeaturesTableComponent,
NumberFormatterPipe,
export * from './user-subscription.service'
export * from './subscribe-button.component'
+export * from './remote-subscribe.component'
--- /dev/null
+<form novalidate [formGroup]="form"
+ (ngSubmit)="formValidated()">
+ <div class="form-group">
+ <input type="email"
+ formControlName="text"
+ class="form-control"
+ (keyup.control.enter)="onValidKey()" (keyup.meta.enter)="onValidKey()"
+ placeholder="jane_doe@example.com">
+ </div>
+ <button type="submit"
+ [disabled]="!form.valid"
+ class="btn btn-sm btn-remote-follow"
+ i18n>
+ <span *ngIf="!interact">Remote subscribe</span>
+ <span *ngIf="interact">Remote interact</span>
+ </button>
+ <my-help *ngIf="!interact && showHelp"
+ helpType="custom"
+ i18n-customHtml customHtml="You can subscribe to the channel via any ActivityPub-capable fediverse instance. For instance with Mastodon or Pleroma you can type the channel URL in the search box and subscribe there.">
+ </my-help>
+ <my-help *ngIf="showHelp && interact"
+ helpType="custom"
+ i18n-customHtml customHtml="You can interact with this via any ActivityPub-capable fediverse instance. For instance with Mastodon or Pleroma you can type the current URL in the search box and interact with it there.">
+ </my-help>
+</form>
\ No newline at end of file
--- /dev/null
+@import '_mixins';
+
+.btn-remote-follow {
+ @include orange-button;
+}
\ No newline at end of file
--- /dev/null
+import { Component, Input, OnInit } from '@angular/core'
+import { FormReactive } from '@app/shared/forms/form-reactive'
+import {
+ FormValidatorService,
+ UserValidatorsService
+} from '@app/shared/forms/form-validators'
+
+@Component({
+ selector: 'my-remote-subscribe',
+ templateUrl: './remote-subscribe.component.html',
+ styleUrls: ['./remote-subscribe.component.scss']
+})
+export class RemoteSubscribeComponent extends FormReactive implements OnInit {
+ @Input() account: string
+ @Input() interact = false
+ @Input() showHelp = false
+
+ constructor (
+ protected formValidatorService: FormValidatorService,
+ private userValidatorsService: UserValidatorsService
+ ) {
+ super()
+ }
+
+ ngOnInit () {
+ this.buildForm({
+ text: this.userValidatorsService.USER_EMAIL
+ })
+ }
+
+ onValidKey () {
+ this.onValueChanged()
+ if (!this.form.valid) return
+
+ this.formValidated()
+ }
+
+ formValidated () {
+ const address = this.form.value['text']
+ const [ , hostname ] = address.split('@')
+ window.open(`https://${hostname}/authorize_interaction?acct=${this.account}`)
+ }
+}
-<span i18n *ngIf="subscribed === false" class="subscribe-button" [ngClass]="size" role="button" (click)="subscribe()">
- <span>Subscribe</span>
- <span *ngIf="displayFollowers && videoChannel.followersCount !== 0" class="followers-count">
- {{ videoChannel.followersCount | myNumberFormatter }}
- </span>
-</span>
-
-<span *ngIf="subscribed === true" class="unsubscribe-button" [ngClass]="size" role="button" (click)="unsubscribe()">
- <span class="subscribed" i18n>Subscribed</span>
- <span class="unsubscribe" i18n>Unsubscribe</span>
-
- <span *ngIf="displayFollowers && videoChannel.followersCount !== 0" class="followers-count">
- {{ videoChannel.followersCount | myNumberFormatter }}
- </span>
-</span>
+<div class="btn-group-subscribe btn-group"
+ [ngClass]="{'subscribe-button': subscribed !== true, 'unsubscribe-button': subscribed === true}">
+ <button *ngIf="subscribed === false && isUserLoggedIn()" type="button"
+ class="btn btn-sm" role="button"
+ (click)="subscribe()" i18n>
+ <span>
+ Subscribe
+ </span>
+ <span *ngIf="displayFollowers && videoChannel.followersCount !== 0" class="followers-count">
+ {{ videoChannel.followersCount | myNumberFormatter }}
+ </span>
+ </button>
+ <button *ngIf="subscribed === true" type="button"
+ class="btn btn-sm" role="button"
+ (click)="unsubscribe()" i18n>Unsubscribe</button>
+
+ <div class="btn-group" ngbDropdown autoClose="outside"
+ placement="bottom-right" role="group"
+ aria-label="Multiple ways to subscribe to the current channel">
+ <button class="btn btn-sm dropdown-toggle-split" ngbDropdownToggle>
+ <span *ngIf="!isUserLoggedIn()">
+ Subscribe
+ </span>
+ <span *ngIf="displayFollowers && videoChannel.followersCount !== 0" class="followers-count">
+ {{ videoChannel.followersCount | myNumberFormatter }}
+ </span>
+ </button>
+ <div class="dropdown-menu" ngbDropdownMenu>
+
+ <h6 class="dropdown-header" i18n>Using an ActivityPub-compatible account</h6>
+ <button class="dropdown-item" (click)="subscribe()"
+ *ngIf="subscribed === false">
+ <span *ngIf="!isUserLoggedIn()" i18n>Subscribe with an account on {{ videoChannel.host }}</span>
+ <span *ngIf="isUserLoggedIn()" i18n>Subscribe with your local account</span>
+ </button>
+ <button class="dropdown-item" i18n>Subscribe with a remote account:</button>
+ <my-remote-subscribe showHelp="true" account="{{ uriAccount }}"></my-remote-subscribe>
+ <div class="dropdown-divider"></div>
+
+ <h6 class="dropdown-header" i18n>Using a syndication feed</h6>
+ <button (click)="rssOpen()" class="dropdown-item" i18n>Subscribe via RSS</button>
+
+ </div>
+ </div>
+</div>
\ No newline at end of file
@import '_variables';
@import '_mixins';
-.subscribe-button {
+.btn-group-subscribe {
@include peertube-button;
- @include orange-button;
-}
+ @include disable-default-a-behaviour;
+ float: right;
+ padding: 0;
-.unsubscribe-button {
- @include peertube-button;
- @include grey-button
-}
+ &.btn-group > .btn:not(.dropdown-toggle) {
+ padding-right: 5px;
+ font-size: 15px;
+ }
+ &.btn-group > .btn-group:not(:first-child) > .btn {
+ padding-left: 2px;
+ }
-.subscribe-button,
-.unsubscribe-button {
- display: inline-block;
+ &.subscribe-button {
+ .btn {
+ @include orange-button;
+ font-weight: 600;
+ }
- &.small {
- min-width: 75px;
- height: 20px;
- line-height: 20px;
- font-size: 13px;
+ span.followers-count {
+ padding-left:5px;
+ }
}
-
- &.normal {
- min-width: 120px;
- height: 30px;
- line-height: 30px;
- font-size: 16px;
+ &.unsubscribe-button {
+ .btn {
+ @include grey-button;
+ font-weight: 600;
+ }
}
-}
-.unsubscribe-button {
- .subscribed {
- display: inline;
+ .dropdown-header {
+ padding-left: 1rem;
}
- .unsubscribe {
- display: none;
+ /deep/ form {
+ padding: 0.25rem 1rem;
}
- &:hover {
- .subscribed {
- display: none;
- }
-
- .unsubscribe {
- display: inline;
- }
+ input {
+ @include peertube-input-text(100%);
}
-}
\ No newline at end of file
+}
import { Component, Input, OnInit } from '@angular/core'
+import { Router } from '@angular/router'
+import { AuthService } from '@app/core'
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'
subscribed: boolean
constructor (
+ private authService: AuthService,
+ private router: Router,
private notificationsService: NotificationsService,
private userSubscriptionService: UserSubscriptionService,
private i18n: I18n
return this.videoChannel.name + '@' + this.videoChannel.host
}
+ get uriAccount () {
+ return this.videoChannel.ownerAccount.name + '@' + this.videoChannel.host
+ }
+
ngOnInit () {
- this.userSubscriptionService.isSubscriptionExists(this.uri)
- .subscribe(
- res => this.subscribed = res[this.uri],
+ if (this.isUserLoggedIn()) {
+ this.userSubscriptionService.isSubscriptionExists(this.uri)
+ .subscribe(
+ res => this.subscribed = res[this.uri],
- err => this.notificationsService.error(this.i18n('Error'), err.message)
- )
+ err => this.notificationsService.error(this.i18n('Error'), err.message)
+ )
+ }
}
subscribe () {
+ if (this.isUserLoggedIn()) {
+ this.localSubscribe()
+ } else {
+ this.gotoLogin()
+ }
+ }
+
+ localSubscribe () {
this.userSubscriptionService.addSubscription(this.uri)
.subscribe(
() => {
}
unsubscribe () {
+ if (this.isUserLoggedIn()) {
+ this.localUnsubscribe()
+ }
+ }
+
+ localUnsubscribe () {
this.userSubscriptionService.deleteSubscription(this.uri)
.subscribe(
() => {
err => this.notificationsService.error(this.i18n('Error'), err.message)
)
}
+
+ isUserLoggedIn () {
+ return this.authService.isLoggedIn()
+ }
+
+ gotoLogin () {
+ this.router.navigate([ '/login' ])
+ }
+
+ rssOpen () {
+ window.open('')
+ }
}
<form novalidate [formGroup]="form" (ngSubmit)="formValidated()">
<div class="avatar-and-textarea">
- <img [src]="user.accountAvatarUrl" alt="Avatar" />
+ <img [src]="getAvatarUrl()" alt="Avatar" />
<div class="form-group">
<textarea i18n-placeholder placeholder="Add comment..." autosize
+ [readonly]="(user === null) ? true : false"
+ (click)="openVisitorModal($event)"
formControlName="text" [ngClass]="{ 'input-error': formErrors['text'] }"
(keyup.control.enter)="onValidKey()" (keyup.meta.enter)="onValidKey()" #textarea>
</button>
</div>
</form>
+
+<ng-template #visitorModal let-modal>
+ <div class="modal-header">
+ <h4 class="modal-title" id="modal-basic-title" i18n>You are one step away from commenting</h4>
+ <button type="button" class="close" aria-label="Close" (click)="hideVisitorModal()"></button>
+ </div>
+ <div class="modal-body" i18n>
+ <span i18n>
+ If you have an account on this instance, you can login:
+ </span>
+ <span class="btn btn-sm mx-3" role="button" (click)="gotoLogin()" i18n>login to comment</span>
+ <span i18n>
+ Otherwise you can comment using an account on an ActivityPub-compatible instance:
+ </span>
+ <my-remote-subscribe [interact]="true" account="{{ uri }}"></my-remote-subscribe>
+ </div>
+ <div class="modal-footer inputs">
+ <span i18n class="action-button action-button-cancel" role="button" (click)="hideVisitorModal()">
+ Cancel
+ </span>
+ </div>
+</ng-template>
button {
@include peertube-button;
- @include orange-button
+ @include orange-button;
}
}
textarea, .submit-comment button {
font-size: 14px !important;
}
+}
+
+.modal-body {
+ .btn {
+ @include peertube-button;
+ @include orange-button;
+ }
+
+ span {
+ float: left;
+ margin-bottom: 20px;
+ }
}
\ No newline at end of file
import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
+import { Router } from '@angular/router'
import { NotificationsService } from 'angular2-notifications'
import { Observable } from 'rxjs'
import { VideoCommentCreate } from '../../../../../../shared/models/videos/video-comment.model'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
import { VideoCommentValidatorsService } from '@app/shared/forms/form-validators/video-comment-validators.service'
+import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
+import { AuthService } from '@app/core/auth'
@Component({
selector: 'my-video-comment-add',
@Output() commentCreated = new EventEmitter<VideoCommentCreate>()
+ @ViewChild('visitorModal') visitorModal: NgbModal
@ViewChild('textarea') private textareaElement: ElementRef
private addingComment = false
+ private uri: string
constructor (
protected formValidatorService: FormValidatorService,
private videoCommentValidatorsService: VideoCommentValidatorsService,
private notificationsService: NotificationsService,
private videoCommentService: VideoCommentService,
+ private authService: AuthService,
+ private modalService: NgbModal,
+ private router: Router,
private i18n: I18n
) {
super()
text: this.videoCommentValidatorsService.VIDEO_COMMENT_TEXT
})
- if (this.focusOnInit === true) {
- this.textareaElement.nativeElement.focus()
- }
+ this.uri = this.router.url
- if (this.parentComment) {
- const mentions = this.parentComments
- .filter(c => c.account.id !== this.user.account.id) // Don't add mention of ourselves
- .map(c => '@' + c.by)
+ if (this.user) {
+ if (this.focusOnInit === true) {
+ this.textareaElement.nativeElement.focus()
+ }
+
+ if (this.parentComment) {
+ const mentions = this.parentComments
+ .filter(c => c.account.id !== this.user.account.id) // Don't add mention of ourselves
+ .map(c => '@' + c.by)
- const mentionsSet = new Set(mentions)
- const mentionsText = Array.from(mentionsSet).join(' ') + ' '
+ const mentionsSet = new Set(mentions)
+ const mentionsText = Array.from(mentionsSet).join(' ') + ' '
- this.form.patchValue({ text: mentionsText })
+ this.form.patchValue({ text: mentionsText })
+ }
}
}
this.formValidated()
}
+ openVisitorModal (event) {
+ if (this.user === null) { // we only open it for visitors
+ // fixing ng-bootstrap ModalService and the "Expression Changed After It Has Been Checked" Error
+ event.srcElement.blur()
+ event.preventDefault()
+
+ this.modalService.open(this.visitorModal)
+ }
+ }
+
+ hideVisitorModal () {
+ this.modalService.dismissAll()
+ }
+
formValidated () {
// If we validate very quickly the comment form, we might comment twice
if (this.addingComment) return
return this.form.value['text']
}
+ getAvatarUrl () {
+ if (this.user) return this.user.accountAvatarUrl
+ return window.location.origin + '/client/assets/images/default-avatar.png'
+ }
+
+ gotoLogin () {
+ this.hideVisitorModal()
+ this.authService.redirectUrl = this.router.url
+ this.router.navigate([ '/login' ])
+ }
+
private addCommentReply (commentCreate: VideoCommentCreate) {
return this.videoCommentService
.addCommentReply(this.video.id, this.parentComment.id, commentCreate)
<ng-template [ngIf]="video.commentsEnabled === true">
<my-video-comment-add
- *ngIf="isUserLoggedIn()"
[video]="video"
[user]="user"
(commentCreated)="onCommentThreadCreated($event)"
<img [src]="video.videoChannelAvatarUrl" alt="Video channel avatar" />
</a>
- <my-subscribe-button #subscribeButton *ngIf="isUserLoggedIn()" [videoChannel]="video.channel" size="small"></my-subscribe-button>
+ <my-subscribe-button #subscribeButton [videoChannel]="video.channel" size="small"></my-subscribe-button>
</div>
<div class="video-info-by">
<span i18n>By {{ video.byAccount }}</span>
<img [src]="video.accountAvatarUrl" alt="Account avatar" />
</a>
-
- <my-help helpType="custom" i18n-customHtml customHtml="You can subscribe to this account via any ActivityPub-capable fediverse instance. For instance with Mastodon or Pleroma you can type in the search box <strong>@{{video.account.name}}@{{video.account.host}}</strong> and subscribe there."></my-help>
</div>
</div>
.dropdown-item {
padding: 3px 15px;
+
+ &:active {
+ color: #000 !important;
+ }
+ }
+
+ button {
+ @include disable-default-a-behaviour;
}
a {
+ @include disable-default-a-behaviour;
color: #000 !important;
}
}
@import '~bootstrap/scss/buttons';
//@import '~bootstrap/scss/transitions';
@import '~bootstrap/scss/dropdown';
-//@import '~bootstrap/scss/button-group';
+@import '~bootstrap/scss/button-group';
@import '~bootstrap/scss/input-group';
//@import '~bootstrap/scss/custom-forms';
@import '~bootstrap/scss/nav';