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>
|
||||
</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('name')" [ngbTooltip]="sortTooltip" container="body" pSortableColumn="name" i18n>{{ 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('sensitive')" i18n>{{ getColumn('sensitive').label }}</th>
|
||||
<th scope="col" *ngIf="isSelected('playlists')" i18n>{{ 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('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">{{ getColumn('name').label }} <p-sortIcon field="name"></p-sortIcon></th>
|
||||
<th scope="col" *ngIf="isSelected('privacy')">{{ getColumn('privacy').label }}</th>
|
||||
<th scope="col" *ngIf="isSelected('sensitive')">{{ getColumn('sensitive').label }}</th>
|
||||
<th scope="col" *ngIf="isSelected('playlists')">{{ getColumn('playlists').label }}</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('state')" i18n>{{ getColumn('state').label }}</th>
|
||||
<th scope="col" *ngIf="isSelected('state')">{{ getColumn('state').label }}</th>
|
||||
|
||||
<th scope="col" width="250px" class="action-head">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
|
@ -162,6 +163,10 @@
|
|||
</a>
|
||||
</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')">
|
||||
{{ video.publishedAt | ptDate: 'short' }}
|
||||
</td>
|
||||
|
|
|
@ -52,7 +52,7 @@ import { VideoPrivacyBadgeComponent } from '../../shared/shared-video/video-priv
|
|||
import { VideoStateBadgeComponent } from '../../shared/shared-video/video-state-badge.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'
|
||||
|
||||
@Component({
|
||||
|
@ -157,6 +157,7 @@ export class MyVideosComponent extends RestTable<Video> implements OnInit, OnDes
|
|||
{ id: 'sensitive', label: $localize`Sensitive`, selected: true },
|
||||
{ id: 'playlists', label: $localize`Playlists`, selected: true },
|
||||
{ id: 'insights', label: $localize`Insights`, selected: true },
|
||||
{ id: 'comments', label: $localize`Comments`, selected: false },
|
||||
{ id: 'published', label: $localize`Published`, selected: true },
|
||||
{ id: 'state', label: $localize`State`, selected: true }
|
||||
]
|
||||
|
|
|
@ -116,6 +116,8 @@ export class Video implements VideoServerModel {
|
|||
|
||||
automaticTags?: string[]
|
||||
|
||||
comments: number
|
||||
|
||||
static buildWatchUrl (video: Partial<Pick<Video, 'uuid' | 'shortUUID'>>) {
|
||||
return buildVideoWatchPath({ shortUUID: video.shortUUID || video.uuid })
|
||||
}
|
||||
|
@ -211,6 +213,8 @@ export class Video implements VideoServerModel {
|
|||
this.aspectRatio = hash.aspectRatio
|
||||
|
||||
this.automaticTags = hash.automaticTags
|
||||
|
||||
this.comments = hash.comments
|
||||
}
|
||||
|
||||
isVideoNSFWForUser (user: User, serverConfig: HTMLServerConfig) {
|
||||
|
|
|
@ -6,9 +6,9 @@ import {
|
|||
LOCALE_ID,
|
||||
OnInit,
|
||||
booleanAttribute,
|
||||
numberAttribute,
|
||||
inject,
|
||||
input,
|
||||
numberAttribute,
|
||||
output
|
||||
} from '@angular/core'
|
||||
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 { VideoPlaylistService } from '../shared-video-playlist/video-playlist.service'
|
||||
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 { VideoActionsDisplayType, VideoActionsDropdownComponent } from './video-actions-dropdown.component'
|
||||
|
||||
export type MiniatureDisplayOptions = {
|
||||
date?: boolean
|
||||
|
|
|
@ -51,6 +51,8 @@ export interface Video extends Partial<VideoAdditionalAttributes> {
|
|||
|
||||
likes: number
|
||||
dislikes: number
|
||||
comments: number
|
||||
|
||||
nsfw: boolean
|
||||
|
||||
account: AccountSummary
|
||||
|
|
|
@ -12,9 +12,12 @@ import {
|
|||
} from '@peertube/peertube-models'
|
||||
import {
|
||||
PeerTubeServer,
|
||||
cleanupTests, createMultipleServers,
|
||||
cleanupTests,
|
||||
createMultipleServers,
|
||||
doubleFollow,
|
||||
makeActivityPubGetRequest, makeActivityPubRawRequest, setAccessTokensToServers,
|
||||
makeActivityPubGetRequest,
|
||||
makeActivityPubRawRequest,
|
||||
setAccessTokensToServers,
|
||||
setDefaultAccountAvatar,
|
||||
waitJobs
|
||||
} 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 () {
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
|
|
|
@ -45,7 +45,6 @@ describe('Test video comments', function () {
|
|||
})
|
||||
|
||||
describe('User comments', function () {
|
||||
|
||||
it('Should not have threads on this video', async function () {
|
||||
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 () {
|
||||
|
||||
const listFunctions = () => ([
|
||||
const listFunctions = () => [
|
||||
command.listForAdmin.bind(command),
|
||||
command.listCommentsOnMyVideos.bind(command)
|
||||
])
|
||||
]
|
||||
|
||||
it('Should list comments', async function () {
|
||||
for (const fn of listFunctions()) {
|
||||
|
@ -401,6 +399,38 @@ describe('Test video comments', function () {
|
|||
// 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 () {
|
||||
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',
|
||||
'views',
|
||||
'likes',
|
||||
'comments',
|
||||
'trending',
|
||||
'hot',
|
||||
'best',
|
||||
|
@ -172,7 +173,7 @@ export const ACTOR_FOLLOW_SCORE = {
|
|||
MAX: 10000
|
||||
}
|
||||
|
||||
export const FOLLOW_STATES: { [ id: string ]: FollowState } = {
|
||||
export const FOLLOW_STATES: { [id: string]: FollowState } = {
|
||||
PENDING: 'pending',
|
||||
ACCEPTED: 'accepted',
|
||||
REJECTED: 'rejected'
|
||||
|
@ -274,7 +275,7 @@ export const JOB_TTL: { [id in JobType]: number } = {
|
|||
'import-user-archive': 60000 * 60 * 24, // 24 hours
|
||||
'video-transcription': 1000 * 3600 * 6 // 6 hours
|
||||
}
|
||||
export const REPEAT_JOBS: { [ id in JobType ]?: RepeatOptions } = {
|
||||
export const REPEAT_JOBS: { [id in JobType]?: RepeatOptions } = {
|
||||
'videos-views-stats': {
|
||||
pattern: randomInt(1, 20) + ' * * * *' // Between 1-20 minutes past the hour
|
||||
},
|
||||
|
@ -528,7 +529,7 @@ export let CONTACT_FORM_LIFETIME = 60000 * 60 // 1 hour
|
|||
export const DEFAULT_AUDIO_RESOLUTION = VideoResolution.H_480P
|
||||
export const DEFAULT_AUDIO_MERGE_RESOLUTION = 25
|
||||
|
||||
export const VIDEO_RATE_TYPES: { [ id: string ]: VideoRateType } = {
|
||||
export const VIDEO_RATE_TYPES: { [id: string]: VideoRateType } = {
|
||||
LIKE: 'like',
|
||||
DISLIKE: 'dislike'
|
||||
}
|
||||
|
@ -579,7 +580,7 @@ export const VIDEO_LICENCES = {
|
|||
|
||||
export const VIDEO_LANGUAGES: { [id: string]: string } = {}
|
||||
|
||||
export const VIDEO_PRIVACIES: { [ id in VideoPrivacyType ]: string } = {
|
||||
export const VIDEO_PRIVACIES: { [id in VideoPrivacyType]: string } = {
|
||||
[VideoPrivacy.PUBLIC]: 'Public',
|
||||
[VideoPrivacy.UNLISTED]: 'Unlisted',
|
||||
[VideoPrivacy.PRIVATE]: 'Private',
|
||||
|
@ -587,7 +588,7 @@ export const VIDEO_PRIVACIES: { [ id in VideoPrivacyType ]: string } = {
|
|||
[VideoPrivacy.PASSWORD_PROTECTED]: 'Password protected'
|
||||
}
|
||||
|
||||
export const VIDEO_STATES: { [ id in VideoStateType ]: string } = {
|
||||
export const VIDEO_STATES: { [id in VideoStateType]: string } = {
|
||||
[VideoState.PUBLISHED]: 'Published',
|
||||
[VideoState.TO_TRANSCODE]: 'To transcode',
|
||||
[VideoState.TO_IMPORT]: 'To import',
|
||||
|
@ -601,7 +602,7 @@ export const VIDEO_STATES: { [ id in VideoStateType ]: string } = {
|
|||
[VideoState.TO_MOVE_TO_FILE_SYSTEM_FAILED]: 'Move to file system failed'
|
||||
}
|
||||
|
||||
export const VIDEO_IMPORT_STATES: { [ id in VideoImportStateType ]: string } = {
|
||||
export const VIDEO_IMPORT_STATES: { [id in VideoImportStateType]: string } = {
|
||||
[VideoImportState.FAILED]: 'Failed',
|
||||
[VideoImportState.PENDING]: 'Pending',
|
||||
[VideoImportState.SUCCESS]: 'Success',
|
||||
|
@ -610,37 +611,37 @@ export const VIDEO_IMPORT_STATES: { [ id in VideoImportStateType ]: string } = {
|
|||
[VideoImportState.PROCESSING]: 'Processing'
|
||||
}
|
||||
|
||||
export const VIDEO_CHANNEL_SYNC_STATE: { [ id in VideoChannelSyncStateType ]: string } = {
|
||||
export const VIDEO_CHANNEL_SYNC_STATE: { [id in VideoChannelSyncStateType]: string } = {
|
||||
[VideoChannelSyncState.FAILED]: 'Failed',
|
||||
[VideoChannelSyncState.SYNCED]: 'Synchronized',
|
||||
[VideoChannelSyncState.PROCESSING]: 'Processing',
|
||||
[VideoChannelSyncState.WAITING_FIRST_RUN]: 'Waiting first run'
|
||||
}
|
||||
|
||||
export const ABUSE_STATES: { [ id in AbuseStateType ]: string } = {
|
||||
export const ABUSE_STATES: { [id in AbuseStateType]: string } = {
|
||||
[AbuseState.PENDING]: 'Pending',
|
||||
[AbuseState.REJECTED]: 'Rejected',
|
||||
[AbuseState.ACCEPTED]: 'Accepted'
|
||||
}
|
||||
|
||||
export const USER_REGISTRATION_STATES: { [ id in UserRegistrationStateType ]: string } = {
|
||||
export const USER_REGISTRATION_STATES: { [id in UserRegistrationStateType]: string } = {
|
||||
[UserRegistrationState.PENDING]: 'Pending',
|
||||
[UserRegistrationState.REJECTED]: 'Rejected',
|
||||
[UserRegistrationState.ACCEPTED]: 'Accepted'
|
||||
}
|
||||
|
||||
export const VIDEO_PLAYLIST_PRIVACIES: { [ id in VideoPlaylistPrivacyType ]: string } = {
|
||||
export const VIDEO_PLAYLIST_PRIVACIES: { [id in VideoPlaylistPrivacyType]: string } = {
|
||||
[VideoPlaylistPrivacy.PUBLIC]: 'Public',
|
||||
[VideoPlaylistPrivacy.UNLISTED]: 'Unlisted',
|
||||
[VideoPlaylistPrivacy.PRIVATE]: 'Private'
|
||||
}
|
||||
|
||||
export const VIDEO_PLAYLIST_TYPES: { [ id in VideoPlaylistType_Type ]: string } = {
|
||||
export const VIDEO_PLAYLIST_TYPES: { [id in VideoPlaylistType_Type]: string } = {
|
||||
[VideoPlaylistType.REGULAR]: 'Regular',
|
||||
[VideoPlaylistType.WATCH_LATER]: 'Watch later'
|
||||
}
|
||||
|
||||
export const RUNNER_JOB_STATES: { [ id in RunnerJobStateType ]: string } = {
|
||||
export const RUNNER_JOB_STATES: { [id in RunnerJobStateType]: string } = {
|
||||
[RunnerJobState.PROCESSING]: 'Processing',
|
||||
[RunnerJobState.COMPLETED]: 'Completed',
|
||||
[RunnerJobState.COMPLETING]: 'Completing',
|
||||
|
@ -652,21 +653,21 @@ export const RUNNER_JOB_STATES: { [ id in RunnerJobStateType ]: string } = {
|
|||
[RunnerJobState.PARENT_CANCELLED]: 'Parent job cancelled'
|
||||
}
|
||||
|
||||
export const USER_EXPORT_STATES: { [ id in UserExportStateType ]: string } = {
|
||||
export const USER_EXPORT_STATES: { [id in UserExportStateType]: string } = {
|
||||
[UserExportState.PENDING]: 'Pending',
|
||||
[UserExportState.PROCESSING]: 'Processing',
|
||||
[UserExportState.COMPLETED]: 'Completed',
|
||||
[UserExportState.ERRORED]: 'Failed'
|
||||
}
|
||||
|
||||
export const USER_IMPORT_STATES: { [ id in UserImportStateType ]: string } = {
|
||||
export const USER_IMPORT_STATES: { [id in UserImportStateType]: string } = {
|
||||
[UserImportState.PENDING]: 'Pending',
|
||||
[UserImportState.PROCESSING]: 'Processing',
|
||||
[UserImportState.COMPLETED]: 'Completed',
|
||||
[UserImportState.ERRORED]: 'Failed'
|
||||
}
|
||||
|
||||
export const VIDEO_COMMENTS_POLICY: { [ id in VideoCommentPolicyType ]: string } = {
|
||||
export const VIDEO_COMMENTS_POLICY: { [id in VideoCommentPolicyType]: string } = {
|
||||
[VideoCommentPolicy.DISABLED]: 'Disabled',
|
||||
[VideoCommentPolicy.ENABLED]: 'Enabled',
|
||||
[VideoCommentPolicy.REQUIRES_APPROVAL]: 'Requires approval'
|
||||
|
@ -699,12 +700,12 @@ export const MIMETYPES = {
|
|||
'audio/vnd.dolby.dd-raw': '.ac3',
|
||||
'audio/ac3': '.ac3'
|
||||
},
|
||||
EXT_MIMETYPE: null as { [ id: string ]: string }
|
||||
EXT_MIMETYPE: null as { [id: string]: string }
|
||||
},
|
||||
VIDEO: {
|
||||
MIMETYPE_EXT: null as { [ id: string ]: string | string[] },
|
||||
MIMETYPE_EXT: null as { [id: string]: string | string[] },
|
||||
MIMETYPES_REGEX: null as string,
|
||||
EXT_MIMETYPE: null as { [ id: string ]: string }
|
||||
EXT_MIMETYPE: null as { [id: string]: string }
|
||||
},
|
||||
IMAGE: {
|
||||
MIMETYPE_EXT: {
|
||||
|
@ -714,7 +715,7 @@ export const MIMETYPES = {
|
|||
'image/jpg': '.jpg',
|
||||
'image/jpeg': '.jpg'
|
||||
},
|
||||
EXT_MIMETYPE: null as { [ id: string ]: string }
|
||||
EXT_MIMETYPE: null as { [id: string]: string }
|
||||
},
|
||||
VIDEO_CAPTIONS: {
|
||||
MIMETYPE_EXT: {
|
||||
|
@ -722,7 +723,7 @@ export const MIMETYPES = {
|
|||
'application/x-subrip': '.srt',
|
||||
'text/plain': '.srt'
|
||||
},
|
||||
EXT_MIMETYPE: null as { [ id: string ]: string }
|
||||
EXT_MIMETYPE: null as { [id: string]: string }
|
||||
},
|
||||
TORRENT: {
|
||||
MIMETYPE_EXT: {
|
||||
|
@ -792,7 +793,7 @@ export const ACTIVITY_PUB = {
|
|||
VIDEO_PLAYLIST_REFRESH_INTERVAL: 3600 * 24 * 1000 * 2 // 2 days
|
||||
}
|
||||
|
||||
export const ACTIVITY_PUB_ACTOR_TYPES: { [ id: string ]: ActivityPubActorType } = {
|
||||
export const ACTIVITY_PUB_ACTOR_TYPES: { [id: string]: ActivityPubActorType } = {
|
||||
GROUP: 'Group',
|
||||
PERSON: 'Person',
|
||||
APPLICATION: 'Application',
|
||||
|
@ -830,7 +831,7 @@ export let JWT_TOKEN_USER_EXPORT_FILE_LIFETIME: `${number} minutes` | `${number}
|
|||
|
||||
export const EMAIL_VERIFY_LIFETIME = 60000 * 60 // 60 minutes
|
||||
|
||||
export const NSFW_POLICY_TYPES: { [ id: string ]: NSFWPolicyType } = {
|
||||
export const NSFW_POLICY_TYPES: { [id: string]: NSFWPolicyType } = {
|
||||
DO_NOT_LIST: 'do_not_list',
|
||||
BLUR: 'blur',
|
||||
DISPLAY: 'display'
|
||||
|
@ -1286,7 +1287,9 @@ export async function buildLanguages () {
|
|||
return (l.iso6391 !== undefined && l.type === 'living') ||
|
||||
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
|
||||
languages['oc'] = 'Occitan'
|
||||
|
@ -1411,7 +1414,7 @@ function updateWebserverConfig () {
|
|||
CONSTRAINTS_FIELDS.VIDEOS.EXTNAME = Object.keys(MIMETYPES.VIDEO.EXT_MIMETYPE)
|
||||
}
|
||||
|
||||
function buildVideoExtMimetype (obj: { [ id: string ]: string | string[] }) {
|
||||
function buildVideoExtMimetype (obj: { [id: string]: string | string[] }) {
|
||||
const result: { [id: string]: string } = {}
|
||||
|
||||
for (const mimetype of Object.keys(obj)) {
|
||||
|
|
|
@ -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 }
|
||||
: undefined,
|
||||
|
||||
comments: video.comments,
|
||||
|
||||
// Can be added by external plugins
|
||||
pluginData: (video as any).pluginData,
|
||||
|
||||
|
|
|
@ -1,12 +1,8 @@
|
|||
/**
|
||||
*
|
||||
* Class to build video attributes/join names we want to fetch from the database
|
||||
*
|
||||
*/
|
||||
export class VideoTableAttributes {
|
||||
|
||||
constructor (private readonly mode: 'get' | 'list') {
|
||||
|
||||
}
|
||||
|
||||
getChannelAttributesForUser () {
|
||||
|
@ -295,7 +291,8 @@ export class VideoTableAttributes {
|
|||
'channelId',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'moveJobsRunning'
|
||||
'moveJobsRunning',
|
||||
'comments'
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,19 +7,27 @@ import {
|
|||
VideoCommentForAdminOrUser,
|
||||
VideoCommentObject
|
||||
} 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 { sequelizeTypescript } from '@server/initializers/database.js'
|
||||
import { getLocalApproveReplyActivityPubUrl } from '@server/lib/activitypub/url.js'
|
||||
import { getServerActor } from '@server/models/application/application.js'
|
||||
import { MAccount, MAccountId, MUserAccountId } from '@server/types/models/index.js'
|
||||
import { Op, Order, QueryTypes, Sequelize, Transaction } from 'sequelize'
|
||||
import {
|
||||
AfterCreate,
|
||||
AfterDestroy,
|
||||
AfterUpdate,
|
||||
AllowNull,
|
||||
BelongsTo, Column,
|
||||
BelongsTo,
|
||||
Column,
|
||||
CreatedAt,
|
||||
DataType,
|
||||
ForeignKey,
|
||||
HasMany,
|
||||
Is, Scopes,
|
||||
Is,
|
||||
Scopes,
|
||||
Table,
|
||||
UpdatedAt
|
||||
} 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 {
|
||||
MComment,
|
||||
MCommentAP,
|
||||
MCommentAdminOrUserFormattable,
|
||||
MCommentAP,
|
||||
MCommentExport,
|
||||
MCommentFormattable,
|
||||
MCommentId,
|
||||
MCommentOwner,
|
||||
MCommentOwnerReplyVideoImmutable, MCommentOwnerVideoFeed,
|
||||
MCommentOwnerReplyVideoImmutable,
|
||||
MCommentOwnerVideoFeed,
|
||||
MCommentOwnerVideoReply,
|
||||
MVideo,
|
||||
MVideoImmutable
|
||||
|
@ -42,7 +51,7 @@ import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse.js'
|
|||
import { AccountModel } from '../account/account.js'
|
||||
import { ActorModel } from '../actor/actor.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 { VideoChannelModel } from './video-channel.js'
|
||||
import { VideoModel } from './video.js'
|
||||
|
@ -222,6 +231,43 @@ export class VideoCommentModel extends SequelizeModel<VideoCommentModel> {
|
|||
})
|
||||
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 = '') {
|
||||
|
@ -442,7 +488,8 @@ export class VideoCommentModel extends SequelizeModel<VideoCommentModel> {
|
|||
order: [ [ 'createdAt', order ] ] as Order,
|
||||
where: {
|
||||
id: {
|
||||
[Op.in]: Sequelize.literal('(' +
|
||||
[Op.in]: Sequelize.literal(
|
||||
'(' +
|
||||
'WITH RECURSIVE children (id, "inReplyToCommentId") AS ( ' +
|
||||
`SELECT id, "inReplyToCommentId" FROM "videoComment" WHERE id = ${comment.id} ` +
|
||||
'UNION ' +
|
||||
|
@ -450,7 +497,8 @@ export class VideoCommentModel extends SequelizeModel<VideoCommentModel> {
|
|||
'INNER JOIN "children" ON "children"."inReplyToCommentId" = "parent"."id"' +
|
||||
') ' +
|
||||
'SELECT id FROM children' +
|
||||
')'),
|
||||
')'
|
||||
),
|
||||
[Op.ne]: comment.id
|
||||
}
|
||||
},
|
||||
|
|
|
@ -507,6 +507,13 @@ export class VideoModel extends SequelizeModel<VideoModel> {
|
|||
@Column
|
||||
dislikes: number
|
||||
|
||||
@AllowNull(false)
|
||||
@Default(0)
|
||||
@IsInt
|
||||
@Min(0)
|
||||
@Column
|
||||
comments: number
|
||||
|
||||
@AllowNull(false)
|
||||
@Column
|
||||
remote: boolean
|
||||
|
|
|
@ -7186,6 +7186,7 @@ components:
|
|||
- -publishedAt
|
||||
- -views
|
||||
- -likes
|
||||
- -comments
|
||||
- -trending
|
||||
- -hot
|
||||
- -best
|
||||
|
@ -8445,6 +8446,9 @@ components:
|
|||
dislikes:
|
||||
type: integer
|
||||
example: 7
|
||||
comments:
|
||||
description: "**PeerTube >= 7.2** Number of comments on the video"
|
||||
type: integer
|
||||
nsfw:
|
||||
type: boolean
|
||||
waitTranscoding:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue