Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Add Comment Count to Video Preview Components #6635

Draft
wants to merge 3 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions client/src/app/shared/shared-main/video/video.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ export class Video implements VideoServerModel {

automaticTags?: string[]

commentCount: number

static buildWatchUrl (video: Partial<Pick<Video, 'uuid' | 'shortUUID'>>) {
return buildVideoWatchPath({ shortUUID: video.shortUUID || video.uuid })
}
Expand Down Expand Up @@ -209,6 +211,8 @@ export class Video implements VideoServerModel {
this.aspectRatio = hash.aspectRatio

this.automaticTags = hash.automaticTags

this.commentCount = hash.commentCount
}

isVideoNSFWForUser (user: User, serverConfig: HTMLServerConfig) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@

<my-video-views-counter *ngIf="displayOptions.views" [video]="video"></my-video-views-counter>
</span>

<span class="comment-count" *ngIf="displayOptions.commentCount">
<my-global-icon iconName="message-circle"></my-global-icon>
<span>{{ video.commentCount }}</span>
</span>
</span>

<a *ngIf="displayOptions.by" class="video-miniature-account" [routerLink]="[ '/c', video.byVideoChannel ]">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,15 @@ my-actor-avatar {

.video-miniature-created-at-views {
display: block;

.comment-count {
float: right;
text-align: right;

span {
margin-left: 5px;
}
}
}

.video-actions {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { NgClass, NgIf, NgFor } from '@angular/common'
import { Video } from '../shared-main/video/video.model'
import { VideoService } from '../shared-main/video/video.service'
import { VideoPlaylistService } from '../shared-video-playlist/video-playlist.service'
import { GlobalIconComponent } from '../shared-icons/global-icon.component'

export type MiniatureDisplayOptions = {
date?: boolean
Expand All @@ -37,7 +38,8 @@ export type MiniatureDisplayOptions = {
nsfw?: boolean

by?: boolean
forceChannelInBy?: boolean
forceChannelInBy?: boolean,
commentCount?: boolean
}
@Component({
selector: 'my-video-miniature',
Expand All @@ -55,7 +57,8 @@ export type MiniatureDisplayOptions = {
VideoViewsCounterComponent,
RouterLink,
NgFor,
VideoActionsDropdownComponent
VideoActionsDropdownComponent,
GlobalIconComponent
]
})
export class VideoMiniatureComponent implements OnInit {
Expand All @@ -67,6 +70,7 @@ export class VideoMiniatureComponent implements OnInit {
date: true,
views: true,
by: true,
commentCount: true,
avatar: false,
privacyLabel: false,
privacyText: false,
Expand Down
4 changes: 3 additions & 1 deletion packages/models/src/videos/video.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ export interface Video extends Partial<VideoAdditionalAttributes> {
currentTime: number
}

pluginData?: any
pluginData?: any,

commentCount: number
}

// Not included by default, needs query params
Expand Down
2 changes: 1 addition & 1 deletion server/core/initializers/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import { CONFIG, registerConfigChangedHandler } from './config.js'

// ---------------------------------------------------------------------------

export const LAST_MIGRATION_VERSION = 865
export const LAST_MIGRATION_VERSION = 870

// ---------------------------------------------------------------------------

Expand Down
112 changes: 112 additions & 0 deletions server/core/initializers/migrations/0870-video-comment-count.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import * as Sequelize from 'sequelize'

async function up (utils: {
transaction: Sequelize.Transaction
queryInterface: Sequelize.QueryInterface
sequelize: Sequelize.Sequelize
}): Promise<void> {
const { transaction } = utils

{
// 1. Add the commentCount column as nullable without a default value
await utils.queryInterface.addColumn('video', 'commentCount', {
type: Sequelize.INTEGER,
allowNull: true // Initially allow nulls
}, { transaction })
}

{
// 2. Backfill the commentCount data in small batches
const batchSize = 1000
let offset = 0
let hasMore = true

while (hasMore) {
const [videos] = await utils.sequelize.query(
`
SELECT v.id
FROM video v
ORDER BY v.id
LIMIT ${batchSize} OFFSET ${offset}
`,
{
transaction,
// Sequelize v6 defaults to SELECT type, so no need to specify QueryTypes.SELECT
}
)

if (videos.length === 0) {
hasMore = false
break
}

const videoIds = videos.map((v: any) => v.id)

// Get comment counts for this batch
const [counts] = await utils.sequelize.query(
`
SELECT "videoId", COUNT(*) AS count
FROM "videoComment"
WHERE "videoId" IN (:videoIds)
GROUP BY "videoId"
`,
{
transaction,
replacements: { videoIds }
}
)

// Create a map of videoId to count
const countMap = counts.reduce((map: any, item: any) => {
map[item.videoId] = parseInt(item.count, 10)
return map
}, {})

// Update videos in this batch
const updatePromises = videoIds.map((id: number) => {
const count = countMap[id] || 0
return utils.sequelize.query(
`
UPDATE video
SET "commentCount" = :count
WHERE id = :id
`,
{
transaction,
replacements: { count, id }
}
)
})

await Promise.all(updatePromises)

offset += batchSize
}
}

{
// 3. Set the default value to 0 for future inserts
await utils.queryInterface.changeColumn('video', 'commentCount', {
type: Sequelize.INTEGER,
allowNull: true,
defaultValue: 0
}, { transaction })
}

{
// 4. Alter the column to be NOT NULL now that data is backfilled
await utils.queryInterface.changeColumn('video', 'commentCount', {
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 0
}, { transaction })
}
}

function down (options) {
throw new Error('Not implemented.')
}

export {
down, up
}
2 changes: 2 additions & 0 deletions server/core/models/video/formatter/video-api-format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ export function videoModelToFormattedJSON (video: MVideoFormattable, options: Vi
? { currentTime: userHistory.currentTime }
: undefined,

commentCount: video.commentCount,

// Can be added by external plugins
pluginData: (video as any).pluginData,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,8 @@ export class VideoTableAttributes {
'channelId',
'createdAt',
'updatedAt',
'moveJobsRunning'
'moveJobsRunning',
'commentCount'
]
}
}
37 changes: 37 additions & 0 deletions server/core/models/video/video-comment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ 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,
CreatedAt,
Expand Down Expand Up @@ -222,6 +225,40 @@ export class VideoCommentModel extends SequelizeModel<VideoCommentModel> {
})
CommentAutomaticTags: Awaited<CommentAutomaticTagModel>[]

@AfterCreate
static async incrementCommentCount(instance: VideoCommentModel, options: any) {
if (instance.heldForReview) return // Don't count held comments

await VideoModel.increment('commentCount', {
by: 1,
where: { id: instance.videoId },
transaction: options.transaction
})
}

@AfterDestroy
static async decrementCommentCount(instance: VideoCommentModel, options: any) {
if (instance.heldForReview) return // Don't count held comments

await VideoModel.decrement('commentCount', {
by: 1,
where: { id: instance.videoId },
transaction: options.transaction
})
}

@AfterUpdate
static async updateCommentCountOnHeldStatusChange(instance: VideoCommentModel, options: any) {
if (instance.changed('heldForReview')) {
const method = instance.heldForReview ? 'decrement' : 'increment'
await VideoModel[method]('commentCount', {
by: 1,
where: { id: instance.videoId },
transaction: options.transaction
})
}
}

// ---------------------------------------------------------------------------

static getSQLAttributes (tableName: string, aliasPrefix = '') {
Expand Down
5 changes: 5 additions & 0 deletions server/core/models/video/video.ts
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,11 @@ export class VideoModel extends SequelizeModel<VideoModel> {
@Column
originallyPublishedAt: Date

@AllowNull(false)
@Default(0)
@Column
commentCount: number

@ForeignKey(() => VideoChannelModel)
@Column
channelId: number
Expand Down