Add mentions to comments
This commit is contained in:
parent
2890b615f3
commit
d7e70384a3
|
@ -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 }}
|
||||||
|
|
|
@ -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 () {
|
||||||
|
|
|
@ -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()"
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,8 @@ export interface ActivityIdentifierObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ActivityTagObject {
|
export interface ActivityTagObject {
|
||||||
type: 'Hashtag'
|
type: 'Hashtag' | 'Mention'
|
||||||
|
href?: string
|
||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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[]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue