<label i18n>Track watch history</label>
</div>
- <button class="delete-history" (click)="deleteHistory()" i18n>
+ <button class="delete-history" (click)="clearAllHistory()" i18n>
<my-global-icon iconName="delete" aria-hidden="true"></my-global-icon>
- Delete history
+ Clear all history
</button>
</div>
[enableSelection]="false"
[disabled]="disabled"
#videosSelection
-></my-videos-selection>
+>
+ <ng-template ptTemplate="rowButtons" let-video>
+ <div class="action-button">
+ <my-delete-button i18n-label label="Delete from history" (click)="deleteHistoryElement(video)"></my-delete-button>
+ </div>
+ </ng-template>
+</my-videos-selection>
@include row-blocks($column-responsive: false);
}
+.action-button {
+ display: flex;
+ align-self: flex-end;
+}
+
@media screen and (max-width: $small-view) {
.top-buttons {
grid-template-columns: auto 1fr auto;
})
}
- async deleteHistory () {
+ deleteHistoryElement (video: Video) {
+ this.userHistoryService.deleteUserVideoHistoryElement(video)
+ .subscribe({
+ next: () => {
+ this.videos = this.videos.filter(v => v.id !== video.id)
+ },
+
+ error: err => this.notifier.error(err.message)
+ })
+ }
+
+ async clearAllHistory () {
const title = $localize`Delete videos history`
const message = $localize`Are you sure you want to delete all your videos history?`
const res = await this.confirmService.confirm(message, title)
if (res !== true) return
- this.userHistoryService.deleteUserVideosHistory()
+ this.userHistoryService.clearAllUserVideosHistory()
.subscribe({
next: () => {
this.notifier.success($localize`Videos history deleted`)
)
}
- deleteUserVideosHistory () {
+ deleteUserVideoHistoryElement (video: Video) {
+ return this.authHttp
+ .delete(UserHistoryService.BASE_USER_VIDEOS_HISTORY_URL + '/' + video.id)
+ .pipe(catchError(err => this.restExtractor.handleError(err)))
+ }
+
+ clearAllUserVideosHistory () {
return this.authHttp
.post(UserHistoryService.BASE_USER_VIDEOS_HISTORY_URL + '/remove', {})
.pipe(
paginationValidator,
setDefaultPagination,
userHistoryListValidator,
- userHistoryRemoveValidator
+ userHistoryRemoveAllValidator,
+ userHistoryRemoveElementValidator
} from '../../../middlewares'
import { UserVideoHistoryModel } from '../../../models/user/user-video-history'
asyncMiddleware(listMyVideosHistory)
)
+myVideosHistoryRouter.delete('/me/history/videos/:videoId',
+ authenticate,
+ userHistoryRemoveElementValidator,
+ asyncMiddleware(removeUserHistoryElement)
+)
+
myVideosHistoryRouter.post('/me/history/videos/remove',
authenticate,
- userHistoryRemoveValidator,
- asyncRetryTransactionMiddleware(removeUserHistory)
+ userHistoryRemoveAllValidator,
+ asyncRetryTransactionMiddleware(removeAllUserHistory)
)
// ---------------------------------------------------------------------------
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
-async function removeUserHistory (req: express.Request, res: express.Response) {
+async function removeUserHistoryElement (req: express.Request, res: express.Response) {
+ const user = res.locals.oauth.token.User
+
+ await UserVideoHistoryModel.removeUserHistoryElement(user, parseInt(req.params.videoId + ''))
+
+ return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
+}
+
+async function removeAllUserHistory (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.User
const beforeDate = req.body.beforeDate || null
import express from 'express'
-import { body, query } from 'express-validator'
-import { exists, isDateValid } from '../../helpers/custom-validators/misc'
+import { body, param, query } from 'express-validator'
+import { exists, isDateValid, isIdValid } from '../../helpers/custom-validators/misc'
import { logger } from '../../helpers/logger'
import { areValidationErrors } from './shared'
}
]
-const userHistoryRemoveValidator = [
+const userHistoryRemoveAllValidator = [
body('beforeDate')
.optional()
.custom(isDateValid).withMessage('Should have a before date that conforms to ISO 8601'),
(req: express.Request, res: express.Response, next: express.NextFunction) => {
- logger.debug('Checking userHistoryRemoveValidator parameters', { parameters: req.body })
+ logger.debug('Checking userHistoryRemoveAllValidator parameters', { parameters: req.body })
+
+ if (areValidationErrors(req, res)) return
+
+ return next()
+ }
+]
+
+const userHistoryRemoveElementValidator = [
+ param('videoId')
+ .custom(isIdValid).withMessage('Should have a valid video id'),
+
+ (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ logger.debug('Checking userHistoryRemoveElementValidator parameters', { parameters: req.params })
if (areValidationErrors(req, res)) return
export {
userHistoryListValidator,
- userHistoryRemoveValidator
+ userHistoryRemoveElementValidator,
+ userHistoryRemoveAllValidator
}
})
}
+ static removeUserHistoryElement (user: MUserId, videoId: number) {
+ const query: DestroyOptions = {
+ where: {
+ userId: user.id,
+ videoId
+ }
+ }
+
+ return UserVideoHistoryModel.destroy(query)
+ }
+
static removeUserHistoryBefore (user: MUserId, beforeDate: string, t: Transaction) {
const query: DestroyOptions = {
where: {
import {
cleanupTests,
createSingleServer,
+ makeDeleteRequest,
makeGetRequest,
makePostBodyRequest,
makePutBodyRequest,
const myHistoryRemove = myHistoryPath + '/remove'
let watchingPath: string
let server: PeerTubeServer
+ let videoId: number
// ---------------------------------------------------------------
await setAccessTokensToServers([ server ])
- const { uuid } = await server.videos.upload()
+ const { id, uuid } = await server.videos.upload()
watchingPath = '/api/v1/videos/' + uuid + '/watching'
+ videoId = id
})
describe('When notifying a user is watching a video', function () {
})
})
- describe('When removing user videos history', function () {
+ describe('When removing a specific user video history element', function () {
+ let path: string
+
+ before(function () {
+ path = myHistoryPath + '/' + videoId
+ })
+
+ it('Should fail with an unauthenticated user', async function () {
+ await makeDeleteRequest({ url: server.url, path, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
+ })
+
+ it('Should fail with a bad videoId parameter', async function () {
+ await makeDeleteRequest({
+ url: server.url,
+ token: server.accessToken,
+ path: myHistoryRemove + '/hi',
+ expectedStatus: HttpStatusCode.BAD_REQUEST_400
+ })
+ })
+
+ it('Should succeed with the correct parameters', async function () {
+ await makeDeleteRequest({
+ url: server.url,
+ token: server.accessToken,
+ path,
+ expectedStatus: HttpStatusCode.NO_CONTENT_204
+ })
+ })
+ })
+
+ describe('When removing all user videos history', function () {
it('Should fail with an unauthenticated user', async function () {
await makePostBodyRequest({ url: server.url, path: myHistoryPath + '/remove', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
})
describe('Test videos history', function () {
let server: PeerTubeServer = null
+ let video1Id: number
let video1UUID: string
let video2UUID: string
let video3UUID: string
command = server.history
{
- const { uuid } = await server.videos.upload({ attributes: { name: 'video 1' } })
+ const { id, uuid } = await server.videos.upload({ attributes: { name: 'video 1' } })
video1UUID = uuid
+ video1Id = id
}
{
})
it('Should watch the first and second video', async function () {
- await command.wathVideo({ videoId: video2UUID, currentTime: 8 })
- await command.wathVideo({ videoId: video1UUID, currentTime: 3 })
+ await command.watchVideo({ videoId: video2UUID, currentTime: 8 })
+ await command.watchVideo({ videoId: video1UUID, currentTime: 3 })
})
it('Should return the correct history when listing, searching and getting videos', async function () {
it('Should have these videos when listing my history', async function () {
video3WatchedDate = new Date()
- await command.wathVideo({ videoId: video3UUID, currentTime: 2 })
+ await command.watchVideo({ videoId: video3UUID, currentTime: 2 })
const body = await command.list()
})
it('Should clear my history', async function () {
- await command.remove({ beforeDate: video3WatchedDate.toISOString() })
+ await command.removeAll({ beforeDate: video3WatchedDate.toISOString() })
})
it('Should have my history cleared', async function () {
videosHistoryEnabled: false
})
- await command.wathVideo({ videoId: video2UUID, currentTime: 8, expectedStatus: HttpStatusCode.CONFLICT_409 })
+ await command.watchVideo({ videoId: video2UUID, currentTime: 8, expectedStatus: HttpStatusCode.CONFLICT_409 })
})
it('Should re-enable videos history', async function () {
videosHistoryEnabled: true
})
- await command.wathVideo({ videoId: video1UUID, currentTime: 8 })
+ await command.watchVideo({ videoId: video1UUID, currentTime: 8 })
const body = await command.list()
expect(body.total).to.equal(2)
expect(body.total).to.equal(0)
})
+ it('Should delete a specific history element', async function () {
+ {
+ await command.watchVideo({ videoId: video1UUID, currentTime: 4 })
+ await command.watchVideo({ videoId: video2UUID, currentTime: 8 })
+ }
+
+ {
+ const body = await command.list()
+ expect(body.total).to.equal(2)
+ }
+
+ {
+ await command.removeElement({ videoId: video1Id })
+
+ const body = await command.list()
+ expect(body.total).to.equal(1)
+ expect(body.data[0].uuid).to.equal(video2UUID)
+ }
+ })
+
after(async function () {
await cleanupTests([ server ])
})
export class HistoryCommand extends AbstractCommand {
- wathVideo (options: OverrideCommandOptions & {
+ watchVideo (options: OverrideCommandOptions & {
videoId: number | string
currentTime: number
}) {
})
}
- remove (options: OverrideCommandOptions & {
+ removeElement (options: OverrideCommandOptions & {
+ videoId: number
+ }) {
+ const { videoId } = options
+ const path = '/api/v1/users/me/history/videos/' + videoId
+
+ return this.deleteRequest({
+ ...options,
+
+ path,
+ implicitToken: true,
+ defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+ })
+ }
+
+ removeAll (options: OverrideCommandOptions & {
beforeDate?: string
} = {}) {
const { beforeDate } = options
schema:
$ref: '#/components/schemas/VideoListResponse'
+ /users/me/history/videos/{videoId}:
+ delete:
+ summary: Delete history element
+ security:
+ - OAuth2: []
+ tags:
+ - My History
+ parameters:
+ - name: videoId
+ in: path
+ required: true
+ schema:
+ $ref: '#/components/schemas/Video/properties/id'
+ responses:
+ '204':
+ description: successful operation
+
/users/me/history/videos/remove:
post:
summary: Clear video history