@@ -1,8 +1,9 @@ | |||
<h1 class="visually-hidden" i18n>Notifications</h1> | |||
<div class="header"> | |||
<a routerLink="/my-account/settings" fragment="notifications" i18n> | |||
<a class="peertube-button-link grey-button" routerLink="/my-account/settings" fragment="notifications"> | |||
<my-global-icon iconName="cog" aria-hidden="true"></my-global-icon> | |||
Notification preferences | |||
<span i18n>Notification preferences</span> | |||
</a> | |||
<div class="peertube-select-container peertube-select-button ms-2 me-2"> | |||
@@ -13,7 +14,7 @@ | |||
</select> | |||
</div> | |||
<button class="btn ms-auto" [disabled]="!hasUnreadNotifications()" (click)="markAllAsRead()"> | |||
<button class="ms-auto peertube-button grey-button" [disabled]="!hasUnreadNotifications()" (click)="markAllAsRead()"> | |||
<ng-container *ngIf="hasUnreadNotifications()"> | |||
<my-global-icon iconName="tick" aria-hidden="true"></my-global-icon> | |||
@@ -3,17 +3,13 @@ | |||
.header { | |||
display: flex; | |||
margin-bottom: 20px; | |||
margin-bottom: 1.25rem; | |||
a { | |||
@include peertube-button-link; | |||
@include grey-button; | |||
@include button-with-icon(18px, 3px, -1px); | |||
} | |||
button { | |||
@include peertube-button; | |||
@include grey-button; | |||
@include button-with-icon(20px, 3px, -1px); | |||
} | |||
@@ -1,6 +1,6 @@ | |||
<my-search-typeahead class="w-100 d-flex justify-content-center"></my-search-typeahead> | |||
<a class="publish-button" routerLink="/videos/upload"> | |||
<a class="peertube-button-link orange-button publish-button" routerLink="/videos/upload"> | |||
<my-global-icon iconName="upload" aria-hidden="true"></my-global-icon> | |||
<span i18n class="publish-button-label">Publish</span> | |||
</a> |
@@ -2,9 +2,7 @@ | |||
@use '_mixins' as *; | |||
.publish-button { | |||
@include peertube-button-link; | |||
@include orange-button; | |||
@include button-with-icon(22px, 3px, -1px); | |||
@include button-with-icon(21px, 3px, -1px); | |||
@include margin-right(25px); | |||
@media screen and (max-width: $mobile-view) { | |||
@@ -2,13 +2,11 @@ | |||
@use '_mixins' as *; | |||
#search-video { | |||
@include peertube-input-text($search-input-width); | |||
@include peertube-input-text($search-input-width, 14px); | |||
@include padding-left(10px); | |||
@include padding-right(40px); // For the search icon | |||
font-size: 14px; | |||
&::placeholder { | |||
color: pvar(--inputPlaceholderColor); | |||
} | |||
@@ -120,9 +120,13 @@ | |||
} | |||
} | |||
my-global-icon { | |||
my-global-icon[iconName=cog] { | |||
width: 20px; | |||
} | |||
my-global-icon[iconName=tick] { | |||
width: 26px; | |||
} | |||
} | |||
.all-notifications { | |||
@@ -1,7 +1,7 @@ | |||
<div class="dropdown-root" ngbDropdown [placement]="placement" [container]="container" *ngIf="areActionsDisplayed(actions, entry)"> | |||
<button | |||
class="action-button" [ngClass]="{ small: buttonSize === 'small', grey: theme === 'grey', orange: theme === 'orange', 'button-styled': buttonStyled }" | |||
ngbDropdownToggle role="button" aria-label="Open actions" i18n-aria-label | |||
ngbDropdownToggle aria-label="Open actions" i18n-aria-label | |||
> | |||
<my-global-icon *ngIf="!label && buttonDirection === 'horizontal'" class="more-icon" iconName="more-horizontal"></my-global-icon> | |||
<my-global-icon *ngIf="!label && buttonDirection === 'vertical'" class="more-icon" iconName="more-vertical"></my-global-icon> | |||
@@ -41,8 +41,7 @@ | |||
&.small { | |||
font-size: 14px; | |||
height: 20px; | |||
line-height: 20px; | |||
padding: 0 10px; | |||
} | |||
} | |||
@@ -80,8 +80,16 @@ | |||
} | |||
} | |||
@mixin peertube-input-text($width) { | |||
padding: 4px 15px; | |||
@mixin rounded-line-height-1-5 ($font-size) { | |||
line-height: $font-size + math.round(math.div($font-size, 2)); | |||
} | |||
@mixin peertube-input-text($width, $font-size: $form-input-font-size) { | |||
@include rounded-line-height-1-5($font-size); | |||
font-size: $font-size; | |||
padding: 3px 15px; | |||
display: inline-block; | |||
width: $width; | |||
max-width: $width; | |||
@@ -89,8 +97,6 @@ | |||
background-color: pvar(--inputBackgroundColor); | |||
border: 1px solid pvar(--inputBorderColor); | |||
border-radius: 3px; | |||
font-size: $form-input-font-size; | |||
line-height: $form-input-line-height; | |||
&::placeholder { | |||
color: pvar(--inputPlaceholderColor); | |||
@@ -241,6 +247,8 @@ | |||
} | |||
@mixin peertube-button { | |||
@include rounded-line-height-1-5($button-font-size); | |||
padding: 4px 13px; | |||
border: 0; | |||
@@ -253,7 +261,6 @@ | |||
cursor: pointer; | |||
font-size: $button-font-size; | |||
line-height: $button-font-size + math.round(math.div($button-font-size, 2)); | |||
my-global-icon + * { | |||
@include margin-right(4px); | |||
@@ -303,10 +310,6 @@ | |||
width: $width; | |||
top: $top; | |||
} | |||
span { | |||
vertical-align: middle; | |||
} | |||
} | |||
@mixin peertube-file { | |||
@@ -397,15 +400,17 @@ | |||
} | |||
select { | |||
padding: 4px 35px 4px 12px; | |||
@include rounded-line-height-1-5($form-input-font-size); | |||
font-size: $form-input-font-size; | |||
padding: 3px 35px 3px 12px; | |||
position: relative; | |||
border: 1px solid pvar(--inputBorderColor); | |||
background: transparent none; | |||
appearance: none; | |||
text-overflow: ellipsis; | |||
color: pvar(--mainForegroundColor); | |||
font-size: $form-input-font-size; | |||
line-height: $form-input-line-height; | |||
&:focus { | |||
outline: none; | |||
@@ -432,6 +437,9 @@ | |||
font-weight: $font-semibold; | |||
color: pvar(--greyForegroundColor); | |||
border: 0; | |||
// No border, add +1 to vertical padding | |||
padding: 4px 35px 4px 12px; | |||
} | |||
} | |||
} | |||
@@ -96,7 +96,6 @@ $activated-action-button-color: #212529; | |||
$focus-box-shadow-form: 0 0 0 .2rem; | |||
$form-input-font-size: 15px; | |||
$form-input-line-height: 1.4; | |||
$video-watch-player-factor: math.div(16, 9); | |||
$video-watch-info-margin-left: 44px; | |||
@@ -35,8 +35,9 @@ $ng-select-input-text: pvar(--mainForegroundColor); | |||
@import '@ng-select/ng-select/scss/default.theme'; | |||
.ng-select { | |||
@include rounded-line-height-1-5($ng-select-value-font-size); | |||
font-size: $ng-select-value-font-size; | |||
line-height: $form-input-line-height; | |||
&.ng-select-focused { | |||
&:not(.ng-select-opened) > .ng-select-container { | |||
@@ -1,6 +1,7 @@ | |||
import { logger, loggerTagsFactory, LoggerTagsFn } from '@server/helpers/logger' | |||
import { sequelizeTypescript } from '@server/initializers/database' | |||
import { Hooks } from '@server/lib/plugins/hooks' | |||
import { autoBlacklistVideoIfNeeded } from '@server/lib/video-blacklist' | |||
import { VideoModel } from '@server/models/video/video' | |||
import { MThumbnail, MVideoFullLight, MVideoThumbnail } from '@server/types/models' | |||
@@ -61,6 +62,8 @@ export class APVideoCreator extends APVideoAbstractBuilder { | |||
logger.info('Remote video with uuid %s inserted.', this.videoObject.uuid, this.lTags()) | |||
Hooks.runAction('action:activity-pub.remote-video.created', { video: videoCreated, videoAPObject: this.videoObject }) | |||
return { autoBlacklisted, videoCreated } | |||
} catch (err) { | |||
// FIXME: Use rollback hook when https://github.com/sequelize/sequelize/pull/13038 is released | |||
@@ -3,6 +3,7 @@ import { resetSequelizeInstance, runInReadCommittedTransaction } from '@server/h | |||
import { logger, loggerTagsFactory, LoggerTagsFn } from '@server/helpers/logger' | |||
import { Notifier } from '@server/lib/notifier' | |||
import { PeerTubeSocket } from '@server/lib/peertube-socket' | |||
import { Hooks } from '@server/lib/plugins/hooks' | |||
import { autoBlacklistVideoIfNeeded } from '@server/lib/video-blacklist' | |||
import { VideoLiveModel } from '@server/models/video/video-live' | |||
import { MActor, MChannelAccountLight, MChannelId, MVideoAccountLightBlacklistAllFiles, MVideoFullLight } from '@server/types/models' | |||
@@ -81,6 +82,8 @@ export class APVideoUpdater extends APVideoAbstractBuilder { | |||
PeerTubeSocket.Instance.sendVideoLiveNewState(videoUpdated) | |||
} | |||
Hooks.runAction('action:activity-pub.remote-video.updated', { video: videoUpdated, videoAPObject: this.videoObject }) | |||
logger.info('Remote video with uuid %s updated', this.videoObject.uuid, this.lTags()) | |||
return videoUpdated | |||
@@ -1,42 +1,53 @@ | |||
async function register ({ registerHook, registerSetting, settingsManager, storageManager, peertubeHelpers }) { | |||
const actionHooks = [ | |||
'action:application.listening', | |||
'action:notifier.notification.created', | |||
{ | |||
const actionHooks = [ | |||
'action:application.listening', | |||
'action:notifier.notification.created', | |||
'action:api.video.updated', | |||
'action:api.video.deleted', | |||
'action:api.video.uploaded', | |||
'action:api.video.viewed', | |||
'action:api.video.updated', | |||
'action:api.video.deleted', | |||
'action:api.video.uploaded', | |||
'action:api.video.viewed', | |||
'action:api.video-channel.created', | |||
'action:api.video-channel.updated', | |||
'action:api.video-channel.deleted', | |||
'action:api.video-channel.created', | |||
'action:api.video-channel.updated', | |||
'action:api.video-channel.deleted', | |||
'action:api.live-video.created', | |||
'action:api.live-video.created', | |||
'action:api.video-thread.created', | |||
'action:api.video-comment-reply.created', | |||
'action:api.video-comment.deleted', | |||
'action:api.video-thread.created', | |||
'action:api.video-comment-reply.created', | |||
'action:api.video-comment.deleted', | |||
'action:api.video-caption.created', | |||
'action:api.video-caption.deleted', | |||
'action:api.video-caption.created', | |||
'action:api.video-caption.deleted', | |||
'action:api.user.blocked', | |||
'action:api.user.unblocked', | |||
'action:api.user.registered', | |||
'action:api.user.created', | |||
'action:api.user.deleted', | |||
'action:api.user.updated', | |||
'action:api.user.oauth2-got-token', | |||
'action:api.user.blocked', | |||
'action:api.user.unblocked', | |||
'action:api.user.registered', | |||
'action:api.user.created', | |||
'action:api.user.deleted', | |||
'action:api.user.updated', | |||
'action:api.user.oauth2-got-token', | |||
'action:api.video-playlist-element.created' | |||
] | |||
'action:api.video-playlist-element.created' | |||
] | |||
for (const h of actionHooks) { | |||
registerHook({ | |||
target: h, | |||
handler: () => peertubeHelpers.logger.debug('Run hook %s.', h) | |||
}) | |||
for (const h of actionHooks) { | |||
registerHook({ | |||
target: h, | |||
handler: () => peertubeHelpers.logger.debug('Run hook %s.', h) | |||
}) | |||
} | |||
for (const h of [ 'action:activity-pub.remote-video.created', 'action:activity-pub.remote-video.updated' ]) { | |||
registerHook({ | |||
target: h, | |||
handler: ({ video, videoAPObject }) => { | |||
peertubeHelpers.logger.debug('Run hook %s - AP %s - video %s.', h, video.name, videoAPObject.name ) | |||
} | |||
}) | |||
} | |||
} | |||
registerHook({ | |||
@@ -4,6 +4,7 @@ import { ServerHookName, VideoPlaylistPrivacy, VideoPrivacy } from '@shared/mode | |||
import { | |||
cleanupTests, | |||
createMultipleServers, | |||
doubleFollow, | |||
killallServers, | |||
PeerTubeServer, | |||
PluginsCommand, | |||
@@ -36,6 +37,8 @@ describe('Test plugin action hooks', function () { | |||
enabled: true | |||
} | |||
}) | |||
await doubleFollow(servers[0], servers[1]) | |||
}) | |||
describe('Application hooks', function () { | |||
@@ -231,6 +234,27 @@ describe('Test plugin action hooks', function () { | |||
}) | |||
}) | |||
describe('Activity Pub hooks', function () { | |||
let videoUUID: string | |||
it('Should run action:activity-pub.remote-video.created', async function () { | |||
this.timeout(30000) | |||
const { uuid } = await servers[1].videos.quickUpload({ name: 'remote video' }) | |||
videoUUID = uuid | |||
await servers[0].servers.waitUntilLog('action:activity-pub.remote-video.created - AP remote video - video remote video') | |||
}) | |||
it('Should run action:activity-pub.remote-video.updated', async function () { | |||
this.timeout(30000) | |||
await servers[1].videos.update({ id: videoUUID, attributes: { name: 'remote video updated' } }) | |||
await servers[0].servers.waitUntilLog('action:activity-pub.remote-video.updated - AP remote video - video remote video') | |||
}) | |||
}) | |||
after(async function () { | |||
await cleanupTests(servers) | |||
}) | |||
@@ -1,4 +1,4 @@ | |||
// {hookType}:{api?}.{location}.{subLocation?}.{actionType}.{target} | |||
// {hookType}:{root}.{location}.{subLocation?}.{actionType}.{target} | |||
export const serverFilterHookObject = { | |||
// Filter params/result used to list videos for the REST API | |||
@@ -184,7 +184,11 @@ export const serverActionHookObject = { | |||
'action:api.user.oauth2-got-token': true, | |||
// Fired when a video is added to a playlist | |||
'action:api.video-playlist-element.created': true | |||
'action:api.video-playlist-element.created': true, | |||
// Fired when a remote video has been created/updated | |||
'action:activity-pub.remote-video.created': true, | |||
'action:activity-pub.remote-video.updated': true | |||
} | |||
export type ServerActionHookName = keyof typeof serverActionHookObject | |||
@@ -16,6 +16,7 @@ | |||
- [Add external auth methods](#add-external-auth-methods) | |||
- [Add new transcoding profiles](#add-new-transcoding-profiles) | |||
- [Server helpers](#server-helpers) | |||
- [Federation](#federation) | |||
- [Client API (themes & plugins)](#client-api-themes--plugins) | |||
- [Get plugin static and router routes](#get-plugin-static-and-router-routes) | |||
- [Notifier](#notifier) | |||
@@ -587,6 +588,49 @@ async function register ({ | |||
See the [plugin API reference](https://docs.joinpeertube.org/api/plugins) to see the complete helpers list. | |||
#### Federation | |||
You can use some server hooks to federate plugin data to other PeerTube instances that may have installed your plugin. | |||
For example to federate additional video metadata: | |||
```js | |||
async function register ({ registerHook }) { | |||
// Send plugin metadata to remote instances | |||
// We also update the JSON LD context because we added a new field | |||
{ | |||
registerHook({ | |||
target: 'filter:activity-pub.video.json-ld.build.result', | |||
handler: async (jsonld, { video }) => { | |||
return Object.assign(jsonld, { recordedAt: 'https://example.com/event' }) | |||
} | |||
}) | |||
registerHook({ | |||
target: 'filter:activity-pub.activity.context.build.result', | |||
handler: jsonld => { | |||
return jsonld.concat([ { recordedAt: 'https://schema.org/recordedAt' } ]) | |||
} | |||
}) | |||
} | |||
// Save remote video metadata | |||
{ | |||
for (const h of [ 'action:activity-pub.remote-video.created', 'action:activity-pub.remote-video.updated' ]) { | |||
registerHook({ | |||
target: h, | |||
handler: ({ video, videoAPObject }) => { | |||
if (videoAPObject.recordedAt) { | |||
// Save information about the video | |||
} | |||
} | |||
}) | |||
} | |||
} | |||
``` | |||
### Client API (themes & plugins) | |||
#### Get plugin static and router routes | |||