Add mentions to comments

This commit is contained in:
Chocobozzz 2018-01-05 11:19:25 +01:00
parent 2890b615f3
commit d7e70384a3
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
11 changed files with 111 additions and 17 deletions

View File

@ -4,6 +4,7 @@
<div class="form-group"> <div class="form-group">
<textarea placeholder="Add comment..." formControlName="text" [ngClass]="{ 'input-error': formErrors['text'] }" #textarea> <textarea placeholder="Add comment..." formControlName="text" [ngClass]="{ 'input-error': formErrors['text'] }" #textarea>
</textarea> </textarea>
<div *ngIf="formErrors.text" class="form-error"> <div *ngIf="formErrors.text" class="form-error">
{{ formErrors.text }} {{ formErrors.text }}

View File

@ -2,7 +2,7 @@ import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild }
import { FormBuilder, FormGroup } from '@angular/forms' import { FormBuilder, FormGroup } from '@angular/forms'
import { NotificationsService } from 'angular2-notifications' import { NotificationsService } from 'angular2-notifications'
import { Observable } from 'rxjs/Observable' 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 { FormReactive } from '../../../shared'
import { VIDEO_COMMENT_TEXT } from '../../../shared/forms/form-validators/video-comment' import { VIDEO_COMMENT_TEXT } from '../../../shared/forms/form-validators/video-comment'
import { User } from '../../../shared/users' import { User } from '../../../shared/users'
@ -19,6 +19,7 @@ export class VideoCommentAddComponent extends FormReactive implements OnInit {
@Input() user: User @Input() user: User
@Input() video: Video @Input() video: Video
@Input() parentComment: VideoComment @Input() parentComment: VideoComment
@Input() parentComments: VideoComment[]
@Input() focusOnInit = false @Input() focusOnInit = false
@Output() commentCreated = new EventEmitter<VideoCommentCreate>() @Output() commentCreated = new EventEmitter<VideoCommentCreate>()
@ -55,6 +56,17 @@ export class VideoCommentAddComponent extends FormReactive implements OnInit {
if (this.focusOnInit === true) { if (this.focusOnInit === true) {
this.textareaElement.nativeElement.focus() 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 () { formValidated () {

View File

@ -18,6 +18,7 @@
[user]="user" [user]="user"
[video]="video" [video]="video"
[parentComment]="comment" [parentComment]="comment"
[parentComments]="newParentComments"
[focusOnInit]="true" [focusOnInit]="true"
(commentCreated)="onCommentReplyCreated($event)" (commentCreated)="onCommentReplyCreated($event)"
></my-video-comment-add> ></my-video-comment-add>
@ -29,6 +30,7 @@
[video]="video" [video]="video"
[inReplyToCommentId]="inReplyToCommentId" [inReplyToCommentId]="inReplyToCommentId"
[commentTree]="commentChild" [commentTree]="commentChild"
[parentComments]="newParentComments"
(wantedToReply)="onWantToReply($event)" (wantedToReply)="onWantToReply($event)"
(wantedToDelete)="onWantToDelete($event)" (wantedToDelete)="onWantToDelete($event)"
(resetReply)="onResetReply()" (resetReply)="onResetReply()"

View File

@ -16,6 +16,7 @@ import { VideoComment } from './video-comment.model'
export class VideoCommentComponent implements OnInit { export class VideoCommentComponent implements OnInit {
@Input() video: Video @Input() video: Video
@Input() comment: VideoComment @Input() comment: VideoComment
@Input() parentComments: VideoComment[] = []
@Input() commentTree: VideoCommentThreadTree @Input() commentTree: VideoCommentThreadTree
@Input() inReplyToCommentId: number @Input() inReplyToCommentId: number
@ -25,6 +26,7 @@ export class VideoCommentComponent implements OnInit {
@Output() resetReply = new EventEmitter() @Output() resetReply = new EventEmitter()
sanitizedCommentHTML = '' sanitizedCommentHTML = ''
newParentComments = []
constructor (private authService: AuthService) {} constructor (private authService: AuthService) {}
@ -36,6 +38,8 @@ export class VideoCommentComponent implements OnInit {
this.sanitizedCommentHTML = sanitizeHtml(this.comment.text, { this.sanitizedCommentHTML = sanitizeHtml(this.comment.text, {
allowedTags: [ 'p', 'span' ] allowedTags: [ 'p', 'span' ]
}) })
this.newParentComments = this.parentComments.concat([ this.comment ])
} }
onCommentReplyCreated (createdComment: VideoComment) { onCommentReplyCreated (createdComment: VideoComment) {

View File

@ -6,6 +6,7 @@
font-size: 15px; font-size: 15px;
cursor: pointer; cursor: pointer;
margin-left: 56px; margin-left: 56px;
margin-bottom: 10px;
} }
.glyphicon, .comment-thread-loading { .glyphicon, .comment-thread-loading {

View File

@ -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) { async function videoCommentController (req: express.Request, res: express.Response, next: express.NextFunction) {
const videoComment: VideoCommentModel = res.locals.videoComment const videoComment: VideoCommentModel = res.locals.videoComment
return res.json(videoComment.toActivityPubObject()) const threadParentComments = await VideoCommentModel.listThreadParentComments(videoComment, undefined)
return res.json(videoComment.toActivityPubObject(threadParentComments))
} }

View File

@ -5,6 +5,7 @@ import { ACTIVITY_PUB } from '../../../initializers'
import { ActorModel } from '../../../models/activitypub/actor' import { ActorModel } from '../../../models/activitypub/actor'
import { ActorFollowModel } from '../../../models/activitypub/actor-follow' import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
import { VideoModel } from '../../../models/video/video' import { VideoModel } from '../../../models/video/video'
import { VideoCommentModel } from '../../../models/video/video-comment'
import { VideoShareModel } from '../../../models/video/video-share' import { VideoShareModel } from '../../../models/video/video-share'
import { activitypubHttpJobScheduler, ActivityPubHttpPayload } from '../../jobs/activitypub-http-job-scheduler' 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[]) { function getObjectFollowersAudience (actorsInvolvedInObject: ActorModel[]) {
return { return {
to: actorsInvolvedInObject.map(a => a.followersUrl), to: actorsInvolvedInObject.map(a => a.followersUrl),
@ -92,10 +121,10 @@ function getObjectFollowersAudience (actorsInvolvedInObject: ActorModel[]) {
} }
async function getActorsInvolvedInVideo (video: VideoModel, t: Transaction) { async function getActorsInvolvedInVideo (video: VideoModel, t: Transaction) {
const actorsToForwardView = await VideoShareModel.loadActorsByShare(video.id, t) const actors = await VideoShareModel.loadActorsByShare(video.id, t)
actorsToForwardView.push(video.VideoChannel.Account.Actor) actors.push(video.VideoChannel.Account.Actor)
return actorsToForwardView return actors
} }
async function getAudience (actorSender: ActorModel, t: Transaction, isPublic = true) { async function getAudience (actorSender: ActorModel, t: Transaction, isPublic = true) {
@ -138,5 +167,6 @@ export {
getActorsInvolvedInVideo, getActorsInvolvedInVideo,
getObjectFollowersAudience, getObjectFollowersAudience,
forwardActivity, forwardActivity,
audiencify audiencify,
getOriginVideoCommentAudience
} }

View File

@ -8,7 +8,8 @@ import { VideoAbuseModel } from '../../../models/video/video-abuse'
import { VideoCommentModel } from '../../../models/video/video-comment' import { VideoCommentModel } from '../../../models/video/video-comment'
import { getVideoAbuseActivityPubUrl, getVideoDislikeActivityPubUrl, getVideoViewActivityPubUrl } from '../url' import { getVideoAbuseActivityPubUrl, getVideoDislikeActivityPubUrl, getVideoViewActivityPubUrl } from '../url'
import { import {
audiencify, broadcastToFollowers, getActorsInvolvedInVideo, getAudience, getObjectFollowersAudience, getOriginVideoAudience, audiencify, broadcastToFollowers, getActorsInvolvedInVideo, getAudience, getObjectFollowersAudience,
getOriginVideoAudience, getOriginVideoCommentAudience,
unicastTo unicastTo
} from './misc' } from './misc'
@ -35,11 +36,12 @@ async function sendVideoAbuse (byActor: ActorModel, videoAbuse: VideoAbuseModel,
async function sendCreateVideoCommentToOrigin (comment: VideoCommentModel, t: Transaction) { async function sendCreateVideoCommentToOrigin (comment: VideoCommentModel, t: Transaction) {
const byActor = comment.Account.Actor 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 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) const data = await createActivityData(comment.url, byActor, commentObject, t, audience)
return unicastTo(data, byActor, comment.Video.VideoChannel.Account.Actor.sharedInboxUrl, t) 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) { async function sendCreateVideoCommentToVideoFollowers (comment: VideoCommentModel, t: Transaction) {
const byActor = comment.Account.Actor 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 actorsInvolvedInVideo = await getActorsInvolvedInVideo(comment.Video, t)
const audience = getObjectFollowersAudience(actorsToForwardView) const audience = getOriginVideoCommentAudience(comment, threadParentComments, actorsInvolvedInVideo)
const commentObject = comment.toActivityPubObject()
const data = await createActivityData(comment.url, byActor, commentObject, t, audience) const data = await createActivityData(comment.url, byActor, commentObject, t, audience)
const followersException = [ byActor ] 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) { async function sendCreateViewToOrigin (byActor: ActorModel, video: VideoModel, t: Transaction) {

View File

@ -3,6 +3,7 @@ import {
AfterDestroy, AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, IFindOptions, Is, Model, Scopes, Table, AfterDestroy, AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, IFindOptions, Is, Model, Scopes, Table,
UpdatedAt UpdatedAt
} from 'sequelize-typescript' } from 'sequelize-typescript'
import { ActivityTagObject } from '../../../shared/models/activitypub/objects/common-objects'
import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object'
import { VideoComment } from '../../../shared/models/videos/video-comment.model' import { VideoComment } from '../../../shared/models/videos/video-comment.model'
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
@ -270,6 +271,30 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
}) })
} }
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 () { isOwned () {
return this.Account.isOwned() return this.Account.isOwned()
} }
@ -289,7 +314,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
} as VideoComment } as VideoComment
} }
toActivityPubObject (): VideoCommentObject { toActivityPubObject (threadParentComments: VideoCommentModel[]): VideoCommentObject {
let inReplyTo: string let inReplyTo: string
// New thread, so in AS we reply to the video // New thread, so in AS we reply to the video
if (this.inReplyToCommentId === null) { if (this.inReplyToCommentId === null) {
@ -298,6 +323,17 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
inReplyTo = this.InReplyToVideoComment.url 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 { return {
type: 'Note' as 'Note', type: 'Note' as 'Note',
id: this.url, id: this.url,
@ -306,7 +342,8 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
updated: this.updatedAt.toISOString(), updated: this.updatedAt.toISOString(),
published: this.createdAt.toISOString(), published: this.createdAt.toISOString(),
url: this.url, url: this.url,
attributedTo: this.Account.Actor.url attributedTo: this.Account.Actor.url,
tag
} }
} }
} }

View File

@ -4,7 +4,8 @@ export interface ActivityIdentifierObject {
} }
export interface ActivityTagObject { export interface ActivityTagObject {
type: 'Hashtag' type: 'Hashtag' | 'Mention'
href?: string
name: string name: string
} }

View File

@ -1,3 +1,5 @@
import { ActivityTagObject } from './common-objects'
export interface VideoCommentObject { export interface VideoCommentObject {
type: 'Note' type: 'Note'
id: string id: string
@ -7,4 +9,5 @@ export interface VideoCommentObject {
updated: string updated: string
url: string url: string
attributedTo: string attributedTo: string
tag: ActivityTagObject[]
} }