diff --git a/client/src/app/videos/+video-watch/comment/video-comment-add.component.html b/client/src/app/videos/+video-watch/comment/video-comment-add.component.html
index 0eaa0d447..41d00da08 100644
--- a/client/src/app/videos/+video-watch/comment/video-comment-add.component.html
+++ b/client/src/app/videos/+video-watch/comment/video-comment-add.component.html
@@ -4,6 +4,7 @@
{{ formErrors.text }}
diff --git a/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts b/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts
index 27655eca7..3e064efcb 100644
--- a/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts
+++ b/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts
@@ -2,7 +2,7 @@ import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild }
import { FormBuilder, FormGroup } from '@angular/forms'
import { NotificationsService } from 'angular2-notifications'
import { Observable } from 'rxjs/Observable'
-import { VideoCommentCreate } from '../../../../../../shared/models/videos/video-comment.model'
+import { VideoCommentCreate, VideoCommentThreadTree } from '../../../../../../shared/models/videos/video-comment.model'
import { FormReactive } from '../../../shared'
import { VIDEO_COMMENT_TEXT } from '../../../shared/forms/form-validators/video-comment'
import { User } from '../../../shared/users'
@@ -19,6 +19,7 @@ export class VideoCommentAddComponent extends FormReactive implements OnInit {
@Input() user: User
@Input() video: Video
@Input() parentComment: VideoComment
+ @Input() parentComments: VideoComment[]
@Input() focusOnInit = false
@Output() commentCreated = new EventEmitter()
@@ -55,6 +56,17 @@ export class VideoCommentAddComponent extends FormReactive implements OnInit {
if (this.focusOnInit === true) {
this.textareaElement.nativeElement.focus()
}
+
+ if (this.parentComment) {
+ const mentions = this.parentComments
+ .filter(c => c.account.id !== this.user.account.id)
+ .map(c => '@' + c.account.name)
+
+ const mentionsSet = new Set(mentions)
+ const mentionsText = Array.from(mentionsSet).join(' ') + ' '
+
+ this.form.patchValue({ text: mentionsText })
+ }
}
formValidated () {
diff --git a/client/src/app/videos/+video-watch/comment/video-comment.component.html b/client/src/app/videos/+video-watch/comment/video-comment.component.html
index 8edd12124..1d325aff9 100644
--- a/client/src/app/videos/+video-watch/comment/video-comment.component.html
+++ b/client/src/app/videos/+video-watch/comment/video-comment.component.html
@@ -18,6 +18,7 @@
[user]="user"
[video]="video"
[parentComment]="comment"
+ [parentComments]="newParentComments"
[focusOnInit]="true"
(commentCreated)="onCommentReplyCreated($event)"
>
@@ -29,6 +30,7 @@
[video]="video"
[inReplyToCommentId]="inReplyToCommentId"
[commentTree]="commentChild"
+ [parentComments]="newParentComments"
(wantedToReply)="onWantToReply($event)"
(wantedToDelete)="onWantToDelete($event)"
(resetReply)="onResetReply()"
diff --git a/client/src/app/videos/+video-watch/comment/video-comment.component.ts b/client/src/app/videos/+video-watch/comment/video-comment.component.ts
index 2ecc8a143..38e603d0d 100644
--- a/client/src/app/videos/+video-watch/comment/video-comment.component.ts
+++ b/client/src/app/videos/+video-watch/comment/video-comment.component.ts
@@ -16,6 +16,7 @@ import { VideoComment } from './video-comment.model'
export class VideoCommentComponent implements OnInit {
@Input() video: Video
@Input() comment: VideoComment
+ @Input() parentComments: VideoComment[] = []
@Input() commentTree: VideoCommentThreadTree
@Input() inReplyToCommentId: number
@@ -25,6 +26,7 @@ export class VideoCommentComponent implements OnInit {
@Output() resetReply = new EventEmitter()
sanitizedCommentHTML = ''
+ newParentComments = []
constructor (private authService: AuthService) {}
@@ -36,6 +38,8 @@ export class VideoCommentComponent implements OnInit {
this.sanitizedCommentHTML = sanitizeHtml(this.comment.text, {
allowedTags: [ 'p', 'span' ]
})
+
+ this.newParentComments = this.parentComments.concat([ this.comment ])
}
onCommentReplyCreated (createdComment: VideoComment) {
diff --git a/client/src/app/videos/+video-watch/comment/video-comments.component.scss b/client/src/app/videos/+video-watch/comment/video-comments.component.scss
index be122eb2c..19ab3b633 100644
--- a/client/src/app/videos/+video-watch/comment/video-comments.component.scss
+++ b/client/src/app/videos/+video-watch/comment/video-comments.component.scss
@@ -6,6 +6,7 @@
font-size: 15px;
cursor: pointer;
margin-left: 56px;
+ margin-bottom: 10px;
}
.glyphicon, .comment-thread-loading {
diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts
index e0ab3188b..717473912 100644
--- a/server/controllers/activitypub/client.ts
+++ b/server/controllers/activitypub/client.ts
@@ -114,5 +114,6 @@ async function videoChannelController (req: express.Request, res: express.Respon
async function videoCommentController (req: express.Request, res: express.Response, next: express.NextFunction) {
const videoComment: VideoCommentModel = res.locals.videoComment
- return res.json(videoComment.toActivityPubObject())
+ const threadParentComments = await VideoCommentModel.listThreadParentComments(videoComment, undefined)
+ return res.json(videoComment.toActivityPubObject(threadParentComments))
}
diff --git a/server/lib/activitypub/send/misc.ts b/server/lib/activitypub/send/misc.ts
index 05f327b29..4aa514c15 100644
--- a/server/lib/activitypub/send/misc.ts
+++ b/server/lib/activitypub/send/misc.ts
@@ -5,6 +5,7 @@ import { ACTIVITY_PUB } from '../../../initializers'
import { ActorModel } from '../../../models/activitypub/actor'
import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
import { VideoModel } from '../../../models/video/video'
+import { VideoCommentModel } from '../../../models/video/video-comment'
import { VideoShareModel } from '../../../models/video/video-share'
import { activitypubHttpJobScheduler, ActivityPubHttpPayload } from '../../jobs/activitypub-http-job-scheduler'
@@ -84,6 +85,34 @@ function getOriginVideoAudience (video: VideoModel, actorsInvolvedInVideo: Actor
}
}
+function getOriginVideoCommentAudience (
+ videoComment: VideoCommentModel,
+ threadParentComments: VideoCommentModel[],
+ actorsInvolvedInVideo: ActorModel[],
+ isOrigin = false
+) {
+ const to = [ ACTIVITY_PUB.PUBLIC ]
+ const cc = [ ]
+
+ // Owner of the video we comment
+ if (isOrigin === false) {
+ cc.push(videoComment.Video.VideoChannel.Account.Actor.url)
+ }
+
+ // Followers of the poster
+ cc.push(videoComment.Account.Actor.followersUrl)
+
+ // Send to actors we reply to
+ for (const parentComment of threadParentComments) {
+ cc.push(parentComment.Account.Actor.url)
+ }
+
+ return {
+ to,
+ cc: cc.concat(actorsInvolvedInVideo.map(a => a.followersUrl))
+ }
+}
+
function getObjectFollowersAudience (actorsInvolvedInObject: ActorModel[]) {
return {
to: actorsInvolvedInObject.map(a => a.followersUrl),
@@ -92,10 +121,10 @@ function getObjectFollowersAudience (actorsInvolvedInObject: ActorModel[]) {
}
async function getActorsInvolvedInVideo (video: VideoModel, t: Transaction) {
- const actorsToForwardView = await VideoShareModel.loadActorsByShare(video.id, t)
- actorsToForwardView.push(video.VideoChannel.Account.Actor)
+ const actors = await VideoShareModel.loadActorsByShare(video.id, t)
+ actors.push(video.VideoChannel.Account.Actor)
- return actorsToForwardView
+ return actors
}
async function getAudience (actorSender: ActorModel, t: Transaction, isPublic = true) {
@@ -138,5 +167,6 @@ export {
getActorsInvolvedInVideo,
getObjectFollowersAudience,
forwardActivity,
- audiencify
+ audiencify,
+ getOriginVideoCommentAudience
}
diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts
index 2f5cdc8c5..e2ee639d9 100644
--- a/server/lib/activitypub/send/send-create.ts
+++ b/server/lib/activitypub/send/send-create.ts
@@ -8,7 +8,8 @@ import { VideoAbuseModel } from '../../../models/video/video-abuse'
import { VideoCommentModel } from '../../../models/video/video-comment'
import { getVideoAbuseActivityPubUrl, getVideoDislikeActivityPubUrl, getVideoViewActivityPubUrl } from '../url'
import {
- audiencify, broadcastToFollowers, getActorsInvolvedInVideo, getAudience, getObjectFollowersAudience, getOriginVideoAudience,
+ audiencify, broadcastToFollowers, getActorsInvolvedInVideo, getAudience, getObjectFollowersAudience,
+ getOriginVideoAudience, getOriginVideoCommentAudience,
unicastTo
} from './misc'
@@ -35,11 +36,12 @@ async function sendVideoAbuse (byActor: ActorModel, videoAbuse: VideoAbuseModel,
async function sendCreateVideoCommentToOrigin (comment: VideoCommentModel, t: Transaction) {
const byActor = comment.Account.Actor
+ const threadParentComments = await VideoCommentModel.listThreadParentComments(comment, t)
+ const commentObject = comment.toActivityPubObject(threadParentComments)
const actorsInvolvedInVideo = await getActorsInvolvedInVideo(comment.Video, t)
- const audience = getOriginVideoAudience(comment.Video, actorsInvolvedInVideo)
+ const audience = getOriginVideoCommentAudience(comment, threadParentComments, actorsInvolvedInVideo)
- const commentObject = comment.toActivityPubObject()
const data = await createActivityData(comment.url, byActor, commentObject, t, audience)
return unicastTo(data, byActor, comment.Video.VideoChannel.Account.Actor.sharedInboxUrl, t)
@@ -47,15 +49,15 @@ async function sendCreateVideoCommentToOrigin (comment: VideoCommentModel, t: Tr
async function sendCreateVideoCommentToVideoFollowers (comment: VideoCommentModel, t: Transaction) {
const byActor = comment.Account.Actor
+ const threadParentComments = await VideoCommentModel.listThreadParentComments(comment, t)
+ const commentObject = comment.toActivityPubObject(threadParentComments)
- const actorsToForwardView = await getActorsInvolvedInVideo(comment.Video, t)
- const audience = getObjectFollowersAudience(actorsToForwardView)
-
- const commentObject = comment.toActivityPubObject()
+ const actorsInvolvedInVideo = await getActorsInvolvedInVideo(comment.Video, t)
+ const audience = getOriginVideoCommentAudience(comment, threadParentComments, actorsInvolvedInVideo)
const data = await createActivityData(comment.url, byActor, commentObject, t, audience)
const followersException = [ byActor ]
- return broadcastToFollowers(data, byActor, actorsToForwardView, t, followersException)
+ return broadcastToFollowers(data, byActor, actorsInvolvedInVideo, t, followersException)
}
async function sendCreateViewToOrigin (byActor: ActorModel, video: VideoModel, t: Transaction) {
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts
index 66fca2484..dbb2fe429 100644
--- a/server/models/video/video-comment.ts
+++ b/server/models/video/video-comment.ts
@@ -3,6 +3,7 @@ import {
AfterDestroy, AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, IFindOptions, Is, Model, Scopes, Table,
UpdatedAt
} from 'sequelize-typescript'
+import { ActivityTagObject } from '../../../shared/models/activitypub/objects/common-objects'
import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object'
import { VideoComment } from '../../../shared/models/videos/video-comment.model'
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
@@ -270,6 +271,30 @@ export class VideoCommentModel extends Model {
})
}
+ static listThreadParentComments (comment: VideoCommentModel, t: Sequelize.Transaction) {
+ const query = {
+ order: [ [ 'createdAt', 'ASC' ] ],
+ where: {
+ [ Sequelize.Op.or ]: [
+ { id: comment.getThreadId() },
+ { originCommentId: comment.getThreadId() }
+ ],
+ id: {
+ [ Sequelize.Op.ne ]: comment.id
+ }
+ },
+ transaction: t
+ }
+
+ return VideoCommentModel
+ .scope([ ScopeNames.WITH_ACCOUNT ])
+ .findAll(query)
+ }
+
+ getThreadId (): number {
+ return this.originCommentId || this.id
+ }
+
isOwned () {
return this.Account.isOwned()
}
@@ -289,7 +314,7 @@ export class VideoCommentModel extends Model {
} as VideoComment
}
- toActivityPubObject (): VideoCommentObject {
+ toActivityPubObject (threadParentComments: VideoCommentModel[]): VideoCommentObject {
let inReplyTo: string
// New thread, so in AS we reply to the video
if (this.inReplyToCommentId === null) {
@@ -298,6 +323,17 @@ export class VideoCommentModel extends Model {
inReplyTo = this.InReplyToVideoComment.url
}
+ const tag: ActivityTagObject[] = []
+ for (const parentComment of threadParentComments) {
+ const actor = parentComment.Account.Actor
+
+ tag.push({
+ type: 'Mention',
+ href: actor.url,
+ name: `@${actor.preferredUsername}@${actor.getHost()}`
+ })
+ }
+
return {
type: 'Note' as 'Note',
id: this.url,
@@ -306,7 +342,8 @@ export class VideoCommentModel extends Model {
updated: this.updatedAt.toISOString(),
published: this.createdAt.toISOString(),
url: this.url,
- attributedTo: this.Account.Actor.url
+ attributedTo: this.Account.Actor.url,
+ tag
}
}
}
diff --git a/shared/models/activitypub/objects/common-objects.ts b/shared/models/activitypub/objects/common-objects.ts
index ea5a503ac..aef728b82 100644
--- a/shared/models/activitypub/objects/common-objects.ts
+++ b/shared/models/activitypub/objects/common-objects.ts
@@ -4,7 +4,8 @@ export interface ActivityIdentifierObject {
}
export interface ActivityTagObject {
- type: 'Hashtag'
+ type: 'Hashtag' | 'Mention'
+ href?: string
name: string
}
diff --git a/shared/models/activitypub/objects/video-comment-object.ts b/shared/models/activitypub/objects/video-comment-object.ts
index 785fbbc0d..1c058b86c 100644
--- a/shared/models/activitypub/objects/video-comment-object.ts
+++ b/shared/models/activitypub/objects/video-comment-object.ts
@@ -1,3 +1,5 @@
+import { ActivityTagObject } from './common-objects'
+
export interface VideoCommentObject {
type: 'Note'
id: string
@@ -7,4 +9,5 @@ export interface VideoCommentObject {
updated: string
url: string
attributedTo: string
+ tag: ActivityTagObject[]
}