mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2025-10-03 09:49:20 +02:00
Add Comment Count to Video Preview Components (#6635)
* WIP: Add backend functionality to store comment count per video and update on comment visibility actions * WIP: Display image icon and comment count on video miniature component * Probably don't need to index the comment count * Added comment count back to mini video component * Added basic tests * Sort by comments, more robust comments count --------- Co-authored-by: Chocobozzz <me@florianbigard.com>
This commit is contained in:
parent
75d7c2a9dc
commit
25a9f37ded
14 changed files with 255 additions and 56 deletions
|
@ -78,14 +78,15 @@
|
||||||
<p-tableHeaderCheckbox ariaLabel="Select all rows" i18n-ariaLabel></p-tableHeaderCheckbox>
|
<p-tableHeaderCheckbox ariaLabel="Select all rows" i18n-ariaLabel></p-tableHeaderCheckbox>
|
||||||
</th>
|
</th>
|
||||||
|
|
||||||
<th scope="col" *ngIf="isSelected('duration')" [ngbTooltip]="sortTooltip" container="body" pSortableColumn="duration" i18n>{{ getColumn('duration').label }} <p-sortIcon field="duration"></p-sortIcon></th>
|
<th scope="col" *ngIf="isSelected('duration')" [ngbTooltip]="sortTooltip" container="body" pSortableColumn="duration">{{ getColumn('duration').label }} <p-sortIcon field="duration"></p-sortIcon></th>
|
||||||
<th scope="col" *ngIf="isSelected('name')" [ngbTooltip]="sortTooltip" container="body" pSortableColumn="name" i18n>{{ getColumn('name').label }} <p-sortIcon field="name"></p-sortIcon></th>
|
<th scope="col" *ngIf="isSelected('name')" [ngbTooltip]="sortTooltip" container="body" pSortableColumn="name">{{ getColumn('name').label }} <p-sortIcon field="name"></p-sortIcon></th>
|
||||||
<th scope="col" *ngIf="isSelected('privacy')" i18n>{{ getColumn('privacy').label }}</th>
|
<th scope="col" *ngIf="isSelected('privacy')">{{ getColumn('privacy').label }}</th>
|
||||||
<th scope="col" *ngIf="isSelected('sensitive')" i18n>{{ getColumn('sensitive').label }}</th>
|
<th scope="col" *ngIf="isSelected('sensitive')">{{ getColumn('sensitive').label }}</th>
|
||||||
<th scope="col" *ngIf="isSelected('playlists')" i18n>{{ getColumn('playlists').label }}</th>
|
<th scope="col" *ngIf="isSelected('playlists')">{{ getColumn('playlists').label }}</th>
|
||||||
<th scope="col" *ngIf="isSelected('insights')" [ngbTooltip]="sortTooltip" container="body" pSortableColumn="views" i18n>{{ getColumn('insights').label }} <p-sortIcon field="views"></p-sortIcon></th>
|
<th scope="col" *ngIf="isSelected('insights')" [ngbTooltip]="sortTooltip" container="body" pSortableColumn="views">{{ getColumn('insights').label }} <p-sortIcon field="views"></p-sortIcon></th>
|
||||||
|
<th scope="col" *ngIf="isSelected('comments')" [ngbTooltip]="sortTooltip" container="body" pSortableColumn="comments">{{ getColumn('comments').label }} <p-sortIcon field="comments"></p-sortIcon></th>
|
||||||
<th scope="col" *ngIf="isSelected('published')" i18n [ngbTooltip]="sortTooltip" container="body" pSortableColumn="publishedAt">{{ getColumn('published').label }} <p-sortIcon field="publishedAt"></p-sortIcon></th>
|
<th scope="col" *ngIf="isSelected('published')" i18n [ngbTooltip]="sortTooltip" container="body" pSortableColumn="publishedAt">{{ getColumn('published').label }} <p-sortIcon field="publishedAt"></p-sortIcon></th>
|
||||||
<th scope="col" *ngIf="isSelected('state')" i18n>{{ getColumn('state').label }}</th>
|
<th scope="col" *ngIf="isSelected('state')">{{ getColumn('state').label }}</th>
|
||||||
|
|
||||||
<th scope="col" width="250px" class="action-head">
|
<th scope="col" width="250px" class="action-head">
|
||||||
<div class="d-flex align-items-center justify-content-between">
|
<div class="d-flex align-items-center justify-content-between">
|
||||||
|
@ -162,6 +163,10 @@
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
<td *ngIf="isSelected('comments')">
|
||||||
|
<span i18n>{video.comments, plural, =0 {No comments} =1 {1 comment} other {{{ video.comments | myNumberFormatter }} comments}}</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
<td *ngIf="isSelected('published')">
|
<td *ngIf="isSelected('published')">
|
||||||
{{ video.publishedAt | ptDate: 'short' }}
|
{{ video.publishedAt | ptDate: 'short' }}
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -52,7 +52,7 @@ import { VideoPrivacyBadgeComponent } from '../../shared/shared-video/video-priv
|
||||||
import { VideoStateBadgeComponent } from '../../shared/shared-video/video-state-badge.component'
|
import { VideoStateBadgeComponent } from '../../shared/shared-video/video-state-badge.component'
|
||||||
import { VideoChangeOwnershipComponent } from './modals/video-change-ownership.component'
|
import { VideoChangeOwnershipComponent } from './modals/video-change-ownership.component'
|
||||||
|
|
||||||
type Column = 'duration' | 'name' | 'privacy' | 'sensitive' | 'playlists' | 'insights' | 'published' | 'state'
|
type Column = 'duration' | 'name' | 'privacy' | 'sensitive' | 'playlists' | 'insights' | 'published' | 'state' | 'comments'
|
||||||
type CommonFilter = 'live' | 'vod' | 'private' | 'internal' | 'unlisted' | 'password-protected' | 'public'
|
type CommonFilter = 'live' | 'vod' | 'private' | 'internal' | 'unlisted' | 'password-protected' | 'public'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -157,6 +157,7 @@ export class MyVideosComponent extends RestTable<Video> implements OnInit, OnDes
|
||||||
{ id: 'sensitive', label: $localize`Sensitive`, selected: true },
|
{ id: 'sensitive', label: $localize`Sensitive`, selected: true },
|
||||||
{ id: 'playlists', label: $localize`Playlists`, selected: true },
|
{ id: 'playlists', label: $localize`Playlists`, selected: true },
|
||||||
{ id: 'insights', label: $localize`Insights`, selected: true },
|
{ id: 'insights', label: $localize`Insights`, selected: true },
|
||||||
|
{ id: 'comments', label: $localize`Comments`, selected: false },
|
||||||
{ id: 'published', label: $localize`Published`, selected: true },
|
{ id: 'published', label: $localize`Published`, selected: true },
|
||||||
{ id: 'state', label: $localize`State`, selected: true }
|
{ id: 'state', label: $localize`State`, selected: true }
|
||||||
]
|
]
|
||||||
|
|
|
@ -116,6 +116,8 @@ export class Video implements VideoServerModel {
|
||||||
|
|
||||||
automaticTags?: string[]
|
automaticTags?: string[]
|
||||||
|
|
||||||
|
comments: number
|
||||||
|
|
||||||
static buildWatchUrl (video: Partial<Pick<Video, 'uuid' | 'shortUUID'>>) {
|
static buildWatchUrl (video: Partial<Pick<Video, 'uuid' | 'shortUUID'>>) {
|
||||||
return buildVideoWatchPath({ shortUUID: video.shortUUID || video.uuid })
|
return buildVideoWatchPath({ shortUUID: video.shortUUID || video.uuid })
|
||||||
}
|
}
|
||||||
|
@ -211,6 +213,8 @@ export class Video implements VideoServerModel {
|
||||||
this.aspectRatio = hash.aspectRatio
|
this.aspectRatio = hash.aspectRatio
|
||||||
|
|
||||||
this.automaticTags = hash.automaticTags
|
this.automaticTags = hash.automaticTags
|
||||||
|
|
||||||
|
this.comments = hash.comments
|
||||||
}
|
}
|
||||||
|
|
||||||
isVideoNSFWForUser (user: User, serverConfig: HTMLServerConfig) {
|
isVideoNSFWForUser (user: User, serverConfig: HTMLServerConfig) {
|
||||||
|
|
|
@ -6,9 +6,9 @@ import {
|
||||||
LOCALE_ID,
|
LOCALE_ID,
|
||||||
OnInit,
|
OnInit,
|
||||||
booleanAttribute,
|
booleanAttribute,
|
||||||
numberAttribute,
|
|
||||||
inject,
|
inject,
|
||||||
input,
|
input,
|
||||||
|
numberAttribute,
|
||||||
output
|
output
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { RouterLink } from '@angular/router'
|
import { RouterLink } from '@angular/router'
|
||||||
|
@ -24,8 +24,8 @@ import { VideoService } from '../shared-main/video/video.service'
|
||||||
import { VideoThumbnailComponent } from '../shared-thumbnail/video-thumbnail.component'
|
import { VideoThumbnailComponent } from '../shared-thumbnail/video-thumbnail.component'
|
||||||
import { VideoPlaylistService } from '../shared-video-playlist/video-playlist.service'
|
import { VideoPlaylistService } from '../shared-video-playlist/video-playlist.service'
|
||||||
import { VideoViewsCounterComponent } from '../shared-video/video-views-counter.component'
|
import { VideoViewsCounterComponent } from '../shared-video/video-views-counter.component'
|
||||||
import { VideoActionsDisplayType, VideoActionsDropdownComponent } from './video-actions-dropdown.component'
|
|
||||||
import { ActorHostComponent } from '../standalone-actor/actor-host.component'
|
import { ActorHostComponent } from '../standalone-actor/actor-host.component'
|
||||||
|
import { VideoActionsDisplayType, VideoActionsDropdownComponent } from './video-actions-dropdown.component'
|
||||||
|
|
||||||
export type MiniatureDisplayOptions = {
|
export type MiniatureDisplayOptions = {
|
||||||
date?: boolean
|
date?: boolean
|
||||||
|
|
|
@ -51,6 +51,8 @@ export interface Video extends Partial<VideoAdditionalAttributes> {
|
||||||
|
|
||||||
likes: number
|
likes: number
|
||||||
dislikes: number
|
dislikes: number
|
||||||
|
comments: number
|
||||||
|
|
||||||
nsfw: boolean
|
nsfw: boolean
|
||||||
|
|
||||||
account: AccountSummary
|
account: AccountSummary
|
||||||
|
|
|
@ -12,9 +12,12 @@ import {
|
||||||
} from '@peertube/peertube-models'
|
} from '@peertube/peertube-models'
|
||||||
import {
|
import {
|
||||||
PeerTubeServer,
|
PeerTubeServer,
|
||||||
cleanupTests, createMultipleServers,
|
cleanupTests,
|
||||||
|
createMultipleServers,
|
||||||
doubleFollow,
|
doubleFollow,
|
||||||
makeActivityPubGetRequest, makeActivityPubRawRequest, setAccessTokensToServers,
|
makeActivityPubGetRequest,
|
||||||
|
makeActivityPubRawRequest,
|
||||||
|
setAccessTokensToServers,
|
||||||
setDefaultAccountAvatar,
|
setDefaultAccountAvatar,
|
||||||
waitJobs
|
waitJobs
|
||||||
} from '@peertube/peertube-server-commands'
|
} from '@peertube/peertube-server-commands'
|
||||||
|
@ -546,6 +549,68 @@ describe('Test comments approval', function () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Comment count with moderation', function () {
|
||||||
|
let videoId: string
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
videoId = await createVideo(VideoCommentPolicy.REQUIRES_APPROVAL)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should not increment comment count when comment is held for review', async function () {
|
||||||
|
await servers[0].comments.createThread({ token: anotherUserToken, videoId, text: 'held comment' })
|
||||||
|
await waitJobs(servers)
|
||||||
|
|
||||||
|
const video = await servers[0].videos.get({ id: videoId })
|
||||||
|
expect(video.comments).to.equal(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should increment comment count after approving comment', async function () {
|
||||||
|
// Create a new comment that will be held for review
|
||||||
|
await servers[0].comments.createThread({ token: anotherUserToken, videoId, text: 'test comment' })
|
||||||
|
await waitJobs(servers)
|
||||||
|
|
||||||
|
// Get the held comment
|
||||||
|
const { data } = await servers[0].comments.listCommentsOnMyVideos({ token: userToken })
|
||||||
|
const heldComment = data.find(c => c.text === 'test comment')
|
||||||
|
expect(heldComment.heldForReview).to.be.true
|
||||||
|
|
||||||
|
// Verify initial count is 0
|
||||||
|
let video = await servers[0].videos.get({ id: videoId })
|
||||||
|
expect(video.comments).to.equal(0)
|
||||||
|
|
||||||
|
// Approve the comment
|
||||||
|
await servers[0].comments.approve({ token: userToken, videoId, commentId: heldComment.id })
|
||||||
|
await waitJobs(servers)
|
||||||
|
|
||||||
|
// Verify count incremented after approval
|
||||||
|
video = await servers[0].videos.get({ id: videoId })
|
||||||
|
expect(video.comments).to.equal(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should not increment comment count when deleting held comment', async function () {
|
||||||
|
// Get initial comment count
|
||||||
|
let video = await servers[0].videos.get({ id: videoId })
|
||||||
|
const initialCount = video.comments
|
||||||
|
|
||||||
|
// Create a new comment that will be held for review
|
||||||
|
await servers[0].comments.createThread({ token: anotherUserToken, videoId, text: 'to be deleted' })
|
||||||
|
await waitJobs(servers)
|
||||||
|
|
||||||
|
// Get the held comment
|
||||||
|
const { data } = await servers[0].comments.listCommentsOnMyVideos({ token: userToken })
|
||||||
|
const heldComment = data.find(c => c.text === 'to be deleted')
|
||||||
|
expect(heldComment.heldForReview).to.be.true
|
||||||
|
|
||||||
|
// Delete the held comment
|
||||||
|
await servers[0].comments.delete({ token: userToken, videoId, commentId: heldComment.id })
|
||||||
|
await waitJobs(servers)
|
||||||
|
|
||||||
|
// Verify count remains unchanged after deleting held comment
|
||||||
|
video = await servers[0].videos.get({ id: videoId })
|
||||||
|
expect(video.comments).to.equal(initialCount)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
after(async function () {
|
after(async function () {
|
||||||
await cleanupTests(servers)
|
await cleanupTests(servers)
|
||||||
})
|
})
|
||||||
|
|
|
@ -45,7 +45,6 @@ describe('Test video comments', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('User comments', function () {
|
describe('User comments', function () {
|
||||||
|
|
||||||
it('Should not have threads on this video', async function () {
|
it('Should not have threads on this video', async function () {
|
||||||
const body = await command.listThreads({ videoId: videoUUID })
|
const body = await command.listThreads({ videoId: videoUUID })
|
||||||
|
|
||||||
|
@ -256,11 +255,10 @@ describe('Test video comments', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Listing comments on my videos and in admin', function () {
|
describe('Listing comments on my videos and in admin', function () {
|
||||||
|
const listFunctions = () => [
|
||||||
const listFunctions = () => ([
|
|
||||||
command.listForAdmin.bind(command),
|
command.listForAdmin.bind(command),
|
||||||
command.listCommentsOnMyVideos.bind(command)
|
command.listCommentsOnMyVideos.bind(command)
|
||||||
])
|
]
|
||||||
|
|
||||||
it('Should list comments', async function () {
|
it('Should list comments', async function () {
|
||||||
for (const fn of listFunctions()) {
|
for (const fn of listFunctions()) {
|
||||||
|
@ -401,6 +399,38 @@ describe('Test video comments', function () {
|
||||||
// Auto tags filter is checked auto tags test file
|
// Auto tags filter is checked auto tags test file
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Video comment count', function () {
|
||||||
|
let testVideoUUID: string
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
const { uuid } = await server.videos.upload()
|
||||||
|
testVideoUUID = uuid
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should start with 0 comments', async function () {
|
||||||
|
const video = await server.videos.get({ id: testVideoUUID })
|
||||||
|
expect(video.commentsEnabled).to.be.true
|
||||||
|
expect(video.comments).to.equal(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should increment comment count when adding comment', async function () {
|
||||||
|
await command.createThread({ videoId: testVideoUUID, text: 'test comment' })
|
||||||
|
|
||||||
|
const video = await server.videos.get({ id: testVideoUUID })
|
||||||
|
expect(video.comments).to.equal(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should decrement count when deleting comment', async function () {
|
||||||
|
const { data } = await command.listThreads({ videoId: testVideoUUID })
|
||||||
|
const commentToDelete = data[0]
|
||||||
|
|
||||||
|
await command.delete({ videoId: testVideoUUID, commentId: commentToDelete.id })
|
||||||
|
|
||||||
|
const video = await server.videos.get({ id: testVideoUUID })
|
||||||
|
expect(video.comments).to.equal(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
after(async function () {
|
after(async function () {
|
||||||
await cleanupTests([ server ])
|
await cleanupTests([ server ])
|
||||||
})
|
})
|
||||||
|
|
|
@ -46,7 +46,7 @@ import { CONFIG, registerConfigChangedHandler } from './config.js'
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export const LAST_MIGRATION_VERSION = 875
|
export const LAST_MIGRATION_VERSION = 880
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@ -119,6 +119,7 @@ export const SORTABLE_COLUMNS = {
|
||||||
'originallyPublishedAt',
|
'originallyPublishedAt',
|
||||||
'views',
|
'views',
|
||||||
'likes',
|
'likes',
|
||||||
|
'comments',
|
||||||
'trending',
|
'trending',
|
||||||
'hot',
|
'hot',
|
||||||
'best',
|
'best',
|
||||||
|
@ -1286,7 +1287,9 @@ export async function buildLanguages () {
|
||||||
return (l.iso6391 !== undefined && l.type === 'living') ||
|
return (l.iso6391 !== undefined && l.type === 'living') ||
|
||||||
additionalLanguages[l.iso6393] === true
|
additionalLanguages[l.iso6393] === true
|
||||||
})
|
})
|
||||||
.forEach(l => { languages[l.iso6391 || l.iso6393] = l.name })
|
.forEach(l => {
|
||||||
|
languages[l.iso6391 || l.iso6393] = l.name
|
||||||
|
})
|
||||||
|
|
||||||
// Override Occitan label
|
// Override Occitan label
|
||||||
languages['oc'] = 'Occitan'
|
languages['oc'] = 'Occitan'
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
import * as Sequelize from 'sequelize'
|
||||||
|
|
||||||
|
async function up (utils: {
|
||||||
|
transaction: Sequelize.Transaction
|
||||||
|
queryInterface: Sequelize.QueryInterface
|
||||||
|
sequelize: Sequelize.Sequelize
|
||||||
|
}): Promise<void> {
|
||||||
|
const { transaction } = utils
|
||||||
|
|
||||||
|
{
|
||||||
|
await utils.queryInterface.addColumn('video', 'comments', {
|
||||||
|
type: Sequelize.INTEGER,
|
||||||
|
defaultValue: 0,
|
||||||
|
allowNull: false
|
||||||
|
}, { transaction })
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const query = 'UPDATE "video" SET "comments" = (SELECT COUNT(*) FROM "videoComment" WHERE "videoComment"."videoId" = "video"."id")'
|
||||||
|
await utils.sequelize.query(query, { transaction })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function down (options) {
|
||||||
|
throw new Error('Not implemented.')
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
down,
|
||||||
|
up
|
||||||
|
}
|
|
@ -127,6 +127,8 @@ export function videoModelToFormattedJSON (video: MVideoFormattable, options: Vi
|
||||||
? { currentTime: userHistory.currentTime }
|
? { currentTime: userHistory.currentTime }
|
||||||
: undefined,
|
: undefined,
|
||||||
|
|
||||||
|
comments: video.comments,
|
||||||
|
|
||||||
// Can be added by external plugins
|
// Can be added by external plugins
|
||||||
pluginData: (video as any).pluginData,
|
pluginData: (video as any).pluginData,
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,8 @@
|
||||||
/**
|
/**
|
||||||
*
|
|
||||||
* Class to build video attributes/join names we want to fetch from the database
|
* Class to build video attributes/join names we want to fetch from the database
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
export class VideoTableAttributes {
|
export class VideoTableAttributes {
|
||||||
|
|
||||||
constructor (private readonly mode: 'get' | 'list') {
|
constructor (private readonly mode: 'get' | 'list') {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getChannelAttributesForUser () {
|
getChannelAttributesForUser () {
|
||||||
|
@ -295,7 +291,8 @@ export class VideoTableAttributes {
|
||||||
'channelId',
|
'channelId',
|
||||||
'createdAt',
|
'createdAt',
|
||||||
'updatedAt',
|
'updatedAt',
|
||||||
'moveJobsRunning'
|
'moveJobsRunning',
|
||||||
|
'comments'
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,19 +7,27 @@ import {
|
||||||
VideoCommentForAdminOrUser,
|
VideoCommentForAdminOrUser,
|
||||||
VideoCommentObject
|
VideoCommentObject
|
||||||
} from '@peertube/peertube-models'
|
} from '@peertube/peertube-models'
|
||||||
|
import { afterCommitIfTransaction, retryTransactionWrapper } from '@server/helpers/database-utils.js'
|
||||||
|
import { logger } from '@server/helpers/logger.js'
|
||||||
import { extractMentions } from '@server/helpers/mentions.js'
|
import { extractMentions } from '@server/helpers/mentions.js'
|
||||||
|
import { sequelizeTypescript } from '@server/initializers/database.js'
|
||||||
import { getLocalApproveReplyActivityPubUrl } from '@server/lib/activitypub/url.js'
|
import { getLocalApproveReplyActivityPubUrl } from '@server/lib/activitypub/url.js'
|
||||||
import { getServerActor } from '@server/models/application/application.js'
|
import { getServerActor } from '@server/models/application/application.js'
|
||||||
import { MAccount, MAccountId, MUserAccountId } from '@server/types/models/index.js'
|
import { MAccount, MAccountId, MUserAccountId } from '@server/types/models/index.js'
|
||||||
import { Op, Order, QueryTypes, Sequelize, Transaction } from 'sequelize'
|
import { Op, Order, QueryTypes, Sequelize, Transaction } from 'sequelize'
|
||||||
import {
|
import {
|
||||||
|
AfterCreate,
|
||||||
|
AfterDestroy,
|
||||||
|
AfterUpdate,
|
||||||
AllowNull,
|
AllowNull,
|
||||||
BelongsTo, Column,
|
BelongsTo,
|
||||||
|
Column,
|
||||||
CreatedAt,
|
CreatedAt,
|
||||||
DataType,
|
DataType,
|
||||||
ForeignKey,
|
ForeignKey,
|
||||||
HasMany,
|
HasMany,
|
||||||
Is, Scopes,
|
Is,
|
||||||
|
Scopes,
|
||||||
Table,
|
Table,
|
||||||
UpdatedAt
|
UpdatedAt
|
||||||
} from 'sequelize-typescript'
|
} from 'sequelize-typescript'
|
||||||
|
@ -27,13 +35,14 @@ import { isActivityPubUrlValid } from '../../helpers/custom-validators/activityp
|
||||||
import { CONSTRAINTS_FIELDS, USER_EXPORT_MAX_ITEMS } from '../../initializers/constants.js'
|
import { CONSTRAINTS_FIELDS, USER_EXPORT_MAX_ITEMS } from '../../initializers/constants.js'
|
||||||
import {
|
import {
|
||||||
MComment,
|
MComment,
|
||||||
MCommentAP,
|
|
||||||
MCommentAdminOrUserFormattable,
|
MCommentAdminOrUserFormattable,
|
||||||
|
MCommentAP,
|
||||||
MCommentExport,
|
MCommentExport,
|
||||||
MCommentFormattable,
|
MCommentFormattable,
|
||||||
MCommentId,
|
MCommentId,
|
||||||
MCommentOwner,
|
MCommentOwner,
|
||||||
MCommentOwnerReplyVideoImmutable, MCommentOwnerVideoFeed,
|
MCommentOwnerReplyVideoImmutable,
|
||||||
|
MCommentOwnerVideoFeed,
|
||||||
MCommentOwnerVideoReply,
|
MCommentOwnerVideoReply,
|
||||||
MVideo,
|
MVideo,
|
||||||
MVideoImmutable
|
MVideoImmutable
|
||||||
|
@ -42,7 +51,7 @@ import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse.js'
|
||||||
import { AccountModel } from '../account/account.js'
|
import { AccountModel } from '../account/account.js'
|
||||||
import { ActorModel } from '../actor/actor.js'
|
import { ActorModel } from '../actor/actor.js'
|
||||||
import { CommentAutomaticTagModel } from '../automatic-tag/comment-automatic-tag.js'
|
import { CommentAutomaticTagModel } from '../automatic-tag/comment-automatic-tag.js'
|
||||||
import { SequelizeModel, buildLocalAccountIdsIn, buildSQLAttributes, throwIfNotValid } from '../shared/index.js'
|
import { buildLocalAccountIdsIn, buildSQLAttributes, SequelizeModel, throwIfNotValid } from '../shared/index.js'
|
||||||
import { ListVideoCommentsOptions, VideoCommentListQueryBuilder } from './sql/comment/video-comment-list-query-builder.js'
|
import { ListVideoCommentsOptions, VideoCommentListQueryBuilder } from './sql/comment/video-comment-list-query-builder.js'
|
||||||
import { VideoChannelModel } from './video-channel.js'
|
import { VideoChannelModel } from './video-channel.js'
|
||||||
import { VideoModel } from './video.js'
|
import { VideoModel } from './video.js'
|
||||||
|
@ -222,6 +231,43 @@ export class VideoCommentModel extends SequelizeModel<VideoCommentModel> {
|
||||||
})
|
})
|
||||||
CommentAutomaticTags: Awaited<CommentAutomaticTagModel>[]
|
CommentAutomaticTags: Awaited<CommentAutomaticTagModel>[]
|
||||||
|
|
||||||
|
@AfterCreate
|
||||||
|
@AfterDestroy
|
||||||
|
static incrementCommentCount (instance: VideoCommentModel, options: any) {
|
||||||
|
if (instance.heldForReview) return // Don't count held comments
|
||||||
|
|
||||||
|
return afterCommitIfTransaction(options.transaction, () => this.rebuildCommentsCount(instance.videoId))
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterUpdate
|
||||||
|
static updateCommentCountOnHeldStatusChange (instance: VideoCommentModel, options: any) {
|
||||||
|
return afterCommitIfTransaction(options.transaction, () => this.rebuildCommentsCount(instance.videoId))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
static async rebuildCommentsCount (videoId: number) {
|
||||||
|
try {
|
||||||
|
await retryTransactionWrapper(() => {
|
||||||
|
return sequelizeTypescript.transaction(async transaction => {
|
||||||
|
const video = await VideoModel.load(videoId, transaction)
|
||||||
|
|
||||||
|
video.comments = await new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, {
|
||||||
|
selectType: 'comment-only',
|
||||||
|
videoId: video.id,
|
||||||
|
heldForReview: false,
|
||||||
|
notDeleted: true,
|
||||||
|
transaction
|
||||||
|
}).countComments()
|
||||||
|
|
||||||
|
await video.save({ transaction })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('Cannot rebuild comments count for video ' + videoId, { err })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
static getSQLAttributes (tableName: string, aliasPrefix = '') {
|
static getSQLAttributes (tableName: string, aliasPrefix = '') {
|
||||||
|
@ -442,7 +488,8 @@ export class VideoCommentModel extends SequelizeModel<VideoCommentModel> {
|
||||||
order: [ [ 'createdAt', order ] ] as Order,
|
order: [ [ 'createdAt', order ] ] as Order,
|
||||||
where: {
|
where: {
|
||||||
id: {
|
id: {
|
||||||
[Op.in]: Sequelize.literal('(' +
|
[Op.in]: Sequelize.literal(
|
||||||
|
'(' +
|
||||||
'WITH RECURSIVE children (id, "inReplyToCommentId") AS ( ' +
|
'WITH RECURSIVE children (id, "inReplyToCommentId") AS ( ' +
|
||||||
`SELECT id, "inReplyToCommentId" FROM "videoComment" WHERE id = ${comment.id} ` +
|
`SELECT id, "inReplyToCommentId" FROM "videoComment" WHERE id = ${comment.id} ` +
|
||||||
'UNION ' +
|
'UNION ' +
|
||||||
|
@ -450,7 +497,8 @@ export class VideoCommentModel extends SequelizeModel<VideoCommentModel> {
|
||||||
'INNER JOIN "children" ON "children"."inReplyToCommentId" = "parent"."id"' +
|
'INNER JOIN "children" ON "children"."inReplyToCommentId" = "parent"."id"' +
|
||||||
') ' +
|
') ' +
|
||||||
'SELECT id FROM children' +
|
'SELECT id FROM children' +
|
||||||
')'),
|
')'
|
||||||
|
),
|
||||||
[Op.ne]: comment.id
|
[Op.ne]: comment.id
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -507,6 +507,13 @@ export class VideoModel extends SequelizeModel<VideoModel> {
|
||||||
@Column
|
@Column
|
||||||
dislikes: number
|
dislikes: number
|
||||||
|
|
||||||
|
@AllowNull(false)
|
||||||
|
@Default(0)
|
||||||
|
@IsInt
|
||||||
|
@Min(0)
|
||||||
|
@Column
|
||||||
|
comments: number
|
||||||
|
|
||||||
@AllowNull(false)
|
@AllowNull(false)
|
||||||
@Column
|
@Column
|
||||||
remote: boolean
|
remote: boolean
|
||||||
|
|
|
@ -7186,6 +7186,7 @@ components:
|
||||||
- -publishedAt
|
- -publishedAt
|
||||||
- -views
|
- -views
|
||||||
- -likes
|
- -likes
|
||||||
|
- -comments
|
||||||
- -trending
|
- -trending
|
||||||
- -hot
|
- -hot
|
||||||
- -best
|
- -best
|
||||||
|
@ -8445,6 +8446,9 @@ components:
|
||||||
dislikes:
|
dislikes:
|
||||||
type: integer
|
type: integer
|
||||||
example: 7
|
example: 7
|
||||||
|
comments:
|
||||||
|
description: "**PeerTube >= 7.2** Number of comments on the video"
|
||||||
|
type: integer
|
||||||
nsfw:
|
nsfw:
|
||||||
type: boolean
|
type: boolean
|
||||||
waitTranscoding:
|
waitTranscoding:
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue