aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2018-01-05 11:19:25 +0100
committerChocobozzz <me@florianbigard.com>2018-01-05 11:19:25 +0100
commitd7e70384a360cda51fe23712331110a5c8b1124c (patch)
tree385609669c92936a5c66ae028c331fb4a9b5943e
parent2890b615f31ab7d519d8be66b49ff8712df90c51 (diff)
downloadPeerTube-d7e70384a360cda51fe23712331110a5c8b1124c.tar.gz
PeerTube-d7e70384a360cda51fe23712331110a5c8b1124c.tar.zst
PeerTube-d7e70384a360cda51fe23712331110a5c8b1124c.zip
Add mentions to comments
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comment-add.component.html1
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comment-add.component.ts14
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comment.component.html2
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comment.component.ts4
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comments.component.scss1
-rw-r--r--server/controllers/activitypub/client.ts3
-rw-r--r--server/lib/activitypub/send/misc.ts38
-rw-r--r--server/lib/activitypub/send/send-create.ts18
-rw-r--r--server/models/video/video-comment.ts41
-rw-r--r--shared/models/activitypub/objects/common-objects.ts3
-rw-r--r--shared/models/activitypub/objects/video-comment-object.ts3
11 files changed, 111 insertions, 17 deletions
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 @@
4 4
5 <div class="form-group"> 5 <div class="form-group">
6 <textarea placeholder="Add comment..." formControlName="text" [ngClass]="{ 'input-error': formErrors['text'] }" #textarea> 6 <textarea placeholder="Add comment..." formControlName="text" [ngClass]="{ 'input-error': formErrors['text'] }" #textarea>
7
7 </textarea> 8 </textarea>
8 <div *ngIf="formErrors.text" class="form-error"> 9 <div *ngIf="formErrors.text" class="form-error">
9 {{ formErrors.text }} 10 {{ 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 }
2import { FormBuilder, FormGroup } from '@angular/forms' 2import { FormBuilder, FormGroup } from '@angular/forms'
3import { NotificationsService } from 'angular2-notifications' 3import { NotificationsService } from 'angular2-notifications'
4import { Observable } from 'rxjs/Observable' 4import { Observable } from 'rxjs/Observable'
5import { VideoCommentCreate } from '../../../../../../shared/models/videos/video-comment.model' 5import { VideoCommentCreate, VideoCommentThreadTree } from '../../../../../../shared/models/videos/video-comment.model'
6import { FormReactive } from '../../../shared' 6import { FormReactive } from '../../../shared'
7import { VIDEO_COMMENT_TEXT } from '../../../shared/forms/form-validators/video-comment' 7import { VIDEO_COMMENT_TEXT } from '../../../shared/forms/form-validators/video-comment'
8import { User } from '../../../shared/users' 8import { User } from '../../../shared/users'
@@ -19,6 +19,7 @@ export class VideoCommentAddComponent extends FormReactive implements OnInit {
19 @Input() user: User 19 @Input() user: User
20 @Input() video: Video 20 @Input() video: Video
21 @Input() parentComment: VideoComment 21 @Input() parentComment: VideoComment
22 @Input() parentComments: VideoComment[]
22 @Input() focusOnInit = false 23 @Input() focusOnInit = false
23 24
24 @Output() commentCreated = new EventEmitter<VideoCommentCreate>() 25 @Output() commentCreated = new EventEmitter<VideoCommentCreate>()
@@ -55,6 +56,17 @@ export class VideoCommentAddComponent extends FormReactive implements OnInit {
55 if (this.focusOnInit === true) { 56 if (this.focusOnInit === true) {
56 this.textareaElement.nativeElement.focus() 57 this.textareaElement.nativeElement.focus()
57 } 58 }
59
60 if (this.parentComment) {
61 const mentions = this.parentComments
62 .filter(c => c.account.id !== this.user.account.id)
63 .map(c => '@' + c.account.name)
64
65 const mentionsSet = new Set(mentions)
66 const mentionsText = Array.from(mentionsSet).join(' ') + ' '
67
68 this.form.patchValue({ text: mentionsText })
69 }
58 } 70 }
59 71
60 formValidated () { 72 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 @@
18 [user]="user" 18 [user]="user"
19 [video]="video" 19 [video]="video"
20 [parentComment]="comment" 20 [parentComment]="comment"
21 [parentComments]="newParentComments"
21 [focusOnInit]="true" 22 [focusOnInit]="true"
22 (commentCreated)="onCommentReplyCreated($event)" 23 (commentCreated)="onCommentReplyCreated($event)"
23 ></my-video-comment-add> 24 ></my-video-comment-add>
@@ -29,6 +30,7 @@
29 [video]="video" 30 [video]="video"
30 [inReplyToCommentId]="inReplyToCommentId" 31 [inReplyToCommentId]="inReplyToCommentId"
31 [commentTree]="commentChild" 32 [commentTree]="commentChild"
33 [parentComments]="newParentComments"
32 (wantedToReply)="onWantToReply($event)" 34 (wantedToReply)="onWantToReply($event)"
33 (wantedToDelete)="onWantToDelete($event)" 35 (wantedToDelete)="onWantToDelete($event)"
34 (resetReply)="onResetReply()" 36 (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'
16export class VideoCommentComponent implements OnInit { 16export class VideoCommentComponent implements OnInit {
17 @Input() video: Video 17 @Input() video: Video
18 @Input() comment: VideoComment 18 @Input() comment: VideoComment
19 @Input() parentComments: VideoComment[] = []
19 @Input() commentTree: VideoCommentThreadTree 20 @Input() commentTree: VideoCommentThreadTree
20 @Input() inReplyToCommentId: number 21 @Input() inReplyToCommentId: number
21 22
@@ -25,6 +26,7 @@ export class VideoCommentComponent implements OnInit {
25 @Output() resetReply = new EventEmitter() 26 @Output() resetReply = new EventEmitter()
26 27
27 sanitizedCommentHTML = '' 28 sanitizedCommentHTML = ''
29 newParentComments = []
28 30
29 constructor (private authService: AuthService) {} 31 constructor (private authService: AuthService) {}
30 32
@@ -36,6 +38,8 @@ export class VideoCommentComponent implements OnInit {
36 this.sanitizedCommentHTML = sanitizeHtml(this.comment.text, { 38 this.sanitizedCommentHTML = sanitizeHtml(this.comment.text, {
37 allowedTags: [ 'p', 'span' ] 39 allowedTags: [ 'p', 'span' ]
38 }) 40 })
41
42 this.newParentComments = this.parentComments.concat([ this.comment ])
39 } 43 }
40 44
41 onCommentReplyCreated (createdComment: VideoComment) { 45 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 @@
6 font-size: 15px; 6 font-size: 15px;
7 cursor: pointer; 7 cursor: pointer;
8 margin-left: 56px; 8 margin-left: 56px;
9 margin-bottom: 10px;
9} 10}
10 11
11.glyphicon, .comment-thread-loading { 12.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
114async function videoCommentController (req: express.Request, res: express.Response, next: express.NextFunction) { 114async function videoCommentController (req: express.Request, res: express.Response, next: express.NextFunction) {
115 const videoComment: VideoCommentModel = res.locals.videoComment 115 const videoComment: VideoCommentModel = res.locals.videoComment
116 116
117 return res.json(videoComment.toActivityPubObject()) 117 const threadParentComments = await VideoCommentModel.listThreadParentComments(videoComment, undefined)
118 return res.json(videoComment.toActivityPubObject(threadParentComments))
118} 119}
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'
5import { ActorModel } from '../../../models/activitypub/actor' 5import { ActorModel } from '../../../models/activitypub/actor'
6import { ActorFollowModel } from '../../../models/activitypub/actor-follow' 6import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
7import { VideoModel } from '../../../models/video/video' 7import { VideoModel } from '../../../models/video/video'
8import { VideoCommentModel } from '../../../models/video/video-comment'
8import { VideoShareModel } from '../../../models/video/video-share' 9import { VideoShareModel } from '../../../models/video/video-share'
9import { activitypubHttpJobScheduler, ActivityPubHttpPayload } from '../../jobs/activitypub-http-job-scheduler' 10import { activitypubHttpJobScheduler, ActivityPubHttpPayload } from '../../jobs/activitypub-http-job-scheduler'
10 11
@@ -84,6 +85,34 @@ function getOriginVideoAudience (video: VideoModel, actorsInvolvedInVideo: Actor
84 } 85 }
85} 86}
86 87
88function getOriginVideoCommentAudience (
89 videoComment: VideoCommentModel,
90 threadParentComments: VideoCommentModel[],
91 actorsInvolvedInVideo: ActorModel[],
92 isOrigin = false
93) {
94 const to = [ ACTIVITY_PUB.PUBLIC ]
95 const cc = [ ]
96
97 // Owner of the video we comment
98 if (isOrigin === false) {
99 cc.push(videoComment.Video.VideoChannel.Account.Actor.url)
100 }
101
102 // Followers of the poster
103 cc.push(videoComment.Account.Actor.followersUrl)
104
105 // Send to actors we reply to
106 for (const parentComment of threadParentComments) {
107 cc.push(parentComment.Account.Actor.url)
108 }
109
110 return {
111 to,
112 cc: cc.concat(actorsInvolvedInVideo.map(a => a.followersUrl))
113 }
114}
115
87function getObjectFollowersAudience (actorsInvolvedInObject: ActorModel[]) { 116function getObjectFollowersAudience (actorsInvolvedInObject: ActorModel[]) {
88 return { 117 return {
89 to: actorsInvolvedInObject.map(a => a.followersUrl), 118 to: actorsInvolvedInObject.map(a => a.followersUrl),
@@ -92,10 +121,10 @@ function getObjectFollowersAudience (actorsInvolvedInObject: ActorModel[]) {
92} 121}
93 122
94async function getActorsInvolvedInVideo (video: VideoModel, t: Transaction) { 123async function getActorsInvolvedInVideo (video: VideoModel, t: Transaction) {
95 const actorsToForwardView = await VideoShareModel.loadActorsByShare(video.id, t) 124 const actors = await VideoShareModel.loadActorsByShare(video.id, t)
96 actorsToForwardView.push(video.VideoChannel.Account.Actor) 125 actors.push(video.VideoChannel.Account.Actor)
97 126
98 return actorsToForwardView 127 return actors
99} 128}
100 129
101async function getAudience (actorSender: ActorModel, t: Transaction, isPublic = true) { 130async function getAudience (actorSender: ActorModel, t: Transaction, isPublic = true) {
@@ -138,5 +167,6 @@ export {
138 getActorsInvolvedInVideo, 167 getActorsInvolvedInVideo,
139 getObjectFollowersAudience, 168 getObjectFollowersAudience,
140 forwardActivity, 169 forwardActivity,
141 audiencify 170 audiencify,
171 getOriginVideoCommentAudience
142} 172}
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'
8import { VideoCommentModel } from '../../../models/video/video-comment' 8import { VideoCommentModel } from '../../../models/video/video-comment'
9import { getVideoAbuseActivityPubUrl, getVideoDislikeActivityPubUrl, getVideoViewActivityPubUrl } from '../url' 9import { getVideoAbuseActivityPubUrl, getVideoDislikeActivityPubUrl, getVideoViewActivityPubUrl } from '../url'
10import { 10import {
11 audiencify, broadcastToFollowers, getActorsInvolvedInVideo, getAudience, getObjectFollowersAudience, getOriginVideoAudience, 11 audiencify, broadcastToFollowers, getActorsInvolvedInVideo, getAudience, getObjectFollowersAudience,
12 getOriginVideoAudience, getOriginVideoCommentAudience,
12 unicastTo 13 unicastTo
13} from './misc' 14} from './misc'
14 15
@@ -35,11 +36,12 @@ async function sendVideoAbuse (byActor: ActorModel, videoAbuse: VideoAbuseModel,
35 36
36async function sendCreateVideoCommentToOrigin (comment: VideoCommentModel, t: Transaction) { 37async function sendCreateVideoCommentToOrigin (comment: VideoCommentModel, t: Transaction) {
37 const byActor = comment.Account.Actor 38 const byActor = comment.Account.Actor
39 const threadParentComments = await VideoCommentModel.listThreadParentComments(comment, t)
40 const commentObject = comment.toActivityPubObject(threadParentComments)
38 41
39 const actorsInvolvedInVideo = await getActorsInvolvedInVideo(comment.Video, t) 42 const actorsInvolvedInVideo = await getActorsInvolvedInVideo(comment.Video, t)
40 const audience = getOriginVideoAudience(comment.Video, actorsInvolvedInVideo) 43 const audience = getOriginVideoCommentAudience(comment, threadParentComments, actorsInvolvedInVideo)
41 44
42 const commentObject = comment.toActivityPubObject()
43 const data = await createActivityData(comment.url, byActor, commentObject, t, audience) 45 const data = await createActivityData(comment.url, byActor, commentObject, t, audience)
44 46
45 return unicastTo(data, byActor, comment.Video.VideoChannel.Account.Actor.sharedInboxUrl, t) 47 return unicastTo(data, byActor, comment.Video.VideoChannel.Account.Actor.sharedInboxUrl, t)
@@ -47,15 +49,15 @@ async function sendCreateVideoCommentToOrigin (comment: VideoCommentModel, t: Tr
47 49
48async function sendCreateVideoCommentToVideoFollowers (comment: VideoCommentModel, t: Transaction) { 50async function sendCreateVideoCommentToVideoFollowers (comment: VideoCommentModel, t: Transaction) {
49 const byActor = comment.Account.Actor 51 const byActor = comment.Account.Actor
52 const threadParentComments = await VideoCommentModel.listThreadParentComments(comment, t)
53 const commentObject = comment.toActivityPubObject(threadParentComments)
50 54
51 const actorsToForwardView = await getActorsInvolvedInVideo(comment.Video, t) 55 const actorsInvolvedInVideo = await getActorsInvolvedInVideo(comment.Video, t)
52 const audience = getObjectFollowersAudience(actorsToForwardView) 56 const audience = getOriginVideoCommentAudience(comment, threadParentComments, actorsInvolvedInVideo)
53
54 const commentObject = comment.toActivityPubObject()
55 const data = await createActivityData(comment.url, byActor, commentObject, t, audience) 57 const data = await createActivityData(comment.url, byActor, commentObject, t, audience)
56 58
57 const followersException = [ byActor ] 59 const followersException = [ byActor ]
58 return broadcastToFollowers(data, byActor, actorsToForwardView, t, followersException) 60 return broadcastToFollowers(data, byActor, actorsInvolvedInVideo, t, followersException)
59} 61}
60 62
61async function sendCreateViewToOrigin (byActor: ActorModel, video: VideoModel, t: Transaction) { 63async 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 {
3 AfterDestroy, AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, IFindOptions, Is, Model, Scopes, Table, 3 AfterDestroy, AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, IFindOptions, Is, Model, Scopes, Table,
4 UpdatedAt 4 UpdatedAt
5} from 'sequelize-typescript' 5} from 'sequelize-typescript'
6import { ActivityTagObject } from '../../../shared/models/activitypub/objects/common-objects'
6import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' 7import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object'
7import { VideoComment } from '../../../shared/models/videos/video-comment.model' 8import { VideoComment } from '../../../shared/models/videos/video-comment.model'
8import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 9import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
@@ -270,6 +271,30 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
270 }) 271 })
271 } 272 }
272 273
274 static listThreadParentComments (comment: VideoCommentModel, t: Sequelize.Transaction) {
275 const query = {
276 order: [ [ 'createdAt', 'ASC' ] ],
277 where: {
278 [ Sequelize.Op.or ]: [
279 { id: comment.getThreadId() },
280 { originCommentId: comment.getThreadId() }
281 ],
282 id: {
283 [ Sequelize.Op.ne ]: comment.id
284 }
285 },
286 transaction: t
287 }
288
289 return VideoCommentModel
290 .scope([ ScopeNames.WITH_ACCOUNT ])
291 .findAll(query)
292 }
293
294 getThreadId (): number {
295 return this.originCommentId || this.id
296 }
297
273 isOwned () { 298 isOwned () {
274 return this.Account.isOwned() 299 return this.Account.isOwned()
275 } 300 }
@@ -289,7 +314,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
289 } as VideoComment 314 } as VideoComment
290 } 315 }
291 316
292 toActivityPubObject (): VideoCommentObject { 317 toActivityPubObject (threadParentComments: VideoCommentModel[]): VideoCommentObject {
293 let inReplyTo: string 318 let inReplyTo: string
294 // New thread, so in AS we reply to the video 319 // New thread, so in AS we reply to the video
295 if (this.inReplyToCommentId === null) { 320 if (this.inReplyToCommentId === null) {
@@ -298,6 +323,17 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
298 inReplyTo = this.InReplyToVideoComment.url 323 inReplyTo = this.InReplyToVideoComment.url
299 } 324 }
300 325
326 const tag: ActivityTagObject[] = []
327 for (const parentComment of threadParentComments) {
328 const actor = parentComment.Account.Actor
329
330 tag.push({
331 type: 'Mention',
332 href: actor.url,
333 name: `@${actor.preferredUsername}@${actor.getHost()}`
334 })
335 }
336
301 return { 337 return {
302 type: 'Note' as 'Note', 338 type: 'Note' as 'Note',
303 id: this.url, 339 id: this.url,
@@ -306,7 +342,8 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
306 updated: this.updatedAt.toISOString(), 342 updated: this.updatedAt.toISOString(),
307 published: this.createdAt.toISOString(), 343 published: this.createdAt.toISOString(),
308 url: this.url, 344 url: this.url,
309 attributedTo: this.Account.Actor.url 345 attributedTo: this.Account.Actor.url,
346 tag
310 } 347 }
311 } 348 }
312} 349}
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 {
4} 4}
5 5
6export interface ActivityTagObject { 6export interface ActivityTagObject {
7 type: 'Hashtag' 7 type: 'Hashtag' | 'Mention'
8 href?: string
8 name: string 9 name: string
9} 10}
10 11
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 @@
1import { ActivityTagObject } from './common-objects'
2
1export interface VideoCommentObject { 3export interface VideoCommentObject {
2 type: 'Note' 4 type: 'Note'
3 id: string 5 id: string
@@ -7,4 +9,5 @@ export interface VideoCommentObject {
7 updated: string 9 updated: string
8 url: string 10 url: string
9 attributedTo: string 11 attributedTo: string
12 tag: ActivityTagObject[]
10} 13}