Soft delete video comments instead of detroy
This commit is contained in:
parent
69c7f7525d
commit
69222afac8
|
@ -1,22 +1,45 @@
|
||||||
<div class="root-comment">
|
<div class="root-comment">
|
||||||
<img [src]="comment.accountAvatarUrl" alt="Avatar" />
|
<img
|
||||||
|
*ngIf="!comment.isDeleted"
|
||||||
|
class="comment-avatar"
|
||||||
|
[src]="comment.accountAvatarUrl"
|
||||||
|
alt="Avatar"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span
|
||||||
|
*ngIf="comment.isDeleted"
|
||||||
|
class="comment-avatar"
|
||||||
|
></span>
|
||||||
|
|
||||||
<div class="comment">
|
<div class="comment">
|
||||||
<div *ngIf="highlightedComment === true" class="highlighted-comment" i18n>Highlighted comment</div>
|
<ng-container *ngIf="!comment.isDeleted">
|
||||||
|
<div *ngIf="highlightedComment === true" class="highlighted-comment" i18n>Highlighted comment</div>
|
||||||
|
|
||||||
<div class="comment-account-date">
|
<div class="comment-account-date">
|
||||||
<a [href]="comment.account.url" target="_blank" rel="noopener noreferrer" class="comment-account">{{ comment.by }}</a>
|
<a [href]="comment.account.url" target="_blank" rel="noopener noreferrer" class="comment-account">{{ comment.by }}</a>
|
||||||
<a [routerLink]="['/videos/watch', video.uuid, { 'threadId': comment.threadId }]" class="comment-date">{{ comment.createdAt | myFromNow }}</a>
|
<a [routerLink]="['/videos/watch', video.uuid, { 'threadId': comment.threadId }]" class="comment-date">{{ comment.createdAt | myFromNow }}</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="comment-html" [innerHTML]="sanitizedCommentHTML"></div>
|
<div class="comment-html" [innerHTML]="sanitizedCommentHTML"></div>
|
||||||
|
|
||||||
<div class="comment-actions">
|
<div class="comment-actions">
|
||||||
<div *ngIf="isUserLoggedIn()" (click)="onWantToReply()" class="comment-action-reply" i18n>Reply</div>
|
<div *ngIf="isUserLoggedIn()" (click)="onWantToReply()" class="comment-action-reply" i18n>Reply</div>
|
||||||
<div *ngIf="isRemovableByUser()" (click)="onWantToDelete()" class="comment-action-delete" i18n>Delete</div>
|
<div *ngIf="isRemovableByUser()" (click)="onWantToDelete()" class="comment-action-delete" i18n>Delete</div>
|
||||||
</div>
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="comment.isDeleted">
|
||||||
|
<div class="comment-account-date">
|
||||||
|
<span class="comment-account" i18n>Deleted</span>
|
||||||
|
<a [routerLink]="['/videos/watch', video.uuid, { 'threadId': comment.threadId }]" class="comment-date">{{ comment.createdAt | myFromNow }}</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="comment.isDeleted" class="comment-html comment-html-deleted">
|
||||||
|
<i i18n>This comment has been deleted</i>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<my-video-comment-add
|
<my-video-comment-add
|
||||||
*ngIf="isUserLoggedIn() && inReplyToCommentId === comment.id"
|
*ngIf="!comment.isDeleted && isUserLoggedIn() && inReplyToCommentId === comment.id"
|
||||||
[user]="user"
|
[user]="user"
|
||||||
[video]="video"
|
[video]="video"
|
||||||
[parentComment]="comment"
|
[parentComment]="comment"
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
img {
|
.comment-avatar {
|
||||||
@include avatar(36px);
|
@include avatar(36px);
|
||||||
|
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
|
@ -48,6 +48,7 @@
|
||||||
|
|
||||||
.comment-html {
|
.comment-html {
|
||||||
@include peertube-word-wrap;
|
@include peertube-word-wrap;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
// Mentions
|
// Mentions
|
||||||
::ng-deep a {
|
::ng-deep a {
|
||||||
|
@ -61,10 +62,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.comment-html-deleted {
|
||||||
|
color: $grey-foreground-color;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-actions {
|
.comment-actions {
|
||||||
margin: 10px 0;
|
margin-bottom: 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
.comment-action-reply,
|
.comment-action-reply,
|
||||||
|
@ -100,7 +105,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.root-comment {
|
.root-comment {
|
||||||
img { margin-right: 10px; }
|
.comment-avatar { margin-right: 10px; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -78,7 +78,7 @@ export class VideoCommentComponent implements OnInit, OnChanges {
|
||||||
}
|
}
|
||||||
|
|
||||||
isRemovableByUser () {
|
isRemovableByUser () {
|
||||||
return this.isUserLoggedIn() &&
|
return this.comment.account && this.isUserLoggedIn() &&
|
||||||
(
|
(
|
||||||
this.user.account.id === this.comment.account.id ||
|
this.user.account.id === this.comment.account.id ||
|
||||||
this.user.hasRight(UserRight.REMOVE_ANY_VIDEO_COMMENT)
|
this.user.hasRight(UserRight.REMOVE_ANY_VIDEO_COMMENT)
|
||||||
|
|
|
@ -12,6 +12,8 @@ export class VideoComment implements VideoCommentServerModel {
|
||||||
videoId: number
|
videoId: number
|
||||||
createdAt: Date | string
|
createdAt: Date | string
|
||||||
updatedAt: Date | string
|
updatedAt: Date | string
|
||||||
|
deletedAt: Date | string
|
||||||
|
isDeleted: boolean
|
||||||
account: AccountInterface
|
account: AccountInterface
|
||||||
totalReplies: number
|
totalReplies: number
|
||||||
by: string
|
by: string
|
||||||
|
@ -28,14 +30,18 @@ export class VideoComment implements VideoCommentServerModel {
|
||||||
this.videoId = hash.videoId
|
this.videoId = hash.videoId
|
||||||
this.createdAt = new Date(hash.createdAt.toString())
|
this.createdAt = new Date(hash.createdAt.toString())
|
||||||
this.updatedAt = new Date(hash.updatedAt.toString())
|
this.updatedAt = new Date(hash.updatedAt.toString())
|
||||||
|
this.deletedAt = hash.deletedAt ? new Date(hash.deletedAt.toString()) : null
|
||||||
|
this.isDeleted = hash.isDeleted
|
||||||
this.account = hash.account
|
this.account = hash.account
|
||||||
this.totalReplies = hash.totalReplies
|
this.totalReplies = hash.totalReplies
|
||||||
|
|
||||||
this.by = Actor.CREATE_BY_STRING(this.account.name, this.account.host)
|
if (this.account) {
|
||||||
this.accountAvatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.account)
|
this.by = Actor.CREATE_BY_STRING(this.account.name, this.account.host)
|
||||||
|
this.accountAvatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.account)
|
||||||
|
|
||||||
const absoluteAPIUrl = getAbsoluteAPIUrl()
|
const absoluteAPIUrl = getAbsoluteAPIUrl()
|
||||||
const thisHost = new URL(absoluteAPIUrl).host
|
const thisHost = new URL(absoluteAPIUrl).host
|
||||||
this.isLocal = this.account.host.trim() === thisHost
|
this.isLocal = this.account.host.trim() === thisHost
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -153,10 +153,6 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
async onWantedToDelete (commentToDelete: VideoComment) {
|
async onWantedToDelete (commentToDelete: VideoComment) {
|
||||||
let message = 'Do you really want to delete this comment?'
|
let message = 'Do you really want to delete this comment?'
|
||||||
|
|
||||||
if (commentToDelete.totalReplies !== 0) {
|
|
||||||
message += this.i18n(' {{totalReplies}} replies will be deleted too.', { totalReplies: commentToDelete.totalReplies })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (commentToDelete.isLocal) {
|
if (commentToDelete.isLocal) {
|
||||||
message += this.i18n(' The deletion will be sent to remote instances, so they remove the comment too.')
|
message += this.i18n(' The deletion will be sent to remote instances, so they remove the comment too.')
|
||||||
} else {
|
} else {
|
||||||
|
@ -169,21 +165,8 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
this.videoCommentService.deleteVideoComment(commentToDelete.videoId, commentToDelete.id)
|
this.videoCommentService.deleteVideoComment(commentToDelete.videoId, commentToDelete.id)
|
||||||
.subscribe(
|
.subscribe(
|
||||||
() => {
|
() => {
|
||||||
// Delete the comment in the tree
|
// Mark the comment as deleted
|
||||||
if (commentToDelete.inReplyToCommentId) {
|
this.softDeleteComment(commentToDelete)
|
||||||
const thread = this.threadComments[commentToDelete.threadId]
|
|
||||||
if (!thread) {
|
|
||||||
console.error(`Cannot find thread ${commentToDelete.threadId} of the comment to delete ${commentToDelete.id}`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.deleteLocalCommentThread(thread, commentToDelete)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete the thread
|
|
||||||
this.comments = this.comments.filter(c => c.id !== commentToDelete.id)
|
|
||||||
this.componentPagination.totalItems--
|
|
||||||
|
|
||||||
if (this.highlightedThread.id === commentToDelete.id) this.highlightedThread = undefined
|
if (this.highlightedThread.id === commentToDelete.id) this.highlightedThread = undefined
|
||||||
},
|
},
|
||||||
|
@ -204,15 +187,11 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private deleteLocalCommentThread (parentComment: VideoCommentThreadTree, commentToDelete: VideoComment) {
|
private softDeleteComment (comment: VideoComment) {
|
||||||
for (const commentChild of parentComment.children) {
|
comment.isDeleted = true
|
||||||
if (commentChild.comment.id === commentToDelete.id) {
|
comment.deletedAt = new Date()
|
||||||
parentComment.children = parentComment.children.filter(c => c.comment.id !== commentToDelete.id)
|
comment.text = ''
|
||||||
return
|
comment.account = null
|
||||||
}
|
|
||||||
|
|
||||||
this.deleteLocalCommentThread(commentChild, commentToDelete)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private resetVideo () {
|
private resetVideo () {
|
||||||
|
|
|
@ -308,13 +308,16 @@ async function videoCommentController (req: express.Request, res: express.Respon
|
||||||
|
|
||||||
const threadParentComments = await VideoCommentModel.listThreadParentComments(videoComment, undefined)
|
const threadParentComments = await VideoCommentModel.listThreadParentComments(videoComment, undefined)
|
||||||
const isPublic = true // Comments are always public
|
const isPublic = true // Comments are always public
|
||||||
const audience = getAudience(videoComment.Account.Actor, isPublic)
|
let videoCommentObject = videoComment.toActivityPubObject(threadParentComments)
|
||||||
|
|
||||||
const videoCommentObject = audiencify(videoComment.toActivityPubObject(threadParentComments), audience)
|
if (videoComment.Account) {
|
||||||
|
const audience = getAudience(videoComment.Account.Actor, isPublic)
|
||||||
|
videoCommentObject = audiencify(videoCommentObject, audience)
|
||||||
|
|
||||||
if (req.path.endsWith('/activity')) {
|
if (req.path.endsWith('/activity')) {
|
||||||
const data = buildCreateActivity(videoComment.url, videoComment.Account.Actor, videoCommentObject, audience)
|
const data = buildCreateActivity(videoComment.url, videoComment.Account.Actor, videoCommentObject, audience)
|
||||||
return activityPubResponse(activityPubContextify(data), res)
|
return activityPubResponse(activityPubContextify(data), res)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return activityPubResponse(activityPubContextify(videoCommentObject), res)
|
return activityPubResponse(activityPubContextify(videoCommentObject), res)
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import * as express from 'express'
|
import * as express from 'express'
|
||||||
|
import { cloneDeep } from 'lodash'
|
||||||
import { ResultList } from '../../../../shared/models'
|
import { ResultList } from '../../../../shared/models'
|
||||||
import { VideoCommentCreate } from '../../../../shared/models/videos/video-comment.model'
|
import { VideoCommentCreate } from '../../../../shared/models/videos/video-comment.model'
|
||||||
import { logger } from '../../../helpers/logger'
|
import { logger } from '../../../helpers/logger'
|
||||||
import { getFormattedObjects } from '../../../helpers/utils'
|
import { getFormattedObjects } from '../../../helpers/utils'
|
||||||
import { sequelizeTypescript } from '../../../initializers'
|
import { sequelizeTypescript } from '../../../initializers'
|
||||||
import { buildFormattedCommentTree, createVideoComment } from '../../../lib/video-comment'
|
import { buildFormattedCommentTree, createVideoComment, markCommentAsDeleted } from '../../../lib/video-comment'
|
||||||
import {
|
import {
|
||||||
asyncMiddleware,
|
asyncMiddleware,
|
||||||
asyncRetryTransactionMiddleware,
|
asyncRetryTransactionMiddleware,
|
||||||
|
@ -177,19 +178,22 @@ async function addVideoCommentReply (req: express.Request, res: express.Response
|
||||||
|
|
||||||
async function removeVideoComment (req: express.Request, res: express.Response) {
|
async function removeVideoComment (req: express.Request, res: express.Response) {
|
||||||
const videoCommentInstance = res.locals.videoCommentFull
|
const videoCommentInstance = res.locals.videoCommentFull
|
||||||
|
const videoCommentInstanceBefore = cloneDeep(videoCommentInstance)
|
||||||
|
|
||||||
await sequelizeTypescript.transaction(async t => {
|
await sequelizeTypescript.transaction(async t => {
|
||||||
await videoCommentInstance.destroy({ transaction: t })
|
|
||||||
|
|
||||||
if (videoCommentInstance.isOwned() || videoCommentInstance.Video.isOwned()) {
|
if (videoCommentInstance.isOwned() || videoCommentInstance.Video.isOwned()) {
|
||||||
await sendDeleteVideoComment(videoCommentInstance, t)
|
await sendDeleteVideoComment(videoCommentInstance, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
markCommentAsDeleted(videoCommentInstance)
|
||||||
|
|
||||||
|
await videoCommentInstance.save()
|
||||||
})
|
})
|
||||||
|
|
||||||
auditLogger.delete(getAuditIdFromRes(res), new CommentAuditView(videoCommentInstance.toFormattedJSON()))
|
auditLogger.delete(getAuditIdFromRes(res), new CommentAuditView(videoCommentInstance.toFormattedJSON()))
|
||||||
logger.info('Video comment %d deleted.', videoCommentInstance.id)
|
logger.info('Video comment %d deleted.', videoCommentInstance.id)
|
||||||
|
|
||||||
Hooks.runAction('action:api.video-comment.deleted', { comment: videoCommentInstance })
|
Hooks.runAction('action:api.video-comment.deleted', { comment: videoCommentInstanceBefore })
|
||||||
|
|
||||||
return res.type('json').status(204).end()
|
return res.type('json').status(204).end()
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
import * as Sequelize from 'sequelize'
|
||||||
|
|
||||||
|
async function up (utils: {
|
||||||
|
transaction: Sequelize.Transaction,
|
||||||
|
queryInterface: Sequelize.QueryInterface,
|
||||||
|
sequelize: Sequelize.Sequelize,
|
||||||
|
db: any
|
||||||
|
}): Promise<void> {
|
||||||
|
{
|
||||||
|
const data = {
|
||||||
|
type: Sequelize.INTEGER,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: null
|
||||||
|
}
|
||||||
|
|
||||||
|
await utils.queryInterface.changeColumn('videoComment', 'accountId', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const data = {
|
||||||
|
type: Sequelize.DATE,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: null
|
||||||
|
}
|
||||||
|
await utils.queryInterface.addColumn('videoComment', 'deletedAt', data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function down (options) {
|
||||||
|
throw new Error('Not implemented.')
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
up,
|
||||||
|
down
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ import { sequelizeTypescript } from '../../../initializers'
|
||||||
import { ActorModel } from '../../../models/activitypub/actor'
|
import { ActorModel } from '../../../models/activitypub/actor'
|
||||||
import { VideoModel } from '../../../models/video/video'
|
import { VideoModel } from '../../../models/video/video'
|
||||||
import { VideoCommentModel } from '../../../models/video/video-comment'
|
import { VideoCommentModel } from '../../../models/video/video-comment'
|
||||||
|
import { markCommentAsDeleted } from '../../video-comment'
|
||||||
import { forwardVideoRelatedActivity } from '../send/utils'
|
import { forwardVideoRelatedActivity } from '../send/utils'
|
||||||
import { VideoPlaylistModel } from '../../../models/video/video-playlist'
|
import { VideoPlaylistModel } from '../../../models/video/video-playlist'
|
||||||
import { APProcessorOptions } from '../../../typings/activitypub-processor.model'
|
import { APProcessorOptions } from '../../../typings/activitypub-processor.model'
|
||||||
|
@ -128,7 +129,11 @@ function processDeleteVideoComment (byActor: MActorSignature, videoComment: Vide
|
||||||
throw new Error(`Account ${byActor.url} does not own video comment ${videoComment.url} or video ${videoComment.Video.url}`)
|
throw new Error(`Account ${byActor.url} does not own video comment ${videoComment.url} or video ${videoComment.Video.url}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
await videoComment.destroy({ transaction: t })
|
await sequelizeTypescript.transaction(async t => {
|
||||||
|
markCommentAsDeleted(videoComment)
|
||||||
|
|
||||||
|
await videoComment.save()
|
||||||
|
})
|
||||||
|
|
||||||
if (videoComment.Video.isOwned()) {
|
if (videoComment.Video.isOwned()) {
|
||||||
// Don't resend the activity to the sender
|
// Don't resend the activity to the sender
|
||||||
|
|
|
@ -73,9 +73,16 @@ function buildFormattedCommentTree (resultList: ResultList<VideoCommentModel>):
|
||||||
return thread
|
return thread
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function markCommentAsDeleted (comment: MCommentOwnerVideoReply): void {
|
||||||
|
comment.text = ''
|
||||||
|
comment.deletedAt = new Date()
|
||||||
|
comment.accountId = null
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export {
|
export {
|
||||||
createVideoComment,
|
createVideoComment,
|
||||||
buildFormattedCommentTree
|
buildFormattedCommentTree,
|
||||||
|
markCommentAsDeleted
|
||||||
}
|
}
|
||||||
|
|
|
@ -201,7 +201,7 @@ export class AccountModel extends Model<AccountModel> {
|
||||||
|
|
||||||
@HasMany(() => VideoCommentModel, {
|
@HasMany(() => VideoCommentModel, {
|
||||||
foreignKey: {
|
foreignKey: {
|
||||||
allowNull: false
|
allowNull: true
|
||||||
},
|
},
|
||||||
onDelete: 'cascade',
|
onDelete: 'cascade',
|
||||||
hooks: true
|
hooks: true
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
|
import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
|
||||||
import { ActivityTagObject } from '../../../shared/models/activitypub/objects/common-objects'
|
import { ActivityTagObject, ActivityTombstoneObject } 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'
|
||||||
|
@ -122,6 +122,10 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
|
||||||
@UpdatedAt
|
@UpdatedAt
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
|
|
||||||
|
@AllowNull(true)
|
||||||
|
@Column(DataType.DATE)
|
||||||
|
deletedAt: Date
|
||||||
|
|
||||||
@AllowNull(false)
|
@AllowNull(false)
|
||||||
@Is('VideoCommentUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
|
@Is('VideoCommentUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
|
||||||
@Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max))
|
@Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max))
|
||||||
|
@ -177,7 +181,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
|
||||||
|
|
||||||
@BelongsTo(() => AccountModel, {
|
@BelongsTo(() => AccountModel, {
|
||||||
foreignKey: {
|
foreignKey: {
|
||||||
allowNull: false
|
allowNull: true
|
||||||
},
|
},
|
||||||
onDelete: 'CASCADE'
|
onDelete: 'CASCADE'
|
||||||
})
|
})
|
||||||
|
@ -436,9 +440,17 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
|
||||||
}
|
}
|
||||||
|
|
||||||
isOwned () {
|
isOwned () {
|
||||||
|
if (!this.Account) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return this.Account.isOwned()
|
return this.Account.isOwned()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isDeleted () {
|
||||||
|
return null !== this.deletedAt
|
||||||
|
}
|
||||||
|
|
||||||
extractMentions () {
|
extractMentions () {
|
||||||
let result: string[] = []
|
let result: string[] = []
|
||||||
|
|
||||||
|
@ -487,12 +499,25 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
|
||||||
videoId: this.videoId,
|
videoId: this.videoId,
|
||||||
createdAt: this.createdAt,
|
createdAt: this.createdAt,
|
||||||
updatedAt: this.updatedAt,
|
updatedAt: this.updatedAt,
|
||||||
|
deletedAt: this.deletedAt,
|
||||||
|
isDeleted: this.isDeleted(),
|
||||||
totalReplies: this.get('totalReplies') || 0,
|
totalReplies: this.get('totalReplies') || 0,
|
||||||
account: this.Account.toFormattedJSON()
|
account: this.Account ? this.Account.toFormattedJSON() : null
|
||||||
} as VideoComment
|
} as VideoComment
|
||||||
}
|
}
|
||||||
|
|
||||||
toActivityPubObject (this: MCommentAP, threadParentComments: MCommentOwner[]): VideoCommentObject {
|
toActivityPubObject (this: MCommentAP, threadParentComments: MCommentOwner[]): VideoCommentObject | ActivityTombstoneObject {
|
||||||
|
if (this.isDeleted()) {
|
||||||
|
return {
|
||||||
|
id: this.url,
|
||||||
|
type: 'Tombstone',
|
||||||
|
formerType: 'Note',
|
||||||
|
published: this.createdAt.toISOString(),
|
||||||
|
updated: this.updatedAt.toISOString(),
|
||||||
|
deleted: this.deletedAt.toISOString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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) {
|
||||||
|
|
|
@ -868,7 +868,7 @@ describe('Test multiple servers', function () {
|
||||||
await waitJobs(servers)
|
await waitJobs(servers)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should not have this comment anymore', async function () {
|
it('Should have this comment marked as deleted', async function () {
|
||||||
for (const server of servers) {
|
for (const server of servers) {
|
||||||
const res1 = await getVideoCommentThreads(server.url, videoUUID, 0, 5)
|
const res1 = await getVideoCommentThreads(server.url, videoUUID, 0, 5)
|
||||||
const threadId = res1.body.data.find(c => c.text === 'my super first comment').id
|
const threadId = res1.body.data.find(c => c.text === 'my super first comment').id
|
||||||
|
@ -880,7 +880,13 @@ describe('Test multiple servers', function () {
|
||||||
|
|
||||||
const firstChild = tree.children[0]
|
const firstChild = tree.children[0]
|
||||||
expect(firstChild.comment.text).to.equal('my super answer to thread 1')
|
expect(firstChild.comment.text).to.equal('my super answer to thread 1')
|
||||||
expect(firstChild.children).to.have.lengthOf(0)
|
expect(firstChild.children).to.have.lengthOf(1)
|
||||||
|
|
||||||
|
const deletedComment = firstChild.children[0].comment
|
||||||
|
expect(deletedComment.isDeleted).to.be.true
|
||||||
|
expect(deletedComment.deletedAt).to.not.be.null
|
||||||
|
expect(deletedComment.account).to.be.null
|
||||||
|
expect(deletedComment.text).to.equal('')
|
||||||
|
|
||||||
const secondChild = tree.children[1]
|
const secondChild = tree.children[1]
|
||||||
expect(secondChild.comment.text).to.equal('my second answer to thread 1')
|
expect(secondChild.comment.text).to.equal('my second answer to thread 1')
|
||||||
|
@ -897,13 +903,13 @@ describe('Test multiple servers', function () {
|
||||||
await waitJobs(servers)
|
await waitJobs(servers)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should have the threads deleted on other servers too', async function () {
|
it('Should have the threads marked as deleted on other servers too', async function () {
|
||||||
for (const server of servers) {
|
for (const server of servers) {
|
||||||
const res = await getVideoCommentThreads(server.url, videoUUID, 0, 5)
|
const res = await getVideoCommentThreads(server.url, videoUUID, 0, 5)
|
||||||
|
|
||||||
expect(res.body.total).to.equal(1)
|
expect(res.body.total).to.equal(2)
|
||||||
expect(res.body.data).to.be.an('array')
|
expect(res.body.data).to.be.an('array')
|
||||||
expect(res.body.data).to.have.lengthOf(1)
|
expect(res.body.data).to.have.lengthOf(2)
|
||||||
|
|
||||||
{
|
{
|
||||||
const comment: VideoComment = res.body.data[0]
|
const comment: VideoComment = res.body.data[0]
|
||||||
|
@ -915,6 +921,20 @@ describe('Test multiple servers', function () {
|
||||||
expect(dateIsValid(comment.createdAt as string)).to.be.true
|
expect(dateIsValid(comment.createdAt as string)).to.be.true
|
||||||
expect(dateIsValid(comment.updatedAt as string)).to.be.true
|
expect(dateIsValid(comment.updatedAt as string)).to.be.true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const deletedComment: VideoComment = res.body.data[1]
|
||||||
|
expect(deletedComment).to.not.be.undefined
|
||||||
|
expect(deletedComment.isDeleted).to.be.true
|
||||||
|
expect(deletedComment.deletedAt).to.not.be.null
|
||||||
|
expect(deletedComment.text).to.equal('')
|
||||||
|
expect(deletedComment.inReplyToCommentId).to.be.null
|
||||||
|
expect(deletedComment.account).to.be.null
|
||||||
|
expect(deletedComment.totalReplies).to.equal(3)
|
||||||
|
expect(dateIsValid(deletedComment.createdAt as string)).to.be.true
|
||||||
|
expect(dateIsValid(deletedComment.updatedAt as string)).to.be.true
|
||||||
|
expect(dateIsValid(deletedComment.deletedAt as string)).to.be.true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -926,12 +946,32 @@ describe('Test multiple servers', function () {
|
||||||
await waitJobs(servers)
|
await waitJobs(servers)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should have the threads deleted on other servers too', async function () {
|
it('Should have the threads marked as deleted on other servers too', async function () {
|
||||||
for (const server of servers) {
|
for (const server of servers) {
|
||||||
const res = await getVideoCommentThreads(server.url, videoUUID, 0, 5)
|
const res = await getVideoCommentThreads(server.url, videoUUID, 0, 5)
|
||||||
|
|
||||||
expect(res.body.total).to.equal(0)
|
expect(res.body.total).to.equal(2)
|
||||||
expect(res.body.data).to.have.lengthOf(0)
|
expect(res.body.data).to.have.lengthOf(2)
|
||||||
|
|
||||||
|
{
|
||||||
|
const comment: VideoComment = res.body.data[0]
|
||||||
|
expect(comment.text).to.equal('')
|
||||||
|
expect(comment.isDeleted).to.be.true
|
||||||
|
expect(comment.createdAt).to.not.be.null
|
||||||
|
expect(comment.deletedAt).to.not.be.null
|
||||||
|
expect(comment.account).to.be.null
|
||||||
|
expect(comment.totalReplies).to.equal(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const comment: VideoComment = res.body.data[1]
|
||||||
|
expect(comment.text).to.equal('')
|
||||||
|
expect(comment.isDeleted).to.be.true
|
||||||
|
expect(comment.createdAt).to.not.be.null
|
||||||
|
expect(comment.deletedAt).to.not.be.null
|
||||||
|
expect(comment.account).to.be.null
|
||||||
|
expect(comment.totalReplies).to.equal(3)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -172,7 +172,7 @@ describe('Test video comments', function () {
|
||||||
|
|
||||||
const tree: VideoCommentThreadTree = res.body
|
const tree: VideoCommentThreadTree = res.body
|
||||||
expect(tree.comment.text).equal('my super first comment')
|
expect(tree.comment.text).equal('my super first comment')
|
||||||
expect(tree.children).to.have.lengthOf(1)
|
expect(tree.children).to.have.lengthOf(2)
|
||||||
|
|
||||||
const firstChild = tree.children[0]
|
const firstChild = tree.children[0]
|
||||||
expect(firstChild.comment.text).to.equal('my super answer to thread 1')
|
expect(firstChild.comment.text).to.equal('my super answer to thread 1')
|
||||||
|
@ -181,20 +181,32 @@ describe('Test video comments', function () {
|
||||||
const childOfFirstChild = firstChild.children[0]
|
const childOfFirstChild = firstChild.children[0]
|
||||||
expect(childOfFirstChild.comment.text).to.equal('my super answer to answer of thread 1')
|
expect(childOfFirstChild.comment.text).to.equal('my super answer to answer of thread 1')
|
||||||
expect(childOfFirstChild.children).to.have.lengthOf(0)
|
expect(childOfFirstChild.children).to.have.lengthOf(0)
|
||||||
|
|
||||||
|
const deletedChildOfFirstChild = tree.children[1]
|
||||||
|
expect(deletedChildOfFirstChild.comment.text).to.equal('')
|
||||||
|
expect(deletedChildOfFirstChild.comment.isDeleted).to.be.true
|
||||||
|
expect(deletedChildOfFirstChild.comment.deletedAt).to.not.be.null
|
||||||
|
expect(deletedChildOfFirstChild.comment.account).to.be.null
|
||||||
|
expect(deletedChildOfFirstChild.children).to.have.lengthOf(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should delete a complete thread', async function () {
|
it('Should delete a complete thread', async function () {
|
||||||
await deleteVideoComment(server.url, server.accessToken, videoId, threadId)
|
await deleteVideoComment(server.url, server.accessToken, videoId, threadId)
|
||||||
|
|
||||||
const res = await getVideoCommentThreads(server.url, videoUUID, 0, 5, 'createdAt')
|
const res = await getVideoCommentThreads(server.url, videoUUID, 0, 5, 'createdAt')
|
||||||
expect(res.body.total).to.equal(2)
|
expect(res.body.total).to.equal(3)
|
||||||
expect(res.body.data).to.be.an('array')
|
expect(res.body.data).to.be.an('array')
|
||||||
expect(res.body.data).to.have.lengthOf(2)
|
expect(res.body.data).to.have.lengthOf(3)
|
||||||
|
|
||||||
expect(res.body.data[0].text).to.equal('super thread 2')
|
expect(res.body.data[0].text).to.equal('')
|
||||||
expect(res.body.data[0].totalReplies).to.equal(0)
|
expect(res.body.data[0].isDeleted).to.be.true
|
||||||
expect(res.body.data[1].text).to.equal('super thread 3')
|
expect(res.body.data[0].deletedAt).to.not.be.null
|
||||||
|
expect(res.body.data[0].account).to.be.null
|
||||||
|
expect(res.body.data[0].totalReplies).to.equal(3)
|
||||||
|
expect(res.body.data[1].text).to.equal('super thread 2')
|
||||||
expect(res.body.data[1].totalReplies).to.equal(0)
|
expect(res.body.data[1].totalReplies).to.equal(0)
|
||||||
|
expect(res.body.data[2].text).to.equal('super thread 3')
|
||||||
|
expect(res.body.data[2].totalReplies).to.equal(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
after(async function () {
|
after(async function () {
|
||||||
|
|
|
@ -89,3 +89,14 @@ export interface ActivityPubAttributedTo {
|
||||||
type: 'Group' | 'Person'
|
type: 'Group' | 'Person'
|
||||||
id: string
|
id: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ActivityTombstoneObject {
|
||||||
|
'@context'?: any
|
||||||
|
id: string
|
||||||
|
type: 'Tombstone'
|
||||||
|
name?: string
|
||||||
|
formerType?: string
|
||||||
|
published: string
|
||||||
|
updated: string
|
||||||
|
deleted: string
|
||||||
|
}
|
||||||
|
|
|
@ -9,6 +9,8 @@ export interface VideoComment {
|
||||||
videoId: number
|
videoId: number
|
||||||
createdAt: Date | string
|
createdAt: Date | string
|
||||||
updatedAt: Date | string
|
updatedAt: Date | string
|
||||||
|
deletedAt: Date | string
|
||||||
|
isDeleted: boolean
|
||||||
totalReplies: number
|
totalReplies: number
|
||||||
account: Account
|
account: Account
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue